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

  • 在 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 效果会在手指触摸控件时直接生效。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,826评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,968评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,234评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,562评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,611评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,482评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,271评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,166评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,608评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,814评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,926评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,644评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,249评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,866评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,991评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,063评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,871评论 2 354