Android Touch事件分发机制

Android Touch事件的分发是 Android 工程师必备技能之一。手指触摸屏幕时,即产生了触摸信息。这个触摸信息由屏幕这个硬件产生,被系统底层驱动获取,交给Android的输入系统服务:InputManagerService,也就是IMS

IMS会对这个触摸信息进行处理,经过WMS找到要分发的window,随后发送给对应的viewRootImpl。因此发送触摸信息的并非WMS,WMS提供的是window的相关信息。

当viewRootImpl接收到触摸信息时,也正是应用程序进程事件分发的开始。

image

事件如何到达Activity

事件分发:
DecorView -> Activity -> PhoneWindow -> DecorView
当屏幕被触摸input系统事件从Native层分发Framework层的InputEventReceiver.dispachInputEvent()调用了

  • ViewRootImpl.WindowInputEventReceiver.dispachInputEvent()->

  • ViewRootImpl中的DecorView.dispatchTouchEvent->Window.Callback.dispatchTouchEvent(ev) or super.dispatchTouchEvent(ev)

  • Activity.dispatchTouchEvent->getWindow().superDispatchTouchEvent(ev) “PhoneWindow

  • window.superDispatchTouchEvent()->mDecor.superDispatchTouchEvent(event)

  • DecorView.superDispatchTouchEvent()->super.dispatchTouchEvent(event)

  • ViewGroup.dispatchTouchEvent()

android的view管理是以window为单位的,每一个window对应一个view树。Window机制不只管理着view的显示,也负责view的事件分发。关于window的本质,能够阅读笔者的另外一篇文章window机制。研究事件分发的来源,须要从window机制入手。布局

因此,首先要了解一个概念:view树,即viewRootImpl。
每一棵view树都有一个根,叫作ViewRootImpl ,他负责管理这整一棵view树的绘制、事件分发等。因此能够说,事件分发是从viewRootImpl开始的。

应用界面通常会有多个view树,activity布局就是一个view树、其余应用的悬浮窗也是一个view树、dialog界面也是一个view树、使用windowManager添加的view也是一个view树等等。最简单的view树能够只有一个view。

android中view的绘制和事件分发,都是以view树为单位。每一棵view树,则为一个window 。系统服务WindowManagerService,管理界面的显示就是以window为单位,也能够说是以view树为单位。而view树是由viewRootImpl来负责管理的,因此能够说,wms(WindowManagerService的简写)管理的是viewRootImpl。
image

对上图作个简单解释。

wms是运行在系统服务进程的,负责管理全部应用的window。应用程序与wms的通讯必须经过Binder进行跨进程通讯。
每一个viewRootImpl在wms中都有一个windowState对应,wms能够经过windowState找到对应的viewRootImpl进行管理
了解window机制的一个重要缘由是:事件分发并非由Activity驱动的,而是由系统服务驱动viewRootImpl来进行分

ViewRootImpl是如何分发事件的

1、viewRootImpl会直接调用管理的view的 dispatchTouchEvent 方法,根据具体的view的类型,调用具体的方法。
2、view树的根view多是一个view,也多是一个viewGroup,view会直接处理事件,而viewGroup则会进行分发。
3、DecorView重写了 dispatchTouchEvent 方法,会先判断是否存在callBack,优先调用callBack的方法,也就是把事件传递给了Activity。
4、其余的viewGroup子类会根据自身的逻辑进行事件分发。

所以,触摸事件必定是从Activity开始的吗?不是,Activity只是其中的一种状况,只有Activity本身负责的那一棵view树,才必定会到达activity,而其余的window,则不会通过Activity。触摸事件是从viewRootImpl开始,而不是Activity。

Touch事件分发中只有两个主角:ViewGroup和View。Activity的Touch事件事实上是调用它内部的ViewGroup的Touch事件,可以直接当成ViewGroup处理。

View在ViewGroup内,ViewGroup也可以在其他ViewGroup内,这时候把内部的ViewGroup当成View来分析。

ViewGroup的相关事件有三个:onInterceptTouchEvent、dispatchTouchEvent、onTouchEvent。View的相关事件只有两个:dispatchTouchEvent、onTouchEvent。

先分析ViewGroup的处理流程:首先得有个结构模型概念:ViewGroup和View组成了一棵树形结构,最顶层为Activity的ViewGroup,下面有若干的ViewGroup节点,每个节点之下又有若干的ViewGroup节点或者View节点,依次类推。如图:

image

Activity 的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);
}

DecorView会调用super.DispatchTouchEvent方法:

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

因为DecorView是一个FrameLayout,它最终还是调用了我们熟悉的ViewGroup的dispatchTouchEvent()

