android View 不同状态分析

写在前面的话

接触过android开发一段时间的人应该至少都听说过android View的五种状态。最明显的就是设置selector来使用它。今天我就再接再厉回顾一下android View的几种不同状态,并顺便总结下selector的用法,因为这些工作迟早都要做,就乘着考试前的闲暇来完成吧。

触摸反馈是android机遇触摸反馈的UI系统的很重要的一部分。所以一般View的状态改变能够在一定程度上反馈出用户的交互轨迹。就先从View的常见的五种状态说明开始吧。

View的六种状态

Pressed

如果用户点击一个了一个clickable View,该View将处于Pressed状态.而clickable属性你可以通过以下方式来设置。

1
2
3
4
XML:
android:clickable="false"
Java:
View.setClickable(boolean)
;

但是,如果你为该View设置了View.OnClickListener,那么该View将会自动地被设置为clickable,而即使你已经设置android:clickable=”false”也不会生效

Focused

focused用于指示当前哪个View获得了input focus。拿到input focus的View会得到输入事件,例如点击按钮。我们可以用requestFocus()方法来为View申请获取焦点,如果requestFocus()返回true,那么申请的View将会拿到焦点。而一般情况下,View只有在focusable 和focusable in touch mode同时满足的条件下才能获得焦点(TextView).

Note:

selected状态用于focus(android:state_focused)状态不完全成立时 (such as when list view has focus and an item within it is selected with a d-pad).

Activated

可以用activated来表示处于交互中的View被选中。所以可以通过该属性来设置ViewPager 的tab高亮来表示当前可见的page(tabView.setActivitated(true))。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* Changes the activated state of this view. A view can be activated or not.
* Note that activation is not the same as selection. Selection is
* a transient property, representing the view (hierarchy) the user is
* currently interacting with. Activation is a longer-term state that the
* user can move views in and out of. For example, in a list view with
* single or multiple selection enabled, the views in the current selection
* set are activated. (Um, yeah, we are deeply sorry about the terminology
* here.) The activated state is propagated down to children of the view it
* is set on.
*
* @param activated true if the view must be activated, false otherwise
*/
public void setActivated(boolean activated) {
}

Enabled

表示当前View是否可用,如果mView.setEnable(false),那么该View将不会响应onTouch事件。这个属性很常用。例如:你可以设置“登录”button是disabled ,知道用户输入了正确的注册或者登录信息。这样的话,就会对用户产生一个视觉上的提示。

window_focused

用于表示当前窗口是否正在处于交互状态,改状态由OS来设置(多任务切换)。

selected

表示View是否被选中,一个窗口中可以有多个View被选中,可以使用setSelected()来设置。

==================

就介绍这几种吧,关于View状态的更多的信息,可以查看官方文档

Selector使用

在drawable下定义一个xxx.xml文件

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_enabled="false" android:drawable="@android:color/darker_gray" />

<item android:state_activated="true" android:state_pressed="true" android:drawable="@android:color/holo_red_light" />
<item android:state_activated="true" android:state_focused="true" android:drawable="@android:color/holo_purple" />
<item android:state_activated="true" android:drawable="@android:color/holo_orange_dark" />

<item android:state_pressed="true" android:drawable="@android:color/holo_blue_light" />
<item android:state_focused="true" android:drawable="@android:color/holo_green_light" />
<item android:drawable="@android:color/transparent" />
</selector>

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<RelativeLayout android:id="@+id/my_layout"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<TextView
android:id="@+id/my_text"
android:gravity="center"
android:layout_width="150dp"
android:layout_height="150dp"
android:background="@drawable/text_view_selector"
android:text="@string/hello_world"/>

</RelativeLayout>

view的drawable更新流程

那么,android中根据View的不同状态来改变其对应的background是如何实现的呢?

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
/**
* Give this view focus. This will cause
* {@link #onFocusChanged(boolean, int, android.graphics.Rect)} to be called.
*
* Note: this does not check whether this {@link View} should get focus, it just
* gives it focus no matter what. It should only be called internally by framework
* code that knows what it is doing, namely {@link #requestFocus(int, Rect)}.
*
* @param direction values are {@link View#FOCUS_UP}, {@link View#FOCUS_DOWN},
* {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT}. This is the direction which
* focus moved when requestFocus() is called. It may not always
* apply, in which case use the default View.FOCUS_DOWN.
* @param previouslyFocusedRect The rectangle of the view that had focus
* prior in this View's coordinate system.
*/
void handleFocusGainInternal(@FocusRealDirection int direction, Rect previouslyFocusedRect) {
if (DBG) {
System.out.println(this + " requestFocus()");
}

if ((mPrivateFlags & PFLAG_FOCUSED) == 0) {
mPrivateFlags |= PFLAG_FOCUSED;

View oldFocus = (mAttachInfo != null) ? getRootView().findFocus() : null;

if (mParent != null) {
mParent.requestChildFocus(this, this);
}

if (mAttachInfo != null) {
mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(oldFocus, this);
}

onFocusChanged(true, direction, previouslyFocusedRect);
refreshDrawableState();
}
}

