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  * &lt;selector xmlns:android="http://schemas.android.com/apk/res/android"&gt;
55  *   &lt;item android:state_focused="true" android:color="@color/testcolor1"/&gt;
56  *   &lt;item android:state_pressed="true" android:state_enabled="false" android:color="@color/testcolor2" /&gt;
57  *   &lt;item android:state_enabled="false" android:color="@color/testcolor3" /&gt;
58  *   &lt;item android:color="@color/testcolor5"/&gt;
59  * &lt;/selector&gt;
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 &lt;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