在 Android 中我们经常使用 StateListDrawable ,来实现按钮、列表项等控件在不同状态下的外观,每种状态对应一个drawable资源。
写了很多 selector 一直没有深入的分析过 StateListDrawable 是如何处理不同状态 Drawable的切换,这也导致了有很多奇怪的问题不好解决,比如 XML 中每个 item 的顺序是会对结果有很大影响的,以及不知道可以多个状态对应一个Drawable。这篇文章的目的在于从源码角度分析StateListDrawable针对不同状态匹配Drawable的过程。
算法主要有3步:
- 计算View当前states
- StateListDrawable初始化
- 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件事:
-
系统内部通过声明名为ViewDrawableStates的属性组可以对VIEW_STATE_IDS重排序。
为了简化理解,假设排序后的orderedIds[]和VIEW_STATE_IDS是一样的。
-
VIEW_STATE_IDS中定义了10中状态,一共有2的10次方1024中组合,所以VIEW_STATE_SETS定义为1024行。
VIEW_STATE_SETS数组的行索引值表示状态组合值,列表时状态属性id。
例如:
VIEW_STATE_IDS[0x0011][0]=R.attr.state_window_focused
,VIEW_STATE_IDS[0x0011][1]=state_selected
-
枚举所有可能性
看下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;
}
复制代码
代码很多,总结就两条规则:
-
stateSpec有的正的属性id,stateSet一定要有相同属性id
-
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