从 Android 源码角度分析 View 的状态改变如何影响 Drawable 的表现

在 Android 开发中,View 和 Drawable 之间关系十分紧密,例如我们经常用 Drawable 作为一个 View 的背景。View 常常会有状态的改变,例如被按下、例如禁用,而不同的状态下 Drawable 也常有不同的表现。今天要探索的问题是 View 的状态改变是如何影响 Drawable 的表现的。
以下将简单介绍我们平时如何在 View 上使用 Drawable,做到在不同状态下表现不一样。接着分析系统源码探索其中的原理。最后以系统的控件和自定义控件 2 个例子来验证和实践在 View 中自定义状态的做法。

注:

  1. 本文的源码分析基于 Android API Level 23,并省略掉部分与本文关系不大的代码。
  2. 在代码中加入了个人对源码的理解,以注释形式呈现。
  3. 本文最后的 DEMO 项目源码托管到 Github 上。

如何给 View 在不同状态下设置不同背景色

可以对一个 View 设置 background 属性,传进去的是一个 Drawable。 如果该 Drawable 是一个 StateListDrawable(对应的 xml 标签为 <selctor>),那么它能在不同状态下显示不同的表现。例如一个 Button,可以在 normal、pressed、disabled 等状态下显示不同的背景色,像这样:

1
2
3
4
5
6
<!-- button_bg.xml -->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="#CCCCCC" android:state_enabled="false"/>
<item android:drawable="#666666" android:state_pressed="true"/>
<item android:drawable="#999999"/>
</selector>

1
2
3
4
5
6
<!-- layout.xml -->
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/button_bg"
android:text="Test Button"/>

这样即可实现使 Button 在不同状态下颜色不一样,normal 为 #999999,pressed 为 #666666,disabled 时为 #CCCCCC。

原理

以上是经常用来设置 Button 背景的用法,那么实际上 Button(View)的不同状态是如何和 Drawable 关联起来的?除了上面说的 pressed 和 enabled 状态,我们可以设置的状态还有哪些?如果系统提供的状态不够用,我们能否自己定义状态?带着这几个问题,我们来看 Android FrameWork 的源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
// View.java
// 首先,按钮被按下的时候,setPressed(boolean pressed) 会被调用。
// 注1:这里以 pressed 状态改变为例,从 setPressed 方法为入口。
// 同理当 enabled 或其他状态改变时,可以看 setEnabled 方法或其他对应方法。
public void setPressed(boolean pressed) {
final boolean needsRefresh =
pressed != ((mPrivateFlags & PFLAG_PRESSED) == PFLAG_PRESSED);
if (pressed) {
mPrivateFlags |= PFLAG_PRESSED;
} else {
mPrivateFlags &= ~PFLAG_PRESSED;
}
// 调用 refreshDrawableState() 方法来刷新 View 的状态
if (needsRefresh) {
refreshDrawableState();
}
dispatchSetPressed(pressed);
}
// 接着看 refreshDrawableState() 方法。
// 该方法会使 View 更新它的 Drawable 的状态,并调用 drawableStateChanged() 方法。
public void refreshDrawableState() {
// 设置 PFLAG_DRAWABLE_STATE_DIRTY 标志位,后面会用到,并调用 drawableStateChanged() 方法
mPrivateFlags |= PFLAG_DRAWABLE_STATE_DIRTY;
drawableStateChanged();
ViewParent parent = mParent;
if (parent != null) {
parent.childDrawableStateChanged(this);
}
}
// 接着看 drawableStateChanged() 方法。
protected void drawableStateChanged() {
// 调用 getDrawableState() 方法得到当前 View 的状态合集,以一个 int 数组的形式存在。
final int[] state = getDrawableState();
// 将状态合集设置给 background,那么 Drawable 就会自己更新状态并通知 View 重新绘制它。
final Drawable bg = mBackground;
if (bg != null && bg.isStateful()) {
bg.setState(state);
}
// 此处省略其他无关源代码...
}
// 接着看 getDrawableState() 方法,它会返回一个 resource ID 数组来表示 View 的当前状态。
public final int[] getDrawableState() {
// 因为 PFLAG_DRAWABLE_STATE_DIRTY 标志位在上面 refreshDrawableState() 方法中已经被设置,
// 所以从 refreshDrawableState() 方法调用进来时肯定会进入下面的 else 分支,
// 从 onCreateDrawableState(0) 方法取得 drawableState
if ((mDrawableState != null) && ((mPrivateFlags & PFLAG_DRAWABLE_STATE_DIRTY) == 0)) {
return mDrawableState;
} else {
mDrawableState = onCreateDrawableState(0);
mPrivateFlags &= ~PFLAG_DRAWABLE_STATE_DIRTY;
return mDrawableState;
}
}
// 接着看 onCreateDrawableState(int extraSpace) 方法。它的作用是生成这个 View 的 Drawable State。
protected int[] onCreateDrawableState(int extraSpace) {
// 如果这个 View 设置了 DUPLICATE_PARENT_STATE 标志位(可通过 setDuplicateParentStateEnabled(boolean enabled)方法来设置),
// 则直接通过父View的状态获得state,并返回。一般的 View 都没有设置这个标志位,所以这个条件一般不满足。
if ((mViewFlags & DUPLICATE_PARENT_STATE) == DUPLICATE_PARENT_STATE &&
mParent instanceof View) {
return ((View) mParent).onCreateDrawableState(extraSpace);
}
int[] drawableState;
int privateFlags = mPrivateFlags;
// 检查这个 View 的 pressed、enabled、focuesed 等状态(系统提供的 View 的状态都会在这里被检查一遍),
// 通过位运算记录在 viewStateIndex 这个整型变量的各个位上
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 &&
HardwareRenderer.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;
}
// 将 viewStateIndex 变量中记录的各个状态转化为一个数组,具体如何转化可以看 StateSet.get 方法,这里不做延伸讨论。
drawableState = StateSet.get(viewStateIndex);
// 如果参数 extraSpace 为 0,那么这个数组就是最终要返回的数组了。
if (extraSpace == 0) {
return drawableState;
}
// 如果 extraSpace 不为 0,那么会将 drawableState 数组的长度扩大 extraSpace 后返回。
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;
}

