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