所谓的事件分发,本质上就是一个递归函数的调用,这个递归函数就是dispatchTouchEvent,至于onIntercepterTouchEvent,onTouchEvent,OnTouchListener,onClickListener…balabala都是在这个递归函数里面的操作而已,最核心,最骨干的还是dispatchTouchEvent。

当一个Touch事件(触摸事件为例)到达根节点,即Acitivty的ViewGroup时,它会依次下发,下发的过程是调用子View(ViewGroup)的dispatchTouchEvent方法实现的。简单来说,就是ViewGroup遍历它包含着的子View,调用每个View的dispatchTouchEvent方法,而当子View为ViewGroup时,又会通过调用ViwGroup的dispatchTouchEvent方法继续调用其内部的View的dispatchTouchEvent方法。上述例子中的消息下发顺序是这样的:①-②-⑤-⑥-⑦-③-④。dispatchTouchEvent方法只负责事件的分发,它拥有boolean类型的返回值,当返回为true时,顺序下发会中断。在上述例子中如果⑤的dispatchTouchEvent返回结果为true,那么⑥-⑦-③-④将都接收不到本次Touch事件。

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
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
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
}
//如果事件以无障碍焦点的View为目标,并且此View就是那个无障碍焦点View则开始
//正常事件分发。
// If the event targets the accessibility focused view and this is it, start
// normal event dispatch. Maybe a descendant is what will handle the click.
if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
ev.setTargetAccessibilityFocus(false);
}

boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
/**
* 第一步:对于ACTION_DOWN进行处理(Handle an initial down)
* 因为ACTION_DOWN是一系列事件的开端,当是ACTION_DOWN时进行一些初始化操作.
* 从源码的注释也可以看出来:清除以往的Touch状态(state)开始新的手势(gesture)
* cancelAndClearTouchTargets(ev)中有一个非常重要的操作:
* 将mFirstTouchTarget设置为null!!!!
* 随后在resetTouchState()中重置Touch状态标识
**/
// 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);
resetTouchState();
}

/**
* 第二步:检查是否要拦截(Check for interception)
* 在哪些情况下会调用该代码呢?有如下几种情况
* 1 处理ACTION_DOWN事件
* 2 当ACTION_DOWN事件被子View消费后处理ACTION_MOVE和ACTION_UP时
* 会调用该代码。因为此时mFirstTouchTarget!=null。所以此时ViewGroup
* 是有机会拦截ACTION_MOVE和ACTION_UP的,但是我们也可以调用方法:
* requestDisallowInterceptTouchEvent来禁止ViewGroup的事件拦截.
* 如果子View没有消费Touch事件,那么那么当后续的ACTION_MOVE和ACTION_UP
* 到来时是不会调用到本处代码的.
*
* 在dispatchTouchEvent(MotionEventev)这一大段代码中
* 使用变量intercepted来标记ViewGroup是否拦截Touch事件的传递.
* 该变量在后续代码中起着很重要的作用.
*
* 从此处if(actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null)及其内部代码可知:
* 当ViewGroup决定拦截事件后,那么后续的点击事件将会默认交给它处理,不再调用
* onInterceptTouchEvent()判断是否需要拦截.
* 这个是为什么?
* 因为在处理ACTION_DOWN时如果Touch事件被子View消费,那么mFirstTouchTarget不为空;
* 反之,如果Touch事件没有被子View消费,那么mFirstTouchTarget为空,即此时Touch由当前
* 的ViewGroup拦截。此时当ACTION_MOVE和ACTION_UP来到时,不再满足:
* if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null)
* 当然也就无法调用其内部的onInterceptTouchEvent()。
* 通俗地说:一旦ViewGroup拦截了ACTION_DOWN事件由自身的onTouchEvent()处理,那么
* 对于后续的ACTION_MOVE和ACTION_UP而言ViewGroup不再调用onInterceptTouchEvent()
* 判断是否拦截.
*
* 这里有个东西需要注意:FLAG_DISALLOW_INTERCEPT
* 在子View中调用requestDisallowInterceptTouchEvent()后造成disallowIntercept为true
* 即禁止拦截.于是不满足if(!disallowIntercept)所以也就调用不到该if内的onInterceptTouchEvent()
* 自然就没有办法拦截了.
* 但是requestDisallowInterceptTouchEvent()对于ACTION_DOWN是无效的.
* 因为对于ACTION_DOWN会调用 cancelAndClearTouchTargets(ev)和resetTouchState();
* 对FLAG_DISALLOW_INTERCEPT等状态值复原重置(参考上面的代码)
*
* 举两种情况说明:
* 1 当处理ACTION_DOWN时当然会满足
* if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null)
* 对于ACTION_DOWN子View有两种处理结果
* 1.1 消耗了Touch事件,那么mFirstTouchTarget不为null.
* 所以处理后续的ACTION_MOVE和ACTION_UP时依然满足该if判断
* 1.2 没有消耗Touch事件.mFirstTouchTarget=null.不满足该if.
* 所以后续的ACTION_MOVE和ACTION_UP由ViewGroup处理,此时再讨论什么拦截也就没有意义了.
* 同样的道理当子View消费了ACTION_DOWN后当处理ACTION_MOVE的时候ViewGroup拦截了该事件
* 那么当ACTION_UP随之到来时由于mFirstTouchTarget=null所以不会再调用该段代码,自然也就
* 不会调用onInterceptTouchEvent()判断是否拦截了.这点在上面的注释也有提及
* 2 当出现1.1的情况时满足该if判断.
* 如果在子View中调用了requestDisallowInterceptTouchEvent()那么就禁止拦截
* 即disallowIntercept=true.所以不满足if (!disallowIntercept)当然也就调用不到
* onInterceptTouchEvent(ev)了,而是执行else{ intercepted = false;}
* 也就是说ViewGroup无法拦截Touch了.
*/

