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
17package android.content.res;
18
19import android.annotation.ColorInt;
20import android.annotation.NonNull;
21import android.annotation.Nullable;
22import android.content.res.Resources.Theme;
23import android.graphics.Color;
24
25import com.android.internal.R;
26import com.android.internal.util.ArrayUtils;
27import com.android.internal.util.GrowingArrayUtils;
28
29import org.xmlpull.v1.XmlPullParser;
30import org.xmlpull.v1.XmlPullParserException;
31
32import android.util.AttributeSet;
33import android.util.Log;
34import android.util.MathUtils;
35import android.util.SparseArray;
36import android.util.StateSet;
37import android.util.Xml;
38import android.os.Parcel;
39import android.os.Parcelable;
40
41import java.io.IOException;
42import java.lang.ref.WeakReference;
43import 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 */
72public 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
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     */
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
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     */
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
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
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
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
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     */
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     */
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     */
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     */
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     */
441    public int getChangingConfigurations() {
442        return mChangingConfigurations;
443    }
444
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     */
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     */
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     */
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
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     */
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     */
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     */
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
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     */
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     */
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
615        public ColorStateListFactory(ColorStateList src) {
616            mSrc = src;
617        }
618
619        @Override
620        public int getChangingConfigurations() {
621            return mSrc.mChangingConfigurations;
622        }
623
624        @Override
625        public ColorStateList newInstance() {
626            return mSrc;
627        }
628
629        @Override
630        public ColorStateList newInstance(Resources res, Theme theme) {
631            return mSrc.obtainForTheme(theme);
632        }
633    }
634
635    @Override
636    public int describeContents() {
637        return 0;
638    }
639
640    @Override
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