1 /* 2 * Copyright (C) 2007 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.NonNull; 21 import android.annotation.Nullable; 22 import android.content.res.Resources.Theme; 23 import android.graphics.Color; 24 25 import com.android.internal.R; 26 import com.android.internal.util.ArrayUtils; 27 import com.android.internal.util.GrowingArrayUtils; 28 29 import org.xmlpull.v1.XmlPullParser; 30 import org.xmlpull.v1.XmlPullParserException; 31 32 import android.util.AttributeSet; 33 import android.util.Log; 34 import android.util.MathUtils; 35 import android.util.SparseArray; 36 import android.util.StateSet; 37 import android.util.Xml; 38 import android.os.Parcel; 39 import android.os.Parcelable; 40 41 import java.io.IOException; 42 import java.lang.ref.WeakReference; 43 import java.util.Arrays; 44 45 /** 46 * 47 * Lets you map {@link android.view.View} state sets to colors. 48 * 49 * {@link android.content.res.ColorStateList}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 "selector" element with a number of "item" elements inside. For example: 52 * 53 * <pre> 54 * <selector xmlns:android="http://schemas.android.com/apk/res/android"> 55 * <item android:state_focused="true" android:color="@color/testcolor1"/> 56 * <item android:state_pressed="true" android:state_enabled="false" android:color="@color/testcolor2" /> 57 * <item android:state_enabled="false" android:color="@color/testcolor3" /> 58 * <item android:color="@color/testcolor5"/> 59 * </selector> 60 * </pre> 61 * 62 * This defines a set of state spec / color pairs where each state spec specifies a set of 63 * states that a view must either be in or not be in and the color specifies the color associated 64 * with that spec. The list of state specs will be processed in order of the items in the XML file. 65 * An item with no state spec is considered to match any set of states and is generally useful as 66 * a final item to be used as a default. Note that if you have such an item before any other items 67 * in the list then any subsequent items will end up being ignored. 68 * <p>For more information, see the guide to <a 69 * href="{@docRoot}guide/topics/resources/color-list-resource.html">Color State 70 * List Resource</a>.</p> 71 */ 72 public class ColorStateList implements Parcelable { 73 private static final String TAG = "ColorStateList"; 74 75 private static final int DEFAULT_COLOR = Color.RED; 76 private static final int[][] EMPTY = new int[][] { new int[0] }; 77 78 /** Thread-safe cache of single-color ColorStateLists. */ 79 private static final SparseArray<WeakReference<ColorStateList>> sCache = new SparseArray<>(); 80 81 /** Lazily-created factory for this color state list. */ 82 private ColorStateListFactory mFactory; 83 84 private int[][] mThemeAttrs; 85 private int mChangingConfigurations; 86 87 private int[][] mStateSpecs; 88 private int[] mColors; 89 private int mDefaultColor; 90 private boolean mIsOpaque; 91 ColorStateList()92 private ColorStateList() { 93 // Not publicly instantiable. 94 } 95 96 /** 97 * Creates a ColorStateList that returns the specified mapping from 98 * states to colors. 99 */ ColorStateList(int[][] states, @ColorInt int[] colors)100 public ColorStateList(int[][] states, @ColorInt int[] colors) { 101 mStateSpecs = states; 102 mColors = colors; 103 104 onColorsChanged(); 105 } 106 107 /** 108 * @return A ColorStateList containing a single color. 109 */ 110 @NonNull valueOf(@olorInt int color)111 public static ColorStateList valueOf(@ColorInt int color) { 112 synchronized (sCache) { 113 final int index = sCache.indexOfKey(color); 114 if (index >= 0) { 115 final ColorStateList cached = sCache.valueAt(index).get(); 116 if (cached != null) { 117 return cached; 118 } 119 120 // Prune missing entry. 121 sCache.removeAt(index); 122 } 123 124 // Prune the cache before adding new items. 125 final int N = sCache.size(); 126 for (int i = N - 1; i >= 0; i--) { 127 if (sCache.valueAt(i).get() == null) { 128 sCache.removeAt(i); 129 } 130 } 131 132 final ColorStateList csl = new ColorStateList(EMPTY, new int[] { color }); 133 sCache.put(color, new WeakReference<>(csl)); 134 return csl; 135 } 136 } 137 138 /** 139 * Creates a ColorStateList with the same properties as another 140 * ColorStateList. 141 * <p> 142 * The properties of the new ColorStateList can be modified without 143 * affecting the source ColorStateList. 144 * 145 * @param orig the source color state list 146 */ ColorStateList(ColorStateList orig)147 private ColorStateList(ColorStateList orig) { 148 if (orig != null) { 149 mChangingConfigurations = orig.mChangingConfigurations; 150 mStateSpecs = orig.mStateSpecs; 151 mDefaultColor = orig.mDefaultColor; 152 mIsOpaque = orig.mIsOpaque; 153 154 // Deep copy, these may change due to applyTheme(). 155 mThemeAttrs = orig.mThemeAttrs.clone(); 156 mColors = orig.mColors.clone(); 157 } 158 } 159 160 /** 161 * Creates a ColorStateList from an XML document. 162 * 163 * @param r Resources against which the ColorStateList should be inflated. 164 * @param parser Parser for the XML document defining the ColorStateList. 165 * @return A new color state list. 166 * 167 * @deprecated Use #createFromXml(Resources, XmlPullParser parser, Theme) 168 */ 169 @NonNull 170 @Deprecated createFromXml(Resources r, XmlPullParser parser)171 public static ColorStateList createFromXml(Resources r, XmlPullParser parser) 172 throws XmlPullParserException, IOException { 173 return createFromXml(r, parser, null); 174 } 175 176 /** 177 * Creates a ColorStateList from an XML document using given a set of 178 * {@link Resources} and a {@link Theme}. 179 * 180 * @param r Resources against which the ColorStateList should be inflated. 181 * @param parser Parser for the XML document defining the ColorStateList. 182 * @param theme Optional theme to apply to the color state list, may be 183 * {@code null}. 184 * @return A new color state list. 185 */ 186 @NonNull createFromXml(@onNull Resources r, @NonNull XmlPullParser parser, @Nullable Theme theme)187 public static ColorStateList createFromXml(@NonNull Resources r, @NonNull XmlPullParser parser, 188 @Nullable Theme theme) throws XmlPullParserException, IOException { 189 final AttributeSet attrs = Xml.asAttributeSet(parser); 190 191 int type; 192 while ((type = parser.next()) != XmlPullParser.START_TAG 193 && type != XmlPullParser.END_DOCUMENT) { 194 // Seek parser to start tag. 195 } 196 197 if (type != XmlPullParser.START_TAG) { 198 throw new XmlPullParserException("No start tag found"); 199 } 200 201 return createFromXmlInner(r, parser, attrs, theme); 202 } 203 204 /** 205 * Create from inside an XML document. Called on a parser positioned at a 206 * tag in an XML document, tries to create a ColorStateList from that tag. 207 * 208 * @throws XmlPullParserException if the current tag is not <selector> 209 * @return A new color state list for the current tag. 210 */ 211 @NonNull createFromXmlInner(@onNull Resources r, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Theme theme)212 private static ColorStateList createFromXmlInner(@NonNull Resources r, 213 @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Theme theme) 214 throws XmlPullParserException, IOException { 215 final String name = parser.getName(); 216 if (!name.equals("selector")) { 217 throw new XmlPullParserException( 218 parser.getPositionDescription() + ": invalid color state list tag " + name); 219 } 220 221 final ColorStateList colorStateList = new ColorStateList(); 222 colorStateList.inflate(r, parser, attrs, theme); 223 return colorStateList; 224 } 225 226 /** 227 * Creates a new ColorStateList that has the same states and colors as this 228 * one but where each color has the specified alpha value (0-255). 229 * 230 * @param alpha The new alpha channel value (0-255). 231 * @return A new color state list. 232 */ 233 @NonNull withAlpha(int alpha)234 public ColorStateList withAlpha(int alpha) { 235 final int[] colors = new int[mColors.length]; 236 final int len = colors.length; 237 for (int i = 0; i < len; i++) { 238 colors[i] = (mColors[i] & 0xFFFFFF) | (alpha << 24); 239 } 240 241 return new ColorStateList(mStateSpecs, colors); 242 } 243 244 /** 245 * Fill in this object based on the contents of an XML "selector" element. 246 */ inflate(@onNull Resources r, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Theme theme)247 private void inflate(@NonNull Resources r, @NonNull XmlPullParser parser, 248 @NonNull AttributeSet attrs, @Nullable Theme theme) 249 throws XmlPullParserException, IOException { 250 final int innerDepth = parser.getDepth()+1; 251 int depth; 252 int type; 253 254 int changingConfigurations = 0; 255 int defaultColor = DEFAULT_COLOR; 256 257 boolean hasUnresolvedAttrs = false; 258 259 int[][] stateSpecList = ArrayUtils.newUnpaddedArray(int[].class, 20); 260 int[][] themeAttrsList = new int[stateSpecList.length][]; 261 int[] colorList = new int[stateSpecList.length]; 262 int listSize = 0; 263 264 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 265 && ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) { 266 if (type != XmlPullParser.START_TAG || depth > innerDepth 267 || !parser.getName().equals("item")) { 268 continue; 269 } 270 271 final TypedArray a = Resources.obtainAttributes(r, theme, attrs, 272 R.styleable.ColorStateListItem); 273 final int[] themeAttrs = a.extractThemeAttrs(); 274 final int baseColor = a.getColor(R.styleable.ColorStateListItem_color, Color.MAGENTA); 275 final float alphaMod = a.getFloat(R.styleable.ColorStateListItem_alpha, 1.0f); 276 277 changingConfigurations |= a.getChangingConfigurations(); 278 279 a.recycle(); 280 281 // Parse all unrecognized attributes as state specifiers. 282 int j = 0; 283 final int numAttrs = attrs.getAttributeCount(); 284 int[] stateSpec = new int[numAttrs]; 285 for (int i = 0; i < numAttrs; i++) { 286 final int stateResId = attrs.getAttributeNameResource(i); 287 switch (stateResId) { 288 case R.attr.color: 289 case R.attr.alpha: 290 // Recognized attribute, ignore. 291 break; 292 default: 293 stateSpec[j++] = attrs.getAttributeBooleanValue(i, false) 294 ? stateResId : -stateResId; 295 } 296 } 297 stateSpec = StateSet.trimStateSet(stateSpec, j); 298 299 // Apply alpha modulation. If we couldn't resolve the color or 300 // alpha yet, the default values leave us enough information to 301 // modulate again during applyTheme(). 302 final int color = modulateColorAlpha(baseColor, alphaMod); 303 if (listSize == 0 || stateSpec.length == 0) { 304 defaultColor = color; 305 } 306 307 if (themeAttrs != null) { 308 hasUnresolvedAttrs = true; 309 } 310 311 colorList = GrowingArrayUtils.append(colorList, listSize, color); 312 themeAttrsList = GrowingArrayUtils.append(themeAttrsList, listSize, themeAttrs); 313 stateSpecList = GrowingArrayUtils.append(stateSpecList, listSize, stateSpec); 314 listSize++; 315 } 316 317 mChangingConfigurations = changingConfigurations; 318 mDefaultColor = defaultColor; 319 320 if (hasUnresolvedAttrs) { 321 mThemeAttrs = new int[listSize][]; 322 System.arraycopy(themeAttrsList, 0, mThemeAttrs, 0, listSize); 323 } else { 324 mThemeAttrs = null; 325 } 326 327 mColors = new int[listSize]; 328 mStateSpecs = new int[listSize][]; 329 System.arraycopy(colorList, 0, mColors, 0, listSize); 330 System.arraycopy(stateSpecList, 0, mStateSpecs, 0, listSize); 331 332 onColorsChanged(); 333 } 334 335 /** 336 * Returns whether a theme can be applied to this color state list, which 337 * usually indicates that the color state list has unresolved theme 338 * attributes. 339 * 340 * @return whether a theme can be applied to this color state list 341 * @hide only for resource preloading 342 */ canApplyTheme()343 public boolean canApplyTheme() { 344 return mThemeAttrs != null; 345 } 346 347 /** 348 * Applies a theme to this color state list. 349 * <p> 350 * <strong>Note:</strong> Applying a theme may affect the changing 351 * configuration parameters of this color state list. After calling this 352 * method, any dependent configurations must be updated by obtaining the 353 * new configuration mask from {@link #getChangingConfigurations()}. 354 * 355 * @param t the theme to apply 356 */ applyTheme(Theme t)357 private void applyTheme(Theme t) { 358 if (mThemeAttrs == null) { 359 return; 360 } 361 362 boolean hasUnresolvedAttrs = false; 363 364 final int[][] themeAttrsList = mThemeAttrs; 365 final int N = themeAttrsList.length; 366 for (int i = 0; i < N; i++) { 367 if (themeAttrsList[i] != null) { 368 final TypedArray a = t.resolveAttributes(themeAttrsList[i], 369 R.styleable.ColorStateListItem); 370 371 final float defaultAlphaMod; 372 if (themeAttrsList[i][R.styleable.ColorStateListItem_color] != 0) { 373 // If the base color hasn't been resolved yet, the current 374 // color's alpha channel is either full-opacity (if we 375 // haven't resolved the alpha modulation yet) or 376 // pre-modulated. Either is okay as a default value. 377 defaultAlphaMod = Color.alpha(mColors[i]) / 255.0f; 378 } else { 379 // Otherwise, the only correct default value is 1. Even if 380 // nothing is resolved during this call, we can apply this 381 // multiple times without losing of information. 382 defaultAlphaMod = 1.0f; 383 } 384 385 // Extract the theme attributes, if any, before attempting to 386 // read from the typed array. This prevents a crash if we have 387 // unresolved attrs. 388 themeAttrsList[i] = a.extractThemeAttrs(themeAttrsList[i]); 389 if (themeAttrsList[i] != null) { 390 hasUnresolvedAttrs = true; 391 } 392 393 final int baseColor = a.getColor( 394 R.styleable.ColorStateListItem_color, mColors[i]); 395 final float alphaMod = a.getFloat( 396 R.styleable.ColorStateListItem_alpha, defaultAlphaMod); 397 mColors[i] = modulateColorAlpha(baseColor, alphaMod); 398 399 // Account for any configuration changes. 400 mChangingConfigurations |= a.getChangingConfigurations(); 401 402 a.recycle(); 403 } 404 } 405 406 if (!hasUnresolvedAttrs) { 407 mThemeAttrs = null; 408 } 409 410 onColorsChanged(); 411 } 412 413 /** 414 * Returns an appropriately themed color state list. 415 * 416 * @param t the theme to apply 417 * @return a copy of the color state list with the theme applied, or the 418 * color state list itself if there were no unresolved theme 419 * attributes 420 * @hide only for resource preloading 421 */ obtainForTheme(Theme t)422 public ColorStateList obtainForTheme(Theme t) { 423 if (t == null || !canApplyTheme()) { 424 return this; 425 } 426 427 final ColorStateList clone = new ColorStateList(this); 428 clone.applyTheme(t); 429 return clone; 430 } 431 432 /** 433 * Returns a mask of the configuration parameters for which this color 434 * state list may change, requiring that it be re-created. 435 * 436 * @return a mask of the changing configuration parameters, as defined by 437 * {@link android.content.pm.ActivityInfo} 438 * 439 * @see android.content.pm.ActivityInfo 440 */ getChangingConfigurations()441 public int getChangingConfigurations() { 442 return mChangingConfigurations; 443 } 444 modulateColorAlpha(int baseColor, float alphaMod)445 private int modulateColorAlpha(int baseColor, float alphaMod) { 446 if (alphaMod == 1.0f) { 447 return baseColor; 448 } 449 450 final int baseAlpha = Color.alpha(baseColor); 451 final int alpha = MathUtils.constrain((int) (baseAlpha * alphaMod + 0.5f), 0, 255); 452 return (baseColor & 0xFFFFFF) | (alpha << 24); 453 } 454 455 /** 456 * Indicates whether this color state list contains more than one state spec 457 * and will change color based on state. 458 * 459 * @return True if this color state list changes color based on state, false 460 * otherwise. 461 * @see #getColorForState(int[], int) 462 */ isStateful()463 public boolean isStateful() { 464 return mStateSpecs.length > 1; 465 } 466 467 /** 468 * Indicates whether this color state list is opaque, which means that every 469 * color returned from {@link #getColorForState(int[], int)} has an alpha 470 * value of 255. 471 * 472 * @return True if this color state list is opaque. 473 */ isOpaque()474 public boolean isOpaque() { 475 return mIsOpaque; 476 } 477 478 /** 479 * Return the color associated with the given set of 480 * {@link android.view.View} states. 481 * 482 * @param stateSet an array of {@link android.view.View} states 483 * @param defaultColor the color to return if there's no matching state 484 * spec in this {@link ColorStateList} that matches the 485 * stateSet. 486 * 487 * @return the color associated with that set of states in this {@link ColorStateList}. 488 */ getColorForState(@ullable int[] stateSet, int defaultColor)489 public int getColorForState(@Nullable int[] stateSet, int defaultColor) { 490 final int setLength = mStateSpecs.length; 491 for (int i = 0; i < setLength; i++) { 492 final int[] stateSpec = mStateSpecs[i]; 493 if (StateSet.stateSetMatches(stateSpec, stateSet)) { 494 return mColors[i]; 495 } 496 } 497 return defaultColor; 498 } 499 500 /** 501 * Return the default color in this {@link ColorStateList}. 502 * 503 * @return the default color in this {@link ColorStateList}. 504 */ 505 @ColorInt getDefaultColor()506 public int getDefaultColor() { 507 return mDefaultColor; 508 } 509 510 /** 511 * Return the states in this {@link ColorStateList}. The returned array 512 * should not be modified. 513 * 514 * @return the states in this {@link ColorStateList} 515 * @hide 516 */ getStates()517 public int[][] getStates() { 518 return mStateSpecs; 519 } 520 521 /** 522 * Return the colors in this {@link ColorStateList}. The returned array 523 * should not be modified. 524 * 525 * @return the colors in this {@link ColorStateList} 526 * @hide 527 */ getColors()528 public int[] getColors() { 529 return mColors; 530 } 531 532 /** 533 * Returns whether the specified state is referenced in any of the state 534 * specs contained within this ColorStateList. 535 * <p> 536 * Any reference, either positive or negative {ex. ~R.attr.state_enabled}, 537 * will cause this method to return {@code true}. Wildcards are not counted 538 * as references. 539 * 540 * @param state the state to search for 541 * @return {@code true} if the state if referenced, {@code false} otherwise 542 * @hide Use only as directed. For internal use only. 543 */ hasState(int state)544 public boolean hasState(int state) { 545 final int[][] stateSpecs = mStateSpecs; 546 final int specCount = stateSpecs.length; 547 for (int specIndex = 0; specIndex < specCount; specIndex++) { 548 final int[] states = stateSpecs[specIndex]; 549 final int stateCount = states.length; 550 for (int stateIndex = 0; stateIndex < stateCount; stateIndex++) { 551 if (states[stateIndex] == state || states[stateIndex] == ~state) { 552 return true; 553 } 554 } 555 } 556 return false; 557 } 558 559 @Override toString()560 public String toString() { 561 return "ColorStateList{" + 562 "mThemeAttrs=" + Arrays.deepToString(mThemeAttrs) + 563 "mChangingConfigurations=" + mChangingConfigurations + 564 "mStateSpecs=" + Arrays.deepToString(mStateSpecs) + 565 "mColors=" + Arrays.toString(mColors) + 566 "mDefaultColor=" + mDefaultColor + '}'; 567 } 568 569 /** 570 * Updates the default color and opacity. 571 */ onColorsChanged()572 private void onColorsChanged() { 573 int defaultColor = DEFAULT_COLOR; 574 boolean isOpaque = true; 575 576 final int[][] states = mStateSpecs; 577 final int[] colors = mColors; 578 final int N = states.length; 579 if (N > 0) { 580 defaultColor = colors[0]; 581 582 for (int i = N - 1; i > 0; i--) { 583 if (states[i].length == 0) { 584 defaultColor = colors[i]; 585 break; 586 } 587 } 588 589 for (int i = 0; i < N; i++) { 590 if (Color.alpha(colors[i]) != 0xFF) { 591 isOpaque = false; 592 break; 593 } 594 } 595 } 596 597 mDefaultColor = defaultColor; 598 mIsOpaque = isOpaque; 599 } 600 601 /** 602 * @return a factory that can create new instances of this ColorStateList 603 * @hide only for resource preloading 604 */ getConstantState()605 public ConstantState<ColorStateList> getConstantState() { 606 if (mFactory == null) { 607 mFactory = new ColorStateListFactory(this); 608 } 609 return mFactory; 610 } 611 612 private static class ColorStateListFactory extends ConstantState<ColorStateList> { 613 private final ColorStateList mSrc; 614 ColorStateListFactory(ColorStateList src)615 public ColorStateListFactory(ColorStateList src) { 616 mSrc = src; 617 } 618 619 @Override getChangingConfigurations()620 public int getChangingConfigurations() { 621 return mSrc.mChangingConfigurations; 622 } 623 624 @Override newInstance()625 public ColorStateList newInstance() { 626 return mSrc; 627 } 628 629 @Override newInstance(Resources res, Theme theme)630 public ColorStateList newInstance(Resources res, Theme theme) { 631 return mSrc.obtainForTheme(theme); 632 } 633 } 634 635 @Override describeContents()636 public int describeContents() { 637 return 0; 638 } 639 640 @Override writeToParcel(Parcel dest, int flags)641 public void writeToParcel(Parcel dest, int flags) { 642 if (canApplyTheme()) { 643 Log.w(TAG, "Wrote partially-resolved ColorStateList to parcel!"); 644 } 645 final int N = mStateSpecs.length; 646 dest.writeInt(N); 647 for (int i = 0; i < N; i++) { 648 dest.writeIntArray(mStateSpecs[i]); 649 } 650 dest.writeIntArray(mColors); 651 } 652 653 public static final Parcelable.Creator<ColorStateList> CREATOR = 654 new Parcelable.Creator<ColorStateList>() { 655 @Override 656 public ColorStateList[] newArray(int size) { 657 return new ColorStateList[size]; 658 } 659 660 @Override 661 public ColorStateList createFromParcel(Parcel source) { 662 final int N = source.readInt(); 663 final int[][] stateSpecs = new int[N][]; 664 for (int i = 0; i < N; i++) { 665 stateSpecs[i] = source.createIntArray(); 666 } 667 final int[] colors = source.createIntArray(); 668 return new ColorStateList(stateSpecs, colors); 669 } 670 }; 671 } 672