Home | History | Annotate | Download | only in res
      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 
     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