View 的事件分发拦截机制

这一个知识点也是写烂了的,可是作为 Android 开发者又不得不学习这部分,学习了呢,总觉得要写点东西出来才觉得有感觉,得,就有这一篇文章了。

API 27

流程介绍

在单点触摸中,我们对屏幕的点击,滑动,抬起等一系的动作都是由一个一个MotionEvent对象组成的触摸事件。MotionEvent 是对一个对一个事件的封装,里面包括动作、坐标等等信息,根据不同动作,主要有以下三种事件类型:

  1. ACTION_DOWN:手指刚接触屏幕,按下去的那一瞬间产生该事件
  2. ACTION_MOVE:手指在屏幕上移动时候产生该事件
  3. ACTION_UP:手指从屏幕上松开的瞬间产生该事件

要要注意触摸事件不是独立的,而是成组的,每一组事件都是由按下事件开始的,由抬起事件或者取消事件结束。我们把由 ACTION_DOWN 开始(按下),ACTION_UP (抬起)或者 ACTION_CANCEL(取消) 结束的一组事件称为事件序列或者说事件流。取消事件是一种特殊的事件,它对应的是事件序列非人为的提前结束。

举个例子:
点击事件:ACTION_DOWN -> ACTION_UP
滑动事件:ACTION_DOWN -> ACTION_MOVE -> … -> ACTION_MOVE -> ACTION_UP

Android 每产生一个 TouchEvent 事件,他会先问最表面是否消费,如果不消费就交给他的ViewGroup,一层一层向上传递,最终被消费掉(消费就是以为着事件被处理了,代码体现为返回值,true为消费,false为不消费,消费后不再传递)。TouchEvent 不断产生,事件就会不断分发,处理,实现对事件对应的操作进行判断和反馈处理。

还是举个栗子:
一个button被点击一下,就会产生两个 TouchEvent 事件,当第一个 TouchEvent 产生,button 发现自己被按下,背景风格变成按下状态,如水波纹、颜色变深等。当第二个Up 的 TouchEvent 产生、分发的时候,button判别自己被点击,背景风格恢复默认状态,并且如果设置了ClickListener的话,调用 OnClick 方法。

那么如果你的ViewGroup里面不止一个View呢(不是废话吗),不止一个ViewGroup呢?那是不是我就要制定一个机制来决定谁来处理这个事件啊?安排

当事件刚触摸到屏幕的时候,即 ACTION_DOWN 这个 MotionEvent 产生的时候,如果ViewGroup中的View消费(返回true),就将这个View记录下来。后续这一个事件流都直接交给它处理。

事件分发机制-简图.png

其实只有 ACTION_DOWN 事件需要返回 true,其后的像 UP啊,Move啊,他们的返回值并没有什么影响,但是还是推荐都写成true,降低维护成本。

当情况复杂,比如说你现在操作的是列表,点一下会触发点击事件,滑一下就会滑动,那么这样的隔着一个View如何实现的呢?这就是依靠着的就是事件拦截机制

我们将这个过程细分,当你触摸的时候(DOWN事件),这个事件其实是先传到Activity、再传到ViewGroup、最终再传到 View,先问问ViewGroup你拦不拦截啊?一层一层的向下问,如果拦截呢,就直接交给他,如果不拦截呢?就直接往下传,直到传到底层的View,底层的View没有拦截方法,直接问他消不消费,不消费,向上分发,问他的ViewGroup是否分发,如果消费就直接交给它消费掉。这样的话,就可以把消费的权力先交给子View,在合适的时候父View可以马上接管过来。

那么滑动的过程呢?就是在DOWN事件发生的时候,先交给子View消费,当出现MOVE事件的时候,列表发现这个是滑动,需要自己处理,就拦截并且消费掉。但是这时候View还等着后续的事件流,就比如说背景风格还是按下状态,那么父View就会发给它一个cancel事件,让他恢复状态,并且后续事件交给拦截的父View来处理。
事件分发拦截机制-详细图解

始于 Activity

