Jaqen's Blog

Android 事件分发机制

字数统计: 3.1k阅读时长: 13 min
2018/10/10 Share

MotionEvent

根据面向对象思想,事件被封装成 MotionEvent 对象,以下是几个与手指触摸相关的常见事件:

  • ACTION_DOWN : 手指初次触摸到屏幕时触发。
  • ACTION_MOVE:手指在屏幕上滑动时触发,会触发多次。
  • ACTION_UP:手指离开屏幕时触发。
  • ACTION_CANCEL:事件被上层拦截时触发。

对于单指操作,一次触摸事件流程是这样的:

按下(ACTION_DOWN)–> 滑动(ACTION_MOVE)–> 离开(ACTION_UP)。如果只是简单的点击,则没有 ACTION_MOVE 事件产生。

事件分发、拦截与消费

与事件分发相关的三个重要方法:

  • dispatchTouchEvent:事件分发机制中的核心,所有的事件调度都归它管。
  • onInterceptTouchEvent:事件拦截。
  • onTouchEvent:事件消费处理。

事件分发流程

事件分发流程示意图:

大致解释一下:

  • 图中 ViewGroup 与 View 之间省略了若干层 ViewGroup。

  • 触摸事件都是先交由 Activity 的 dispatchTouchEvent 方法(在此之间还有一系列的操作,在此省略了),再一层层往下分发。当中间的 ViewGroup 不进行拦截时,事件会分发给最底层的 View,由 View 的 onTouchEvent 方法进行处理,如果事件一直未被处理,最后会返回到 Activity 的 onTouchEvent

  • 图中 View/ViewGroup 的 onTouchEvent 返回 false,并不是直接调用上层的 onTouchEvent 方法。而是上层的 dispatchTouchEvent 方法接收到下层的 false 返回值时,再将事件分发给自己的 onTouchEvent 处理。

  • onInterceptTouchEvent 只存在于 ViewGroup 中。ViewGroup 是根据 onInterceptTouchEvent 的返回值来确定是调用子 View 的 dispatchTouchEvent 还是自身的 onTouchEvent, 并没有将调用交给 onInterceptTouchEvent

源码分析

事件是从 Activity 开始分发,Activity 的 dispatchTouchEvent 是如何接受到触摸事件,还有一系列的前期工作,后面会单独写一篇文章总结。

Activity 对事件的分发流程

Activity.dispatchTouchEvent

1
2
3
4
5
6
7
8
9
10
11
12
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
// 第一次按下时,用户希望与设备进行交互时,可以重写该方法实现
onUserInteraction();
}
// 获取PhoneWindow, 传递事件
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
// 没有任何View处理事件时,交给Activity的onTouchEvent处理
return onTouchEvent(ev);
}

其中 getWindow 返回的是 Activity 的 mWindow 成员变量,而 Window 类是一个抽象类,唯一实现类是 PhoneWindow,所以该方法获取到的是 PhoneWindow 对象。

getWindow().superDispatchTouchEvent

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

可以看到,PhoneWindow 中直接将事件交给了 DecorView 处理,DecorView 的 superDispatchTouchEvent 方法如下。

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

DecorView 调用的是父类的 dispatchTouchEvent 方法,而 DecorView 的父类是 ViewGroup,所以接着会调用 ViewGroup.dispatchTouchEvent

Activity.onTouchEvent

如果没有任何 View 处理事件,最后会交给 Activity 的 onTouchEvent 处理。

1
2
3
4
5
6
7
8
public boolean onTouchEvent(MotionEvent event) {
// 当窗口需要关闭时,消费掉当前事件
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
}

ViewGroup 对事件的分发流程

ViewGroup.dispatchTouchEvent

ViewGroup 的 dispatchTouchEvent 方法内容较多,这里拆分为检测拦截、寻找子 View、分发事件。

1、 检测拦截

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 检测拦截
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
// 可以通过调用 requestDisallowInterceptTouchEvent,不让父 View 拦截事件
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) { // 允许父 View 拦截
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else {
intercepted = false;
}
} else {
// 当前不是 ACTION_DOWN 事件,且没有触摸目标
// 即子视图如果不消费 ACTION_DOWN,那么后续事件也不会分发到,当前父视图拦截处理
intercepted = true;
}

这一段代码的目的是检测 ViewGroup 是否拦截事件。

mFirstTouchTarget 用来记录已经消费事件的子 View,目的是为了后续其他事件分发时直接将事件分发给 mFirstTouchTarget 指向的 View。

