1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.content.res; 18 19 import android.annotation.ColorInt; 20 import android.annotation.IntDef; 21 import android.annotation.NonNull; 22 import android.annotation.Nullable; 23 import android.content.pm.ActivityInfo.Config; 24 import android.content.res.Resources.Theme; 25 26 import com.android.internal.R; 27 import com.android.internal.util.GrowingArrayUtils; 28 29 import org.xmlpull.v1.XmlPullParser; 30 import org.xmlpull.v1.XmlPullParserException; 31 32 import android.graphics.LinearGradient; 33 import android.graphics.RadialGradient; 34 import android.graphics.Shader; 35 import android.graphics.SweepGradient; 36 import android.graphics.drawable.GradientDrawable; 37 import android.util.AttributeSet; 38 import android.util.Log; 39 import android.util.Xml; 40 41 import java.io.IOException; 42 import java.lang.annotation.Retention; 43 import java.lang.annotation.RetentionPolicy; 44 45 /** 46 * Lets you define a gradient color, which is used inside 47 * {@link android.graphics.drawable.VectorDrawable}. 48 * 49 * {@link android.content.res.GradientColor}s are created from XML resource files defined in the 50 * "color" subdirectory directory of an application's resource directory. The XML file contains 51 * a single "gradient" element with a number of attributes and elements inside. For example: 52 * <pre> 53 * <gradient xmlns:android="http://schemas.android.com/apk/res/android"> 54 * <android:startColor="?android:attr/colorPrimary"/> 55 * <android:endColor="?android:attr/colorControlActivated"/> 56 * <.../> 57 * <android:type="linear"/> 58 * </gradient> 59 * </pre> 60 * 61 * This can describe either a {@link android.graphics.LinearGradient}, 62 * {@link android.graphics.RadialGradient}, or {@link android.graphics.SweepGradient}. 63 * 64 * Note that different attributes are relevant for different types of gradient. 65 * For example, android:gradientRadius is only applied to RadialGradient. 66 * android:centerX and android:centerY are only applied to SweepGradient or RadialGradient. 67 * android:startX, android:startY, android:endX and android:endY are only applied to LinearGradient. 68 * 69 * Also note if any color "item" element is defined, then startColor, centerColor and endColor will 70 * be ignored. 71 * @hide 72 */ 73 public class GradientColor extends ComplexColor { 74 private static final String TAG = "GradientColor"; 75 76 private static final boolean DBG_GRADIENT = false; 77 78 @IntDef({TILE_MODE_CLAMP, TILE_MODE_REPEAT, TILE_MODE_MIRROR}) 79 @Retention(RetentionPolicy.SOURCE) 80 private @interface GradientTileMode {} 81 private static final int TILE_MODE_CLAMP = 0; 82 private static final int TILE_MODE_REPEAT = 1; 83 private static final int TILE_MODE_MIRROR = 2; 84 85 /** Lazily-created factory for this GradientColor. */ 86 private GradientColorFactory mFactory; 87 88 private @Config int mChangingConfigurations; 89 private int mDefaultColor; 90 91 // After parsing all the attributes from XML, this shader is the ultimate result containing 92 // all the XML information. 93 private Shader mShader = null; 94 95 // Below are the attributes at the root element <gradient>. 96 // NOTE: they need to be copied in the copy constructor! 97 private int mGradientType = GradientDrawable.LINEAR_GRADIENT; 98 99 private float mCenterX = 0f; 100 private float mCenterY = 0f; 101 102 private float mStartX = 0f; 103 private float mStartY = 0f; 104 private float mEndX = 0f; 105 private float mEndY = 0f; 106 107 private int mStartColor = 0; 108 private int mCenterColor = 0; 109 private int mEndColor = 0; 110 private boolean mHasCenterColor = false; 111 112 private int mTileMode = 0; // Clamp mode. 113 114 private float mGradientRadius = 0f; 115 116 // Below are the attributes for the <item> element. 117 private int[] mItemColors; 118 private float[] mItemOffsets; 119 120 // Theme attributes for the root and item elements. 121 private int[] mThemeAttrs; 122 private int[][] mItemsThemeAttrs; 123 GradientColor()124 private GradientColor() { 125 } 126 GradientColor(GradientColor copy)127 private GradientColor(GradientColor copy) { 128 if (copy != null) { 129 mChangingConfigurations = copy.mChangingConfigurations; 130 mDefaultColor = copy.mDefaultColor; 131 mShader = copy.mShader; 132 mGradientType = copy.mGradientType; 133 mCenterX = copy.mCenterX; 134 mCenterY = copy.mCenterY; 135 mStartX = copy.mStartX; 136 mStartY = copy.mStartY; 137 mEndX = copy.mEndX; 138 mEndY = copy.mEndY; 139 mStartColor = copy.mStartColor; 140 mCenterColor = copy.mCenterColor; 141 mEndColor = copy.mEndColor; 142 mHasCenterColor = copy.mHasCenterColor; 143 mGradientRadius = copy.mGradientRadius; 144 mTileMode = copy.mTileMode; 145 146 if (copy.mItemColors != null) { 147 mItemColors = copy.mItemColors.clone(); 148 } 149 if (copy.mItemOffsets != null) { 150 mItemOffsets = copy.mItemOffsets.clone(); 151 } 152 153 if (copy.mThemeAttrs != null) { 154 mThemeAttrs = copy.mThemeAttrs.clone(); 155 } 156 if (copy.mItemsThemeAttrs != null) { 157 mItemsThemeAttrs = copy.mItemsThemeAttrs.clone(); 158 } 159 } 160 } 161 162 // Set the default to clamp mode. parseTileMode(@radientTileMode int tileMode)163 private static Shader.TileMode parseTileMode(@GradientTileMode int tileMode) { 164 switch (tileMode) { 165 case TILE_MODE_CLAMP: 166 return Shader.TileMode.CLAMP; 167 case TILE_MODE_REPEAT: 168 return Shader.TileMode.REPEAT; 169 case TILE_MODE_MIRROR: 170 return Shader.TileMode.MIRROR; 171 default: 172 return Shader.TileMode.CLAMP; 173 } 174 } 175 176 /** 177 * Update the root level's attributes, either for inflate or applyTheme. 178 */ updateRootElementState(TypedArray a)179 private void updateRootElementState(TypedArray a) { 180 // Extract the theme attributes, if any. 181 mThemeAttrs = a.extractThemeAttrs(); 182 183 mStartX = a.getFloat( 184 R.styleable.GradientColor_startX, mStartX); 185 mStartY = a.getFloat( 186 R.styleable.GradientColor_startY, mStartY); 187 mEndX = a.getFloat( 188 R.styleable.GradientColor_endX, mEndX); 189 mEndY = a.getFloat( 190 R.styleable.GradientColor_endY, mEndY); 191 192 mCenterX = a.getFloat( 193 R.styleable.GradientColor_centerX, mCenterX); 194 mCenterY = a.getFloat( 195 R.styleable.GradientColor_centerY, mCenterY); 196 197 mGradientType = a.getInt( 198 R.styleable.GradientColor_type, mGradientType); 199 200 mStartColor = a.getColor( 201 R.styleable.GradientColor_startColor, mStartColor); 202 mHasCenterColor |= a.hasValue( 203 R.styleable.GradientColor_centerColor); 204 mCenterColor = a.getColor( 205 R.styleable.GradientColor_centerColor, mCenterColor); 206 mEndColor = a.getColor( 207 R.styleable.GradientColor_endColor, mEndColor); 208 209 mTileMode = a.getInt( 210 R.styleable.GradientColor_tileMode, mTileMode); 211 212 if (DBG_GRADIENT) { 213 Log.v(TAG, "hasCenterColor is " + mHasCenterColor); 214 if (mHasCenterColor) { 215 Log.v(TAG, "centerColor:" + mCenterColor); 216 } 217 Log.v(TAG, "startColor: " + mStartColor); 218 Log.v(TAG, "endColor: " + mEndColor); 219 Log.v(TAG, "tileMode: " + mTileMode); 220 } 221 222 mGradientRadius = a.getFloat(R.styleable.GradientColor_gradientRadius, 223 mGradientRadius); 224 } 225 226 /** 227 * Check if the XML content is valid. 228 * 229 * @throws XmlPullParserException if errors were found. 230 */ validateXmlContent()231 private void validateXmlContent() throws XmlPullParserException { 232 if (mGradientRadius <= 0 233 && mGradientType == GradientDrawable.RADIAL_GRADIENT) { 234 throw new XmlPullParserException( 235 "<gradient> tag requires 'gradientRadius' " 236 + "attribute with radial type"); 237 } 238 } 239 240 /** 241 * The shader information will be applied to the native VectorDrawable's path. 242 * @hide 243 */ getShader()244 public Shader getShader() { 245 return mShader; 246 } 247 248 /** 249 * A public method to create GradientColor from a XML resource. 250 */ createFromXml(Resources r, XmlResourceParser parser, Theme theme)251 public static GradientColor createFromXml(Resources r, XmlResourceParser parser, Theme theme) 252 throws XmlPullParserException, IOException { 253 final AttributeSet attrs = Xml.asAttributeSet(parser); 254 255 int type; 256 while ((type = parser.next()) != XmlPullParser.START_TAG 257 && type != XmlPullParser.END_DOCUMENT) { 258 // Seek parser to start tag. 259 } 260 261 if (type != XmlPullParser.START_TAG) { 262 throw new XmlPullParserException("No start tag found"); 263 } 264 265 return createFromXmlInner(r, parser, attrs, theme); 266 } 267 268 /** 269 * Create from inside an XML document. Called on a parser positioned at a 270 * tag in an XML document, tries to create a GradientColor from that tag. 271 * 272 * @return A new GradientColor for the current tag. 273 * @throws XmlPullParserException if the current tag is not <gradient> 274 */ 275 @NonNull createFromXmlInner(@onNull Resources r, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Theme theme)276 static GradientColor createFromXmlInner(@NonNull Resources r, 277 @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Theme theme) 278 throws XmlPullParserException, IOException { 279 final String name = parser.getName(); 280 if (!name.equals("gradient")) { 281 throw new XmlPullParserException( 282 parser.getPositionDescription() + ": invalid gradient color tag " + name); 283 } 284 285 final GradientColor gradientColor = new GradientColor(); 286 gradientColor.inflate(r, parser, attrs, theme); 287 return gradientColor; 288 } 289 290 /** 291 * Fill in this object based on the contents of an XML "gradient" element. 292 */ inflate(@onNull Resources r, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Theme theme)293 private void inflate(@NonNull Resources r, @NonNull XmlPullParser parser, 294 @NonNull AttributeSet attrs, @Nullable Theme theme) 295 throws XmlPullParserException, IOException { 296 final TypedArray a = Resources.obtainAttributes(r, theme, attrs, R.styleable.GradientColor); 297 updateRootElementState(a); 298 mChangingConfigurations |= a.getChangingConfigurations(); 299 a.recycle(); 300 301 // Check correctness and throw exception if errors found. 302 validateXmlContent(); 303 304 inflateChildElements(r, parser, attrs, theme); 305 306 onColorsChange(); 307 } 308 309 /** 310 * Inflates child elements "item"s for each color stop. 311 * 312 * Note that at root level, we need to save ThemeAttrs for theme applied later. 313 * Here similarly, at each child item, we need to save the theme's attributes, and apply theme 314 * later as applyItemsAttrsTheme(). 315 */ inflateChildElements(@onNull Resources r, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @NonNull Theme theme)316 private void inflateChildElements(@NonNull Resources r, @NonNull XmlPullParser parser, 317 @NonNull AttributeSet attrs, @NonNull Theme theme) 318 throws XmlPullParserException, IOException { 319 final int innerDepth = parser.getDepth() + 1; 320 int type; 321 int depth; 322 323 // Pre-allocate the array with some size, for better performance. 324 float[] offsetList = new float[20]; 325 int[] colorList = new int[offsetList.length]; 326 int[][] themeAttrsList = new int[offsetList.length][]; 327 328 int listSize = 0; 329 boolean hasUnresolvedAttrs = false; 330 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 331 && ((depth = parser.getDepth()) >= innerDepth 332 || type != XmlPullParser.END_TAG)) { 333 if (type != XmlPullParser.START_TAG) { 334 continue; 335 } 336 if (depth > innerDepth || !parser.getName().equals("item")) { 337 continue; 338 } 339 340 final TypedArray a = Resources.obtainAttributes(r, theme, attrs, 341 R.styleable.GradientColorItem); 342 boolean hasColor = a.hasValue(R.styleable.GradientColorItem_color); 343 boolean hasOffset = a.hasValue(R.styleable.GradientColorItem_offset); 344 if (!hasColor || !hasOffset) { 345 throw new XmlPullParserException( 346 parser.getPositionDescription() 347 + ": <item> tag requires a 'color' attribute and a 'offset' " 348 + "attribute!"); 349 } 350 351 final int[] themeAttrs = a.extractThemeAttrs(); 352 int color = a.getColor(R.styleable.GradientColorItem_color, 0); 353 float offset = a.getFloat(R.styleable.GradientColorItem_offset, 0); 354 355 if (DBG_GRADIENT) { 356 Log.v(TAG, "new item color " + color + " " + Integer.toHexString(color)); 357 Log.v(TAG, "offset" + offset); 358 } 359 mChangingConfigurations |= a.getChangingConfigurations(); 360 a.recycle(); 361 362 if (themeAttrs != null) { 363 hasUnresolvedAttrs = true; 364 } 365 366 colorList = GrowingArrayUtils.append(colorList, listSize, color); 367 offsetList = GrowingArrayUtils.append(offsetList, listSize, offset); 368 themeAttrsList = GrowingArrayUtils.append(themeAttrsList, listSize, themeAttrs); 369 listSize++; 370 } 371 if (listSize > 0) { 372 if (hasUnresolvedAttrs) { 373 mItemsThemeAttrs = new int[listSize][]; 374 System.arraycopy(themeAttrsList, 0, mItemsThemeAttrs, 0, listSize); 375 } else { 376 mItemsThemeAttrs = null; 377 } 378 379 mItemColors = new int[listSize]; 380 mItemOffsets = new float[listSize]; 381 System.arraycopy(colorList, 0, mItemColors, 0, listSize); 382 System.arraycopy(offsetList, 0, mItemOffsets, 0, listSize); 383 } 384 } 385 386 /** 387 * Apply theme to all the items. 388 */ applyItemsAttrsTheme(Theme t)389 private void applyItemsAttrsTheme(Theme t) { 390 if (mItemsThemeAttrs == null) { 391 return; 392 } 393 394 boolean hasUnresolvedAttrs = false; 395 396 final int[][] themeAttrsList = mItemsThemeAttrs; 397 final int N = themeAttrsList.length; 398 for (int i = 0; i < N; i++) { 399 if (themeAttrsList[i] != null) { 400 final TypedArray a = t.resolveAttributes(themeAttrsList[i], 401 R.styleable.GradientColorItem); 402 403 // Extract the theme attributes, if any, before attempting to 404 // read from the typed array. This prevents a crash if we have 405 // unresolved attrs. 406 themeAttrsList[i] = a.extractThemeAttrs(themeAttrsList[i]); 407 if (themeAttrsList[i] != null) { 408 hasUnresolvedAttrs = true; 409 } 410 411 mItemColors[i] = a.getColor(R.styleable.GradientColorItem_color, mItemColors[i]); 412 mItemOffsets[i] = a.getFloat(R.styleable.GradientColorItem_offset, mItemOffsets[i]); 413 if (DBG_GRADIENT) { 414 Log.v(TAG, "applyItemsAttrsTheme Colors[i] " + i + " " + 415 Integer.toHexString(mItemColors[i])); 416 Log.v(TAG, "Offsets[i] " + i + " " + mItemOffsets[i]); 417 } 418 419 // Account for any configuration changes. 420 mChangingConfigurations |= a.getChangingConfigurations(); 421 422 a.recycle(); 423 } 424 } 425 426 if (!hasUnresolvedAttrs) { 427 mItemsThemeAttrs = null; 428 } 429 } 430 onColorsChange()431 private void onColorsChange() { 432 int[] tempColors = null; 433 float[] tempOffsets = null; 434 435 if (mItemColors != null) { 436 int length = mItemColors.length; 437 tempColors = new int[length]; 438 tempOffsets = new float[length]; 439 440 for (int i = 0; i < length; i++) { 441 tempColors[i] = mItemColors[i]; 442 tempOffsets[i] = mItemOffsets[i]; 443 } 444 } else { 445 if (mHasCenterColor) { 446 tempColors = new int[3]; 447 tempColors[0] = mStartColor; 448 tempColors[1] = mCenterColor; 449 tempColors[2] = mEndColor; 450 451 tempOffsets = new float[3]; 452 tempOffsets[0] = 0.0f; 453 // Since 0.5f is default value, try to take the one that isn't 0.5f 454 tempOffsets[1] = 0.5f; 455 tempOffsets[2] = 1f; 456 } else { 457 tempColors = new int[2]; 458 tempColors[0] = mStartColor; 459 tempColors[1] = mEndColor; 460 } 461 } 462 if (tempColors.length < 2) { 463 Log.w(TAG, "<gradient> tag requires 2 color values specified!" + tempColors.length 464 + " " + tempColors); 465 } 466 467 if (mGradientType == GradientDrawable.LINEAR_GRADIENT) { 468 mShader = new LinearGradient(mStartX, mStartY, mEndX, mEndY, tempColors, tempOffsets, 469 parseTileMode(mTileMode)); 470 } else { 471 if (mGradientType == GradientDrawable.RADIAL_GRADIENT) { 472 mShader = new RadialGradient(mCenterX, mCenterY, mGradientRadius, tempColors, 473 tempOffsets, parseTileMode(mTileMode)); 474 } else { 475 mShader = new SweepGradient(mCenterX, mCenterY, tempColors, tempOffsets); 476 } 477 } 478 mDefaultColor = tempColors[0]; 479 } 480 481 /** 482 * For Gradient color, the default color is not very useful, since the gradient will override 483 * the color information anyway. 484 */ 485 @Override 486 @ColorInt getDefaultColor()487 public int getDefaultColor() { 488 return mDefaultColor; 489 } 490 491 /** 492 * Similar to ColorStateList, setup constant state and its factory. 493 * @hide only for resource preloading 494 */ 495 @Override getConstantState()496 public ConstantState<ComplexColor> getConstantState() { 497 if (mFactory == null) { 498 mFactory = new GradientColorFactory(this); 499 } 500 return mFactory; 501 } 502 503 private static class GradientColorFactory extends ConstantState<ComplexColor> { 504 private final GradientColor mSrc; 505 GradientColorFactory(GradientColor src)506 public GradientColorFactory(GradientColor src) { 507 mSrc = src; 508 } 509 510 @Override getChangingConfigurations()511 public @Config int getChangingConfigurations() { 512 return mSrc.mChangingConfigurations; 513 } 514 515 @Override newInstance()516 public GradientColor newInstance() { 517 return mSrc; 518 } 519 520 @Override newInstance(Resources res, Theme theme)521 public GradientColor newInstance(Resources res, Theme theme) { 522 return mSrc.obtainForTheme(theme); 523 } 524 } 525 526 /** 527 * Returns an appropriately themed gradient color. 528 * 529 * @param t the theme to apply 530 * @return a copy of the gradient color the theme applied, or the 531 * gradient itself if there were no unresolved theme 532 * attributes 533 * @hide only for resource preloading 534 */ 535 @Override obtainForTheme(Theme t)536 public GradientColor obtainForTheme(Theme t) { 537 if (t == null || !canApplyTheme()) { 538 return this; 539 } 540 541 final GradientColor clone = new GradientColor(this); 542 clone.applyTheme(t); 543 return clone; 544 } 545 546 /** 547 * Returns a mask of the configuration parameters for which this gradient 548 * may change, requiring that it be re-created. 549 * 550 * @return a mask of the changing configuration parameters, as defined by 551 * {@link android.content.pm.ActivityInfo} 552 * 553 * @see android.content.pm.ActivityInfo 554 */ getChangingConfigurations()555 public int getChangingConfigurations() { 556 return super.getChangingConfigurations() | mChangingConfigurations; 557 } 558 applyTheme(Theme t)559 private void applyTheme(Theme t) { 560 if (mThemeAttrs != null) { 561 applyRootAttrsTheme(t); 562 } 563 if (mItemsThemeAttrs != null) { 564 applyItemsAttrsTheme(t); 565 } 566 onColorsChange(); 567 } 568 applyRootAttrsTheme(Theme t)569 private void applyRootAttrsTheme(Theme t) { 570 final TypedArray a = t.resolveAttributes(mThemeAttrs, R.styleable.GradientColor); 571 // mThemeAttrs will be set to null if if there are no theme attributes in the 572 // typed array. 573 mThemeAttrs = a.extractThemeAttrs(mThemeAttrs); 574 // merging the attributes update inside the updateRootElementState(). 575 updateRootElementState(a); 576 577 // Account for any configuration changes. 578 mChangingConfigurations |= a.getChangingConfigurations(); 579 a.recycle(); 580 } 581 582 583 /** 584 * Returns whether a theme can be applied to this gradient color, which 585 * usually indicates that the gradient color has unresolved theme 586 * attributes. 587 * 588 * @return whether a theme can be applied to this gradient color. 589 * @hide only for resource preloading 590 */ 591 @Override canApplyTheme()592 public boolean canApplyTheme() { 593 return mThemeAttrs != null || mItemsThemeAttrs != null; 594 } 595 596 } 597