点击事件产生最先传递到当前的Activity,由Acivity的dispatchTouchEvent方法来对事件进行分发。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* Called to process touch screen events. You can override this to
* intercept all touch screen events before they are dispatched to the
* window. Be sure to call this implementation for touch screen events
* that should be handled normally.
*
* @param ev The touch screen event.
*
* @return boolean Return true if this event was consumed.
*/
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}

代码很简单,我们来一行一行进行解析。最开始,就是就是判断当前这个事件是否是按下这个事件( MotionEvent.ACTION_DOWN),如果是,就执行一个空方法( onUserInteraction() 等待程序猿大爷重写)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* Called whenever a key, touch, or trackball event is dispatched to the
* activity. Implement this method if you wish to know that the user has
* interacted with the device in some way while your activity is running.
* This callback and {@link #onUserLeaveHint} are intended to help
* activities manage status bar notifications intelligently; specifically,
* for helping activities determine the proper time to cancel a notfication.
*
* <p>All calls to your activity's {@link #onUserLeaveHint} callback will
* be accompanied by calls to {@link #onUserInteraction}. This
* ensures that your activity will be told of relevant user activity such
* as pulling down the notification pane and touching an item there.
*
* <p>Note that this callback will be invoked for the touch down action
* that begins a touch gesture, but may not be invoked for the touch-moved
* and touch-up actions that follow.
*
* @see #onUserLeaveHint()
*/
public void onUserInteraction() {
}

这里多说几句,这个空方法是在哪些时候会调用呢?毕竟我们也是要重写的嘛,那就必须知道其执行的时期:activity在分发各种事件的时候会调用该方法,旨在提供帮助Activity智能地管理状态栏通知。当此activity在栈顶时,触屏点击按home,back,menu键等都会触发此方法。下拉statubar、旋转屏幕、锁屏不会触发此方法。所以它会用在屏保应用上,因为当你触屏机器,就会立马触发一个事件,而这个事件又不太明确是什么,正好屏保满足此需求;或者对于一个Activity,控制多长时间没有用户点响应的时候,自己消失等。

我们接着往下看getWindow().superDispatchTouchEvent(ev)

1
2
3
public Window getWindow() {
return mWindow;
}

直接返回当前界面的 mWindow,mWindow 是什么啊,是 Window ,Window 我们都知道,是一个 抽象类,它的唯一实现类就是 PhoneWindow,那我们来点一下 superDispatchTouchEvent(MotionEvent)

1
2
3
4
5
6
7
/**
* Used by custom windows, such as Dialog, to pass the touch screen event
* further down the view hierarchy. Application developers should
* not need to implement or call this.
*
*/
public abstract boolean superDispatchTouchEvent(MotionEvent event);

Window 的抽象方法啊,那我们在 PhoneWindow找一找

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

哇,实现要不要就这么简单,直接由Window 直接传递给了 mDecor,mDecor是什么啊?是 DecorView。

1
2
3
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, 
WindowCallbacks {
}

DecorView就是Window的顶级View,是一个ViewGroup,我们通过setContentView设置的View是它的子View(Activity的setContentView,最终是调用PhoneWindow的setContentView).

这里放一张 Activity->视图 的图片
Activity 结构

是不是简单几步就实现了由Activity到ViewGroup的传递,这个中间传递者呢,就是Window。

上面传递到了 DecorView,他直接调用了 ViewGroup 的dispatchTouchEvent()进行分发了。

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

在陷入复杂的分发逻辑之前,我们先看 Acivity#dispatchTouchEvent留下的一个尾巴 – 最后这个return onTouchEvent(ev);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* Called when a touch screen event was not handled by any of the views
* under it. This is most useful to process touch events that happen
* outside of your window bounds, where there is no view to receive it.
*
* @param event The touch screen event being processed.
*
* @return Return true if you have consumed the event, false if you haven't.
* The default implementation always returns false.
*/
public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) { // 当超出边界要关闭Window,且超出边界,且顶层的 DecorView 不为空
finish();
return true;
}

return false; // 默认情况
}

Activity#onTouchEvent 是我们经常重写的方法,执行了 onTouchEvent表示 getWindow().superDispatchTouchEvent(ev)返回的是 false,我们都知道在事件分发体系中,true 表示消费了这个事件(处理了这个事件),那么onTouchEvent 被调用表示这个事件没有任何View消费,只能交给 Activity 处理,如何处理?就是调用 onTouchEvent 这个方法。

