Android View 事件传递机制

源码基于 sdk 30(Android 11.0/R)。

Android 事件机制包含系统启动流程、输入管理(InputManager)、系统服务和 UI 的通信(WindowManagerService + ViewRootImpl + Window)、事件分发等一系列的环节。

Android 系统中将输入事件定义为 InputEvent,根据输入事件的类型又分为了 KeyEvent(键盘事件) 和 MotionEvent(屏幕触摸事件)。这些事件统一由系统输入管理器 InputManager 进行分发。

在系统启动的时候,SystemServer 会启动 WindowManagerService,WMS 在启动的时候通过 InputManager 来负责监控键盘消息。

InputManager 负责从硬件接收输入事件,并将事件通过 ViewRootImpl 分发给当前激活的窗口处理,进而分发给 View。

Window 和 InputManagerService 之间通过 InputChannel 来通信,底层通过 socket 进行通信。

Android Touch 事件的基础知识:

  1. 所有的 Touch 事件都封装到 MotionEvent 里面。
  2. 事件类型分为 ACTION_DOWN(手指按下屏幕的瞬间), ACTION_MOVE(手指在屏幕上移动),ACTION_UP(手指离开屏幕瞬间), ACTION_CANCEL(取消手势,一般由程序产生,不会由用户产生)等。每个事件传递过程都是由一个 ACTION_DOWN 开始,经过 n 个 ACTION_MOVE,最终以一个 ACTION_UP/ACTION_CANCEL 结束。
  3. 事件处理包括三种情况,分别为:
    1. 传递 View#dispatchTouchEvent(MotionEvent event)
    2. 拦截 ViewGroup#onInterceptTouchEvent(MotionEvent ev)
    3. 消费 View#OnTouchListener.onTouch(View v, MotionEvent event) 和 View#onTouchEvent(MotionEvent event)。
  4. 所有的触摸事件都会到达 Activity,Activity 内部通过 Window 连接着一个 View 的根布局 DecorView,事件会从 Activity 一层一层传递到最内层的 View。

View 事件分发

事件架构

Android 事件分发体系

InputEvent:输入事件抽象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* Common base class for input events.
*/
public abstract class InputEvent implements Parcelable {}

/**
* Object used to report key and button events.
*/
public class KeyEvent extends InputEvent implements Parcelable {}

/**
* Object used to report movement (mouse, pen, finger, trackball) events.
*/
public final class MotionEvent extends InputEvent implements Parcelable {}

KeyEvent 对应了键盘的输入事件;MotionEvent 就是手势事件,鼠标、笔、手指、轨迹球等相关输入设备的事件都属于 MotionEvent。

InputManager:输入管理器

InputEvent 统一由 InputManager 进行分发,负责与硬件通信并接收输入事件。

system_server 进程启动时会创建 InputManagerService 服务。

1
2
3
4
5
6
7
8
// SystemServer.java
private void startOtherServices(@NonNull TimingsTraceAndSlog t) {
InputManagerService inputManager = new InputManagerService(context);
// 实例化 InputManagerCallback
WindowManagerService wm = WindowManagerService.main(context, inputManager, ...);
inputManager.setWindowManagerCallbacks(wm.getInputManagerCallback());
inputManager.start();
}
1
2
3
4
5
6
7
8
9
10
public class InputManagerService extends IInputManager.Stub {
public InputManagerService(Context context) {
this.mContext = context;
this.mHandler = new InputManagerHandler(DisplayThread.get().getLooper());
mPtr = nativeInit(this, mContext, mHandler.getLooper().getQueue());
}

private static native long nativeInit(InputManagerService service,
Context context, MessageQueue messageQueue);
}

system_server 进程启动时同时会启动 WMS,WMS 在启动的时候就会通过 IMS 启动 InputManager 来监控键盘消息。

InputEventReceiver:事件接收器

