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.graphics.Color;
20
21import com.android.internal.util.ArrayUtils;
22import com.android.internal.util.GrowingArrayUtils;
23
24import org.xmlpull.v1.XmlPullParser;
25import org.xmlpull.v1.XmlPullParserException;
26
27import android.util.AttributeSet;
28import android.util.MathUtils;
29import android.util.SparseArray;
30import android.util.StateSet;
31import android.util.Xml;
32import android.os.Parcel;
33import android.os.Parcelable;
34
35import java.io.IOException;
36import java.lang.ref.WeakReference;
37import 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 */
66public 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