来看一下Window#shouldCloseOnTouch

1
2
3
4
5
6
7
8
9
10
/** @hide */
public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
final boolean isOutside =
event.getAction() == MotionEvent.ACTION_DOWN && isOutOfBounds(context, event)
|| event.getAction() == MotionEvent.ACTION_OUTSIDE;
if (mCloseOnTouchOutside && peekDecorView() != null && isOutside) {
return true;
}
return false;
}

这里判断mCloseOnTouchOutside标记及是否为ACTION_DOWN事件,同时判断event的x、y坐标是不是超出Bounds,然后检查FrameLayout的content的id的DecorView是否为空,进行简单判断,由此决定是否销毁这个 Activity。

到这里 Activity 这一层就分析完了。我们在这里理一下:

  1. 先判断是否是按下事件,是则 调用onUserInteraction();空方法
  2. 在 if 括号中分发,首先是交给Activity上的 Window,Window交给顶级视图 DecorView,DecorView 调用父类 ViewGroup#dispatchTouchEvent 进行分发。
  3. 如果在分发结束后,没人消费这个事件,就调用Activity#onTouchEvent 进行处理,处理得很简单,就是判断是否需要超出边界就销毁当前的Activity,需要且超出边界就finish 并且返回true,默认为false。

ViewGroup

书接上文,当我们将事件交给 ViewGroup#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
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
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
    @Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// 检查合法性代码省略

boolean handled = false; // 是否消费
if (onFilterTouchEventForSecurity(ev)) { // 以安全策略判断是否可以分发,true->可以分发
final int action = ev.getAction(); // 事件动作 不同的位存储有不同的信息
final int actionMasked = action & MotionEvent.ACTION_MASK; // 事件类型

// 注释1
// Handle an initial down. 处理第一次按下
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); // 将当前事件分发下去,并且将整个TouchTarget链表回收
resetTouchState(); // 重置Touch状态标识
}

// Check for interception. 标记ViewGroup是否拦截Touch事件的传递
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) { // 当事件是按下或者已经找到能够接收touch事件的目标组件
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; // 是否禁止拦截 注释2
if (!disallowIntercept) { // 如果自己可以拦截,默认可以
intercepted = onInterceptTouchEvent(ev); // 注释3 默认不拦截,用于重写
ev.setAction(action); // restore action in case it was changed
} else { // 不可以拦截,直接将intercepted 设置为false
intercepted = false;
}
} else { // 注意,重点,当不是事件序列开始,而且还没有设置分发的子View,那么只有一种可能,就是在这之前就被我自己拦截过了,后续序列我默认拦截消费
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
// 不是事件流开始的 ACTION_DOWN,也没有事件流的消费组件,那么直接拦截。
intercepted = true;
}

// If intercepted, start normal event dispatch. Also if there is already
// a view that is handling the gesture, do normal event dispatch.
if (intercepted || mFirstTouchTarget != null) {
ev.setTargetAccessibilityFocus(false);
}

// Check for cancelation. 检查 cancel 事件
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;

// 开始事件分发
// Update list of touch targets for pointer down, if needed.
// 是否把事件分发给多个子View,设置: ViewGroup#setMotionEventSplittingEnabled
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null; // 用于存储已经是事件流承受者的TargetView(在mFirstTouchTarget 这个事件流消费者链表中)
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) { // 不取消,不拦截,就分发

// If the event is targeting accessiiblity focus we give it to the
// view that has accessibility focus and if it does not handle it
// we clear the flag and dispatch the event to all children as usual.
// We are looking up the accessibility focused host to avoid keeping
// state since these events are very rare.
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
? findChildWithAccessibilityFocus() : null;

// 处理ACTION_DOWN事件
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int actionIndex = ev.getActionIndex(); // always 0 for down
// 当前 MotionEvent 的动作标识
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;

// Clean up earlier touch targets for this pointer id in case they
// have become out of sync.
removePointersFromTouchTargets(idBitsToAssign);

final int childrenCount = mChildrenCount; // 子View数量
if (newTouchTarget == null && childrenCount != 0) { // 有子View可分发
final float x = ev.getX(actionIndex); // 得到点击的X坐标
final float y = ev.getY(actionIndex); // 得到y坐标
// Find a child that can receive the event.
// Scan children from front to back.
final ArrayList<View> preorderedList = buildTouchDispatchChildList(); // 子View的集合 注释4(顺序问题)
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren; // 也是所有子View
for (int i = childrenCount - 1; i >= 0; i--) { // 倒序访问
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder); // 得到下标,正常情况下就是 i
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex); // 取出 i 对用的View

// 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) // 注意,这就是主要的筛选条件:1. 能不能接收事件(不可见或者在动画)
|| !isTransformedTouchPointInView(x, y, child, null)) { // 2. 是不是在他的范围内
ev.setTargetAccessibilityFocus(false);
continue;
}

