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.graphics.Color;
     20 
     21 import com.android.internal.util.ArrayUtils;
     22 import com.android.internal.util.GrowingArrayUtils;
     23 
     24 import org.xmlpull.v1.XmlPullParser;
     25 import org.xmlpull.v1.XmlPullParserException;
     26 
     27 import android.util.AttributeSet;
     28 import android.util.MathUtils;
     29 import android.util.SparseArray;
     30 import android.util.StateSet;
     31 import android.util.Xml;
     32 import android.os.Parcel;
     33 import android.os.Parcelable;
     34 
     35 import java.io.IOException;
     36 import java.lang.ref.WeakReference;
     37 import java.util.Arrays;
     38 
     39 /**
     40  *
     41  * Lets you map {@link android.view.View} state sets to colors.
     42  *
     43  * {@link android.content.res.ColorStateList}s are created from XML resource files defined in the
     44  * "color" subdirectory directory of an application's resource directory.  The XML file contains
     45  * a single "selector" element with a number of "item" elements inside.  For example:
     46  *
     47  * <pre>
     48  * &lt;selector xmlns:android="http://schemas.android.com/apk/res/android"&gt;
     49  *   &lt;item android:state_focused="true" android:color="@color/testcolor1"/&gt;
     50  *   &lt;item android:state_pressed="true" android:state_enabled="false" android:color="@color/testcolor2" /&gt;
     51  *   &lt;item android:state_enabled="false" android:color="@color/testcolor3" /&gt;
     52  *   &lt;item android:color="@color/testcolor5"/&gt;
     53  * &lt;/selector&gt;
     54  * </pre>
     55  *
     56  * This defines a set of state spec / color pairs where each state spec specifies a set of
     57  * states that a view must either be in or not be in and the color specifies the color associated
     58  * with that spec.  The list of state specs will be processed in order of the items in the XML file.
     59  * An item with no state spec is considered to match any set of states and is generally useful as
     60  * a final item to be used as a default.  Note that if you have such an item before any other items
     61  * in the list then any subsequent items will end up being ignored.
     62  * <p>For more information, see the guide to <a
     63  * href="{@docRoot}guide/topics/resources/color-list-resource.html">Color State
     64  * List Resource</a>.</p>
     65  */
     66 public class ColorStateList implements Parcelable {
     67     private int[][] mStateSpecs; // must be parallel to mColors
     68     private int[] mColors;      // must be parallel to mStateSpecs
     69     private int mDefaultColor = 0xffff0000;
     70 
     71     private static final int[][] EMPTY = new int[][] { new int[0] };
     72     private static final SparseArray<WeakReference<ColorStateList>> sCache =
     73                             new SparseArray<WeakReference<ColorStateList>>();
     74 
     75     private ColorStateList() { }
     76 
     77     /**
     78      * Creates a ColorStateList that returns the specified mapping from
     79      * states to colors.
     80      */
     81     public ColorStateList(int[][] states, int[] colors) {
     82         mStateSpecs = states;
     83         mColors = colors;
     84 
     85         if (states.length > 0) {
     86             mDefaultColor = colors[0];
     87 
     88             for (int i = 0; i < states.length; i++) {
     89                 if (states[i].length == 0) {
     90                     mDefaultColor = colors[i];
     91                 }
     92             }
     93         }
     94     }
     95 
     96     /**
     97      * Creates or retrieves a ColorStateList that always returns a single color.
     98      */
     99     public static ColorStateList valueOf(int color) {
    100         // TODO: should we collect these eventually?
    101         synchronized (sCache) {
    102             final WeakReference<ColorStateList> ref = sCache.get(color);
    103 
    104             ColorStateList csl = ref != null ? ref.get() : null;
    105             if (csl != null) {
    106                 return csl;
    107             }
    108 
    109             csl = new ColorStateList(EMPTY, new int[] { color });
    110             sCache.put(color, new WeakReference<ColorStateList>(csl));
    111             return csl;
    112         }
    113     }
    114 
    115     /**
    116      * Create a ColorStateList from an XML document, given a set of {@link Resources}.
    117      */
    118     public static ColorStateList createFromXml(Resources r, XmlPullParser parser)
    119             throws XmlPullParserException, IOException {
    120         final AttributeSet attrs = Xml.asAttributeSet(parser);
    121 
    122         int type;
    123         while ((type=parser.next()) != XmlPullParser.START_TAG
    124                    && type != XmlPullParser.END_DOCUMENT) {
    125         }
    126 
    127         if (type != XmlPullParser.START_TAG) {
    128             throw new XmlPullParserException("No start tag found");
    129         }
    130 
    131         return createFromXmlInner(r, parser, attrs);
    132     }
    133 
    134     /**
    135      * Create from inside an XML document. Called on a parser positioned at a
    136      * tag in an XML document, tries to create a ColorStateList from that tag.
    137      *
    138      * @throws XmlPullParserException if the current tag is not &lt;selector>
    139      * @return A color state list for the current tag.
    140      */
    141     private static ColorStateList createFromXmlInner(Resources r, XmlPullParser parser,
    142             AttributeSet attrs) throws XmlPullParserException, IOException {
    143         final ColorStateList colorStateList;
    144         final String name = parser.getName();
    145         if (name.equals("selector")) {
    146             colorStateList = new ColorStateList();
    147         } else {
    148             throw new XmlPullParserException(
    149                     parser.getPositionDescription() + ": invalid drawable tag " + name);
    150         }
    151 
    152         colorStateList.inflate(r, parser, attrs);
    153         return colorStateList;
    154     }
    155 
    156     /**
    157      * Creates a new ColorStateList that has the same states and
    158      * colors as this one but where each color has the specified alpha value
    159      * (0-255).
    160      */
    161     public ColorStateList withAlpha(int alpha) {
    162         final int[] colors = new int[mColors.length];
    163         final int len = colors.length;
    164         for (int i = 0; i < len; i++) {
    165             colors[i] = (mColors[i] & 0xFFFFFF) | (alpha << 24);
    166         }
    167 
    168         return new ColorStateList(mStateSpecs, colors);
    169     }
    170 
    171     /**
    172      * Fill in this object based on the contents of an XML "selector" element.
    173      */
    174     private void inflate(Resources r, XmlPullParser parser, AttributeSet attrs)
    175             throws XmlPullParserException, IOException {
    176         int type;
    177 
    178         final int innerDepth = parser.getDepth()+1;
    179         int depth;
    180 
    181         int[][] stateSpecList = ArrayUtils.newUnpaddedArray(int[].class, 20);
    182         int[] colorList = new int[stateSpecList.length];
    183         int listSize = 0;
    184 
    185         while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
    186                && ((depth=parser.getDepth()) >= innerDepth
    187                    || type != XmlPullParser.END_TAG)) {
    188             if (type != XmlPullParser.START_TAG) {
    189                 continue;
    190             }
    191 
    192             if (depth > innerDepth || !parser.getName().equals("item")) {
    193                 continue;
    194             }
    195 
    196             int alphaRes = 0;
    197             float alpha = 1.0f;
    198             int colorRes = 0;
    199             int color = 0xffff0000;
    200             boolean haveColor = false;
    201 
    202             int i;
    203             int j = 0;
    204             final int numAttrs = attrs.getAttributeCount();
    205             int[] stateSpec = new int[numAttrs];
    206             for (i = 0; i < numAttrs; i++) {
    207                 final int stateResId = attrs.getAttributeNameResource(i);
    208                 if (stateResId == 0) break;
    209                 if (stateResId == com.android.internal.R.attr.alpha) {
    210                     alphaRes = attrs.getAttributeResourceValue(i, 0);
    211                     if (alphaRes == 0) {
    212                         alpha = attrs.getAttributeFloatValue(i, 1.0f);
    213                     }
    214                 } else if (stateResId == com.android.internal.R.attr.color) {
    215                     colorRes = attrs.getAttributeResourceValue(i, 0);
    216                     if (colorRes == 0) {
    217                         color = attrs.getAttributeIntValue(i, color);
    218                         haveColor = true;
    219                     }
    220                 } else {
    221                     stateSpec[j++] = attrs.getAttributeBooleanValue(i, false)
    222                             ? stateResId : -stateResId;
    223                 }
    224             }
    225             stateSpec = StateSet.trimStateSet(stateSpec, j);
    226 
    227             if (colorRes != 0) {
    228                 color = r.getColor(colorRes);
    229             } else if (!haveColor) {
    230                 throw new XmlPullParserException(
    231                         parser.getPositionDescription()
    232                         + ": <item> tag requires a 'android:color' attribute.");
    233             }
    234 
    235             if (alphaRes != 0) {
    236                 alpha = r.getFloat(alphaRes);
    237             }
    238 
    239             // Apply alpha modulation.
    240             final int alphaMod = MathUtils.constrain((int) (Color.alpha(color) * alpha), 0, 255);
    241             color = (color & 0xFFFFFF) | (alphaMod << 24);
    242 
    243             if (listSize == 0 || stateSpec.length == 0) {
    244                 mDefaultColor = color;
    245             }
    246 
    247             colorList = GrowingArrayUtils.append(colorList, listSize, color);
    248             stateSpecList = GrowingArrayUtils.append(stateSpecList, listSize, stateSpec);
    249             listSize++;
    250         }
    251 
    252         mColors = new int[listSize];
    253         mStateSpecs = new int[listSize][];
    254         System.arraycopy(colorList, 0, mColors, 0, listSize);
    255         System.arraycopy(stateSpecList, 0, mStateSpecs, 0, listSize);
    256     }
    257 
    258     /**
    259      * Indicates whether this color state list contains more than one state spec
    260      * and will change color based on state.
    261      *
    262      * @return True if this color state list changes color based on state, false
    263      *         otherwise.
    264      * @see #getColorForState(int[], int)
    265      */
    266     public boolean isStateful() {
    267         return mStateSpecs.length > 1;
    268     }
    269 
    270     /**
    271      * Indicates whether this color state list is opaque, which means that every
    272      * color returned from {@link #getColorForState(int[], int)} has an alpha
    273      * value of 255.
    274      *
    275      * @return True if this color state list is opaque.
    276      */
    277     public boolean isOpaque() {
    278         final int n = mColors.length;
    279         for (int i = 0; i < n; i++) {
    280             if (Color.alpha(mColors[i]) != 0xFF) {
    281                 return false;
    282             }
    283         }
    284         return true;
    285     }
    286 
    287     /**
    288      * Return the color associated with the given set of {@link android.view.View} states.
    289      *
    290      * @param stateSet an array of {@link android.view.View} states
    291      * @param defaultColor the color to return if there's not state spec in this
    292      * {@link ColorStateList} that matches the stateSet.
    293      *
    294      * @return the color associated with that set of states in this {@link ColorStateList}.
    295      */
    296     public int getColorForState(int[] stateSet, int defaultColor) {
    297         final int setLength = mStateSpecs.length;
    298         for (int i = 0; i < setLength; i++) {
    299             int[] stateSpec = mStateSpecs[i];
    300             if (StateSet.stateSetMatches(stateSpec, stateSet)) {
    301                 return mColors[i];
    302             }
    303         }
    304         return defaultColor;
    305     }
    306 
    307     /**
    308      * Return the default color in this {@link ColorStateList}.
    309      *
    310      * @return the default color in this {@link ColorStateList}.
    311      */
    312     public int getDefaultColor() {
    313         return mDefaultColor;
    314     }
    315 
    316     /**
    317      * Return the states in this {@link ColorStateList}.
    318      * @return the states in this {@link ColorStateList}
    319      * @hide
    320      */
    321     public int[][] getStates() {
    322         return mStateSpecs;
    323     }
    324 
    325     /**
    326      * Return the colors in this {@link ColorStateList}.
    327      * @return the colors in this {@link ColorStateList}
    328      * @hide
    329      */
    330     public int[] getColors() {
    331         return mColors;
    332     }
    333 
    334     /**
    335      * If the color state list does not already have an entry matching the
    336      * specified state, prepends a state set and color pair to a color state
    337      * list.
    338      * <p>
    339      * This is a workaround used in TimePicker and DatePicker until we can
    340      * add support for theme attributes in ColorStateList.
    341      *
    342      * @param colorStateList the source color state list
    343      * @param state the state to prepend
    344      * @param color the color to use for the given state
    345      * @return a new color state list, or the source color state list if there
    346      *         was already a matching state set
    347      *
    348      * @hide Remove when we can support theme attributes.
    349      */
    350     public static ColorStateList addFirstIfMissing(
    351             ColorStateList colorStateList, int state, int color) {
    352         final int[][] inputStates = colorStateList.getStates();
    353         for (int i = 0; i < inputStates.length; i++) {
    354             final int[] inputState = inputStates[i];
    355             for (int j = 0; j < inputState.length; j++) {
    356                 if (inputState[j] == state) {
    357                     return colorStateList;
    358                 }
    359             }
    360         }
    361 
    362         final int[][] outputStates = new int[inputStates.length + 1][];
    363         System.arraycopy(inputStates, 0, outputStates, 1, inputStates.length);
    364         outputStates[0] = new int[] { state };
    365 
    366         final int[] inputColors = colorStateList.getColors();
    367         final int[] outputColors = new int[inputColors.length + 1];
    368         System.arraycopy(inputColors, 0, outputColors, 1, inputColors.length);
    369         outputColors[0] = color;
    370 
    371         return new ColorStateList(outputStates, outputColors);
    372     }
    373 
    374     @Override
    375     public String toString() {
    376         return "ColorStateList{" +
    377                "mStateSpecs=" + Arrays.deepToString(mStateSpecs) +
    378                "mColors=" + Arrays.toString(mColors) +
    379                "mDefaultColor=" + mDefaultColor + '}';
    380     }
    381 
    382     @Override
    383     public int describeContents() {
    384         return 0;
    385     }
    386 
    387     @Override
    388     public void writeToParcel(Parcel dest, int flags) {
    389         final int N = mStateSpecs.length;
    390         dest.writeInt(N);
    391         for (int i = 0; i < N; i++) {
    392             dest.writeIntArray(mStateSpecs[i]);
    393         }
    394         dest.writeIntArray(mColors);
    395     }
    396 
    397     public static final Parcelable.Creator<ColorStateList> CREATOR =
    398             new Parcelable.Creator<ColorStateList>() {
    399         @Override
    400         public ColorStateList[] newArray(int size) {
    401             return new ColorStateList[size];
    402         }
    403 
    404         @Override
    405         public ColorStateList createFromParcel(Parcel source) {
    406             final int N = source.readInt();
    407             final int[][] stateSpecs = new int[N][];
    408             for (int i = 0; i < N; i++) {
    409                 stateSpecs[i] = source.createIntArray();
    410             }
    411             final int[] colors = source.createIntArray();
    412             return new ColorStateList(stateSpecs, colors);
    413         }
    414     };
    415 }
    416