// Check for interception.
final boolean intercepted;
// 事件为ACTION_DOWN或者mFirstTouchTarget不为null(即已经找到能够接收touch事件的目标组件)时if成立
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {

//判断disallowIntercept(禁止拦截)标志位,可以理解为一个是否允许ViewGroup拦截的开关
//因为在其他地方可能调用了requestDisallowInterceptTouchEvent()设置mGroupFlags 改变该值.
//对于此方法的作用其实看requestDisallowInterceptTouchEvent()这个方法名就可明白了
//disallowIntercept 默认值为FALSE
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
//当禁止拦截为false时(即disallowIntercept为false)调用onInterceptTouchEvent(ev)方法
if (!disallowIntercept) {

//既然disallowIntercept为false那么就调用onInterceptTouchEvent()方法将结果赋值给intercepted
//常说事件传递中的流程是:dispatchTouchEvent->onInterceptTouchEvent->onTouchEvent
//其实在这就是一个体现,在dispatchTouchEvent()中调用了onInterceptTouchEvent()
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {

//禁止拦截的FLAG为ture说明没有必要去执行是否需要拦截了能够顺利通过,所以设置拦截变量为false
//即当禁止拦截为true时(即disallowIntercept为true)设置intercepted = false
//父view无法拦截事件
intercepted = false;
}
} else {

//当事件不是ACTION_DOWN并且mFirstTouchTarget为null(即没有Touch的目标组件)时
//设置 intercepted = true表示ViewGroup执行Touch事件拦截的操作。
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
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);
}

/**
* 第三步:检查cancel(Check for cancelation)
*
*/
// Check for cancelation.
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;

// Update list of touch targets for pointer down, if needed.
final boolean isMouseEvent = ev.getSource() == InputDevice.SOURCE_MOUSE;
/**
* 第四步:事件分发(Update list of touch targets for pointer down, if needed)
*/

