在 Android 开发中,View 和 Drawable 之间关系十分紧密,例如我们经常用 Drawable 作为一个 View 的背景。View 常常会有状态的改变,例如被按下、例如禁用,而不同的状态下 Drawable 也常有不同的表现。今天要探索的问题是 View 的状态改变是如何影响 Drawable 的表现的。
以下将简单介绍我们平时如何在 View 上使用 Drawable,做到在不同状态下表现不一样。接着分析系统源码探索其中的原理。最后以系统的控件和自定义控件 2 个例子来验证和实践在 View 中自定义状态的做法。
注:
- 本文的源码分析基于 Android API Level 23,并省略掉部分与本文关系不大的代码。
- 在代码中加入了个人对源码的理解,以注释形式呈现。
- 本文最后的 DEMO 项目源码托管到 Github 上。
如何给 View 在不同状态下设置不同背景色
可以对一个 View 设置 background 属性,传进去的是一个 Drawable。 如果该 Drawable 是一个 StateListDrawable
(对应的 xml 标签为 <selctor>
),那么它能在不同状态下显示不同的表现。例如一个 Button,可以在 normal、pressed、disabled 等状态下显示不同的背景色,像这样:
|
|
这样即可实现使 Button 在不同状态下颜色不一样,normal 为 #999999,pressed 为 #666666,disabled 时为 #CCCCCC。
原理
以上是经常用来设置 Button 背景的用法,那么实际上 Button(View)的不同状态是如何和 Drawable 关联起来的?除了上面说的 pressed 和 enabled 状态,我们可以设置的状态还有哪些?如果系统提供的状态不够用,我们能否自己定义状态?带着这几个问题,我们来看 Android FrameWork 的源码。
|
|
到此,我们从 View 的 pressed 状态改变开始,根据源码看完了 View 内部如何改变 backgroundDrawable 的状态。简单总结一下:
- View 的 pressed 状态改变会调用
setPressed
方法。 setPressed
方法会调用refreshDrawableState
方法。- 在
refreshDrawableState
中会调用drawableStateChanged
,去更新 drawable 的状态,其中就包括 backgroundDrawable。 - 在
drawableStateChanged
方法中,通过getDrawableState
方法得到 DrawableState 并设置为 backgroundDrawable,那么 Drawable 就会自己更新状态并通知 View 重新绘制。 - 而
getDrawableState
方法是通过onCreateDrawableState(int extraSpace)
方法来得到 DrawableState 的。
所以,View 的 backgroundDrawable 状态其实是由onCreateDrawableState(int extraSpace)
方法决定的,而setPressed
方法只是作为状态改变的整个流程的起点。
看完了源码,我们应该可以解决上面提出的几个问题:
Button(View)的不同状态是如何和 Drawable 关联起来的?
View 在状态改变时调用refreshDrawableState
去刷新 Drawable 的状态,而这些状态最终由onCreateDrawableState(int extraSpace)
方法返回。除了上面说的 pressed 和 enabled 状态,我们可以设置的状态还有哪些?
见View#onCreateDrawableState(int extraSpace)
方法,其中检查了 pressed、enabled、focused、selected、window_focused、activated、hardware_accelerated、hovered、drag_can_accept、drag_hovered 状态。所以,对于 View,我们可以控制这些状态。如果系统提供的状态不够用,我们能否自己定义状态?
当然可以,不可以的话我怎么会在这篇文章提出这个问题?其实 View 提供的状态很有限,而很多时候更底层的控件都需要定义更多状态栏满足特定的需求。接下来我们看自定义状态。
自定义状态在系统控件中的使用
我们先来看看系统控件自定义状态的做法。以 CheckBox 为例,CheckBox 是 View 的间接子类(两者中间还有好几层继承关系),提供了一个可勾选框的功能,它可以被 setChecked(boolean checked),并在 checked 为 true/false 时有不同的表现,那么 CheckBox 是如何在 View 的基础上实现 checked 状态的?
搜一下 CheckBox 的 setChecked
方法,实际上这个方法在其父类 CompoundButton 实现。
至此,就完成了对 checked 状态的自定义,并能通过 setChecked(boolean checked)
方法来改变 checked 状态。总结一下自定义状态需要做的几件事:
- 提供一个改变 View 状态的方法,并在状态改变时调用
refreshDrawableState()
方法。 - 在
drawableStateChanged()
方法中,调用自己维护的 Drawable 的setState
方法,传入getDrawableState()
返回的值,从而更新 Drawable 的状态。 - 定义一个 int 数组,存放自定义的状态。
- 在
onCreateDrawableState(int extraSpace)
方法中,调用super.onCreateDrawableState(int)
,传入 extraSpace 加上上述 int 数组的长度,并将 super 返回的结果与上述 int 数组用mergeDrawableStates()
方法合并,最终返回合并后的结果。
实践
看完原理和系统控件的例子,我们也可以来自定义View的状态了。假设我们要实现这样一个需求:有一个 ListView,它的每个 Item 左侧有一个 CheckBox 可对整个列表进行多选操作。
这种情况可以使用自定义状态来完成,Item 是否被 checked 将影响 Drawable 的表现,以下以 Item 的最外层 View 为 LinearLayout 为例,自定义一个 CheckableLinearLayout。
以下是 dialog_check_mark.xml
文件的内容,设置了 normal 情况和 checked 情况下的不同表现。
到此,就完成了对 LinearLayout 加上 Checked 状态管理的功能,在被调用 setCheck(boolean checked) 方法时,Drawable 的表现会随之改变。
总结
我们从 Button 被 pressed 时的源码入手,分析了 Button(View)和 Drawable 如何关联起来,状态改变时如何通知 Drawable 改变。接着分析了系统控件 CompoundButton 的状态管理。最后自定义了一个包含 Checked 状态的 LinearLayout。