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