StateListDrawable is often used in Android to achieve the appearance of buttons, list items, and other controls in different states. Each state corresponds to a drawable resource.

I’ve written a lot of selectors and I haven’t done a thorough analysis of how StateListDrawable handles switching between different states of Drawable, which leads to a lot of weird problems that are difficult to solve, like the order of each item in XML can have a big impact on the results, And that you can have multiple states corresponding to a Drawable. The purpose of this article is to analyze the process of matching a StateListDrawable for different states from a source code perspective.

The algorithm mainly consists of three steps:

  1. Calculate the current states of the View
  2. StateListDrawable initialization
  3. The View states and StateListDrawable default statesSets are matched

Source code from Android API 30

Calculate the current state of the View

When a View object is initialized or calls a method such as setSelected(), the refreshDrawableState() method refreshable DrawableState() is used to refresh a Drawable object such as the background:

public void refreshDrawableState() {
    mPrivateFlags |= PFLAG_DRAWABLE_STATE_DIRTY;
    drawableStateChanged();

    ViewParent parent = mParent;
    if (parent != null) {
        parent.childDrawableStateChanged(this);
    }
}
Copy the code

Call drawableStateChanged() again:

protected void drawableStateChanged() { final int[] state = getDrawableState(); boolean changed = false; final Drawable bg = mBackground; if (bg ! = null && bg.isStateful()) { changed |= bg.setState(state); }... if (changed) { invalidate(); }}Copy the code

GetDrawableState () gets the current state from the View:

public final int[] getDrawableState() {
    if ((mDrawableState != null) && ((mPrivateFlags & PFLAG_DRAWABLE_STATE_DIRTY) == 0)) {
        return mDrawableState;
    } else {
        mDrawableState = onCreateDrawableState(0);
        mPrivateFlags &= ~PFLAG_DRAWABLE_STATE_DIRTY;
        return mDrawableState;
    }
}
Copy the code

As you can see, the onCreateDrawableState() method is called whenever a View is initialized or its state changes to calculate the new state:

