-
在 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 效果会在手指触摸控件时直接生效。
网友评论