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