// 注释5 如果在 mFirstTouchTarget中,就返回当前这个封装了child 的 TouchTarget,没有就返回null(注意,这时候这个View已经是在)
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) { // 在mFirstTouchTarget 这个事件流消费者链表中,找到事件流的消费者,跳出循环
// Child is already receiving touch within its bounds.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break; // 像UP、MOVE等事件就是从这里跳出循环的
}

resetCancelNextUpFlag(child); // 重置flag:cancel next up
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { // 注释6 重中之重 就是这里分发,看子View是否消费
// Child wants to receive touch within its bounds. 如果消费了
mLastTouchDownTime = ev.getDownTime(); // 更新按下事件
if (preorderedList != null) {
// childIndex points into presorted list, find original index
// 找到在ViewGroup 中存储的child,最原始的下标
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j; // 找到ViewGroup 中的数组的原始下标,保存在ViewGroup的成员变量中
break;
}
}
} else { // 临时的排过序的数组为null
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX(); // 被消费的事件流的DOWN事件的触摸点X(起点x坐标)
mLastTouchDownY = ev.getY(); // 起点y坐标
newTouchTarget = addTouchTarget(child, idBitsToAssign); // 将消费事件流的子View的父View(当前ViewGroup)记录在消费的链表头 插入操作可见注释7
alreadyDispatchedToNewTouchTarget = true; // 表示已经成功分发给自己的子View
break;
}

// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
} // for循环结束
if (preorderedList != null) preorderedList.clear();
} // 处理是 if (newTouchTarget == null && childrenCount != 0),意味着子View不为0并且没有记录的情况下的处理

// dispatchTransformedTouchEvent方法返回false,意味着子View也不消费
if (newTouchTarget == null && mFirstTouchTarget != null) {
// Did not find a child to receive the event.没有child接收事件
// Assign the pointer to the least recently added target.
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
} // DOWN 事件的处理结束
}

// Dispatch to touch targets.
if (mFirstTouchTarget == null) { // 子View不消费
// 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.
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget; // 头节点
while (target != null) {
final TouchTarget next = target.next; // 后驱节点
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) { // 这两个值是在第一次dispatchTransformedTouchEvent的时候返回true赋值的,意味着事件被子View消费
handled = true; // 如果被消费了
} else {
// 不分发给子View,意味着被拦截或者子View与父ViewGroup临时视图分离(mPrivateFlags设置了PFLAG_CANCEL_NEXT_UP_EVENT),就向记录在的
// 是否分发给子View
final boolean cancelChild =
resetCancelNextUpFlag(target.child)
|| intercepted; // 当前ViewGroup是否拦截
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) { // 如果不分发分发子View,调用dispatchTransformedTouchEvent发送cancel事件,已经分发过了就排除新的触摸目标
handled = true; // 是否自己或者子View消费
}
if (cancelChild) { // 事件不分发给子View,有可能是被拦截了
if (predecessor == null) { // 具体链表操作看 注释8
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}

// Update list of touch targets for pointer up or cancel, if needed.
if (canceled
|| actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
resetTouchState();
} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
final int actionIndex = ev.getActionIndex();
final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
removePointersFromTouchTargets(idBitsToRemove);
}
}

if (!handled && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
}
return handled;
}

