StateListDrawable.java revision d6570d11e4d1e43c2cfe1d10e27a7786c4283169
1/*
2 * Copyright (C) 2006 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.graphics.drawable;
18
19import com.android.internal.R;
20
21import org.xmlpull.v1.XmlPullParser;
22import org.xmlpull.v1.XmlPullParserException;
23
24import java.io.IOException;
25import java.util.Arrays;
26
27import android.content.res.Resources;
28import android.content.res.TypedArray;
29import android.content.res.Resources.Theme;
30import android.util.AttributeSet;
31import android.util.StateSet;
32
33/**
34 * Lets you assign a number of graphic images to a single Drawable and swap out the visible item by a string
35 * ID value.
36 * <p/>
37 * <p>It can be defined in an XML file with the <code>&lt;selector></code> element.
38 * Each state Drawable is defined in a nested <code>&lt;item></code> element. For more
39 * information, see the guide to <a
40 * href="{@docRoot}guide/topics/resources/drawable-resource.html">Drawable Resources</a>.</p>
41 *
42 * @attr ref android.R.styleable#StateListDrawable_visible
43 * @attr ref android.R.styleable#StateListDrawable_variablePadding
44 * @attr ref android.R.styleable#StateListDrawable_constantSize
45 * @attr ref android.R.styleable#DrawableStates_state_focused
46 * @attr ref android.R.styleable#DrawableStates_state_window_focused
47 * @attr ref android.R.styleable#DrawableStates_state_enabled
48 * @attr ref android.R.styleable#DrawableStates_state_checkable
49 * @attr ref android.R.styleable#DrawableStates_state_checked
50 * @attr ref android.R.styleable#DrawableStates_state_selected
51 * @attr ref android.R.styleable#DrawableStates_state_activated
52 * @attr ref android.R.styleable#DrawableStates_state_active
53 * @attr ref android.R.styleable#DrawableStates_state_single
54 * @attr ref android.R.styleable#DrawableStates_state_first
55 * @attr ref android.R.styleable#DrawableStates_state_middle
56 * @attr ref android.R.styleable#DrawableStates_state_last
57 * @attr ref android.R.styleable#DrawableStates_state_pressed
58 */
59public class StateListDrawable extends DrawableContainer {
60    private static final String TAG = StateListDrawable.class.getSimpleName();
61
62    private static final boolean DEBUG = false;
63
64    /**
65     * To be proper, we should have a getter for dither (and alpha, etc.)
66     * so that proxy classes like this can save/restore their delegates'
67     * values, but we don't have getters. Since we do have setters
68     * (e.g. setDither), which this proxy forwards on, we have to have some
69     * default/initial setting.
70     *
71     * The initial setting for dither is now true, since it almost always seems
72     * to improve the quality at negligible cost.
73     */
74    private static final boolean DEFAULT_DITHER = true;
75
76    private StateListState mStateListState;
77    private boolean mMutated;
78
79    public StateListDrawable() {
80        this(null, null);
81    }
82
83    /**
84     * Add a new image/string ID to the set of images.
85     *
86     * @param stateSet - An array of resource Ids to associate with the image.
87     *                 Switch to this image by calling setState().
88     * @param drawable -The image to show.
89     */
90    public void addState(int[] stateSet, Drawable drawable) {
91        if (drawable != null) {
92            mStateListState.addStateSet(stateSet, drawable);
93            // in case the new state matches our current state...
94            onStateChange(getState());
95        }
96    }
97
98    @Override
99    public boolean isStateful() {
100        return true;
101    }
102
103    @Override
104    protected boolean onStateChange(int[] stateSet) {
105        int idx = mStateListState.indexOfStateSet(stateSet);
106        if (DEBUG) android.util.Log.i(TAG, "onStateChange " + this + " states "
107                + Arrays.toString(stateSet) + " found " + idx);
108        if (idx < 0) {
109            idx = mStateListState.indexOfStateSet(StateSet.WILD_CARD);
110        }
111        if (selectDrawable(idx)) {
112            return true;
113        }
114        return super.onStateChange(stateSet);
115    }
116
117    @Override
118    public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme)
119            throws XmlPullParserException, IOException {
120        final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.StateListDrawable);
121        super.inflateWithAttributes(r, parser, a, R.styleable.StateListDrawable_visible);
122        updateStateFromTypedArray(a);
123        a.recycle();
124
125        inflateChildElements(r, parser, attrs, theme);
126
127        onStateChange(getState());
128    }
129
130    /**
131     * Updates the constant state from the values in the typed array.
132     */
133    private void updateStateFromTypedArray(TypedArray a) {
134        final StateListState state = mStateListState;
135
136        // Account for any configuration changes.
137        state.mChangingConfigurations |= a.getChangingConfigurations();
138
139        // Extract the theme attributes, if any.
140        state.mThemeAttrs = a.extractThemeAttrs();
141
142        state.mVariablePadding = a.getBoolean(
143                R.styleable.StateListDrawable_variablePadding, state.mVariablePadding);
144        state.mConstantSize = a.getBoolean(
145                R.styleable.StateListDrawable_constantSize, state.mConstantSize);
146        state.mEnterFadeDuration = a.getInt(
147                R.styleable.StateListDrawable_enterFadeDuration, state.mEnterFadeDuration);
148        state.mExitFadeDuration = a.getInt(
149                R.styleable.StateListDrawable_exitFadeDuration, state.mExitFadeDuration);
150        state.mDither = a.getBoolean(
151                R.styleable.StateListDrawable_dither, state.mDither);
152        state.mAutoMirrored = a.getBoolean(
153                R.styleable.StateListDrawable_autoMirrored, state.mAutoMirrored);
154    }
155
156    /**
157     * Inflates child elements from XML.
158     */
159    private void inflateChildElements(Resources r, XmlPullParser parser, AttributeSet attrs,
160            Theme theme) throws XmlPullParserException, IOException {
161        final StateListState state = mStateListState;
162        final int innerDepth = parser.getDepth() + 1;
163        int type;
164        int depth;
165        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
166                && ((depth = parser.getDepth()) >= innerDepth
167                || type != XmlPullParser.END_TAG)) {
168            if (type != XmlPullParser.START_TAG) {
169                continue;
170            }
171
172            if (depth > innerDepth || !parser.getName().equals("item")) {
173                continue;
174            }
175
176            // This allows state list drawable item elements to be themed at
177            // inflation time but does NOT make them work for Zygote preload.
178            final TypedArray a = obtainAttributes(r, theme, attrs,
179                    R.styleable.StateListDrawableItem);
180            Drawable dr = a.getDrawable(R.styleable.StateListDrawableItem_drawable);
181            a.recycle();
182
183            final int[] states = extractStateSet(attrs);
184
185            // Loading child elements modifies the state of the AttributeSet's
186            // underlying parser, so it needs to happen after obtaining
187            // attributes and extracting states.
188            if (dr == null) {
189                while ((type = parser.next()) == XmlPullParser.TEXT) {
190                }
191                if (type != XmlPullParser.START_TAG) {
192                    throw new XmlPullParserException(
193                            parser.getPositionDescription()
194                                    + ": <item> tag requires a 'drawable' attribute or "
195                                    + "child tag defining a drawable");
196                }
197                dr = Drawable.createFromXmlInner(r, parser, attrs, theme);
198            }
199
200            state.addStateSet(states, dr);
201        }
202    }
203
204    /**
205     * Extracts state_ attributes from an attribute set.
206     *
207     * @param attrs The attribute set.
208     * @return An array of state_ attributes.
209     */
210    int[] extractStateSet(AttributeSet attrs) {
211        int j = 0;
212        final int numAttrs = attrs.getAttributeCount();
213        int[] states = new int[numAttrs];
214        for (int i = 0; i < numAttrs; i++) {
215            final int stateResId = attrs.getAttributeNameResource(i);
216            switch (stateResId) {
217                case 0:
218                    break;
219                case R.attr.drawable:
220                case R.attr.id:
221                    // Ignore attributes from StateListDrawableItem and
222                    // AnimatedStateListDrawableItem.
223                    continue;
224                default:
225                    states[j++] = attrs.getAttributeBooleanValue(i, false)
226                            ? stateResId : -stateResId;
227            }
228        }
229        states = StateSet.trimStateSet(states, j);
230        return states;
231    }
232
233    StateListState getStateListState() {
234        return mStateListState;
235    }
236
237    /**
238     * Gets the number of states contained in this drawable.
239     *
240     * @return The number of states contained in this drawable.
241     * @hide pending API council
242     * @see #getStateSet(int)
243     * @see #getStateDrawable(int)
244     */
245    public int getStateCount() {
246        return mStateListState.getChildCount();
247    }
248
249    /**
250     * Gets the state set at an index.
251     *
252     * @param index The index of the state set.
253     * @return The state set at the index.
254     * @hide pending API council
255     * @see #getStateCount()
256     * @see #getStateDrawable(int)
257     */
258    public int[] getStateSet(int index) {
259        return mStateListState.mStateSets[index];
260    }
261
262    /**
263     * Gets the drawable at an index.
264     *
265     * @param index The index of the drawable.
266     * @return The drawable at the index.
267     * @hide pending API council
268     * @see #getStateCount()
269     * @see #getStateSet(int)
270     */
271    public Drawable getStateDrawable(int index) {
272        return mStateListState.getChild(index);
273    }
274
275    /**
276     * Gets the index of the drawable with the provided state set.
277     *
278     * @param stateSet the state set to look up
279     * @return the index of the provided state set, or -1 if not found
280     * @hide pending API council
281     * @see #getStateDrawable(int)
282     * @see #getStateSet(int)
283     */
284    public int getStateDrawableIndex(int[] stateSet) {
285        return mStateListState.indexOfStateSet(stateSet);
286    }
287
288    @Override
289    public Drawable mutate() {
290        if (!mMutated && super.mutate() == this) {
291            final int[][] sets = mStateListState.mStateSets;
292            final int count = sets.length;
293            mStateListState.mStateSets = new int[count][];
294            for (int i = 0; i < count; i++) {
295                final int[] set = sets[i];
296                if (set != null) {
297                    mStateListState.mStateSets[i] = set.clone();
298                }
299            }
300            mMutated = true;
301        }
302        return this;
303    }
304
305    /**
306     * @hide
307     */
308    public void clearMutated() {
309        super.clearMutated();
310        mMutated = false;
311    }
312
313    /** @hide */
314    @Override
315    public void setLayoutDirection(int layoutDirection) {
316        super.setLayoutDirection(layoutDirection);
317
318        // Let the container handle setting its own layout direction. Otherwise,
319        // we're accessing potentially unused states.
320        mStateListState.setLayoutDirection(layoutDirection);
321    }
322
323    static class StateListState extends DrawableContainerState {
324        int[] mThemeAttrs;
325        int[][] mStateSets;
326
327        StateListState(StateListState orig, StateListDrawable owner, Resources res) {
328            super(orig, owner, res);
329
330            if (orig != null) {
331                // Perform a deep copy.
332                final int[][] sets = orig.mStateSets;
333                final int count = sets.length;
334                mStateSets = new int[count][];
335                for (int i = 0; i < count; i++) {
336                    final int[] set = sets[i];
337                    if (set != null) {
338                        mStateSets[i] = set.clone();
339                    }
340                }
341
342                mThemeAttrs = orig.mThemeAttrs;
343                mStateSets = Arrays.copyOf(orig.mStateSets, orig.mStateSets.length);
344            } else {
345                mThemeAttrs = null;
346                mStateSets = new int[getCapacity()][];
347            }
348        }
349
350        int addStateSet(int[] stateSet, Drawable drawable) {
351            final int pos = addChild(drawable);
352            mStateSets[pos] = stateSet;
353            return pos;
354        }
355
356        int indexOfStateSet(int[] stateSet) {
357            final int[][] stateSets = mStateSets;
358            final int N = getChildCount();
359            for (int i = 0; i < N; i++) {
360                if (StateSet.stateSetMatches(stateSets[i], stateSet)) {
361                    return i;
362                }
363            }
364            return -1;
365        }
366
367        @Override
368        public Drawable newDrawable() {
369            return new StateListDrawable(this, null);
370        }
371
372        @Override
373        public Drawable newDrawable(Resources res) {
374            return new StateListDrawable(this, res);
375        }
376
377        @Override
378        public boolean canApplyTheme() {
379            return mThemeAttrs != null || super.canApplyTheme();
380        }
381
382        @Override
383        public void growArray(int oldSize, int newSize) {
384            super.growArray(oldSize, newSize);
385            final int[][] newStateSets = new int[newSize][];
386            System.arraycopy(mStateSets, 0, newStateSets, 0, oldSize);
387            mStateSets = newStateSets;
388        }
389    }
390
391    @Override
392    public void applyTheme(Theme theme) {
393        super.applyTheme(theme);
394
395        onStateChange(getState());
396    }
397
398    void setConstantState(StateListState state) {
399        super.setConstantState(state);
400
401        mStateListState = state;
402    }
403
404    private StateListDrawable(StateListState state, Resources res) {
405        final StateListState newState = new StateListState(state, this, res);
406        setConstantState(newState);
407        onStateChange(getState());
408    }
409
410    /**
411     * This constructor exists so subclasses can avoid calling the default
412     * constructor and setting up a StateListDrawable-specific constant state.
413     */
414    StateListDrawable(StateListState state) {
415        if (state != null) {
416            setConstantState(state);
417        }
418    }
419}
420
421