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