注释1:
这里呢,就是当一个 ACTION_DOWN 事件来了以后,需要清除一些以前事件序列的标记,开始下一个事件序列。在 cancelAndClearTouchTargets(ev) 方法中有一个非常重要的操作就是将mFirstTouchTarget设置为了null,在resetTouchState()方法中重置Touch状态标识。

mFirstTouchTarget 是 TouchTarget,ViewGroup 的成员变量,记录要消费整个事件流的View,一个触摸事件可能有多个View可以接收到,该参数把他们连接成链状。

注释2
这里介绍一下几个基础知识,让大家知道为什么有这个事件拦截。

当我们按下的时候,即 ACTION_DOWN 发生的时候,标志着整个事件流的开始,这时候我们会去找整个事件流的处理者,对应的就是整个事件分发流程,一旦找到这个事件流的处理者(消费了这个事件的ACTION_DOWN),那么后续的整个事件流都会直接发送给这个处理者进行消费掉。

就比如说屏幕上有一个button,我滑动一下按钮,则从 ACTION_DOWN 的时候找到消费这个事件的组件了,然后button表现出按下状态。而后续整个 ACTION_MOVE 事件和 ACTION_UP 事件都直接发送给这个button处理。当下一个事件流来到又重复上述过程。

当情况变复杂的时候,比如说是列表,首先一来就是一个 ACTION_DOWN 事件,可是我也不知道他是点击还是按下啊,所以只能分发下去,交给了item消费了,可是我发现他是滑动事件,那么我就要从子View 中把消费事件的权利抢过来,就是拦截了。而item呢?还是一个按下状态,就发送一个 ACTION_CANCEL 事件给他让他恢复状态。这里呢,意思就是说,当一个事件流我交给子View消费过后,后续不再分发给我,但是在整个事件流处理过程中,我可以随时拦截,交给我来处理

而假如我是子View,我又不希望我的ViewGroup拦截怎么办呢?当然有办法:ViewGroup#requestDisallowInterceptTouchEvent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {

if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
// We're already in this state, assume our ancestors are too
// 已经处于这种状态
return;
}

if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}

// Pass it up to our parent
if (mParent != null) {
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}

很简单,设置 ViewGroup的标志位,并递归告诉父ViewGroup不要拦截。

注释3

1
2
3
4
5
6
7
8
9
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;
}

在当前ViewGroup可以拦截的情况下,看自己拦不拦截呢?不拦截,鼠标那个事件就不考虑了,看到没有,默认返回false,不拦截。当然这个方法主要也是用于我们重写。

注释4
preorderedList中的顺序:按照addView或者XML布局文件中的顺序来的,后addView添加的子View,会添加在列表的后面,会因为Android的UI后刷新机制显示在上层;

在事件分发的时候倒序遍历分发,那么最上层的View就可以最先接收到这个事件流,并决定是否消费这个事件流。

注释5

1
2
3
4
5
6
7
8
9
10
11
12
/**
* Gets the touch target for specified child view.
* Returns null if not found.
*/
private TouchTarget getTouchTarget(@NonNull View child) {
for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
if (target.child == child) {
return target;
}
}
return null;
}

从这里我们可以很清楚的明白,首先存储消费事件的目标组件的数据结构是链表,其次 mFirstTouchTarget 就是头节点。而 getTouchTarget 就是遍历整个链表,如果有就返回这个TouchTarget,没有就返回null,最后返回的值存储在 newTouchTarget 中。

