blob: 2920acbe514c5bc6a738c7a28ea34e570fe99c69 [file] [log] [blame]
/*
* 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.compat.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>&lt;selector></code> element.
* Each state Drawable is defined in a nested <code>&lt;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);
}
}
}