protected int[] onCreateDrawableState(int extraSpace) { if ((mViewFlags & DUPLICATE_PARENT_STATE) == DUPLICATE_PARENT_STATE && mParent instanceof View) { return ((View) mParent).onCreateDrawableState(extraSpace); } int[] drawableState; int privateFlags = mPrivateFlags; int viewStateIndex = 0; if ((privateFlags & PFLAG_PRESSED) ! = 0) viewStateIndex |= StateSet.VIEW_STATE_PRESSED; if ((mViewFlags & ENABLED_MASK) == ENABLED) viewStateIndex |= StateSet.VIEW_STATE_ENABLED; if (isFocused()) viewStateIndex |= StateSet.VIEW_STATE_FOCUSED; if ((privateFlags & PFLAG_SELECTED) ! = 0) viewStateIndex |= StateSet.VIEW_STATE_SELECTED; if (hasWindowFocus()) viewStateIndex |= StateSet.VIEW_STATE_WINDOW_FOCUSED; if ((privateFlags & PFLAG_ACTIVATED) ! = 0) viewStateIndex |= StateSet.VIEW_STATE_ACTIVATED; if (mAttachInfo ! = null && mAttachInfo.mHardwareAccelerationRequested && ThreadedRenderer.isAvailable()) { // This is set if HW acceleration is requested, even if the current // process doesn't allow it. This is just to allow app preview // windows to better match their app.  viewStateIndex |= StateSet.VIEW_STATE_ACCELERATED; } if ((privateFlags & PFLAG_HOVERED) ! = 0) viewStateIndex |= StateSet.VIEW_STATE_HOVERED; final int privateFlags2 = mPrivateFlags2; if ((privateFlags2 & PFLAG2_DRAG_CAN_ACCEPT) ! = 0) { viewStateIndex |= StateSet.VIEW_STATE_DRAG_CAN_ACCEPT; } if ((privateFlags2 & PFLAG2_DRAG_HOVERED) ! = 0) { viewStateIndex |= StateSet.VIEW_STATE_DRAG_HOVERED; } drawableState = StateSet.get(viewStateIndex); //noinspection ConstantIfStatement if (false) { Log.i("View", "drawableStateIndex=" + viewStateIndex); Log.i("View", toString() + " pressed=" + ((privateFlags & PFLAG_PRESSED) ! = 0) + " en=" + ((mViewFlags & ENABLED_MASK) == ENABLED) + " fo=" + hasFocus() + " sl=" + ((privateFlags & PFLAG_SELECTED) ! = 0) + " wf=" + hasWindowFocus() + ": " + Arrays.toString(drawableState)); } if (extraSpace == 0) { return drawableState; } final int[] fullState; if (drawableState ! = null) { fullState = new int[drawableState.length + extraSpace]; System.arraycopy(drawableState, 0, fullState, 0, drawableState.length); } else { fullState = new int[extraSpace]; } return fullState; }Copy the code

From the code, the viewStateIndex calculation and drawableState retrieval are dependent on the StateSet class. We’ve come back to talk about StateSet.

  • StateSet

    StateSet defines common Android states in binary bits to save memory:

    /** @hide */
    public static final int VIEW_STATE_WINDOW_FOCUSED = 1;
    /** @hide */
    public static final int VIEW_STATE_SELECTED = 1 << 1;
    /** @hide */
    public static final int VIEW_STATE_FOCUSED = 1 << 2;
    /** @hide */
    public static final int VIEW_STATE_ENABLED = 1 << 3;
    /** @hide */
    public static final int VIEW_STATE_PRESSED = 1 << 4;
    /** @hide */
    public static final int VIEW_STATE_ACTIVATED = 1 << 5;
    /** @hide */
    public static final int VIEW_STATE_ACCELERATED = 1 << 6;
    /** @hide */
    public static final int VIEW_STATE_HOVERED = 1 << 7;
    /** @hide */
    public static final int VIEW_STATE_DRAG_CAN_ACCEPT = 1 << 8;
    /** @hide */
    public static final int VIEW_STATE_DRAG_HOVERED = 1 << 9;
    Copy the code

    VIEW_STATE_IDS = key; VIEW_STATE_IDS = key; VIEW_STATE_IDS = key;

    static final int[] VIEW_STATE_IDS = new int[] {
    				R.attr.state_window_focused,    VIEW_STATE_WINDOW_FOCUSED,
    				R.attr.state_selected,          VIEW_STATE_SELECTED,
    				R.attr.state_focused,           VIEW_STATE_FOCUSED,
    				R.attr.state_enabled,           VIEW_STATE_ENABLED,
    				R.attr.state_pressed,           VIEW_STATE_PRESSED,
    				R.attr.state_activated,         VIEW_STATE_ACTIVATED,
    				R.attr.state_accelerated,       VIEW_STATE_ACCELERATED,
    				R.attr.state_hovered,           VIEW_STATE_HOVERED,
    				R.attr.state_drag_can_accept,   VIEW_STATE_DRAG_CAN_ACCEPT,
    				R.attr.state_drag_hovered,      VIEW_STATE_DRAG_HOVERED
    };
    Copy the code

    VIEW_STATE_SETS is a static variable of type int[][]. Initialization process:

    static { if ((VIEW_STATE_IDS.length / 2) ! = R.styleable.ViewDrawableStates.length) { throw new IllegalStateException( "VIEW_STATE_IDs array length does not match ViewDrawableStates style array"); } final int[] orderedIds = new int[VIEW_STATE_IDS.length]; for (int i = 0; i < R.styleable.ViewDrawableStates.length; i++) { final int viewState = R.styleable.ViewDrawableStates[i]; for (int j = 0; j < VIEW_STATE_IDS.length; j += 2) { if (VIEW_STATE_IDS[j] == viewState) { orderedIds[i * 2] = viewState; orderedIds[i * 2 + 1] = VIEW_STATE_IDS[j + 1]; } } } final int NUM_BITS = VIEW_STATE_IDS.length / 2; VIEW_STATE_SETS = new int[1 << NUM_BITS][]; for (int i = 0; i < VIEW_STATE_SETS.length; i++) { final int numBits = Integer.bitCount(i); final int[] set = new int[numBits]; int pos = 0; for (int j = 0; j < orderedIds.length; j += 2) { if ((i & orderedIds[j + 1]) ! = 0) { set[pos++] = orderedIds[j]; } } VIEW_STATE_SETS[i] = set; }}Copy the code

    The above code does three main things:

    1. VIEW_STATE_IDS can be reordered internally by declaring a property group named ViewDrawableStates.

      For simplicity, assume that the sorted orderedIds[] and VIEW_STATE_IDS are the same.

    2. There are 10 states defined in VIEW_STATE_IDS, and there are 2 to the power of 10 combinations of 1024 states, so VIEW_STATE_SETS are defined as 1024 rows.

      The row index value of the VIEW_STATE_SETS array represents the state combination value, and the state attribute ID is listed.

      For example, VIEW_STATE_IDS[0x0011][0]= R.atr.STATE_WINDOW_FOCUSED, VIEW_STATE_IDS[0x0011][1]=state_selected

    3. Enumerate all possibilities

    Take a look at the get() method a View uses to get state:

    public static int[] get(int mask) {
        if (mask >= VIEW_STATE_SETS.length) {
            throw new IllegalArgumentException("Invalid state set mask");
        }
        return VIEW_STATE_SETS[mask];
    }
    Copy the code

    It just returns the row corresponding to VIEW_STATE_IDS.

View state calculation process is introduced to complete.

StateListDrawable initialization

After the XML file that defines
is parsed, create a StateListDrawable object, call the inflate() method, and inflateChildElements() method inside the infalte() method to resolve
:

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); }}Copy the code

