View事件体系(二)

View事件体系(二)

第一篇文章,我们讲解了Android View监听的一系列手段,以及事件分发的流程。而这篇文章,我将对部分源码进行解析,使大家对事件分发流程有一个具体的印象。

Activity的dispatchEvent源码
1
2
3
4
5
6
7
8
9
10
11
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
//交给老板(Activity)的秘书(Window)一步步往下给员工(View)分发事件,返回true表示事件循环结束
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
//若返回false,表示没有员工(View)处理,则交给老板 (Activity)的onTouchEvent处理
return onTouchEvent(ev);
}
Window的dispatchEvent源码

由于window中的dispatchEvent是一个抽象方法,我们只有在PhoneWindow,也就是window的实现类中,找到该方法

1
2
3
public boolean superDispatchTouchEvent(MotionEvent event){
return mDecor.superDispatchTouchEvent(event);
}

这里PhoneWindow将事件传递给了DecorView,DecorView是Window最高等级的View(根View),包含着Activity
的所有的View。在Activity里面setContentView就是它的一个子View。

ViewGroup的dispatchEvent源码

当window把事件传递给DecorView后,因为DecorView是一个继承FrameLayout的类,所以说这里会调用ViewGroup的dispatchEvent方法。

下面是部分关键源码

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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {

......

第一部分 if (actionMasked == MotionEvent.ACTION_DOWN) {
// Throw away all previous state when starting a new touch gesture.
// The framework may have dropped the up or cancel event for the previous gesture
// due to an app switch, ANR, or some other state change.
cancelAndClearTouchTargets(ev);
resetTouchState();
}
=================================================================================================
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
第二部分 } else {
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}

......

=================================================================================================
if (!canceled && !intercepted) {

//..... 一系列判断


final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
第三部分 final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);

// If there is a view that has accessibility focus we want it
// to get the event first and if not handled we will perform a
// normal dispatch. We may do a double iteration but this is
// safer given the timeframe.
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}

if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}

newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}

resetCancelNextUpFlag(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j**) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}

.....
}

....
}
=================================================================================================
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
第四部分 handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
...
}

return handled;
}

以上我将源码分为了四个部分。

第一部分:状态清除
1
2
3
4
5
6
7
8
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Throw away all previous state when starting a new touch gesture.
// The framework may have dropped the up or cancel event for the previous gesture
// due to an app switch, ANR, or some other state change.

cancelAndClearTouchTargets(ev);
resetTouchState();
}

源码的翻译大致为:在开始一个新的触摸手势之前,要取消所有之前的状态。framework可能以及放弃了前一个手势的事件,由于app的切换,ANR,或者其他一些状态的改变。

里面只有两行代码:

  • cancelAndClearTouchTargets(ev)
  • resetTouchState();

我们点开第二个方法,里面的源码是:

1
2
3
4
5
6
7
8
9
/**
* Resets all touch state in preparation for a new cycle.
*/
private void resetTouchState() {
clearTouchTargets();
resetCancelNextUpFlag(this);
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
mNestedScrollAxes = SCROLL_AXIS_NONE;
}

这里对 FLAG_DISALLOW_INTERCEPT (之后第二部分会讲)等状态进行了重置,并且在cancelAndClearTouchTargets(ev)和resetTouchState()中,都有clearTouchTargets()方法,我这个人管不住自己的手,由点进去看了看这个方法的源码!!!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* Clears all touch targets.
*/
private void clearTouchTargets() {
TouchTarget target = mFirstTouchTarget;
if (target != null) {
do {
TouchTarget next = target.next;
target.recycle();
target = next;
} while (target != null);
mFirstTouchTarget = null;
}
}

这个方法的实现,在我们这学期数据结构上貌似见过啊~原来TouchTarget里面的数据结构是链表构成的!这个在之后第二部分也会讲到。

第二部分:View是否拦截点击
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}


我们分为外层和内层分析源码


外层:

1
2
3
4
5
6
7
8
9
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
...
} else {

intercepted = true;
}

由上图源码可以分析出View是否拦截当前事件得满足两个条件之一:

  • 事件类型为ACTION_DOWN
  • mFirstTouchTarget != null

ACTION_DOWN毋庸置疑,也就是按下屏幕的事件。而mFirstTouchTarget是什么呢?—- 我们可以点进TouchTarget看看这个类的注释

1
2
3
4
5
6
7
/* Describes a touched view and the ids of the pointers that it has captured.
*
* This code assumes that pointer ids are always in the range 0..31 such that
* it can use a bitfield to track which pointer ids are present.
* As it happens, the lower layers of the input dispatch pipeline also use the
* same trick so the assumption should be safe here...
*/

大意为描述一个触摸的视图和它捕获的指针的id,看不懂没关系,大概就是一个指向表头的单链表,表示当前View的子View是否消耗了该事件。若子View消耗了该事件,则mFirstTouchTarget != null 成立,也就是说若该View拦截了事件,则子View无法获取事件,mFirstTouchTarget==null。