FLAG_DISALLOW_INTERCEPT 这个标志位可以影响到 ViewGroup 是否拦截事件,可以通过调用 requestDisallowInterceptTouchEvent 方法来设置,一般用于子 View 当中,禁止父 View 拦截事件,处理滑动冲突。但要注意,requestDisallowInterceptTouchEvent 方法对 ACTION_DOWN 事件是无效的,为什么呢?因为 ViewGroup 的 dispatchTouchEvent 方法每次接收到 ACTION_DOWN 事件时,都会初始化状态。代码如下:

1
2
3
4
5
6
7
// 处理一个初始按下事件 ACTION_DOWN
if (actionMasked == MotionEvent.ACTION_DOWN) {
// 发生 Action_DOWN 事件,取消清除之前所有的触摸目标
cancelAndClearTouchTargets(ev);
// 重置触摸状态,清除 FLAG_DISALLOW_INTERCEPT;设置 mFirstTouchTarget = null
resetTouchState();
}

2、寻找子 View

如果 ViewGroup 没有拦截事件,事件没有被取消,并且是 ACTION_DOWN 事件时,首先会去寻找可以接收事件的子 View。代码如下:

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
// 不取消事件,同时不拦截事件, 并且是Down事件才进入该区域
if (!canceled && !intercepted) {
// 寻找可以获取焦点的视图
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
? findChildWithAccessibilityFocus() : null;

if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int actionIndex = ev.getActionIndex();
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;

// 清空早先的触摸对象
removePointersFromTouchTargets(idBitsToAssign);

final int childrenCount = mChildrenCount;
// 第一次 down 事件,同时子视图不为空时
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// 从视图最上层到最下层,获取所有能接收该事件的子视图
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
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);

// 如果子view无法获取用户焦点,跳过本次循环
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
// 子view可见,并且没有播放动画;触摸事件的坐标落在子view的范围内
// 如果这两个条件有一项不满足,则跳过此次循环
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}

newTouchTarget = getTouchTarget(child);
// 子 view 已经接受触摸事件,将新指针id赋值给它,退出循环
if (newTouchTarget != null) {
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}

resetCancelNextUpFlag(child);
// 如果触摸位置在子view的区域内,把事件分发给子view或者ViewGroup
// 实际调用的是chlid的dispatchTouchEvent方法
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// 子view消费了事件
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();
// 设置接收事件的子view为新的触摸目标,设置为触摸目标链表的头
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
// 子view处理了事件,就跳出for循环
break;
}

ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}

if (newTouchTarget == null && mFirstTouchTarget != null) {
// 将 mFirstTouchTarget 的链表最后的 TouchTarget 赋给 newToughTarget
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
}

寻找子 View 进行分发事件的方法就是遍历子 View,有这样两个条件:

1
2
3
4
5
6
// 1、子view可见,并且没有播放动画;2、触摸事件的坐标落在子view的范围内
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}

这两个条件如果同时满足,则将事件分发给子 View。

接着会调用 dispatchTransformedTouchEvent 方法,可以猜到这个方法中肯定做了事件分发的操作。

如果这个方法返回 true,表示子 View 消费了事件,则会在 addTouchTarget 方法中设置 mFirstTouchTarget ,后续事件(ACTION_MOVE、ACTION_UP)分发时会直接将事件分发给 mFirstTouchTarget 指向的 View。

换句话说,如果子 View 没有消费 ACTION_DOWN 事件,mFirstTouchTarget 就会为 null,也就不会接收其他 ACTION_MOVE、ACTION_UP 等事件,如下代码:

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
if (mFirstTouchTarget == null) {
// 没有触摸target,则由当前ViewGroup来处理(第三个参数child为null)
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// 如果View消费ACTION_DOWN事件,那么ACTION_MOVE、ACTION_UP等事件相继开始执行
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}

3、事件分发

dispatchTransformedTouchEvent 的伪代码如下:

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

如果不存在子 View,ViewGroup 会调用父类 View 的 dispatchTouchEvent 方法,再调用 onTouchEvent 方法处理事件。

如果存在子 View ,将事件分发给子 View 的 dispatchTouchEvent,子 View 如果是 ViewGroup,则会调用 ViewGroup.dispatchTouchEvent,进行拦截检测、寻找子 View、分发事件操作。

View 对事件的分发流程

View.dispatchTouchEvent

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

final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
// 在ACION_DOWN事件之前,如果存在滚动操作则停止。不存在则不进行操作
stopNestedScroll();
}