App 端与服务端建立了双向通信之后,InputManager 就能够将产生的输入事件从底层硬件分发过来,Android 提供了 InputEventReceiver 类,以接收分发这些消息:

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
/**
* Provides a low-level mechanism for an application to receive input events.
* @hide
*/
public abstract class InputEventReceiver {
public InputEventReceiver(InputChannel inputChannel, Looper looper) {
mInputChannel = inputChannel;
mMessageQueue = looper.getQueue();
mReceiverPtr = nativeInit(new WeakReference<InputEventReceiver>(this),
inputChannel, mMessageQueue);
}

// Called from native code.
private void dispatchInputEvent(int seq, InputEvent event) {
mSeqMap.put(event.getSequenceNumber(), seq);
onInputEvent(event);
}

public void onInputEvent(InputEvent event) {
finishInputEvent(event, false);
}

public final void finishInputEvent(InputEvent event, boolean handled) {
nativeFinishInputEvent(mReceiverPtr, seq, handled);
event.recycleIfNeededAfterDispatch();
}
}
1
2
3
4
5
6
7
8
// ViewRootImpl.java
final class WindowInputEventReceiver extends InputEventReceiver {
@Override
public void onInputEvent(InputEvent event) {
// 将输入事件加入队列,开始事件分发
enqueueInputEvent(event, this, 0, true);
}
}

InputChannel

Window 和 IMS 之间通过 InputChannel 通信。InputChannel 是一个 pipe,底层通过 socket 进行通信。在 ViewRootImpl.setView() 过程中注册 InputChannel。

1
2
3
4
5
6
/**
* An input channel specifies the file descriptors used to send input events to a window in another process.
* It is Parcelable so that it can be sent to the process that is to receive events.
* Only one thread should be reading from an InputChannel at a time.
**/
public final class InputChannel implements Parcelable {}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ViewRootImpl.java
public void setView(){
requestLayout();
InputChannel inputChannel = new InputChannel();
// 通过 Binder 在 system_server 进程中完成 InputChannel 的注册
res = mWindowSession.addToDisplayAsUser(mWindow, inputChannel, ...);
if (mInputQueueCallback != null) {
mInputQueue = new InputQueue();
mInputQueueCallback.onInputQueueCreated(mInputQueue);
}
mInputEventReceiver = new WindowInputEventReceiver(inputChannel, Looper.myLooper());

// 设置 InputStage
mSyntheticInputStage = new SyntheticInputStage();
InputStage viewPostImeStage = new ViewPostImeInputStage(mSyntheticInputStage);
// nativePostImeStage earlyPostImeStage imeStage viewPreImeStage nativePreImeStage
}

InputStage

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
public class ViewRootImpl{
/**
* Base class for implementing a stage in the chain of responsibility
* for processing input events.
*/
abstract class InputStage {
protected int onProcess(QueuedInputEvent q) {
return FORWARD;
}
}

/**
* Delivers post-ime input events to the view hierarchy.
*/
final class ViewPostImeInputStage extends InputStage {
@Override
protected int onProcess(QueuedInputEvent q) {
if (q.mEvent instanceof KeyEvent) {
return processKeyEvent(q);
} else {
final int source = q.mEvent.getSource();
if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {
return processPointerEvent(q);
} else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {
return processTrackballEvent(q);
} else {
return processGenericMotionEvent(q);
}
}
}
}

private int processPointerEvent(QueuedInputEvent q) {
final MotionEvent event = (MotionEvent)q.mEvent;
// mView 是 DecorView 类型
boolean handled = mView.dispatchPointerEvent(event);
return handled ? FINISH_HANDLED : FORWARD;
}
}

public class DecorView {
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
final Window.Callback cb = mWindow.getCallback();
return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
}
}
  1. ViewPostImeInputStage 是 Touch 事件传递的起点;
  2. Activity 实现了 Window.Callback 接口,DecorView 会把事件分发到 Activity#dispatchTouchEvent(ev) 方法。

事件传递流程

Android 事件传递机制是先分发再处理,先由外部的 View 接收,然后依次传递给其内层的 View,再从最内层 View 反向依次向外层传递。

  1. 事件从 Activity#dispatchTouchEvent() 开始传递,再传递给 DecorView;
  2. DecorView 中先执行 dispatchTouchEvent() 方法,在其内部会先调用 onInterceptTouchEvent() 询问是否拦截事件,若拦截则执行 onTouchEvent() 方法处理这个事件;
  3. 若不拦截,则执行子 View 的 dispatchTouchEvent() 方法,进入向下分发的传递,直到事件被处理;
  4. 如果事件从外向内传递过程中一直没有被拦截,且最底层子 View 没有消费事件,则事件会反向向上传递。这时父 View 可以进行消费,如果还是没有被消费,最后会传递到 Activity#onTouchEvent() 方法;
  5. 如果 View 没有对 ACTION_DOWN 进行消费,之后的其它类型的事件也不会传递过来,也就是说 ACTION_DOWN 必须返回 true,之后的事件才会传递进来。
  6. 事件处理优先级是
    OnTouhListener#onTouch(View v, MotionEvent event) -> onTouchEvent() -> OnClickListener#onClick(View v)