这里我们介绍一下 TouchTarget ,TouchTarget 作为 ViewGroup 的内部类,原理很像Message的原理。Android 的消息机制 介绍传送门

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
/* 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...
*/
private static final class TouchTarget {
private static final int MAX_RECYCLED = 32; // 回收池最大容量
private static final Object sRecycleLock = new Object[0]; // 回收时候同步控制需要持有的对象锁
private static TouchTarget sRecycleBin; // 回收池的头节点,注意是 static
private static int sRecycledCount; // 当前回收池的数量

public static final int ALL_POINTER_IDS = -1; // all ones

// The touched child view.
public View child; //存储的数据:View。整个事件流的消费者

// The combined bit mask of pointer ids for all pointers captured by the target.
public int pointerIdBits;

// The next target in the target list.
public TouchTarget next; //下一个节点

private TouchTarget() { // 不能在外部new出来
}

// 将传入的数据封装成一个TouchTarget链表的结点
public static TouchTarget obtain(@NonNull View child, int pointerIdBits) {
if (child == null) { // 需要传入封装的对象吖
throw new IllegalArgumentException("child must be non-null");
}

final TouchTarget target; // 最后构建出来存储的链表节点
synchronized (sRecycleLock) { // 拿到同步锁
if (sRecycleBin == null) {
target = new TouchTarget(); // 回收池为空,直接内部new出来
} else {
target = sRecycleBin; // 将头节点作为目标节点
sRecycleBin = target.next; // 将头节点下移一个
sRecycledCount--; // 回收池数量减一
target.next = null; // 将取出的节点与链表的联系断掉
}
}
target.child = child; // 装进节点
target.pointerIdBits = pointerIdBits;
return target;
}

// 提供回收当前节点的方法
public void recycle() {
if (child == null) {
throw new IllegalStateException("already recycled once");
}

synchronized (sRecycleLock) { // 拿到同步锁
if (sRecycledCount < MAX_RECYCLED) { // 没有超过回收池容量
next = sRecycleBin; // 当前回收节点指向回收池链表的头结点
sRecycleBin = this; // 回收池头结点指向自己,相当于上移
sRecycledCount += 1; // 数量加1
} else {
next = null; // 置空,help Gc
}
child = null; // 抹除记录的数据
}
}
}

既然最后是一条以为头结点的链表,那么他到底存的是哪些View呢?上一张图:

mFirstTouchTarget 链表

当我们按下 button2 的时候,会一层一层的传下去,最下层的消费了,然后返回上层接着执行代码(方法调用的时候是当前方法就被压入栈中,调用方法执行结束再弹出执行),上层会在 if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign))的时候得到true,将刚刚消费的子View(ViewGroup/View)记录进链表。

注释6
下面就是在第一次什么都没有的时候进行分发,注意哦,这里还在循环里面,就意味着这次循环没找到记录,并且触摸点在这个ViewGroup范围内,可见,那我就分发。

接下来详细看一下ViewGroup#dispatchTransformedTouchEvent

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
/**
* Transforms a motion event into the coordinate space of a particular child view,
* filters out irrelevant pointer ids, and overrides its action if necessary.
* If child is null, assumes the MotionEvent will be sent to this ViewGroup instead.
*/
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled; // 是否消费

// Canceling motions is a special case. We don't need to perform any transformations
// or filtering. The important part is the action, not the contents.
final int oldAction = event.getAction(); // 获取当前事件
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) { // 取消,或者是取消事件
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) { // 传进来的子View为空
handled = super.dispatchTouchEvent(event); // 当前ViewGroup 来执行,调用的是父类View的方法
} else {
handled = child.dispatchTouchEvent(event); // 直接交给传进来的子View,在这里就是循环的时候倒序获取的View
}
event.setAction(oldAction); // 设置为 ACTION_CANCEL
return handled;
}

// Calculate the number of pointers to deliver.计算要传递的指针数。
final int oldPointerIdBits = event.getPointerIdBits();
final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;

// If for some reason we ended up in an inconsistent state where it looks like we
// might produce a motion event with no pointers in it, then drop the event.
if (newPointerIdBits == 0) { // 异常情况,放弃处理
return false;
}

// If the number of pointers is the same and we don't need to perform any fancy
// irreversible transformations, then we can reuse the motion event for this
// dispatch as long as we are careful to revert any changes we make.
// Otherwise we need to make a copy.
final MotionEvent transformedEvent;
if (newPointerIdBits == oldPointerIdBits) {
if (child == null || child.hasIdentityMatrix()) {
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
event.offsetLocation(offsetX, offsetY);

handled = child.dispatchTouchEvent(event);

event.offsetLocation(-offsetX, -offsetY);
}
return handled;
}
transformedEvent = MotionEvent.obtain(event);
} else {
transformedEvent = event.split(newPointerIdBits);
}