所以,当满足了上面两个条件之一,就表示该View不拦截事件,否则 intercepted = true,拦截该事件。






内层:

1
2
3
4
5
6
7
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) { //若拦截
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {//不拦截
intercepted = false;
}

这里有一个特殊的情况,FLAG_DISALLOW_INTERCEPT标记位,这个标记为是通过requestDisallowInterceptTouchEvent设置的,一般用于子View中。一旦设置了这个标记位,ViewGroup将无法拦截除了 ACTION_DOWN 外的其他事件,子View达到了阻止父View拦截事件的目的。但在第一部分讲过,当点击事件为 ACTION_DOWN 的时候,会清除这个FLAG,所以,每次遇见ACTION_DOWN事件时,ViewGroup都会调用onInterceptTouchEvent来询问自己是否拦截事件。

当父View不拦截事件时候,即intercepted = false时,我们分析第三部分。

第三部分
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
if (!canceled && !intercepted) {

//..... 一系列判断


final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {//遍历元素
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);

...

if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {//是否能够点击到
ev.setTargetAccessibilityFocus(false);
continue;
}


newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}

resetCancelNextUpFlag(child);

//下面的方法实际是调用子VIew的dispatchTouchEvent
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j**) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();

//给mFirstTouchTarget赋值并跳出元素
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}

.....
}

....
}

我们来分析一下上面的步骤:

  1. ViewGroup遍历自身的所有子元素
  2. 判断子View是否在播放动画,和点击事件的坐标是否落在子View的区域,若某个子View满足这两个条件,则事件会交给它处理
  3. 调用dispatchTransformedTouchEvent,内部实际上是调用子View的dispatchTouchEvent。其内部有这样一段代码,若child不是null 则会调用子View的dispatchTouchEvent。

dispatchTransformedTouchEvent源码

1
2
3
4
5
if(child == null){
handled = super.dispatchTouchEvent(event);
}else{
handled = child.dispatchTouchEvent(event);
}

  1. 若子View的dispatchTouchEvent返回的是true,也就是dispatchTransformedTouchEvent为true时,最后会给mFirstTouchTarget赋值并且跳出循环。
  2. 1
    2
    3
    newTouchTarget = addTouchTarget(child, idBitsToAssign);
    alreadyDispatchedToNewTouchTarget = true;
    break;
第四部分:
1
2
3
4
5
6
7
8
9
10
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
...
}

由此,第四部分中,若遍历所有子View都没有被合适的处理,结合第三部分mFirstTouchTarget未被赋值等于null时,则会调用dispatchTransformedTouchEvent,这个方法第三个参数为null时,由上面的源码可以看到,会调用super.dispatchTouchEvent方法。

View的dispatchEvent
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
public boolean dispatchTouchEvent(MotionEvent event) {

...

if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;

//判断是否由onTouchListener
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}

if (!result && onTouchEvent(event)) {
result = true;
}
}

if (!result && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}

...

return result;
}

在源码中,会判断有没有设置OnTouchListener,如果OnTouchListener.OnTouch返回true,那么onTouchEvent就不会被调用,所以说OnTouchListener的优先级高于onTouchEvent,方便在外部设置Listener监听事件。

View的onTouchEvent部分源码
1
2
3
4
5
6
7
8
9
10
11
12
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return clickable;

从上面可知,当View为DISABLE的时候,也会消耗点击事件。

1
2
3
4
5
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}

若View设置了代理,即代理不为null则会执行TouchDelegate的onTouchEvent方法。由于我们探究的是View的事件分发,就不深入研究了。

接下来是onTouchEvent对点击事件的具体处理

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
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
...若不可点击 移除回调等

if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {

...获取焦点

if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// This is a tap, so remove the longpress check
removeLongPressCallback();

// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
}

...
}
mIgnoreNextUpEvent = false;
break;

...

return true;
}

可以看到,首先会判断是否是clickable或者是否为短时间滑动/滚动。然后,在ACTION_UP发生的时候,会触发performClick()方法,如果View设置了OnClickListener,那么performClick方法回调用它内部的onClick方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}

sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);

notifyEnterOrExitForAutoFillIfNeeded(true);

return result;
}

当我们设置OnClickListener或者OnLongClickListener时,View会自动将其CLICKABLE设置为true。

1
2
3
4
5
6
public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
1
2
3
4
5
6
public void setOnLongClickListener(@Nullable OnLongClickListener l) {
if (!isLongClickable()) {
setLongClickable(true);
}
getListenerInfo().mOnLongClickListener = l;
}

到此,事件分发相关源码告一段落,感觉自己的分析还不是很到位,没有继续再往深了拓展,目前就先了解基本流程和基本源码吧,以后有时间继续深入。

0%