| /* |
| * Copyright (C) 2006 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.graphics.drawable; |
| |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.annotation.UnsupportedAppUsage; |
| import android.content.res.Resources; |
| import android.content.res.Resources.Theme; |
| import android.content.res.TypedArray; |
| import android.util.AttributeSet; |
| import android.util.StateSet; |
| |
| import com.android.internal.R; |
| |
| import org.xmlpull.v1.XmlPullParser; |
| import org.xmlpull.v1.XmlPullParserException; |
| |
| import java.io.IOException; |
| import java.util.Arrays; |
| |
| /** |
| * Lets you assign a number of graphic images to a single Drawable and swap out the visible item by a string |
| * ID value. |
| * <p/> |
| * <p>It can be defined in an XML file with the <code><selector></code> element. |
| * Each state Drawable is defined in a nested <code><item></code> element. For more |
| * information, see the guide to <a |
| * href="{@docRoot}guide/topics/resources/drawable-resource.html">Drawable Resources</a>.</p> |
| * |
| * @attr ref android.R.styleable#StateListDrawable_visible |
| * @attr ref android.R.styleable#StateListDrawable_variablePadding |
| * @attr ref android.R.styleable#StateListDrawable_constantSize |
| * @attr ref android.R.styleable#DrawableStates_state_focused |
| * @attr ref android.R.styleable#DrawableStates_state_window_focused |
| * @attr ref android.R.styleable#DrawableStates_state_enabled |
| * @attr ref android.R.styleable#DrawableStates_state_checkable |
| * @attr ref android.R.styleable#DrawableStates_state_checked |
| * @attr ref android.R.styleable#DrawableStates_state_selected |
| * @attr ref android.R.styleable#DrawableStates_state_activated |
| * @attr ref android.R.styleable#DrawableStates_state_active |
| * @attr ref android.R.styleable#DrawableStates_state_single |
| * @attr ref android.R.styleable#DrawableStates_state_first |
| * @attr ref android.R.styleable#DrawableStates_state_middle |
| * @attr ref android.R.styleable#DrawableStates_state_last |
| * @attr ref android.R.styleable#DrawableStates_state_pressed |
| */ |
| public class StateListDrawable extends DrawableContainer { |
| private static final String TAG = "StateListDrawable"; |
| |
| private static final boolean DEBUG = false; |
| |
| @UnsupportedAppUsage |
| private StateListState mStateListState; |
| private boolean mMutated; |
| |
| public StateListDrawable() { |
| this(null, null); |
| } |
| |
| /** |
| * Add a new image/string ID to the set of images. |
| * |
| * @param stateSet An array of resource Ids to associate with the image. |
| * Switch to this image by calling setState(). |
| * @param drawable The image to show. Note this must be a unique Drawable that is not shared |
| * between any other View or Drawable otherwise the results are |
| * undefined and can lead to unexpected rendering behavior |
| */ |
| public void addState(int[] stateSet, Drawable drawable) { |
| if (drawable != null) { |
| mStateListState.addStateSet(stateSet, drawable); |
| // in case the new state matches our current state... |
| onStateChange(getState()); |
| } |
| } |
| |
| @Override |
| public boolean isStateful() { |
| return true; |
| } |
| |
| /** @hide */ |
| @Override |
| public boolean hasFocusStateSpecified() { |
| return mStateListState.hasFocusStateSpecified(); |
| } |
| |
| @Override |
| protected boolean onStateChange(int[] stateSet) { |
| final boolean changed = super.onStateChange(stateSet); |
| |
| int idx = mStateListState.indexOfStateSet(stateSet); |
| if (DEBUG) android.util.Log.i(TAG, "onStateChange " + this + " states " |
| + Arrays.toString(stateSet) + " found " + idx); |
| if (idx < 0) { |
| idx = mStateListState.indexOfStateSet(StateSet.WILD_CARD); |
| } |
| |
| return selectDrawable(idx) || changed; |
| } |
| |
| @Override |
| public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme) |
| throws XmlPullParserException, IOException { |
| final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.StateListDrawable); |
| super.inflateWithAttributes(r, parser, a, R.styleable.StateListDrawable_visible); |
| updateStateFromTypedArray(a); |
| updateDensity(r); |
| a.recycle(); |
| |
| inflateChildElements(r, parser, attrs, theme); |
| |
| onStateChange(getState()); |
| } |
| |
| /** |
| * Updates the constant state from the values in the typed array. |
| */ |
| @UnsupportedAppUsage |
| private void updateStateFromTypedArray(TypedArray a) { |
| final StateListState state = mStateListState; |
| |
| // Account for any configuration changes. |
| state.mChangingConfigurations |= a.getChangingConfigurations(); |
| |
| // Extract the theme attributes, if any. |
| state.mThemeAttrs = a.extractThemeAttrs(); |
| |
| state.mVariablePadding = a.getBoolean( |
| R.styleable.StateListDrawable_variablePadding, state.mVariablePadding); |
| state.mConstantSize = a.getBoolean( |
| R.styleable.StateListDrawable_constantSize, state.mConstantSize); |
| state.mEnterFadeDuration = a.getInt( |
| R.styleable.StateListDrawable_enterFadeDuration, state.mEnterFadeDuration); |
| state.mExitFadeDuration = a.getInt( |
| R.styleable.StateListDrawable_exitFadeDuration, state.mExitFadeDuration); |
| state.mDither = a.getBoolean( |
| R.styleable.StateListDrawable_dither, state.mDither); |
| state.mAutoMirrored = a.getBoolean( |
| R.styleable.StateListDrawable_autoMirrored, state.mAutoMirrored); |
| } |
| |
| /** |
| * Inflates child elements from XML. |
| */ |
| private void inflateChildElements(Resources r, XmlPullParser parser, AttributeSet attrs, |
| Theme theme) throws XmlPullParserException, IOException { |
| final StateListState state = mStateListState; |
| final int innerDepth = parser.getDepth() + 1; |
| int type; |
| int depth; |
| while ((type = parser.next()) != XmlPullParser.END_DOCUMENT |
| && ((depth = parser.getDepth()) >= innerDepth |
| || type != XmlPullParser.END_TAG)) { |
| if (type != XmlPullParser.START_TAG) { |
| continue; |
| } |
| |
| if (depth > innerDepth || !parser.getName().equals("item")) { |
| continue; |
| } |
| |
| // This allows state list drawable item elements to be themed at |
| // inflation time but does NOT make them work for Zygote preload. |
| final TypedArray a = obtainAttributes(r, theme, attrs, |
| R.styleable.StateListDrawableItem); |
| Drawable dr = a.getDrawable(R.styleable.StateListDrawableItem_drawable); |
| a.recycle(); |
| |
| final int[] states = extractStateSet(attrs); |
| |
| // Loading child elements modifies the state of the AttributeSet's |
| // underlying parser, so it needs to happen after obtaining |
| // attributes and extracting states. |
| if (dr == null) { |
| while ((type = parser.next()) == XmlPullParser.TEXT) { |
| } |
| if (type != XmlPullParser.START_TAG) { |
| throw new XmlPullParserException( |
| parser.getPositionDescription() |
| + ": <item> tag requires a 'drawable' attribute or " |
| + "child tag defining a drawable"); |
| } |
| dr = Drawable.createFromXmlInner(r, parser, attrs, theme); |
| } |
| |
| state.addStateSet(states, dr); |
| } |
| } |
| |
| /** |
| * Extracts state_ attributes from an attribute set. |
| * |
| * @param attrs The attribute set. |
| * @return An array of state_ attributes. |
| */ |
| @UnsupportedAppUsage |
| int[] extractStateSet(AttributeSet attrs) { |
| int j = 0; |
| final int numAttrs = attrs.getAttributeCount(); |
| int[] states = new int[numAttrs]; |
| for (int i = 0; i < numAttrs; i++) { |
| final int stateResId = attrs.getAttributeNameResource(i); |
| switch (stateResId) { |
| case 0: |
| break; |
| case R.attr.drawable: |
| case R.attr.id: |
| // Ignore attributes from StateListDrawableItem and |
| // AnimatedStateListDrawableItem. |
| continue; |
| default: |
| states[j++] = attrs.getAttributeBooleanValue(i, false) |
| ? stateResId : -stateResId; |
| } |
| } |
| states = StateSet.trimStateSet(states, j); |
| return states; |
| } |
| |
| StateListState getStateListState() { |
| return mStateListState; |
| } |
| |
| /** |
| * Gets the number of states contained in this drawable. |
| * |
| * @return The number of states contained in this drawable. |
| * @see #getStateSet(int) |
| * @see #getStateDrawable(int) |
| */ |
| public int getStateCount() { |
| return mStateListState.getChildCount(); |
| } |
| |
| /** |
| * Gets the state set at an index. |
| * |
| * @param index The index of the state set. |
| * @return The state set at the index. |
| * @see #getStateCount() |
| * @see #getStateDrawable(int) |
| */ |
| public @NonNull int[] getStateSet(int index) { |
| return mStateListState.mStateSets[index]; |
| } |
| |
| /** |
| * Gets the drawable at an index. |
| * |
| * @param index The index of the drawable. |
| * @return The drawable at the index. |
| * @see #getStateCount() |
| * @see #getStateSet(int) |
| */ |
| public @Nullable Drawable getStateDrawable(int index) { |
| return mStateListState.getChild(index); |
| } |
| |
| /** |
| * Gets the index of the drawable with the provided state set. |
| * |
| * @param stateSet the state set to look up |
| * @return the index of the provided state set, or -1 if not found |
| * @see #getStateDrawable(int) |
| * @see #getStateSet(int) |
| */ |
| public int findStateDrawableIndex(@NonNull int[] stateSet) { |
| return mStateListState.indexOfStateSet(stateSet); |
| } |
| |
| @Override |
| public Drawable mutate() { |
| if (!mMutated && super.mutate() == this) { |
| mStateListState.mutate(); |
| mMutated = true; |
| } |
| return this; |
| } |
| |
| @Override |
| StateListState cloneConstantState() { |
| return new StateListState(mStateListState, this, null); |
| } |
| |
| /** |
| * @hide |
| */ |
| public void clearMutated() { |
| super.clearMutated(); |
| mMutated = false; |
| } |
| |
| static class StateListState extends DrawableContainerState { |
| int[] mThemeAttrs; |
| int[][] mStateSets; |
| |
| StateListState(StateListState orig, StateListDrawable owner, Resources res) { |
| super(orig, owner, res); |
| |
| if (orig != null) { |
| // Perform a shallow copy and rely on mutate() to deep-copy. |
| mThemeAttrs = orig.mThemeAttrs; |
| mStateSets = orig.mStateSets; |
| } else { |
| mThemeAttrs = null; |
| mStateSets = new int[getCapacity()][]; |
| } |
| } |
| |
| void mutate() { |
| mThemeAttrs = mThemeAttrs != null ? mThemeAttrs.clone() : null; |
| |
| final int[][] stateSets = new int[mStateSets.length][]; |
| for (int i = mStateSets.length - 1; i >= 0; i--) { |
| stateSets[i] = mStateSets[i] != null ? mStateSets[i].clone() : null; |
| } |
| mStateSets = stateSets; |
| } |
| |
| @UnsupportedAppUsage |
| int addStateSet(int[] stateSet, Drawable drawable) { |
| final int pos = addChild(drawable); |
| mStateSets[pos] = stateSet; |
| return pos; |
| } |
| |
| int indexOfStateSet(int[] stateSet) { |
| final int[][] stateSets = mStateSets; |
| final int N = getChildCount(); |
| for (int i = 0; i < N; i++) { |
| if (StateSet.stateSetMatches(stateSets[i], stateSet)) { |
| return i; |
| } |
| } |
| return -1; |
| } |
| |
| boolean hasFocusStateSpecified() { |
| return StateSet.containsAttribute(mStateSets, R.attr.state_focused); |
| } |
| |
| @Override |
| public Drawable newDrawable() { |
| return new StateListDrawable(this, null); |
| } |
| |
| @Override |
| public Drawable newDrawable(Resources res) { |
| return new StateListDrawable(this, res); |
| } |
| |
| @Override |
| public boolean canApplyTheme() { |
| return mThemeAttrs != null || super.canApplyTheme(); |
| } |
| |
| @Override |
| public void growArray(int oldSize, int newSize) { |
| super.growArray(oldSize, newSize); |
| final int[][] newStateSets = new int[newSize][]; |
| System.arraycopy(mStateSets, 0, newStateSets, 0, oldSize); |
| mStateSets = newStateSets; |
| } |
| } |
| |
| @Override |
| public void applyTheme(Theme theme) { |
| super.applyTheme(theme); |
| |
| onStateChange(getState()); |
| } |
| |
| protected void setConstantState(@NonNull DrawableContainerState state) { |
| super.setConstantState(state); |
| |
| if (state instanceof StateListState) { |
| mStateListState = (StateListState) state; |
| } |
| } |
| |
| private StateListDrawable(StateListState state, Resources res) { |
| // Every state list drawable has its own constant state. |
| final StateListState newState = new StateListState(state, this, res); |
| setConstantState(newState); |
| onStateChange(getState()); |
| } |
| |
| /** |
| * This constructor exists so subclasses can avoid calling the default |
| * constructor and setting up a StateListDrawable-specific constant state. |
| */ |
| StateListDrawable(@Nullable StateListState state) { |
| if (state != null) { |
| setConstantState(state); |
| } |
| } |
| } |
| |