一、什么是DrawableState?
我第一次见到这个词的时候,也是一脸懵逼。感觉应该是一个很复杂的东西,平时开发中应该用不到,即使用到了应该也很复杂。但如果我告诉你平时用到的selector就是通过DrawableState来控制View在不同状态(比如被按下,不可用,被选中等)显示不同内容的时候,似乎离我们平时的开发特别近。
比如一个Button,它有pressed,focused,或者其它状态。当我们为按下button的是,背景色就会发生变化;当button不可用的时候也会显示不同的color。这些都是DrawableState的功劳。
DrawableState是View的属性,一般是配合Drawable对象进行使用。
二、DrawableState有哪些状态?
android官方API上列出了下面的几个状态,这些状态值都是true/false。
- state_pressed:被按下;
- state_focused:获得焦点,例如当用户选择文本输入时;
- state_hovered:当光标悬停在对象上时应使用此项目,这个通常与focused state相同;
- state_selected:被选中;
- state_checkable:能否被选中;
- state_checked:被选中;
- state_enabled:能够接受触摸或者点击事件
- state_activated:被激活
- android:state_window_focused:应用程序是否在前台。
看完了上面这些状态,再来看下在selector中的使用方法:
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true"
android:drawable="@drawable/button_pressed" /> <!-- pressed -->
<item android:state_focused="true"
android:drawable="@drawable/button_focused" /> <!-- focused -->
<item android:state_hovered="true"
android:drawable="@drawable/button_focused" /> <!-- hovered -->
<item android:drawable="@drawable/button_normal" /> <!-- default -->
</selector>
selector就是标注了不同状态下对应显示不同的内容。
三、View中和DrawableState相关的方法
首先DrawableState是View的方法,是一个int数组,用来储存各种状态。
然后我们来看下相关的方法:(这里主要参考《Android 中 View 的中的 DrawableState》里面写的非常详细)
1. protected int[] onCreateDrawableState(int extraSpace)
- jjavadoc: 这个方法会在 View 的状态改变并影响到正在显示的 drawable 的状态的时候会被调用。如果这个 View 拥有一个 StateListAnimator 的话,这个 StateListAnimator也会被调用来执行必要的状态改变动画。如果重写这个方法的话要确保调用父类的方法。
- View 中的实现: 调用 getDrawableState() 获得当前的 drawable state,并把它赋值给 mBackground 和 mStateListAnimator。
- ViewGroup 中的实现: 如果有 FLAG_NOTIFY_CHILDREN_ON_DRAWABLE_STATE_CHANGE 标志的话,还会遍历每个子 View,如果子 View 有 DUPLICATE_PARENT_STATE 标志,就调用子 View 的 refreshDrawableState() 方法。
2. public final int[] getDrawableState()
- javadoc: 返回一个资源 id 的数组,这些 id 代表着 View 的当前状态。
- View中的实现: 如果没有 PFLAG_DRAWABLE_STATE_DIRTY 标志,直接返回缓存的 mDrawableState;否则,调用 onCreateDrawableState 获取新的状态返回,并把 PFLAG_DRAWABLE_STATE_DIRTY 标志去掉。
- ViewGroup中的实现: 没有重载。
3. protected void drawableStateChanged()
- javadoc: 这个方法会在 View 的状态改变并影响到正在显示的 drawable 的状态的时候会被调用。如果这个 View 拥有一个 StateListAnimator 的话,这个 StateListAnimator也会被调用来执行必要的状态改变动画。如果重写这个方法的话要确保调用父类的方法。
- View 中的实现: 调用 getDrawableState() 获得当前的 drawable state,并把它赋值给 mBackground 和 mStateListAnimator。
- ViewGroup 中的实现: 如果有 FLAG_NOTIFY_CHILDREN_ON_DRAWABLE_STATE_CHANGE 标志的话,还会遍历每个子 View,如果子 View 有 DUPLICATE_PARENT_STATE 标志,就调用子 View 的 refreshDrawableState() 方法。
4. public void refreshDrawableState()
- javadoc: 这个方法被调用来强制更新一个 View 的 drawable state。这个方法会导致 View 的 drawableStateChanged 方法被调用。如果对新的 drawable state 感兴趣,可以通过 getDrawableState 方法获取。
- View中的实现: 设置上 PFLAG_DRAWABLE_STATE_DIRTY 标志,调用 drawableStateChanged(),如果有父 View,调用父 View 的 childDrawableStateChanged() 方法。
- ViewGroup中的实现: 没有重载。
5. protected static int[] mergeDrawableStates(int[] baseState, int[] additionalState)
- javadoc: 将你存储在 additionalState 中的状态和 baseState 中的状态合并到一起(baseState 通常是由 getDrawableState 方法得到的),为了简化,baseState 会作为参数被返回,也就是,baseArray 中必须提前预留存放 additionalState 的空间,否则也无法合并成功。
- View中的实现: 从 baseState 数组第一个为 0 的元素开始,将 additionalState 数组中的内容拷贝过来,最后把 baseState 返回。
- ViewGroup中的实现: 没有重载。
6. public void childDrawableStateChanged(View child)
- javadoc: 这个是 ViewGroup 的方法。如果有 FLAG_ADD_STATES_FROM_CHILDREN 标志的话,刷新这个 ViewGroup 的 drawable state,将子 View 的状态加入进去.
- View中的实现: 没有定义。
- ViewGroup中的实现: 如果有FLAG_ADD_STATES_FROM_CHILDREN 标志,调用 refreshDrawableState() 方法。
四、Java代码中是如何判断DrawableState?
我们在selector中设置不同state下的drawable。接下来当view的state发生了变化,是如何根据不同state,以及怎样去刷新新状态下的drawable?我们从源码上来看一下。
以最简单的button为例,我们设置了state_press时的color,当按下时就会显示state_press = true时的颜色。Button的继承了TextView,TextView又继承了View。所以下面会主要从这三个类的源码上来分析。
- 我们先定义一个selector,内容就是设置normal状态下的颜色,还有state_press=true时的背景色。代码太简单,就不放了。然后在设置Button的background为我们刚才定义的selector。
-
再来看下,Java代码中是如何获取我们设置的selector。background属性是在View中获取的:
然后赋值给一个Drawable对象background。关于Drawable,如果不会的同学可以自己学习一下。
-
当Button被按下,这时候会调用refreshDrawableState()方法来通知View的DawableState状态发生了变化。比如上面说的被按下press:
-
View在刷新DrawableState的时候
这里在刷新的时候,会调用drawableStateChanged通知状态改变。然后通知parent,有子view的DrawableState发生了变化。
-
然后就来看一下Button在drawableStateChanged的时候,做了什么。
结果就是Button中没有对drawableStateChanged的处理,所以要从它的父类中寻找。
Button是继承了TextView,先来看下TextView中是如何处理的。直接上源码:
TextView主要进行了文字颜色(包括textcolor、hint、link时的字体颜色)和Drawables的刷新。这也就是说,如果我们对Button或者TextView、EditText(EditText也是继承了TextView)的字体颜色这是了selector,那么当控件的状态发生了变化,字体颜色也会发生变化。
因为我们在最开始的时候说的是被按下时背景色的变化,所以就先不去看TextColor了。继续找关于background的设置,去super(也就是view)中去找。 -
View中drawableStateChanged()方法进行了哪些处理
- 先获取当前的drawableState和background对象;
- 把新drawableState通过setState方法设置给background对象。如果新的state影响到了显示效果,那么就会返回true,通知要发生变化。
- 然后再判断前景色和滑动,这里和咱们无关。
- 然后判断是否有状态变化的动画效果。如果有的话,根据新状态进行显示。
- 最后如果新状态导致显示效果发生了变化,那么调用invalidate进行view重绘。
- 这样新状态下的drawable就会重新绘到了View的背景上。也就有了我们看到的按下效果。
- 当我们松开按钮后,drawable的状态又恢复了,继续走这么一遍流程。这样按下和松开的效果就有了。