final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0
&& !isMouseEvent;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
//不是ACTION_CANCEL并且ViewGroup的拦截标志位intercepted为false(不拦截)
if (!canceled && !intercepted) {
// If the event is targeting accessibility 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
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;
// 依据Touch坐标寻找子View来接收Touch事件
if (newTouchTarget == null && childrenCount != 0) {
final float x =
isMouseEvent ? ev.getXCursorPosition() : ev.getX(actionIndex);
final float y =
isMouseEvent ? ev.getYCursorPosition() : ev.getY(actionIndex);
// Find a child that can receive the event.
// Scan children from front to back.
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
// 遍历子View判断哪个子View接受Touch事件
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {
continue;
}

newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {

// 找到接收Touch事件的子View!!!!!!!即为newTouchTarget.
// 既然已经找到了,所以执行break跳出for循环
// 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不满足,当然也不会执行break语句.
* 于是代码会执行到这里来.
*
*
* 调用方法dispatchTransformedTouchEvent()将Touch事件传递给子View做
* 递归处理(也就是遍历该子View的View树)
* 该方法很重要,看一下源码中关于该方法的描述:
* 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.
* 将Touch事件传递给特定的子View.
* 该方法十分重要!!!!!!!!!!!!!!!!!
* 在该方法中为一个递归调用,会递归调用dispatchTouchEvent()方法!!!!!!!!!!
* 在dispatchTouchEvent()中:
* 如果子View为ViewGroup并且Touch没有被拦截那么递归调用dispatchTouchEvent()
* 如果子View为View那么就会调用其onTouchEvent(),这个不再赘述.
*
*
* 该方法返回true则表示子View消费掉该事件,同时进入该if判断.
* 满足if语句后重要的操作有:
* 1 给newTouchTarget赋值
* 2 给alreadyDispatchedToNewTouchTarget赋值为true.
* 看这个比较长的英语名字也可知其含义:已经将Touch派发给新的TouchTarget
* 3 执行break.
* 因为该for循环遍历子View判断哪个子View接受Touch事件,既然已经找到了
* 那么就跳出该for循环.
* 4 注意:
* 如果dispatchTransformedTouchEvent()返回false即子View的onTouchEvent返回false
* (即Touch事件未被消费)那么就不满足该if条件.所以也就无法执行addTouchTarget().
* 在此简单说一下addTouchTarget()中涉及到的ViewGroup的一个内部类TouchTarget——它是一个事件链.
* 该处的mFirstTouchTarget就是一个TouchTarget.它保存了可以消耗Touch事件的View.
* 在该处,如果dispatchTransformedTouchEvent()返回true即子View的onTouchEvent返回true则说明
* 该View消耗了Touch事件,那么将该View加入到事件链中!!!!!!!!!!!!!!!
* 尤其注意:
* 这个操作是在处理ACTION_DOWN的代码块里进行的.即是在:
* if (actionMasked == MotionEvent.ACTION_DOWN||
* (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) ||
* actionMasked == MotionEvent.ACTION_HOVER_MOVE)
* 这个大的if判断中处理的.
* 当处理ACTION_MOVE事件和ACTION_UP事件的时候是不会进入这个if判断的!!!!!
* 而是直接从去判断mFirstTouchTarget!!!!!!!!!!!!!!!!
* 所以如果一个View不处理ACTION_DOWN那么该,那么该View是不会保存在mFirstTouchTarget
* 中的,也就无法继续处理ACTION_MOVE事件和ACTION_UP事件!!!!!!!!!!即若该View不消耗
* ACTION_DOWN事件那么系统是不会讲ACTION_MOVE和ACTION_UP事件传给给该View的
* 5 注意:
* 如果dispatchTransformedTouchEvent()返回true即子View
* 的onTouchEvent返回true(即Touch事件被消费)那么就满足该if条件.
* 从而mFirstTouchTarget不为null!!!!!!!!!!!!!!!!!!!
* 6 小结:
* 对于此处ACTION_DOWN的处理具体体现在dispatchTransformedTouchEvent()
* 该方法返回boolean,如下:
* true---->事件被消费----->mFirstTouchTarget!=null
* false--->事件未被消费--->mFirstTouchTarget==null
* 因为在dispatchTransformedTouchEvent()会调用递归调用dispatchTouchEvent()和onTouchEvent()
* 所以dispatchTransformedTouchEvent()的返回值实际上是由onTouchEvent()决定的.
*
* 简单地说onTouchEvent()是否消费了Touch事件(true or false)的返回值决定了
* dispatchTransformedTouchEvent()的返回值!!!!从而决定了mFirstTouchTarget是否为null!!!!!!
* 从而进一步决定了ViewGroup是否处理Touch事件.这一点在下面的代码中很有体现.
*/

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();

//调用addTouchTarget()将child添加到mFirstTouchTarget链表的表头
//注意在addTouchTarget()方法内部会对mFirstTouchTarget操作,使其不为null

newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}

// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}
/**
* 该if条件表示:
* 经过前面的for循环没有找到子View接收Touch事件并且之前的mFirstTouchTarget不为空
*/
if (newTouchTarget == null && mFirstTouchTarget != null) {
// Did not find a child to receive the event.
// Assign the pointer to the least recently added target.
newTouchTarget = mFirstTouchTarget;

while (newTouchTarget.next != null) {
//newTouchTarget指向了最初的TouchTarget
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
}

/**
* 经过上面对于ACTION_DOWN的处理后mFirstTouchTarget有两种情况:
* (当然如果不是ACTION_DOWN就不会经过上面较繁琐的流程而是从此处开始执行,比如ACTION_MOVE和ACTION_UP)
*
* 情况1 mFirstTouchTarget为null
* 即没有找到能够消费touch事件的子组件或者是touch事件被拦截了
* 情况2 mFirstTouchTarget不为null
* 即找到了能够消费touch事件的子组件则后续的touch事件都可以传递到子View
* 这两种情况的详细分析见下.
*
* 这两种情况下都会去调用方法:
* dispatchTransformedTouchEvent(MotionEvent event,boolean cancel,View child,int desiredPointerIdBits)
* 我们重点关注该方法的第三个参数View child.
* 详情请参加下面dispatchTransformedTouchEvent()源码分析
* 在该源码中解释了:
* 为什么子view对于Touch事件处理返回true那么其上层的ViewGroup就无法处理Touch事件了!!!!!!!!!
* 为什么子view对于Touch事件处理返回false那么其上层的ViewGroup才可以处理Touch事件!!!!!!!!!!
*
*/

// 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 {

/**
* 情况2:mFirstTouchTarget不为null
* 即找到了可以消费Touch事件的子View且后续Touch事件可以传递到该子View
* 在源码中的注释为:
* Dispatch to touch targets, excluding the new touch target if we already dispatched to it.
* Cancel touch targets if necessary.
*/

// 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) {

//如果前面利用ACTION_DOWN事件寻找符合接收条件的子组件的同时消费掉了ACTION_DOWN事件
//那么这里为handled赋值为true
handled = true;
} else {

//对于非ACTION_DOWN事件继续传递给目标子组件进行处理
//依然是递归调用dispatchTransformedTouchEvent()
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;
}
}

/**
* 处理ACTION_UP和ACTION_CANCEL
* Update list of touch targets for pointer up or cancel, if needed.
* 在此主要的操作是还原状态
*/

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

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
 /**
* Pass the touch screen motion event down to the target view, or this
* view if it is the target.
*
* @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) {
//判断当前事件是否能获得焦点,如果不能获得焦点或者不存在一个View,那我们就直接返回False跳出循环
// 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)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
//由自己内部的onTouchEvent处理。
if (!result && onTouchEvent(event)) {
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;
}

负责对事件进行分发的方法主要有三个,分别是:

dispatchTouchEvent()
onTouchEvent()
onInterceptTouchEvent()

image

Down事件的分发决定了那个view要捕获事件,如果捕获了,后续的事件就直接分发给它,也就是说move up等事件的分发交给谁,取决于它们的起始事件Down由谁捕获。

在以上可看出,ViewGroup的dispatchTouchEvent是真正在执行“分发”工作,而View的dispatchTouchEvent方法,并不执行分发工作,或者说它分发的对象就是自己,决定是否把touch事件交给自己处理,而处理的方法,便是onTouchEvent事件,事实上子View的dispatchTouchEvent方法真正执行的代码是自己内部的onTouchEvent方法。

一般情况下,我们不该在普通View内重写dispatchTouchEvent方法,因为它并不执行分发逻辑。当Touch事件到达View时,我们该做的就是是否在onTouchEvent事件中处理它。

那么,ViewGroup的onTouchEvent事件是什么时候处理的呢?当ViewGroup所有的子View都返回false时,onTouchEvent事件便会执行。由于ViewGroup是继承于View的,它其实也是通过调用View的dispatchTouchEvent方法来执行onTouchEvent事件。

在目前的情况看来,似乎只要我们把所有的onTouchEvent都返回false,就能保证所有的子控件都响应本次Touch事件了。但必须要说明的是,这里的Touch事件,只限于Acition_Down事件,即触摸按下事件,而Aciton_UP和Action_MOVE却不会执行。事实上,一次完整的Touch事件,应该是由一个Down、一个Up和若干个Move组成的。Down方式通过dispatchTouchEvent分发,分发的目的是为了找到真正需要处理完整Touch请求的View。当某个View或者ViewGroup的onTouchEvent事件返回true时,便表示它是真正要处理这次请求的View,之后的Aciton_UP和Action_MOVE将由它处理。当所有子View的onTouchEvent都返回false时,这次的Touch请求就由根ViewGroup,即Activity自己处理了。

Down事件的分发决定了那个view要捕获事件,如果捕获了,后续的事件就直接分发给它,也就是说move up等事件的分发交给谁,取决于它们的起始事件Down由谁捕获。

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
//本源码来自 api 28,不同版本略有不同。
public boolean dispatchTouchEvent(MotionEvent ev) {
// 第一步:处理拦截
boolean intercepted;
// 注意这个条件,后者代表着有子view消费事件。后面会讲
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
// 子view调用了parent.requestDisallowInterceptTouchEvent干预父布局的拦截,不让它爸拦截它
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else {
intercepted = false;
}
} else {
//既不是DOWN事件,mFirstTouchTarget还是null,这种情况挺常见:如果ViewGroup的所有的子View都不消费 //事件,那么当ACTION_MOVE等非DOWN事件到来时,都被拦截了。
intercepted = true;
}

// 第二步,分发ACTION_DOWN
boolean handled = false;
boolean alreadyDispatchedToNewTouchTarget = false; //注意这个变量,会用到
// 不拦截才会分发它,如果拦截了,就不分发ACTION_DOWN了
if (!intercepted) {
//处理DOWN事件,捕获第一个被触摸的mFirstTouchTarget,mFirstTouchTarget很重要,
保存了消费了ACTION_DOWN事件的子view
if (ev.getAction == MotionEvent.ACTION_DOWN) {
//遍历所有子view(看源码知子View是按照Z轴排好序的)
for (int i = childrenCount - 1; i >= 0; i--) {
//子view如果:1.不包含事件坐标 2. 在动画 则跳过
if (!isTransformedTouchPointInView() || !canViewReceivePointerEvents()) {
continue;
}
//将事件传递给子view的坐标空间,并且判断该子view是否消费这个触摸事件(分发Down事件)
if (dispatchTransformedTouchEvent()) {
//将该view加入头节点,并且赋值给mFirstTouchTarget
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}

}
}
}

//第三步:分发非DOWN事件
//如果没有子view捕获ACTION_DOWN,则交给本ViewGroup处理这个事件。我们看到,这里并没有判断是否拦截,
//为什么呢?因为如果拦截的话,上面的代码不会执行,就会导致mFirstTouchTarget== null,于是就走下面第一 //个条件里的逻辑了
if (mFirstTouchTarget == null) {
super.dispatchTouchEvent(ev); //调用View的dispatchTouchEvent,也就是自己处理
} else {
//遍历touchTargets链表,依次分发事件
TouchTarget target = mFirstTouchTarget;
while (target != null) {
if (alreadyDispatchedToNewTouchTarget) {
handled = true
} else {
if (dispatchTransformedTouchEvent()) {
handled = true;
}
target = target.next;
}
}
}

//处理ACTION_UP和CANCEL,手指抬起来以后相关变量重置
if (ev.getAction == MotionEvent.ACTION_UP) {
reset();
}
}
return handled;
}

拦截事件

ViewGroup还有个onInterceptTouchEvent,看名字便知道这是个拦截事件。这个拦截事件需要分两种情况来说明:

1.假如我们在某个ViewGroup的onInterceptTouchEvent中,将Action为Down的Touch事件返回true,那便表示将该ViewGroup的所有下发操作拦截掉,这种情况下,mTarget会一直为null,因为mTarget是在Down事件中赋值的。由于mTarge为null,该ViewGroup的onTouchEvent事件被执行。这种情况下可以把这个ViewGroup直接当成View来对待。

2.假如我们在某个ViewGroup的onInterceptTouchEvent中,将Acion为Down的Touch事件都返回false,其他的都返回True,这种情况下,Down事件能正常分发,若子View都返回false,那mTarget还是为空,无影响。若某个子View返回了true,mTarget被赋值了,在ACTION_MOVE和ACTION_UP分发到该ViewGroup时,便会给mTarget分发一个ACTION_CANCEL的MotionEvent,同时清空mTarget的值,使得接下去的ACTION_MOVE(如果上一个操作不是UP)将由ViewGroup的onTouchEvent处理。

事件分发汇总

1、IMS从系统底层接收到事件以后,会从WMS中获取window信息,并将事件信息发送给对应的viewRootImpl
2、viewRootImpl接收到事件信息,封装成motionEvent对象后,发送给管理的view
3、view会根据自身的类型,对事件进行分发仍是本身处理
4、顶层viewGroup通常是DecorView,DecorView会根据自身callBack的状况,选择调用callBack或者调用父类ViewGroup的方法
5、后面的Touch事件分发中有两个主角:ViewGroup和View。ViewGroup包含onInterceptTouchEvent、dispatchTouchEvent、onTouchEvent三个相关事件。View包含dispatchTouchEvent、onTouchEvent两个相关事件。其中ViewGroup又继承于View。

6、ViewGroup和View组成了一个树状结构,根节点为Activity内部包含的一个ViwGroup。

7、当Acitivty接收到Touch事件时,将遍历子View进行Down事件的分发。ViewGroup的遍历可以看成是递归的。分发的目的是为了找到真正要处理本次完整触摸事件的View,这个View会在onTouchuEvent结果返回true。

8、当某个子View返回true时,会中止Down事件的分发,同时在ViewGroup中记录该子View。接下去的Move和Up事件将由该子View直接进行处理。由于子View是保存在ViewGroup中的,多层ViewGroup的节点结构时,上级ViewGroup保存的会是真实处理事件的View所在的ViewGroup对象:如ViewGroup0-ViewGroup1-TextView的结构中,TextView返回了true,它将被保存在ViewGroup1中,而ViewGroup1也会返回true,被保存在ViewGroup0中。当Move和UP事件来时,会先从ViewGroup0传递至ViewGroup1,再由ViewGroup1传递至TextView。

9、当ViewGroup中所有子View都不捕获Down事件时,将触发ViewGroup自身的onTouch事件。触发的方式是调用super.dispatchTouchEvent函数,即父类View的dispatchTouchEvent方法。在所有子View都不处理的情况下,触发Acitivity的onTouchEvent方法。

10、onInterceptTouchEvent有两个作用:1.拦截Down事件的分发。2.中止Up和Move事件向目标View传递,使得目标View所在的ViewGroup捕获Up和Move事件。

11、触摸事件由ACTION_DOWN、ACTION_MOVE、ACTION_UP组成,其中一次完整的触摸事件中,DOWN只有一个,MOVE有若干个,可以为0个。UP可以为1个或0个。

滑动冲突处理

滑动冲突场景

Android中有许多控件支持用户进行拖拽,滑动等操作,比如SeekBar,ViewPager,ScrollView,RecyclerView等等。随着业务的展现的需求变化,UI越来越复杂,不可避免的就会出现嵌套的多个可滑动View的情况,比如ViewPager中套ViewPager,ScrollView中套ViewPager,ViewPager中套RecyclerView,还有一些开发者自行开发的,可接受滑动手势的控件与标准控件的嵌套。

当ViewTree中从根到某一叶子节点的路径上,存在多个可接受滑动手势的控件时,就有可能发生滑动冲突。

滑动冲突原因

一般而言,产生滑动冲突的时候,一定有一个可以滑动的父控件作为容器,包裹着一个可以滑动的子控件。

Android的touch事件分发的方向是从父控件到子控件,而事件消费方向则是从子控件到父控件,对于一个可滑动的ViewGroup,假如他有一个子View是一个按钮,那么当用户手触摸该按钮时,该按钮默认会消费掉这一个touch事件序列中的所有的touch事件,直到用户抬手。

理论上父控件没有机会处理滑动事件,因为父控件的onTouchEvent并不会收到touch事件。

此时通常的做法是重写ViewGroup#onInterceptTouchEvent,在其中判断用户的手指在该控件上滑动的距离,如果距离超过一个阈值,则认为用户是在滑动而不是点击,此时ViewGroup#onInterceptTouchEvent返回true,所有事件均直接传给该ViewGroup的onTouchEvent,由拦截事件的控件自身进行处理。

当然重写ViewGroup#dispatchTouchEvent也可以做到,只不过一般不重写它,重写它的要么是经验丰富的人,要么就是略懂的新人。

一般而言,Android官方控件,包括support包中的控件,对滑动冲突都有一定的避免能力,天然就能互相嵌套,且滑动效果符合开发者预期,这是多种手段互相配合的结果。

滑动冲突解决思想

由于事件一定是通过父控件派发,因此父控件可以监听触摸事件,识别滑动手势,在需要处理滑动时让ViewGroup#onInterceptTouchEvent返回true。但这并不足够,因为父控件并不知道自己内部的子控件到底是什么业务逻辑,可滑动的子控件也不知道自己的父控件到底知不知道什么时候能拦截,什么时候不能拦截,因此父控件提供了ViewGroup#requestDisallowInterceptTouchEvent方法给子控件调用,让子控件能及时通知父控件,什么时候可以拦截,什么时候不能拦截。

一般的,如果有嵌套的可滑动控件,一定是子控件优先滑动,父控件在适当时机拦截事件,自行处理滑动事件。对于父控件如何识别滑动手势,并识别是否可以拦截,也有两种常见的方案。

滑动阈值
事件流经父控件时,父控件不对事件做拦截操作,但时刻计算用户的滑动方向和距离,一旦用户的滑动方向与自己可滑动的方向夹角小于一定程度,并且滑动距离超过一个阈值,同时子控件没有禁止父控件拦截的情况下,父控件在ViewGroup#onInterceptTouchEvent中返回true,以拦截事件,之后交由ViewGroup#onTouchEvent处理滑动的具体事务。如果子控件禁止父控件拦截事件,则父控件不拦截事件,也不需要识别滑动手势。

与此同时,如果子控件识别到自己可滑动,将会通过requestDisallowInterceptTouchEvent来禁止父控件对自己可能的拦截行为,并在合适的时机重新允许父控件拦截事件。

从原理上讲,滑动阈值本身并不是为了处理滑动冲突,因为一个正常的可滑动容器,必须要能做到识别滑动手势并拦截,如果不拦截,一旦内部有任何控件吃一切事件,它就滑不动了,不要觉得吃一切事件的控件是极端情况,一个clickable的View,默认就会吃全部事件,也就是说,如果父控件不拦截滑动事件,那么当用户手指落在按钮上开始滑动时,父控件永远收不到事件。

只不过很多人在写一些自定义的可滑动容器时,第一反应就是做阈值判断拦截事件,因此也算时处理滑动冲突的方案。

是不是只要有滑动阈值判断就高枕无忧了呢?

并不是。

一个有滑动阈值的父控件,我们可以说它对子控件自行处理滑动事件是宽容的,而子控件,一般而言没那么宽容,比如SeekBar只要收到DOWN事件,就会请求父控件不拦截事件,相当于当可滑动的子控件完全不给父控件机会拦截自己,当然也就不会有冲突。这种场景下,当用户滑动子控件时,父控件是无论如何不会滑动的。

但假如子控件也是一个有滑动阈值的控件,也就是说两个宽容的控件凑一块了,会怎么样呢?

原生Android事件分发体系里面,涉及到滑动事件的处理,要么是父控件拦截掉事件并处理滑动手势;要么是子控件自行处理滑动,禁止父控件拦截,无论如何,只有一方会处理滑动事件。

而我们知道事件是从父控件派发到子控件的,父控件拦截发生在子控件收到事件之前,假如父控件的阈值是10,子控件的阈值是20,那么一旦达到阈值

最先判断需要处理滑动事件的一定是父控件,因为父控件拦截在前,且阈值小于子控件,子控件根本没机会检测到滑动手势。

有人说将父控件的阈值调整到大于子控件就可以了,这样就能让子控件率先达到阈值,自行处理滑动了。

这种想法还是忽略了一个问题,用户滑动的距离并不是一个从0开始平滑增长的值,而是一系列离散的数,用户的两个touch时间之间的距离,是可能突然变得很大的,比如一上来距离就达到了40,假如父控件的阈值是30,子控件的阈值是10,由于父控件的拦截判断在先,还是父控件先拦截的事件,而不是我们想要的子控件来处理滑动。

所以遇到两个有滑动阈值的控件嵌套,且他们滑动的方向一致时,滑动冲突无法避免。

主动检测
既然滑动阈值这种纯靠父子控件自我感觉的方案在某些情况下行不通,那么就需要有主动检测的手段。

即父控件检测到滑动事件后,首先对子控件在该方向和距离上的可滑动性进行检测,如果子控件不可滑动,则事件由父控件拦截;如果子控件可以滑动,则正常放行,由子控件自行处理滑动事件并禁止父控件拦截。恰好有这么两个方法:

View.canScrollHorizontally和View.canScrollVertically。support包中还有兼容版本的实现。

最典型的例子就是ViewPager,我们知道多个ViewPager嵌套是不会有滑动冲突的,并且还能在子ViewPager无法滑动时,改为滑动父ViewPager,它的原理就是使用View.canScrollHorizontally对子控件的可滑动性进行检测。

大部分原生控件都正确实现了这两个方法。

嵌套滑动机制
嵌套滑动机制本身是为了解决可滑动父子View的联动问题,正如前面所说,一个滑动事件要么是父控件处理,要么是子控件处理,很难做到子控件处理一部分之后再交给父控件处理,或者父控件处理一部分之后再交给子控件处理。

嵌套滑动机制可以解决可滑动View的联动问题,天然就是解决滑动冲突的方案,只是嵌套滑动机制,对于早期版本的支持有限,我并没有深入了解过,这里就不讨论了。

实践思路

如果是写可滑动的父控件(即逻辑上的View容器,内部可能嵌套其他可滑动View)

一般使用滑动阈值的方法就可以正确实现,如果想实现实现更精确的控制,可以使用View.canScrollXXX来检测子控件的可滑动性。

如果是写可滑动的子控件(即逻辑上的子控件,内部不再嵌套其他可滑动View),务必不要通过阈值来判断是否需要禁止父控件拦截事件,而是在收到ACTION_DOWN的时候立即请求禁止拦截,在合适的时机再取消禁止。如果能准确知道自己的父控件会使用View.canScrollXXX来检测自己,也可以直接通过正确实现该方法来与父控件配合。

如果在实践中,遇到两个嵌套的可滑动View,均使用了滑动阈值来判断是否处理滑动,且这俩View的源码我们均不能修改,那么可以考虑给子控件设置一个OnTouchListener,遇到ACTION_DOWN直接请求禁止拦截,在合适的时候再取消禁止,虽然体验上会有些奇怪,至少能保证不出很明显的滑动冲突问题。