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