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.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 
StateListDrawable()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      */
addState(int[] stateSet, Drawable drawable)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
isStateful()101     public boolean isStateful() {
102         return true;
103     }
104 
105     @Override
onStateChange(int[] stateSet)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
inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme)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      */
updateStateFromTypedArray(TypedArray a)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      */
inflateChildElements(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme)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      */
extractStateSet(AttributeSet attrs)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 
getStateListState()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      */
getStateCount()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      */
getStateSet(int index)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      */
getStateDrawable(int index)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      */
getStateDrawableIndex(int[] stateSet)286     public int getStateDrawableIndex(int[] stateSet) {
287         return mStateListState.indexOfStateSet(stateSet);
288     }
289 
290     @Override
mutate()291     public Drawable mutate() {
292         if (!mMutated && super.mutate() == this) {
293             mStateListState.mutate();
294             mMutated = true;
295         }
296         return this;
297     }
298 
299     @Override
cloneConstantState()300     StateListState cloneConstantState() {
301         return new StateListState(mStateListState, this, null);
302     }
303 
304     /**
305      * @hide
306      */
clearMutated()307     public void clearMutated() {
308         super.clearMutated();
309         mMutated = false;
310     }
311 
312     /** @hide */
313     @Override
setLayoutDirection(int layoutDirection)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 
StateListState(StateListState orig, StateListDrawable owner, Resources res)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 
mutate()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 
addStateSet(int[] stateSet, Drawable drawable)348         int addStateSet(int[] stateSet, Drawable drawable) {
349             final int pos = addChild(drawable);
350             mStateSets[pos] = stateSet;
351             return pos;
352         }
353 
indexOfStateSet(int[] stateSet)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
newDrawable()366         public Drawable newDrawable() {
367             return new StateListDrawable(this, null);
368         }
369 
370         @Override
newDrawable(Resources res)371         public Drawable newDrawable(Resources res) {
372             return new StateListDrawable(this, res);
373         }
374 
375         @Override
canApplyTheme()376         public boolean canApplyTheme() {
377             return mThemeAttrs != null || super.canApplyTheme();
378         }
379 
380         @Override
growArray(int oldSize, int newSize)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
applyTheme(Theme theme)390     public void applyTheme(Theme theme) {
391         super.applyTheme(theme);
392 
393         onStateChange(getState());
394     }
395 
setConstantState(@onNull DrawableContainerState state)396     protected void setConstantState(@NonNull DrawableContainerState state) {
397         super.setConstantState(state);
398 
399         if (state instanceof StateListState) {
400             mStateListState = (StateListState) state;
401         }
402     }
403 
StateListDrawable(StateListState state, Resources res)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      */
StateListDrawable(@ullable StateListState state)415     StateListDrawable(@Nullable StateListState state) {
416         if (state != null) {
417             setConstantState(state);
418         }
419     }
420 }
421 
422