到此,我们从 View 的 pressed 状态改变开始,根据源码看完了 View 内部如何改变 backgroundDrawable 的状态。简单总结一下:

  1. View 的 pressed 状态改变会调用 setPressed 方法。
  2. setPressed 方法会调用 refreshDrawableState 方法。
  3. refreshDrawableState 中会调用 drawableStateChanged,去更新 drawable 的状态,其中就包括 backgroundDrawable。
  4. drawableStateChanged 方法中,通过 getDrawableState 方法得到 DrawableState 并设置为 backgroundDrawable,那么 Drawable 就会自己更新状态并通知 View 重新绘制。
  5. getDrawableState 方法是通过 onCreateDrawableState(int extraSpace) 方法来得到 DrawableState 的。
    所以,View 的 backgroundDrawable 状态其实是由 onCreateDrawableState(int extraSpace) 方法决定的,而 setPressed 方法只是作为状态改变的整个流程的起点。

看完了源码,我们应该可以解决上面提出的几个问题:

  1. Button(View)的不同状态是如何和 Drawable 关联起来的?
    View 在状态改变时调用 refreshDrawableState 去刷新 Drawable 的状态,而这些状态最终由 onCreateDrawableState(int extraSpace) 方法返回。

  2. 除了上面说的 pressed 和 enabled 状态,我们可以设置的状态还有哪些?
    View#onCreateDrawableState(int extraSpace) 方法,其中检查了 pressed、enabled、focused、selected、window_focused、activated、hardware_accelerated、hovered、drag_can_accept、drag_hovered 状态。所以,对于 View,我们可以控制这些状态。

  3. 如果系统提供的状态不够用,我们能否自己定义状态?
    当然可以,不可以的话我怎么会在这篇文章提出这个问题?其实 View 提供的状态很有限,而很多时候更底层的控件都需要定义更多状态栏满足特定的需求。接下来我们看自定义状态。

自定义状态在系统控件中的使用

我们先来看看系统控件自定义状态的做法。以 CheckBox 为例,CheckBox 是 View 的间接子类(两者中间还有好几层继承关系),提供了一个可勾选框的功能,它可以被 setChecked(boolean checked),并在 checked 为 true/false 时有不同的表现,那么 CheckBox 是如何在 View 的基础上实现 checked 状态的?
搜一下 CheckBox 的 setChecked 方法,实际上这个方法在其父类 CompoundButton 实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public void setChecked(boolean checked) {
if (mChecked != checked) {
mChecked = checked;
refreshDrawableState();
// 此处省略其他无关源代码...
}
}
// 该方法同样调用了 refreshDrawableState() 方法,且在这个类中没有重写 refreshDrawableState() 方法,说明接下来的代码流程会与上述流程一样。
// 但是这个类重写了 drawableStateChanged() 方法和 onCreateDrawableState(int extraSpace) 方法。
@Override
protected void drawableStateChanged() {
super.drawableStateChanged();
// 除了调用 super 的方法,还更新了自己持有的 mButtonDrawable 的状态
if (mButtonDrawable != null) {
int[] myDrawableState = getDrawableState();
// Set the state of the Drawable
mButtonDrawable.setState(myDrawableState);
invalidate();
}
}
// 用数组保存了要自定义的状态的 resource ID,这里自定义了 *checked* 状态
private static final int[] CHECKED_STATE_SET = {
R.attr.state_checked
};
@Override
protected int[] onCreateDrawableState(int extraSpace) {
// 调用 super 的方法时,extraSpace 参数加了 1,
// 实际上这个 1 就是 CHECKED_STATE_SET.length,即自定义的状态的个数
final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
if (isChecked()) {
// 如果当前状态是 checked,则把 super 返回的 drawableState 数组与 CHECKED_STATE_SET 数组合并,
// 合并的结果是在 super 返回的 drawableState 数组的基础上,往数组后面追加了 CHECKED_STATE_SET 数组的内容。
// 最后将数组返回。
mergeDrawableStates(drawableState, CHECKED_STATE_SET);
}
return drawableState;
}

