阅读 163

深入解析Android的StateListDrawable

在 Android 中我们经常使用 StateListDrawable ,来实现按钮、列表项等控件在不同状态下的外观,每种状态对应一个drawable资源。

写了很多 selector 一直没有深入的分析过 StateListDrawable 是如何处理不同状态 Drawable的切换,这也导致了有很多奇怪的问题不好解决,比如 XML 中每个 item 的顺序是会对结果有很大影响的,以及不知道可以多个状态对应一个Drawable。这篇文章的目的在于从源码角度分析StateListDrawable针对不同状态匹配Drawable的过程。

算法主要有3步:

  1. 计算View当前states
  2. StateListDrawable初始化
  3. View的states和StateListDrawable预设的statesSets进行匹配

源码均来源于Android API 30

计算View当前state

View对象在初始化或调用setSelected()等方法,都会通过refreshDrawableState()方法刷新如背景等Drawable对象:

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

    ViewParent parent = mParent;
    if (parent != null) {
        parent.childDrawableStateChanged(this);
    }
}
复制代码

又调用drawableStateChanged()方法:

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();
    }
}
复制代码

看到View是通过getDrawableState()获取当前state的:

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;
    }
}
复制代码

可以看到当View初始化或状态发生改变都会调用onCreateDrawableState()方法计算新的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;
}
复制代码

从代码来看,viewStateIndex的计算,drawableState获取都离不开StateSet类。下面插播一下StateSet的内容。

  • StateSet

    StateSet里定义了Android的常见状态,为了节省内存,用二进制的位来表示:

    /** @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;
    复制代码

    预先定义了一个key-value形式的数组VIEW_STATE_IDS,key表示属性id,value表示状态值:

    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
    };
    复制代码

    VIEW_STATE_SETS是一个静态变量,类型为int[][]。初始化过程:

    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;
        }
    }
    复制代码

    上面的代码主要做了3件事:

    1. 系统内部通过声明名为ViewDrawableStates的属性组可以对VIEW_STATE_IDS重排序。

      为了简化理解,假设排序后的orderedIds[]和VIEW_STATE_IDS是一样的。

    2. VIEW_STATE_IDS中定义了10中状态,一共有2的10次方1024中组合,所以VIEW_STATE_SETS定义为1024行。

      VIEW_STATE_SETS数组的行索引值表示状态组合值,列表时状态属性id。

      例如:VIEW_STATE_IDS[0x0011][0]=R.attr.state_window_focusedVIEW_STATE_IDS[0x0011][1]=state_selected

    3. 枚举所有可能性

    看下View在获取state使用的get()方法:

    public static int[] get(int mask) {
        if (mask >= VIEW_STATE_SETS.length) {
            throw new IllegalArgumentException("Invalid state set mask");
        }
        return VIEW_STATE_SETS[mask];
    }
    复制代码

    只是返回了VIEW_STATE_IDS对应的行而已。

View的状态计算过程就介绍完成了。

StateListDrawable初始化

定义 <selector/> 的xml文件被解析后,创建StateListDrawable对象,调用inflate()方法,infalte()方法里调用inflateChildElements()方法解析 <item/>

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);
    }
}
复制代码

以解析单个<item/>为例,解析过程如下:

  • extractStateSet()方法解析<item/>定义的android:state_xxx属性,返回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;
    }
    复制代码

    android:state_xxx为true,用属性id表示;反之,用属性id的负值表示。

  • 解析<item/>android:drawable属性,创建Drawable对象。

  • 通过 state.addStateSet(states, dr)将states和drawable联系起来。

    state为StateListState对象,addStateSet()实现如下:

    int addStateSet(int[] stateSet, Drawable drawable) {
        final int pos = addChild(drawable);
        mStateSets[pos] = stateSet;
        return pos;
    }
    复制代码

    mStateSets是int[][]数组,上面方法把Drawable对象和stateSet建立了一一对应的关系。

    看下addChild()的实现,主要工作把Drawable对象存入数组:

    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;
    }
    复制代码

    至此,StateListDrawable对象初始化完成。

match过程

View的states计算好了,StateListDrawable也初始化完成了,接下来就是match,找到对应Drawable的过程了。

View计算完当前states会将StatelistDrawable对象也设置为当前state:

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();
    }
}
复制代码

由于StateListDrawable没有重写setState()方法,所有看下Drawable的setState()实现:

public boolean setState(@NonNull final int[] stateSet) {
    if (!Arrays.equals(mStateSet, stateSet)) {
        mStateSet = stateSet;
        return onStateChange(stateSet);
    }
    return false;
}
复制代码

StateListDrawable重写了onStateChange()方法,又回到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;
}
复制代码

最关键的地方来了。mStateListState是StateListState对象,很熟悉是不是,在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;
}
复制代码

很简单,也很暴力,直接用解析到的mStateSets的每一行和被设置的stateSet进行匹配。匹配规则由stateSetMatches()方法决定:

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;
}
复制代码

代码很多,总结就两条规则:

  1. stateSpec有的正的属性id,stateSet一定要有相同属性id

  2. stateSpec有负的属性id,stateSet一定不能有正的属性id

如果匹配成功,那么就可以通过mStateSets和mDrawables的一一对应关系找到Drawable对象了。

总结

1.计算View的viewStateIndex

2.viewStateIndex作为索引,在VIEW_STATE_SETS查找对应state

3.state和StateListDrawable的stateSets逐行比对

4.如果匹配成功,根据index找到Drawable对象

Communication

等你来Android泡泡群冒泡哦!

QQ: 905487701

文章分类
Android
文章标签