三个方法的关系如下:

1
2
3
4
5
6
7
8
9
public boolean dispatchTouchEvent(MotionEvent event){
boolean consum = false;
if(onInterceptTouchEvent(event)){
consum = onTouchEvent(event);
} else {
consum = child.dispatchTouchEvent(event);
}
return consum;
}

思维导图

源码分析

Activity 的事件处理

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
public class Activity {
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
// 调用 mDecor.superDispatchTouchEvent(event);
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}

public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}

return false;
}
}

public class PhoneWindow extends Window {
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
}

public class Window{
public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
final boolean isOutside =
event.getAction() == MotionEvent.ACTION_UP && isOutOfBounds(context, event)
|| event.getAction() == MotionEvent.ACTION_OUTSIDE;
if (mCloseOnTouchOutside && peekDecorView() != null && isOutside) {
return true;
}
return false;
}
}
  1. Activity 接收到 ACTION_DOWN 事件后,调用 onUserInteraction() 方法,表示和用户进行了交互;
  2. Activity 接收到事件后经 PhoneWindow 传递给 DecorView;
  3. 传递给 DecorView 后如果此事件没有被消费,则事件交给 Activity#onTouchEvent() 处理。

ViewGroup 的事件处理

分发事件:

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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
// ViewGroup.java
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean handled = false;
// ACTION_DOWN 说明是新事件,重置状态
if (actionMasked == MotionEvent.ACTION_DOWN) {
// 清空 TouchTarget 链表
cancelAndClearTouchTargets(ev);
// 重置事件状态
resetTouchState();
}

final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
// 判断是否拦截事件
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
// 如果拦截,事件由当前 ViewGroup 处理
intercepted = onInterceptTouchEvent(ev);
// 防止onInterceptTouchEvent()的时候改变Action
ev.setAction(action);
} else {
intercepted = false;
}
} else {
//没有 TouchTarget 而且非 ACTION_DOWN 类型的事件,由 ViewGroup 处理
intercepted = true;
}

// 如果给子View发送cancel事件后mFirstTouchTarget会变null,
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
// 事件没有取消也没有拦截,向子 View 传递
if (!canceled && !intercepted) {
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int childrenCount = mChildrenCount;
// 遍历查找第一个消费这个事件的子View,并设置为 Target
if (newTouchTarget == null && childrenCount != 0) {
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final View[] children = mChildren;
// 按 View 添加顺序倒序查找
for (int i = childrenCount - 1; i >= 0; i--) {
final View child = getAndVerifyPreorderedView(...);
// 判断事件坐标是否在 View 内
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {
continue;
}
// 更新 target
newTouchTarget = getTouchTarget(child);
// 分发事件到点击的 View
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// View 处理该事件,添加到 TouchTarget 调用链,赋值 mFirstTouchTarget
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
if (newTouchTarget == null && mFirstTouchTarget != null) {
// Did not find a child to receive the event.
// Assign the pointer to the least recently added target.
// ...
}
}
}

if (mFirstTouchTarget == null) {
// 没有找到处理此事件的子 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.
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
// 如果拦截了事件,给子 View 发送 cancel 事件
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
// 发送 Cancel 事件后,将 target 节点从链表中删除
// 也就是为什么 View 收到 cancel 事件后,后续的事件接收不到的原因
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
}
return handled;
}

// 第二个参数cancel包含两种含义,一种是外部收到了取消事件,另一种是事件被拦截
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
final int oldAction = event.getAction();
// cancel 事件处理
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}

if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
handled = child.dispatchTouchEvent(transformedEvent);
}

return handled;
}

应用了树的深度优先搜索算法(Depth-First-Search,简称 DFS 算法),每个 ViewGroup 都持有一个 mFirstTouchTarget, 当接收到 ACTION_DOWN 时,通过递归遍历找到 View 树中真正对事件进行消费的 Child,并保存在 mFirstTouchTarget 属性中,依此类推组成一个完整的分发链。在这之后,当接收到同一事件序列的其它事件如 ACTION_MOVE、ACTION_UP 时,则会跳过递归流程,将事件直接分发给下一级的 Child。

