添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

在 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 等状态下显示不同的背景色,像这样:

    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 的状态。简单总结一下:

  • 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 实现。

    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 状态。总结一下自定义状态需要做的几件事:

  • 提供一个改变 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。

    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。