至此,就完成了对 checked 状态的自定义,并能通过 setChecked(boolean checked) 方法来改变 checked 状态。总结一下自定义状态需要做的几件事:

  1. 提供一个改变 View 状态的方法,并在状态改变时调用 refreshDrawableState() 方法。
  2. drawableStateChanged() 方法中,调用自己维护的 Drawable 的 setState 方法,传入 getDrawableState() 返回的值,从而更新 Drawable 的状态。
  3. 定义一个 int 数组,存放自定义的状态。
  4. onCreateDrawableState(int extraSpace) 方法中,调用 super.onCreateDrawableState(int),传入 extraSpace 加上上述 int 数组的长度,并将 super 返回的结果与上述 int 数组用 mergeDrawableStates() 方法合并,最终返回合并后的结果。

实践

看完原理和系统控件的例子,我们也可以来自定义View的状态了。假设我们要实现这样一个需求:有一个 ListView,它的每个 Item 左侧有一个 CheckBox 可对整个列表进行多选操作。
这种情况可以使用自定义状态来完成,Item 是否被 checked 将影响 Drawable 的表现,以下以 Item 的最外层 View 为 LinearLayout 为例,自定义一个 CheckableLinearLayout。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
public class CheckableLinearLayout extends LinearLayout implements Checkable {
private boolean mIsChecked = false;
private Drawable mCheckboxDrawable;
private static final int[] CHECKED_STATE_SET = {
android.R.attr.state_checked
};
public CheckableLinearLayout(Context context) {
super(context);
init();
}
public CheckableLinearLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public CheckableLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mCheckboxDrawable = getResources().getDrawable(R.drawable.qmui_s_dialog_check_mark);
// 恢复 ViewGroup 的 draw 功能(默认关闭),使 onDraw 方法会被调用
setWillNotDraw(false);
}
@Override
protected void drawableStateChanged() {
super.drawableStateChanged();
// 将 getDrawableState 返回的状态数组设置给 mCheckboxDrawable,并触发重绘
if (mCheckboxDrawable != null) {
int[] drawableState = getDrawableState();
mCheckboxDrawable.setState(drawableState);
invalidate();
}
}
@Override
protected int[] onCreateDrawableState(int extraSpace) {
// 调用 super 时参数加上状态集的长度
final int[] drawableState = super.onCreateDrawableState(extraSpace + CHECKED_STATE_SET.length);
if (isChecked()) {
// 被 checked 状态下,在 super 返回的数组上追加自己的状态集合
mergeDrawableStates(drawableState, CHECKED_STATE_SET);
}
return drawableState;
}
@Override
public void setChecked(boolean checked) {
if (mIsChecked != checked) {
mIsChecked = checked;
// checked 状态改变时调用 refreshDrawableState()
refreshDrawableState();
}
}
@Override
public boolean isChecked() {
return mIsChecked;
}
@Override
public void toggle() {
setChecked(!isChecked());
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 将 mCheckboxDrawable 画到 Canvas 上
if (mCheckboxDrawable != null) {
int left = QMUIDisplayHelper.dpToPx(5);
mCheckboxDrawable.setBounds(left, getPaddingTop(),
left + mCheckboxDrawable.getIntrinsicWidth(),
getPaddingTop() + mCheckboxDrawable.getIntrinsicHeight());
mCheckboxDrawable.draw(canvas);
}
}
}

以下是 dialog_check_mark.xml 文件的内容,设置了 normal 情况和 checked 情况下的不同表现。

1
2
3
4
5
6
<!-- dialog_check_mark.xml -->
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/checkbox_checked" android:state_checked="true" />
<item android:drawable="@drawable/checkbox_normal" />
</selector>

到此,就完成了对 LinearLayout 加上 Checked 状态管理的功能,在被调用 setCheck(boolean checked) 方法时,Drawable 的表现会随之改变。

总结

我们从 Button 被 pressed 时的源码入手,分析了 Button(View)和 Drawable 如何关联起来,状态改变时如何通知 Drawable 改变。接着分析了系统控件 CompoundButton 的状态管理。最后自定义了一个包含 Checked 状态的 LinearLayout。