可以看到View.java中的这个方法绘制View获得焦点时触发,而它的实质最后是调用了refreshDrawableState();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* Call this to force a view to update its drawable state. This will cause
* drawableStateChanged to be called on this view. Views that are interested
* in the new state should call getDrawableState.
*
* @see #drawableStateChanged
* @see #getDrawableState
*/
public void refreshDrawableState() {
mPrivateFlags |= PFLAG_DRAWABLE_STATE_DIRTY;
drawableStateChanged();

ViewParent parent = mParent;
if (parent != null) {
parent.childDrawableStateChanged(this);
}
}

注释写的很清楚,这个方法的做同事强制更新一个View的drawable state,它会在view状态改变时被回调而改变该View的drawable state。然后它主要是调用了drawableStateChanged()方法。

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
/**
* This function is called whenever the state of the view changes in such
* a way that it impacts the state of drawables being shown.
* <p>
* If the View has a StateListAnimator, it will also be called to run necessary state
* change animations.
* <p>
* Be sure to call through to the superclass when overriding this function.
*
* @see Drawable#setState(int[])
*/
protected void drawableStateChanged() {
//传入该View的drawable对象
final Drawable d = mBackground;
if (d != null && d.isStateful()) {
d.setState(getDrawableState());//更新View为对应状态下的Drawable对象
}

if (mStateListAnimator != null) {
mStateListAnimator.setState(getDrawableState());
}
}

/**
* Return an array of resource IDs of the drawable states representing the
* current state of the view.
*返回一个drawable states对应的resource IDs的array来代表view的当前state
*
* @return The current drawable state
*
* @see Drawable#setState(int[])
* @see #drawableStateChanged()
* @see #onCreateDrawableState(int)
*/
public final int[] getDrawableState() {
if ((mDrawableState != null) && ((mPrivateFlags &
PFLAG_DRAWABLE_STATE_DIRTY) == 0)) {
return mDrawableState;
} else {
mDrawableState = onCreateDrawableState(0);
mPrivateFlags &= ~PFLAG_DRAWABLE_STATE_DIRTY;
return mDrawableState;
}
}

/**
* Specify a set of states for the drawable. These are use-case specific,
* so see the relevant documentation. As an example, the background for
* widgets like Button understand the following states:
* [{@link android.R.attr#state_focused},
* {@link android.R.attr#state_pressed}].
*
* <p>If the new state you are supplying causes the appearance of the
* Drawable to change, then it is responsible for calling
* {@link #invalidateSelf} in order to have itself redrawn, <em>and</em>
* true will be returned from this function.
*
* <p>Note: The Drawable holds a reference on to <var>stateSet</var>
* until a new state array is given to it, so you must not modify this
* array during that time.</p>
*
* @param stateSet The new set of states to be displayed.
*
* @return Returns true if this change in state has caused the appearance
* of the Drawable to change (hence requiring an invalidate), otherwise
* returns false.
*/

//Drawable.java中
public boolean setState(final int[] stateSet) {
if (!Arrays.equals(mStateSet, stateSet)) {
//如果状态态值改变,就回调onStateChange()
mStateSet = stateSet;
return onStateChange(stateSet);
}
return false;
}

可以看到Drawable类abstract class,所以的找到一个该方法的extends类,通过查看文档发现了如下的继承关系。

而StateListDrawable类的作用就是处理selector。看文档说明

1
2
3
4
5
Class Overview

Lets you assign a number of graphic images to a single Drawable and swap out the visible item by a string ID value.

It can be defined in an XML file with the <selector> element. Each state Drawable is defined in a nested <item> element. For more information, see the guide to Drawable Resources.

所以,接下来让我们进入StateListDrawable.onStateChange(int[] stateSet)方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
protected boolean onStateChange(int[] stateSet) {
//返回第一个匹配当前状态的drawable对象的索引
int idx = mStateListState.indexOfStateSet(stateSet);
if (DEBUG) android.util.Log.i(TAG, "onStateChange " + this + " states "
+ Arrays.toString(stateSet) + " found " + idx);
if (idx < 0) {
idx = mStateListState.indexOfStateSet(StateSet.WILD_CARD);
}
//获取idx索引所对应的drawable对象,并保存在mCurrDrawable
if (selectDrawable(idx)) {
return true;
}
return super.onStateChange(stateSet);
}

在看一下StateListDrawable类相关的知识

StateListDrawable类

这个类我觉得常用的就这个方法吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* Add a new image/string ID to the set of images.
*
* @param stateSet - An array of resource Ids to associate with the image.
* Switch to this image by calling setState().
* @param drawable -The image to show.
*/
public void addState(int[] stateSet, Drawable drawable) {
if (drawable != null) {
mStateListState.addStateSet(stateSet, drawable);
// in case the new state matches our current state...
onStateChange(getState());
}
}

作用就是给制定state set 添加a new image/string

感觉整个drawable的就只流程逻辑相对于View绘制流程而言还是比较简单的。今天就看到这里了。健身搞起!