if (onFilterTouchEventForSecurity(event)) {
// 当存在OnTouchListener,且视图状态为ENABLED时,调用onTouch方法
// mOnTouchListener.onTouch优先于onTouchEvent执行
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true; // 如果已经消费事件,则返回True
}
//如果OnTouch没有消费Touch事件则调用OnTouchEvent
if (!result && onTouchEvent(event)) {
result = true; //如果已经消费事件,则返回True
}
}

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

// 处理取消或抬起操作
if (actionMasked == MotionEvent.ACTION_UP ||
actionMasked == MotionEvent.ACTION_CANCEL ||
(actionMasked == MotionEvent.ACTION_DOWN && !result)) {
stopNestedScroll();
}

return result;
}

如果存在 OnTouchListener,且视图状态为 ENABLED 时,调用onTouch 方法,onTouch 方法会优先处理事件。

如果 onTouch 方法返回 true,表示已经消费了事件,也就不再执行 onTouchEvent 。否则, onTouchEvent 处理事件,返回 true,消费事件,否则不处理事件。

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
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
113
114
115
116
117
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;

// 当View状态为DISABLED,如果可点击或可长按,则返回True,即消费事件
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
return (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
}

if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}

//当View状态为ENABLED,如果可点击或可长按,则返回True,即消费事件;
//与前面的的结合,可得出结论:只要view是可点击或可长按,则消费该事件.
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}

if (prepressed) {
setPressed(true, x, y);
}

if (!mHasPerformedLongPress) {
//这是Tap操作,移除长按回调方法
removeLongPressCallback();

if (!focusTaken) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
//调用View.OnClickListener
if (!post(mPerformClick)) {
performClick();
}
}
}

if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}

if (prepressed) {
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
mUnsetPressedState.run();
}

removeTapCallback();
}
break;

case MotionEvent.ACTION_DOWN:
mHasPerformedLongPress = false;

if (performButtonActionOnTouchDown(event)) {
break;
}

//获取是否处于可滚动的视图内
boolean isInScrollingContainer = isInScrollingContainer();

if (isInScrollingContainer) {
mPrivateFlags |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
//当处于可滚动视图内,则延迟TAP_TIMEOUT,再反馈按压状态
// 用来判断用户是否想要滚动。默认延时为100ms
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
//当不再滚动视图内,则立刻反馈按压状态
setPressed(true, x, y);
checkForLongClick(0); //检测是否是长按
}
break;

case MotionEvent.ACTION_CANCEL:
setPressed(false);
removeTapCallback();
removeLongPressCallback();
break;

case MotionEvent.ACTION_MOVE:
drawableHotspotChanged(x, y);

if (!pointInView(x, y, mTouchSlop)) {
removeTapCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
removeLongPressCallback();
setPressed(false);
}
}
break;
}

return true;
}
return false;
}

只要 view 是可点击或可长按,则消费该事件。

长按事件是在 ACTION_DOWN 事件中检测,单击事件需要两个事件 ACTION_DOWN、ACTION_UP 才能触发。

与 View 相关的各个方法调用顺序应该是这样的:

onTouchListener > onTouchEvent > onLongClickListener > onClickListener

总结

结合源码和事件分发示意图,对事件分发机制总结一下:

1、事件在从 Activity 的 dispatchTouchEvent 往下分发,如果没有 View 消费事件,事件最后会回到 Activity 的 onTouchEvent 方法处理。

2、ViewGroup 可以对事件进行拦截,拦截后事件不再往子 View 分发,交由发生拦截操作的 ViewGroup 的 onTouchEvent 处理。

3、子 View 可以调用 requestDisallowInterceptTouchEvent 方法进行设置,从而阻止父 ViewGroup 的 onInterceptTouchEvent 拦截事件。

4、如果 View 没有消费 ACTION_DOWN 事件,则之后的 ACTION_MOVE 等事件都不会再接收。

5、只要 View 是可点击或者可长按的,则消费该事件。

6、如果当前正在处理的事件被上层拦截,会收到一个 ACTION_CANCEL,后续事件不会再传递过来。

参考

CATALOG
  1. 1. MotionEvent
  2. 2. 事件分发、拦截与消费
  3. 3. 事件分发流程
  4. 4. 源码分析
    1. 4.1. Activity 对事件的分发流程
      1. 4.1.1. Activity.dispatchTouchEvent
      2. 4.1.2. getWindow().superDispatchTouchEvent
      3. 4.1.3. Activity.onTouchEvent
    2. 4.2. ViewGroup 对事件的分发流程
      1. 4.2.1. ViewGroup.dispatchTouchEvent
    3. 4.3. View 对事件的分发流程
      1. 4.3.1. View.dispatchTouchEvent
      2. 4.3.2. View.onTouchEvent
  5. 5. 总结
  6. 6. 参考