ViewGroup 分发事件的主要的任务是找一个 Target,并且用这个 Target 处理事件,主要逻辑如下 :

  1. 在 ACTION_DOWN 事件并且当前 ViewGroup 不拦截时会查找 TouchTarget。按 View 添加顺序倒序遍历子 View,判断子 View 的 dispatchTouchEvent() 是否为 true。查找 TouchTarget 的过程只有在 ACTION_DOWN 事件中才会触发。
  2. 如果子 View 的 dispatchTouchEvent() 返回 true,则这个子 View 就是当前 ViewGroup 的 Target。
  3. 如果 TouchTarget 不存在,则调用当前 ViewGroup#onTouchEvent() 方法,仍不消费,会向上调传递直到 Activity#onTouchEvent()。
  4. 子 View 可以调用父 View 的 requestDisallowInterceptTouchEvent(true) 请求父 View 不拦截此事件,交给 子 View 处理。只限于一个 Touch 过程(Down->Up/Cancel)。
  5. 处理 Cancel 事件,将 View 从链表中删除。

为什么倒序查找 TouchTarget?
如果按添加顺序遍历,当 View 重叠时(FrameLayout),先添加的 View 总是能消费事件,而后添加的 View 不可能获取到事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// ViewGroup.java
public boolean dispatchTouchEvent(MotionEvent ev) {
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = ...;
final View child = ...;
// 重叠的子 View 都包含点击区域
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {
continue;
}
// 非容器 View 默认都会消费事件,从而跳出循环
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
}

拦截事件:

1
2
3
4
5
6
7
8
9
10
// ViewGroup.java
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
&& ev.getAction() == MotionEvent.ACTION_DOWN
&& ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
&& isOnScrollbarThumb(ev.getX(), ev.getY())) {
return true;
}
return false;
}
  1. 默认返回 false,不拦截事件。
  2. ViewGroup 子类可以重写该方法,决定是否拦截事件。
  3. 子 View 通过 parentView.requestDisallowInterceptTouchEvent(true) 请求父 View 不拦截此事件。

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
// View.java
public boolean dispatchTouchEvent(MotionEvent event) {
if (event.isTargetAccessibilityFocus()) {
if (!isAccessibilityFocusedViewOrHost()) {
return false;
}
event.setTargetAccessibilityFocus(false);
}
boolean result = false;

// 停止嵌套滑动
if (actionMasked == MotionEvent.ACTION_DOWN) {
stopNestedScroll();
}

if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
// 如果 View 可用并设置了 OnTouchListener,如果回调方法 onTouch() 返回 true 则消费事件
// 并且不会调用 onTouchEvent() 方法
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}

if (!result && onTouchEvent(event)) {
result = true;
}
return result;
}
  1. 安全监测。如果 View#setFilterTouchesWhenObscured(true) 开启了安全检测,当 View 所在的 Window 被覆盖时不处理 Touch 事件;
  2. 停止嵌套滑动(5.0 以后添加);
  3. View 可用时才调用 OnTouchListener#onTouch() 方法;
  4. 不管 View 是否可用,只要 OnTouchListener 不消费事件,就会让 onTouchEvent() 处理;
  5. View 不可用时,不处理 OnTouchListener#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
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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
// View.java
public boolean onTouchEvent(MotionEvent event) {
// 判断是否可以点击
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

// View 不可用时可以消费事件,只是没有响应
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
return clickable;
}

// 事件代理
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}

// 不管是否可用,只有是可点击的就消费事件
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
// 只有在 press 的情况下,才 click,longClick
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
// 如果我们在当前View还没获取焦点,并且能在touch下foucus
// 那么第一次点击只会将这个View的状态改成focus,而不会触发click
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}

// 设置 Pressed 状态,更新 View 背景
if (prepressed) {
setPressed(true, x, y);
}

// 检查 longClick 是否已执行
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)) {
performClickInternal();
}
}
}

removeTapCallback();
}
break;

case MotionEvent.ACTION_DOWN:

if (!clickable) {
// 400 ms
checkForLongClick(ViewConfiguration.getLongPressTimeout(),...);
break;
}

if (performButtonActionOnTouchDown(event)) {
break;
}

boolean isInScrollingContainer = isInScrollingContainer();

if (isInScrollingContainer) {
// ...
} else {
setPressed(true, x, y);k
checkForLongClick(ViewConfiguration.getLongPressTimeout(),...);
}
break;

case MotionEvent.ACTION_CANCEL:
if (clickable) {
setPressed(false);
}
removeTapCallback();
removeLongPressCallback();
break;

