XML文件中给Button控件设置android:background属性或者在代码里直接调用View.setBackgroundDrawable函数设置背景,这些恐怕每个android开发人员都干过。的确,为了让我们的应用表现的更加人性化,这些控件的状态变化是必不可少的。可是,对于为什么这样用就起作用,不知道大家分析过没有,最近我分析了一下,现在和大家共享一下。一,从XML中解析出StateListDrawable的过程。见下图:
图1 从XML中解析出StateListDrawable的过程
从图1中,我们可以看到一个完整的从XML中解析出StateListDrawable的过程,觉得图已经画的比较清楚了,所以就不在多说了。
二,View的DrawableState的设置过程
图2 View的DrawableState的设置过程
图2有些地方感觉还要多说两句,
1,onCreateDrawableState函数里有完整当前视图控件状态判断的方法,具体代码如下:
int viewStateIndex = (((privateFlags & PRESSED) != 0) ? 1 : 0); // 下压状态判断。这个状态的设置一般是在setPressed函数里进行的。
viewStateIndex = (viewStateIndex << 1)
+ (((mViewFlags & ENABLED_MASK) == ENABLED) ? 1 : 0); // 使能状态判断。这个状态的设置一般是setEnabled函数里进行的。
viewStateIndex = (viewStateIndex << 1) + (isFocused() ? 1 : 0); // 焦点状态判断。这个状态的设置一般是requestFocus函数里进行的。
viewStateIndex = (viewStateIndex << 1)
+ (((privateFlags & SELECTED) != 0) ? 1 : 0); // 选择状态判断。这个状态的设置一般是setSelected函数里进行的。
final boolean hasWindowFocus = hasWindowFocus();
viewStateIndex = (viewStateIndex << 1) + (hasWindowFocus ? 1 : 0); // 视图所在窗口焦点状态判断。
drawableState = VIEW_STATE_SETS[viewStateIndex];
2,一个窗口中可以有多个视图处于selected状态,但是只能有一个处于focused状态;
3,视图处于focused状态时,不一定处于window_focused状态。典型的情况是,窗口初次创建时,ViewRoot会执行performTraversals(),执行中根视图会执行requestFocus()操作,此时一般会有一个视图处于focused状态;但是这个时候视图并不一定是处于window_focused状态,因为window_focused是由系统发出WINDOW_FOCUS_CHANGED消息时,让ViewRoot执行windowFocusChanged()来改变的。这些可以通过重写View的requestFocus()和onWindowFocusChanged()来证明;
4,数组View.VIEW_STATE_SETS是一个非常重要的成员变量,它记录View的所有可能状态。View.VIEW_STATE_SETS这个数组里的部分状态组合是用View.stateSetUnion函数计算出来的。考虑到这个数组成员在后面StateSet.stateSetMatches函数里将被使用来判断状态是否匹配,觉得有必要具体说一下,下面是它的实现代码:
private static int[] stateSetUnion(final int[] stateSet1,
final int[] stateSet2) {
final int stateSet1Length = stateSet1.length;
final int stateSet2Length = stateSet2.length;
final int[] newSet = new int[stateSet1Length + stateSet2Length]; // 新状态集合newSet大小是stateSet1和stateSet2大小之和,默认值都是0
int k = 0;
int i = 0;
int j = 0;
// This is a merge of the two input state sets and assumes that the
// input sets are sorted by the order imposed by ViewDrawableStates.
for (int viewState : R.styleable.ViewDrawableStates) {
if (i < stateSet1Length && stateSet1[i] == viewState) {
newSet[k++] = viewState; // 把stateSet1里的状态值赋给newSet
i++;
} else if (j < stateSet2Length && stateSet2[j] == viewState) {
newSet[k++] = viewState; // 把stateSet2里的状态值赋给newSet
j++;
}
if (k > 1) {
assert(newSet[k - 1] > newSet[k - 2]);
}
}
return newSet;
}
我们来看一下R.styleable.ViewDrawableStates的定义:
<declare-styleable name="ViewDrawableStates">
<attr name="state_pressed" />
<attr name="state_focused" />
<attr name="state_selected" />
<attr name="state_window_focused" />
<attr name="state_enabled" />
</declare-styleable>
从这里我们看到,其实数组R.styleable.ViewDrawableStates里其实就是R.attr.state_pressed,R.attr.state_focused,R.attr.state_selected ,R.attr.state_window_focused和 R.attr.state_enabled 的集合。用语言总结一下View.stateSetUnion函数的作用,将状态集合stateSet1和stateSet2里的状态值,按照状态值在数组R.styleable.ViewDrawableStates里的先后顺序赋给新状态集合newSet。
5,考虑到完整性和对称性,这里把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;
}
三,View的DrawableState变化过程。有了上面的基础,这里就非常简单了,我就举一个例子来具体说明一下吧,下图是View.setPressed的时序图:
图3 View.setPressed时序图
把图3和图2结合起来看,就可以清楚地理解状态变化是如何实现的了。
四,相关的一些小技巧
有时候UI可能不给开发人员背景图资源,但是还是希望开发人员做到Button下压时,Button上的文字颜色变暗。这个时候,就可以用这个小技巧来实现了:设置android:textColor属性。具体例子请参见《Android 中设置TextView的颜色setTextColor》
备注:
1,Button是TextView的子类。
2,相关代码实现在TextView.drawableStateChanged函数里
protected void drawableStateChanged() {
super.drawableStateChanged(); // background在这里起作用了
if (mTextColor != null && mTextColor.isStateful()
|| (mHintTextColor != null && mHintTextColor.isStateful())
|| (mLinkTextColor != null && mLinkTextColor.isStateful())) {
updateTextColors(); // 就是这里textColor开始起作用了
}
final Drawables dr = mDrawables;
if (dr != null) {
int[] state = getDrawableState();
if (dr.mDrawableTop != null && dr.mDrawableTop.isStateful()) {
dr.mDrawableTop.setState(state);
}
if (dr.mDrawableBottom != null && dr.mDrawableBottom.isStateful()) {
dr.mDrawableBottom.setState(state);
}
if (dr.mDrawableLeft != null && dr.mDrawableLeft.isStateful()) {
dr.mDrawableLeft.setState(state);
}
if (dr.mDrawableRight != null && dr.mDrawableRight.isStateful()) {
dr.mDrawableRight.setState(state);
}
}
}