Home | History | Annotate | Download | only in drawable
      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 
     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