// Perform any necessary transformations and dispatch.
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
if (! child.hasIdentityMatrix()) {
transformedEvent.transform(child.getInverseMatrix());
}

handled = child.dispatchTouchEvent(transformedEvent);
}

// Done.
transformedEvent.recycle(); // 回收TouchTarget
return handled;
}

这里引用大神的分析:

在dispatchTouchEvent()中多次调用了dispatchTransformedTouchEvent()方法,而且有时候第三个参数为null,有时又不是,他们到底有啥区别呢?这段源码中很明显展示了结果。在dispatchTransformedTouchEvent()源码中可以发现多次对于child是否为null的判断,并且均做出如下类似的操作。其中,当child == null时会将Touch事件传递给该ViewGroup自身的dispatchTouchEvent()处理,即super.dispatchTouchEvent(event)(也就是View的这个方法,因为ViewGroup的父类是View);当child != null时会调用该子view(当然该view可能是一个View也可能是一个ViewGroup)的dispatchTouchEvent(event)处理,即child.dispatchTouchEvent(event)。别的代码几乎没啥需要具体注意分析的。

具体的什么时候会传空呢,我们接着往下看,后面会分析和总结。

注释7

1
2
3
4
5
6
7
8
9
10
/**
* Adds a touch target for specified child to the beginning of the list.
* Assumes the target child is not already present.
*/
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits); // 获取节点,并将数据装进去
target.next = mFirstTouchTarget; // 将新节点的next指向下一个节点
mFirstTouchTarget = target; // 头结点记录为当前节点
return target; // 返回头节点
}

到这里,整个 ViewGroup 层就结束啦,这里来总结下,dispatchTransformedTouchEvent()什么时候会传入一个null的child呢?

  • ViewGroup 没有子View
  • 子元素处理了点击事件,但是在 dispatchTouchEvent 中返回了false,这一般都是因为子 View 在onTouchEvent 中返回了 false。

注释8
这里主要分析的是循环中的链表操作

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
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;
}

View 最后可能接收到进行消费

我们知道前面按着正常情况下,就是调用View的dispatchTouchEvent方法,将事件传递给子View,接下来就是View的show time。

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
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
/**
* Pass the touch screen motion event down to the target view, or this
* view if it is the target.
* 传递给目标View 或者 查看它是否是目标
*
* @param event The motion event to be dispatched.
* @return True if the event was handled by the view, false otherwise.
*/
public boolean dispatchTouchEvent(MotionEvent event) {
// If the event should be handled by accessibility focus first.
if (event.isTargetAccessibilityFocus()) { // 可访问焦点优先处理
// We don't have focus or no virtual descendant has it, do not handle the event.
if (!isAccessibilityFocusedViewOrHost()) {
return false;
}
// We have focus and got the event, then use normal event dispatch.
event.setTargetAccessibilityFocus(false);
}

boolean result = false; // 是否被处理、消费

if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}

final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) { // 当按下事件发生
// Defensive cleanup for new gesture
stopNestedScroll(); // 停止嵌套滚动
}

if (onFilterTouchEventForSecurity(event)) { // 根据参数确定是否可以分发:这是一种安全策略(正常情况况下为true)
if ((mViewFlags & ENABLED_MASK) == ENABLED &&
handleScrollBarDragging(event)) { // 作为滚动条拖动就直接处理滚动事件,并直接消费,返回true
result = true; // 滚动条的时候
}
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo; // 各种listener定义在一起的静态内部类,包括我们熟悉的 onClickListener
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED // 验证 li 中的 mOnTouchListener 不为空,可以调用
&& li.mOnTouchListener.onTouch(this, event)) { // 调用onTouch 方法
result = true; // onTouch返回true就消费
}

if (!result && onTouchEvent(event)) { // onTouch 不消费就交给onTouchEvent,消费就变true
result = true;
}
}

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

// Clean up after nested scrolls if this is the end of a gesture;
// also cancel it if we tried an ACTION_DOWN but we didn't want the rest
// of the gesture.
if (actionMasked == MotionEvent.ACTION_UP ||
actionMasked == MotionEvent.ACTION_CANCEL ||
(actionMasked == MotionEvent.ACTION_DOWN && !result)) {
stopNestedScroll();
}