For example, parsing a single
is as follows:

  • The extractStateSet() method resolves the Android :state_xxx property defined by
    and returns States:

    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;
    }
    Copy the code

    Android :state_xxx is true, represented by the attribute ID; Instead, use the negative value of the attribute ID.

  • Parse the
    Android :drawable property to create a drawable object.

  • Connect states to Drawable with state.addstateset (States, Dr).

    State is the StateListState object. AddStateSet () implements the following:

    int addStateSet(int[] stateSet, Drawable drawable) {
        final int pos = addChild(drawable);
        mStateSets[pos] = stateSet;
        return pos;
    }
    Copy the code

    MStateSets is an int[][] array. The above methods match the Drawable object with the stateSet.

    Take a look at the implementation of addChild(), which stores the Drawable object into an array:

    public final int addChild(Drawable dr) {
        final int pos = mNumChildren;
        if (pos >= mDrawables.length) {
            growArray(pos, pos+10);
        }
    
        dr.mutate();
        dr.setVisible(false, true);
        dr.setCallback(mOwner);
    
        mDrawables[pos] = dr;
        mNumChildren++;
        mChildrenChangingConfigurations |= dr.getChangingConfigurations();
    
        invalidateCache();
    
        mConstantPadding = null;
        mCheckedPadding = false;
        mCheckedConstantSize = false;
        mCheckedConstantState = false;
    
        return pos;
    }
    Copy the code

    At this point, the StateListDrawable object is initialized.

The match process

The states of the View are calculated, the StateListDrawable is initialized, and then the match is found.

StatelistDrawable: StatelistDrawable: StatelistDrawable: StatelistDrawable: StatelistDrawable

protected void drawableStateChanged() { final int[] state = getDrawableState(); boolean changed = false; final Drawable bg = mBackground; if (bg ! = null && bg.isStateful()) { changed |= bg.setState(state); }... if (changed) { invalidate(); }}Copy the code

Since StateListDrawable does not override setState(), let’s look at the setState() implementation of Drawable:

public boolean setState(@NonNull final int[] stateSet) { if (! Arrays.equals(mStateSet, stateSet)) { mStateSet = stateSet; return onStateChange(stateSet); } return false; }Copy the code

StateListDrawable overwrites onStateChange() and goes back to StateListDrawable:

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;
}
Copy the code

Here comes the big part. MStateListState is a StateListState object, which is familiar to you, and it was mentioned in the initialization of StateListDrawable. IndexOfStateSet () :

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;
}
Copy the code

Simple and violent, use the resolved mStateSets to match each line with the set stateSet. The matching rule is determined by the stateSetMatches() method:

public static boolean stateSetMatches(int[] stateSpec, int[] stateSet) { if (stateSet == null) { return (stateSpec == null || isWildCard(stateSpec)); } int stateSpecSize = stateSpec.length; int stateSetSize = stateSet.length; for (int i = 0; i < stateSpecSize; i++) { int stateSpecState = stateSpec[i]; if (stateSpecState == 0) { // We've reached the end of the cases to match against. return true; } final boolean mustMatch; if (stateSpecState > 0) { mustMatch = true; } else { // We use negative values to indicate must-NOT-match states. mustMatch = false; stateSpecState = -stateSpecState; } boolean found = false; for (int j = 0; j < stateSetSize; j++) { final int state = stateSet[j]; if (state == 0) { // We've reached the end of states to match. if (mustMatch) { // We didn't find this must-match state.  return false; } else { // Continue checking other must-not-match states. break; } } if (state == stateSpecState) { if (mustMatch) { found = true; // Continue checking other other must-match states. break; } else { // Any match of a must-not-match state returns false. return false; } } } if (mustMatch && ! found) { // We've reached the end of states to match and we didn't // find a must-match state. return false; } } return true; }Copy the code

There’s a lot of code, but I’ll summarize it with two rules:

  1. StateSpec has a positive attribute ID, so stateSet must have the same attribute ID

  2. StateSpec has a negative attribute ID. StateSet must not have a positive attribute ID

If the match is successful, you can find the Drawable object using the one-to-one relationship between mStateSets and mDrawables.

conclusion

1. Calculate the viewStateIndex of a View

2. Use viewStateIndex as the index and find the corresponding state in VIEW_STATE_SETS

State and StateListDrawable stateSets are compared line by line

4. If the match is successful, locate the Drawable object according to index

Communication

Wait for you to come to Android bubble group bubble oh!

QQ: 905487701