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 android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.compat.annotation.UnsupportedAppUsage; 22 import android.content.res.Resources; 23 import android.content.res.Resources.Theme; 24 import android.content.res.TypedArray; 25 import android.os.Build; 26 import android.util.AttributeSet; 27 import android.util.StateSet; 28 29 import com.android.internal.R; 30 31 import org.xmlpull.v1.XmlPullParser; 32 import org.xmlpull.v1.XmlPullParserException; 33 34 import java.io.IOException; 35 import java.util.Arrays; 36 37 /** 38 * Lets you assign a number of graphic images to a single Drawable and swap out the visible item by a string 39 * ID value. 40 * <p/> 41 * <p>It can be defined in an XML file with the <code><selector></code> element. 42 * Each state Drawable is defined in a nested <code><item></code> element. For more 43 * information, see the guide to <a 44 * href="{@docRoot}guide/topics/resources/drawable-resource.html">Drawable Resources</a>.</p> 45 * 46 * @attr ref android.R.styleable#StateListDrawable_visible 47 * @attr ref android.R.styleable#StateListDrawable_variablePadding 48 * @attr ref android.R.styleable#StateListDrawable_constantSize 49 * @attr ref android.R.styleable#DrawableStates_state_focused 50 * @attr ref android.R.styleable#DrawableStates_state_window_focused 51 * @attr ref android.R.styleable#DrawableStates_state_enabled 52 * @attr ref android.R.styleable#DrawableStates_state_checkable 53 * @attr ref android.R.styleable#DrawableStates_state_checked 54 * @attr ref android.R.styleable#DrawableStates_state_selected 55 * @attr ref android.R.styleable#DrawableStates_state_activated 56 * @attr ref android.R.styleable#DrawableStates_state_active 57 * @attr ref android.R.styleable#DrawableStates_state_single 58 * @attr ref android.R.styleable#DrawableStates_state_first 59 * @attr ref android.R.styleable#DrawableStates_state_middle 60 * @attr ref android.R.styleable#DrawableStates_state_last 61 * @attr ref android.R.styleable#DrawableStates_state_pressed 62 */ 63 public class StateListDrawable extends DrawableContainer { 64 private static final String TAG = "StateListDrawable"; 65 66 private static final boolean DEBUG = false; 67 68 @UnsupportedAppUsage 69 private StateListState mStateListState; 70 private boolean mMutated; 71 StateListDrawable()72 public StateListDrawable() { 73 this(null, null); 74 } 75 76 /** 77 * Add a new image/string ID to the set of images. 78 * 79 * @param stateSet An array of resource Ids to associate with the image. 80 * Switch to this image by calling setState(). 81 * @param drawable The image to show. Note this must be a unique Drawable that is not shared 82 * between any other View or Drawable otherwise the results are 83 * undefined and can lead to unexpected rendering behavior 84 */ addState(int[] stateSet, Drawable drawable)85 public void addState(int[] stateSet, Drawable drawable) { 86 if (drawable != null) { 87 mStateListState.addStateSet(stateSet, drawable); 88 // in case the new state matches our current state... 89 onStateChange(getState()); 90 } 91 } 92 93 @Override isStateful()94 public boolean isStateful() { 95 return true; 96 } 97 98 @Override hasFocusStateSpecified()99 public boolean hasFocusStateSpecified() { 100 return mStateListState.hasFocusStateSpecified(); 101 } 102 103 @Override onStateChange(int[] stateSet)104 protected boolean onStateChange(int[] stateSet) { 105 final boolean changed = super.onStateChange(stateSet); 106 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 114 return selectDrawable(idx) || changed; 115 } 116 117 @Override inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme)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 updateDensity(r); 124 a.recycle(); 125 126 inflateChildElements(r, parser, attrs, theme); 127 128 onStateChange(getState()); 129 } 130 131 /** 132 * Updates the constant state from the values in the typed array. 133 */ 134 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 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 */ 212 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) extractStateSet(AttributeSet attrs)213 int[] extractStateSet(AttributeSet attrs) { 214 int j = 0; 215 final int numAttrs = attrs.getAttributeCount(); 216 int[] states = new int[numAttrs]; 217 for (int i = 0; i < numAttrs; i++) { 218 final int stateResId = attrs.getAttributeNameResource(i); 219 switch (stateResId) { 220 case 0: 221 break; 222 case R.attr.drawable: 223 case R.attr.id: 224 // Ignore attributes from StateListDrawableItem and 225 // AnimatedStateListDrawableItem. 226 continue; 227 default: 228 states[j++] = attrs.getAttributeBooleanValue(i, false) 229 ? stateResId : -stateResId; 230 } 231 } 232 states = StateSet.trimStateSet(states, j); 233 return states; 234 } 235 getStateListState()236 StateListState getStateListState() { 237 return mStateListState; 238 } 239 240 /** 241 * Gets the number of states contained in this drawable. 242 * 243 * @return The number of states contained in this drawable. 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 * @see #getStateCount() 257 * @see #getStateDrawable(int) 258 */ getStateSet(int index)259 public @NonNull int[] getStateSet(int index) { 260 return mStateListState.mStateSets[index]; 261 } 262 263 /** 264 * Gets the drawable at an index. 265 * 266 * @param index The index of the drawable. 267 * @return The drawable at the index. 268 * @see #getStateCount() 269 * @see #getStateSet(int) 270 */ getStateDrawable(int index)271 public @Nullable 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 * @see #getStateDrawable(int) 281 * @see #getStateSet(int) 282 */ findStateDrawableIndex(@onNull int[] stateSet)283 public int findStateDrawableIndex(@NonNull int[] stateSet) { 284 return mStateListState.indexOfStateSet(stateSet); 285 } 286 287 @Override mutate()288 public Drawable mutate() { 289 if (!mMutated && super.mutate() == this) { 290 mStateListState.mutate(); 291 mMutated = true; 292 } 293 return this; 294 } 295 296 @Override cloneConstantState()297 StateListState cloneConstantState() { 298 return new StateListState(mStateListState, this, null); 299 } 300 301 /** 302 * @hide 303 */ clearMutated()304 public void clearMutated() { 305 super.clearMutated(); 306 mMutated = false; 307 } 308 309 static class StateListState extends DrawableContainerState { 310 int[] mThemeAttrs; 311 int[][] mStateSets; 312 StateListState(StateListState orig, StateListDrawable owner, Resources res)313 StateListState(StateListState orig, StateListDrawable owner, Resources res) { 314 super(orig, owner, res); 315 316 if (orig != null) { 317 // Perform a shallow copy and rely on mutate() to deep-copy. 318 mThemeAttrs = orig.mThemeAttrs; 319 mStateSets = orig.mStateSets; 320 } else { 321 mThemeAttrs = null; 322 mStateSets = new int[getCapacity()][]; 323 } 324 } 325 mutate()326 void mutate() { 327 mThemeAttrs = mThemeAttrs != null ? mThemeAttrs.clone() : null; 328 329 final int[][] stateSets = new int[mStateSets.length][]; 330 for (int i = mStateSets.length - 1; i >= 0; i--) { 331 stateSets[i] = mStateSets[i] != null ? mStateSets[i].clone() : null; 332 } 333 mStateSets = stateSets; 334 } 335 336 @UnsupportedAppUsage addStateSet(int[] stateSet, Drawable drawable)337 int addStateSet(int[] stateSet, Drawable drawable) { 338 final int pos = addChild(drawable); 339 mStateSets[pos] = stateSet; 340 return pos; 341 } 342 indexOfStateSet(int[] stateSet)343 int indexOfStateSet(int[] stateSet) { 344 final int[][] stateSets = mStateSets; 345 final int N = getChildCount(); 346 for (int i = 0; i < N; i++) { 347 if (StateSet.stateSetMatches(stateSets[i], stateSet)) { 348 return i; 349 } 350 } 351 return -1; 352 } 353 hasFocusStateSpecified()354 boolean hasFocusStateSpecified() { 355 return StateSet.containsAttribute(mStateSets, R.attr.state_focused); 356 } 357 358 @Override newDrawable()359 public Drawable newDrawable() { 360 return new StateListDrawable(this, null); 361 } 362 363 @Override newDrawable(Resources res)364 public Drawable newDrawable(Resources res) { 365 return new StateListDrawable(this, res); 366 } 367 368 @Override canApplyTheme()369 public boolean canApplyTheme() { 370 return mThemeAttrs != null || super.canApplyTheme(); 371 } 372 373 @Override growArray(int oldSize, int newSize)374 public void growArray(int oldSize, int newSize) { 375 super.growArray(oldSize, newSize); 376 final int[][] newStateSets = new int[newSize][]; 377 System.arraycopy(mStateSets, 0, newStateSets, 0, oldSize); 378 mStateSets = newStateSets; 379 } 380 } 381 382 @Override applyTheme(Theme theme)383 public void applyTheme(Theme theme) { 384 super.applyTheme(theme); 385 386 onStateChange(getState()); 387 } 388 setConstantState(@onNull DrawableContainerState state)389 protected void setConstantState(@NonNull DrawableContainerState state) { 390 super.setConstantState(state); 391 392 if (state instanceof StateListState) { 393 mStateListState = (StateListState) state; 394 } 395 } 396 StateListDrawable(StateListState state, Resources res)397 private StateListDrawable(StateListState state, Resources res) { 398 // Every state list drawable has its own constant state. 399 final StateListState newState = new StateListState(state, this, res); 400 setConstantState(newState); 401 onStateChange(getState()); 402 } 403 404 /** 405 * This constructor exists so subclasses can avoid calling the default 406 * constructor and setting up a StateListDrawable-specific constant state. 407 */ StateListDrawable(@ullable StateListState state)408 StateListDrawable(@Nullable StateListState state) { 409 if (state != null) { 410 setConstantState(state); 411 } 412 } 413 } 414 415