return result;
}

看着注释基本都可以看懂,但是这里又一个东西得看一下,方便对一些事件的理解,那就是 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
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182

/**
* Implement this method to handle touch screen motion events.
* <p>
* If this method is used to detect click actions, it is recommended that
* the actions be performed by implementing and calling
* {@link #performClick()}. This will ensure consistent system behavior,
* including:
* <ul>
* <li>obeying click sound preferences
* <li>dispatching OnClickListener calls
* <li>handling {@link AccessibilityNodeInfo#ACTION_CLICK ACTION_CLICK} when
* accessibility features are enabled
* </ul>
*
* @param event The motion event.
* @return True if the event was handled, false otherwise.
*/
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX(); // 获取点击坐标
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction(); // 获取Action类型

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;
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}

if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP: // 抬起的时候
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
if ((viewFlags & TOOLTIP) == TOOLTIP) {
handleTooltipUp(); // 处理弹窗类型的抬起事件
}
if (!clickable) { // 如果不可点击,移除相关接口设置和设置不可点击,并跳出选择
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
}
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
// take focus if we don't have it already and we should in
// touch mode.
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}

if (prepressed) {
// The button is being released before we actually
// showed it as pressed. Make it show the pressed
// state now (before scheduling the click) to ensure
// the user sees it.
// 标志着被按下,背景风格转化为按下状态
setPressed(true, x, y);
}

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)) { // post到主线程执行这个Runnable,这Runnable是由View实现,内部调用li.mOnClickListener.onClick(this);
performClick();
}
}
}

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

if (prepressed) {
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
}

removeTapCallback();
}
mIgnoreNextUpEvent = false;
break;

case MotionEvent.ACTION_DOWN: // 按下状态
if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
}
mHasPerformedLongPress = false;

if (!clickable) { // 不是点击的话,有可能就是长按
checkForLongClick(0, x, y);
break;
}

if (performButtonActionOnTouchDown(event)) {
break;
}

// Walk up the hierarchy to determine if we're inside a scrolling container.
boolean isInScrollingContainer = isInScrollingContainer();

// For views inside a scrolling container, delay the pressed feedback for
// a short period in case this is a scroll.
if (isInScrollingContainer) {
mPrivateFlags |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
// Not inside a scrolling container, so show the feedback right away 视图不是在滚动中,就把自己变为按下状态
setPressed(true, x, y); // 按下状态,为点击事件做准备
checkForLongClick(0, x, y); // 为长按做准备
}
break;

case MotionEvent.ACTION_CANCEL: // 恢复默认状态
if (clickable) {
setPressed(false); // 恢复默认背景风格
}
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
break;

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

// Be lenient about moving outside of buttons
if (!pointInView(x, y, mTouchSlop)) {
// Outside button
// Remove any future long press/tap checks
removeTapCallback();
removeLongPressCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
}
break;
}

return true;
}

return false;
}

所有的流程最后都可以归结到这张图上

事件分发拦截机制-详细图解
整个事件传递就这样结束了,在这个过程中,拦截分发的代码交错在一起,我这里总结一下流程:

  1. 事件分发开始于Activity#dispatchTouchEvent,先交给getWindow().superDispatchTouchEvent(ev),返回false再交给Activity#onTouchEvent(ev)

  2. 在 PhoneWindow()#superDispatchTouchEvent(ev) 中,直接交给了顶层View:DecorView#superDispatchTouchEvent

  3. 在 DecorView#superDispatchTouchEvent 直接 super.dispatchTouchEvent(event),意味着调用父类ViewGroup#dispatchTouchEvent 处理。

  4. 调用 ViewGroup#onInterceptTouchEvent 判断是否拦截

    如果拦截,就super.

如果不拦截并且是事件流的开始的话(DOWN 事件),就调用ViewGroup#dispatchTransformedTouchEven 分发下去

如果分发成功,就将分发成功的View存在 mFirstTouchTarget 链表中

如果遍历分发,没人消费,或没有子View的话,就调用父类(也是View啊)的 dispatchTouchEvent,这里面就会执行onTouch / onTouchEvent 方法