case MotionEvent.ACTION_MOVE:
if (clickable) {
drawableHotspotChanged(x, y);
}

final int motionClassification = event.getClassification();
final boolean ambiguousGesture = ...;
int touchSlop = mTouchSlop;
if (ambiguousGesture && hasPendingLongPressCallback()) {
if (!pointInView(x, y, touchSlop)) {
removeLongPressCallback();
checkForLongClick(...);
}
}

// Be lenient about moving outside of buttons
if (!pointInView(x, y, touchSlop)) {
removeTapCallback();
removeLongPressCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
}

final boolean deepPress = ;
if (deepPress && hasPendingLongPressCallback()) {
// process the long click action immediately
removeLongPressCallback();
checkForLongClick(...);
}

break;
}

return true;
}

return false;
}
  1. 如果 View 不可用但是可点击,会直接消费事件,只是不做其他任何操作;
  2. 如果 View 可用并设置了 TouchDelegate,则事件交给 TouchDelegate 处理;
  3. 不管 View 是否可用,只要 View 可点击,默认都会消费事件;
  4. 处理 focus,press,click,longclick 等。

TouchDelegate

1
2
3
4
5
6
7
8
9
// View.java
public boolean onTouchEvent(MotionEvent event) {
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
// ...
}

从源码看出,如果 View 设置了事件代理,则事件会首先交给 mTouchDelegate 对象处理,如果消费了事件,当前 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
/**
* Helper class to handle situations where you want a view to have a larger touch area than its
* actual view bounds. The view whose touch area is changed is called the delegate view. This
* class should be used by an ancestor of the delegate. To use a TouchDelegate, first create an
* instance that specifies the bounds that should be mapped to the delegate and the delegate
* view itself.
* The ancestor should then forward all of its touch events received in its
* {@link android.view.View#onTouchEvent(MotionEvent)} to {@link #onTouchEvent(MotionEvent)}.
*/
public class TouchDelegate {
public TouchDelegate(Rect bounds, View delegateView) {
mBounds = bounds;

mSlop = ViewConfiguration.get(delegateView.getContext()).getScaledTouchSlop();
mSlopBounds = new Rect(bounds);
mSlopBounds.inset(-mSlop, -mSlop);
mDelegateView = delegateView;
}

public boolean onTouchEvent(@NonNull MotionEvent event) {
int x = (int)event.getX();
int y = (int)event.getY();
boolean sendToDelegate = false;
boolean hit = true;
boolean handled = false;

switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
// DOWN 事件在设置的区域内,消费事件
mDelegateTargeted = mBounds.contains(x, y);
sendToDelegate = mDelegateTargeted;
break;
case MotionEvent.ACTION_POINTER_DOWN:
case MotionEvent.ACTION_POINTER_UP:
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_MOVE:
sendToDelegate = mDelegateTargeted;
if (sendToDelegate) {
Rect slopBounds = mSlopBounds;
if (!slopBounds.contains(x, y)) {
hit = false;
}
}
break;
case MotionEvent.ACTION_CANCEL:
sendToDelegate = mDelegateTargeted;
mDelegateTargeted = false;
break;
}
if (sendToDelegate) {
if (hit) {
// Offset event coordinates to be inside the target view
event.setLocation(mDelegateView.getWidth() / 2, mDelegateView.getHeight() / 2);
} else {
// Offset event coordinates to be outside the target view (in case it does
// something like tracking pressed state)
int slop = mSlop;
event.setLocation(-(slop * 2), -(slop * 2));
}
handled = mDelegateView.dispatchTouchEvent(event);
}
return handled;
}
}
  1. bounds 是指消费事件的区域;
  2. mDelegateView 是指消费事件后将事件分发给代理 View;

示例一,扩大 View 的点击范围:

1
2
3
4
5
6
7
8
9
10
11
// 使用 post() 获取 View 的宽高
textView.post {
with(textView) {
// textView 在父 View 中的位置
val rect = Rect(left, top, left + width, top + height)
// 扩大响应范围。负数扩大区域;正数缩小区域
rect.inset(-100, -100)
// 给 textView 的父 View 设置代理,将本该属于父 View 的区域代理给了子 View 处理
(parent as View).touchDelegate = TouchDelegate(rect, textView)
}
}

示例二,点击 View 时响应的是其他 View 的逻辑:

