美文网首页Android拾萃
合理使用 selector,彻底告别手动改变 drawable、

合理使用 selector,彻底告别手动改变 drawable、

作者: Ronnie_火老师 | 来源:发表于2020-02-03 13:21 被阅读0次
    • 在 Android 中,如果实现下面效果:

      目标效果
    • 左图为默认状态(简称默认态),右图为选中状态(简称选中态),对比二者可以发现,选中态除了多出“✔️”图标外,所有元素的颜色(或背景色)都发生了变化。

    • 如果让你实现,会不会又在代码中根据当前的选中状态,动态去设置整体背景色、文本颜色和⭐️的 ImageDrawable 呢?

    认识和使用 selector

    • drawable 有一类根节点是 selector,selector 一般被称为背景选择器,selector 可以包含多个 item 标签,每个 item 又可以设置 drawable 或 color,由此来表示一系列的 drawable 或 color,并且 item 有一些与 View 的状态有关的属性,比如 android:state_selected、android:state_pressed 等,相当于在某个状态下,有一个 item 与之对应,当符合该状态时,便呈现该 item 标志的 drawable 效果。

    • Item 标签里除了直接设置 drawable 或 color 外,也可以包含 shape 标签,shape 标签又可以包含 solid、corners 标签等,来自定义颜色和圆角程度。其实,因为 color 和 shape 都可以视为 drawable,所以可以直接认为,selector 是一个 drawable list,每一个 item 都可以设置对应的 drawable,并可以有对应的状态。

    • View 的状态

      • 上面提到 View 的状态,总结一下 View 的状态,这里介绍常用的四种(还有其他状态):

        状态 xml 标签 含义
        enable android:state_enabled 是否可点击,可通过 setEnable() 设置
        focused android:state_focused 是否处于获取焦点状态,由用户交互导致,一般由系统切换
        pressed android:state_pressed 处于按下状态,一般是用户交互(触摸)导致
        selected android:state_selected 是否处于被选中状态,可以通过 setSelected 设置
        • 注意 selected、focused 与 pressed 的区别,focused 和 pressed 状态一般是由按键操作引起的,系统自动处理;selected 则完全是由应用程序主动调用 setSelected() 进行控制,一个窗口只能有一个视图获得焦点(focus),而一个窗口可以有多个视图处于”selected”状态中。
        • 所以,当我们使用 state_pressed 状态时,是不需要手动设置的,用户触摸了控件,就会自动触发 state_pressed 状态,当手指离开控件后 state_pressed 状态自然也就消失,回到 normal 状态;而使用 state_selected 状态时,需要手动调用 setSelected(boolean selected),根据传入的 boolean 变量控制显示样式,若想改变样式就需要再次调用 setSelected(boolean selected)。
        • 补充说明一下 StateListDrawable 类,可以认为这是 java 中与 xml 中的 selector 对应的类,该类定义了不同状态值下与之对应的资源,完全可以在 java 逻辑中控制。
    • 重新回到文章开头的需求,通过上面的分析,selector 状态更适合解决上面的问题:

      • 对于整个布局,我们可以最外层使用一种 ViewGroup(比如 LinearLayout),内部分别使用 TextView 和 ImageView 实现视觉样式。显然,我们可以给最外层的 ViewGroup 设置 background 为 selector 类型的 drawable 资源,当选中态时为右侧图示浅橘色背景,非选中态时为左侧浅灰色背景,该 drawable 如下(注意其中标注了 android:state_selected 属性):

      • <?xml version="1.0" encoding="utf-8"?>
        <selector xmlns:android="http://schemas.android.com/apk/res/android">
        
            <item android:state_selected="true">
                <shape android:shape="rectangle">
                    <solid android:color="#FFF6EE" />
                    <corners android:radius="2dp" />
                </shape>
            </item>
        
            <item>
                <shape android:shape="rectangle">
                    <solid android:color="#F5F5F5" />
                    <corners android:radius="2dp" />
                </shape>
            </item>
        
        </selector>
        
      • 对于其中的 TextView,同样对其 color 属性使用 selector drawable,如:

      • <?xml version="1.0" encoding="utf-8"?>
        <selector xmlns:android="http://schemas.android.com/apk/res/android">
            <item android:color="#FFFF8E2F" android:state_selected="true" />
            <item android:color="#99222222" />
        </selector>
        
        • 稍微需要注意的是,如果在 Java 代码中给 TextView 设置这种 color 资源,通过 id 获取资源时,需要使用 Resources 的 getColorStateList (而不是 getColor)方法获取。
      • 对于 ImageView 同样可以类似设置,无非是把上面的 color 换成 drawable;

    设置选中状态

    • 按照上面设置好,就能自动在选中态和非选中态进行切换了吗,答案是否定的。与 android:state_pressed 属性不同,pressed 是系统自动触发的,但 selected 属性是需要手动设置的。另外一个问题是,如果选中状态发生变化时,需要调用所有控件的 setSelected 方法来使得控件颜色或背景色发生变化,也是比较麻烦的,幸运的是,我们只需要调用最外层 ViewGroup 的 setSelected 方法就可以了。

    • 我们从代码上揭示这个秘密吧,下面是 View 的 setSelected 方法:

      • /**
             * Changes the selection state of this view. A view can be selected or not.
             * Note that selection is not the same as focus. Views are typically
             * selected in the context of an AdapterView like ListView or GridView;
             * the selected view is the view that is highlighted.
             *
             * @param selected true if the view must be selected, false otherwise
             */
            public void setSelected(boolean selected) {
                //noinspection DoubleNegation
                if (((mPrivateFlags & PFLAG_SELECTED) != 0) != selected) {
                    mPrivateFlags = (mPrivateFlags & ~PFLAG_SELECTED) | (selected ? PFLAG_SELECTED : 0);
                    if (!selected) resetPressedState();
                    invalidate(true);
                    refreshDrawableState();
                    dispatchSetSelected(selected);
                    if (selected) {
                        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
                    } else {
                        notifyViewAccessibilityStateChangedIfNeeded(
                                AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
                    }
                }
        
      • 从代码可以看到,当 selected 发生改变时:

        • 如果是非选中态,调用 resetPressedState() 重置为正常状态

        • 调用 invalidate() 方法让当前 View 重绘

        • 调用 refreshDrawableState() 强制 View 更新其 drawable state

        • 调用 dispatchSetSelected(selected),将当前 selected 状态进行分发,这是非常重要的一步,这个方法在 View 中是个空方法,在 ViewGroup 中有重写实现如下:

        • @Override
              public void dispatchSetSelected(boolean selected) {
                  final View[] children = mChildren;
                  final int count = mChildrenCount;
                  for (int i = 0; i < count; i++) {
                      children[i].setSelected(selected);
                  }
              }
          
          • 显然,ViewGroup 会依次遍历子 View,然后将 selected 状态传递下去,即调用子 View 的 setSelected() 方法。
          • 从而,我们只需调用最外层 ViewGroup 的 setSelected() 方法,系统会遍历左右的子View,并依次调用子 View 的 setSelected() 方法,所有控件的效果也随即发生了变化,是不需要我们依次调用的。
    • 综上所述,要实现文中开始提到的需求,只需要为所有 View 设置好 selector 背景选择器(或是 colorList),然后在代码中合适时机,调用最外层 ViewGroup 的 setSelected(boolean selected) 方法,告知系统是否为选中状态,系统自动遍历并设置所有子 View 的选中状态,最终效果就会如预期了。

    补充

    • 你可能会疑惑,为什么 pressed 状态不需要手动设置,但 selected 状态需要手动设置呢,其实原因很简单,在 View 的 onTouchEvent 方法中,在 MotionEvent.ACTION_DOWN 事件处理最后,调用了 setPressed(true, x, y) 方法,所以 pressed 效果会在手指触摸控件时直接生效。

    相关文章

      网友评论

        本文标题:合理使用 selector,彻底告别手动改变 drawable、

        本文链接:https://www.haomeiwen.com/subject/aqjhxhtx.html