1
2
3
4
5
6
7
8
9
// 使用 post() 获取 View 的宽高
textView.post {
with(textView) {
// textView 的点击区域
val rect = Rect(0, 0, right, bottom)
// 给 textView 设置代理,如果事件在此区域内,则将事件传递给 anotherView 处理
touchDelegate = TouchDelegate(rect, anotherView)
}
}

View 滑动冲突

FAQ

  1. View.GONE 后仍消费事件的原因?
    可能该 View 执行了动画,要移除。
    android View with View.GONE still receives onTouch and onClick
    View with visibility View.GONE still generates touch events
  2. 触摸 button 后滑动到外部抬起会触发点击事件吗?如果再滑动回去会触发吗?
    不会触发点击事件。因为按下(ACTION_DOWN)时设置了 PFLAG_PRESSED 标记,移动(ACTION_MOVE)到 View 外部后清除了该标记,即使再滑动到 View 内也不会重新设置。当抬起(ACTION_UP)时,判断设置了 PFLAG_PRESSED 才会触发点击事件。
  3. ACTION_CANCEL 事件怎么产生,作用是什么?
    如果子 View 处理了 ACTION_DOWN 事件,那么正常情况下后面的 ACTION_MOVE 和 ACTION_UP 事件也会交给它处理。但是交给它处理之前,父 View 还是可以拦截事件的,如果拦截了事件,系统会生成 ACTION_CANCEL 事件给子 View,并且不会收到后续的事件,用于重置状态。

总结

  1. 一个点击事件是从 Activity->Window->DecorView->若干 ViewGroup->View,传递过程如下:
    Activity#dispatchTouchEvent() -> DecorView#dispatchTouchEvent() -> View#OnTouchListener.onTouch() -> View#onTouchEvent() -> View#OnClickListener.onClick()
  2. 如果一个 View 的 onTouchEvent 方法返回 false,那么将会交给父容器的 onTouchEvent 方法进行处理,逐级往上,如果所有的 View 都不处理该事件,则交由 Activity#onTouchEvent() 处理。
  3. ViewGroup 默认不拦截任何事件,可以通过重写 onInterceptTouchEvent() 方法拦截事件,执行自己对应的 onTouchEvent() 方法。
  4. 子 View 可以通过调用父 View 的 requestDisallowInterceptTouchEvent(true) 请求 ViewGroup 把事件交给子 View 处理;
  5. 如果 ViewGroup 找到了能够处理该事件的 View(TouchTarget),默认情况后续的事件也会直接交给子 View 处理;
  6. 如果某一个 View 不消耗 ACTION_DOWN 事件,则同一事件序列中不会再交给该 View 处理;
  7. 非容器的 View,一旦接收到事件默认都会消耗事件,除非它是不可点击的(clickable 和 longClickable 都为 false),那么就会由父容器的 onTouchEvent() 处理;
  8. View 的 visible 属性对事件传递没有影响;
  9. 如果当前 View 是可点击的,并且它收到了 down 和 up 事件,则它的 click 事件就会触发;对于 onLongClick,则只要当前 View 接收到 down 事件超过了系统默认的时间;
  10. 当某个子 View 消费事件时,会中止 Down 事件的分发,同时在 ViewGroup 中记录该子 View。同一序列中的 Move 和 Up 事件将由该子 View 直接进行处理。
  11. 当子 View 都不消费 Down 事件时,将调用 ViewGroup#onTouchEvent() 方法。触发的方式是调用 super.dispatchTouchEvent() 函数,即父类 View#dispatchTouchEvent 方法。如果所有子 View 都不处理,则调用 Acitivity#onTouchEvent() 方法。
  12. 如果 View 没有对 ACTION_DOWN 进行消费,那后续事件不会传递过来。如果 View 消费了 ACTION_DOWN,在不拦截的情况下,后续事件会直接传递给这个 View;
  13. 接收了 ACTION_DOWN 事件的函数不一定能收到后续的 ACTION_MOVE 和 ACTION_UP 事件,有可能事件被父 View 拦截了。
  14. 如果当前正在处理的事件被父 View 拦截,子 View 会收到一个 ACTION_CANCEL 事件,并从 TouchTarget 链中移除,所以后续事件不会再传递过来。
  15. 子 View 重叠并都是可点击时,最后添加的 View,即离用户最近的 View 最先消费事件。

参考

[1] Android 事件分发机制的设计与实现
[2] Android 事件拦截机制的设计与实现