事件传递是 Android 开发必须掌握的基础内容,在处理自定义 View 交互时尤其重要。之前看过不少分析事件传递的文章,但理解的不够透彻,遇到具体问题还是无从下手。
为了解决这个问题,决定从源码角度梳理一下 ViewGroup
View
Activity
的事件传递流程。
分析使用版本 4.0.1 r1
源码地址View
GroupView
PhoneWindow
基础知识
触摸事件分为 ACTION_DOWN, ACTION_UP, ACTION_MOVE, ACTION_POINTER_DOWN, ACTION_POINTER_UP, ACTION_CANCEL 。
- 在支持多点触摸的屏幕上,当第一根手指按下时,触发 ACTION_DOWN 事件,之后的按下操作触发 ACTION_POINTER_DOWN 事件。同理,非最后一根手指的抬起操作触发 ACTION_POINTER_UP 事件,最后一根手指的抬起操作触发 ACTION_UP 事件。
- 用户的操作不会直接触发 ACTION_CANCEL 事件。
- 一个完整的事件流都是以 ACTION_DOWN 开始,以 ACTION_UP 结束。
- 事件的传递是从
Activity.dispatchTouchEvent
方法开始,如果一直没有被消费,最后会回到Activity.onTouchEvent
。
源码解析
View 源码解析
View.onTouchEvent
1 | public boolean onTouchEvent(MotionEvent event) { |
View对事件处理分析1
switch
View.DispathTouchEvent
1 | public boolean dispatchTouchEvent(MotionEvent event) { |
从以上源码可以看出,View
的 dispatchTouchEvent
方法只是调用 OnTouchListener
或者 onTouchEvent
方法。如果 OnTouchListener
的 onTouch()
方法返回 true
,onTouchEvent
将不会执行。
ViewGroup 源码分析
ViewGroup.onTouchEvent
ViewGroup
未复写该方法,与 View
行为一致。
ViewGroup.onInterceptToucheEvent
1 | public boolean onInterceptTouchEvent(MotionEvent ev) { |
默认不拦截事件。
ViewGroup.dispatchTouch
1 | public boolean dispatchTouchEvent(MotionEvent ev) { |
下面分析 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
77
78
79
80private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
//分发 action_cancle 事件
// 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) {
/** super.dispatch 即 View.dispatch 方法,即交给当前 ViewGroup 的 onTouchListener
* 或者 onTouchEvent 方法处理
*/
handled = super.dispatchTouchEvent(event);
} else {
//子 view 处理该事件
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
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) {
//前后id相同,不需要转换,直接进行分发
//知道是什么,但是不知道为什么???
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();
return handled;
}
ViewGroup
的事件分发分为如下几个步骤:
- 如果是
action_down
事件,清除mFirstTouchTarget
并重置触摸状态。 - 判断是否拦截该事件。如果是
action_down
或者已找到mFirstTouchTarget
并且允许事件拦截时,会调用onInterceptTouchEvent
方法。 - 判断是否取消事件。
- 寻找目标
view
,即mFirstTouchTarget
。当满足事件未取消、未拦截且当前事件为down
pointer_down
hover_move
其一的才会进入该流程。 - 事件分发。根据是否找到目标
view
决定,分发给mFirstTouchTarget
还是分发给自己。
Activity 源码分析
Activity.onTouchEvent
1 | public boolean onTouchEvent(MotionEvent event) { |
Activity.dispatchTouchEvent
1 | public boolean dispatchTouchEvent(MotionEvent ev) { |
getWindow()
返回的 Window
实例其实是 PhoneWindow
类型。此处不再具体说明。PhoneWindow
的实现仅仅调用了 DecorView
的 superDispatchTouchEvent(event)
方法。
一路追查下去,DecorView
调用其父类 FrameLayout
的 dispatchTouchEvent
方法,FrameLayout
方法继承自 ViewGroup
,且没有复写该方法。
综上,Activity
的 diapatchTouchEvent
的方法最终调用 ViewGroup
的 dispatchTouchEvent
方法。
代码测试
测试一
所有方法均返回父类方法调用。CustonView
和 CustomViewGourp
的 onTouch
方法对 down
事件返回 false
,不能成为 mFirstTouchTarget
。之后,DecorView 的 dispatchTouchEvent
返回 false
,Activity
中 getWindow().superDispatchTouchEvent()
返回 false
,最后调用 Activity
的 onTouchEvent
方法。
进一步,当 DecorView
收到 action_move
事件时,不进行寻找 mFirstTouchTarget
操作且之前没有找到 mFirstTouchTarget
,所以 DecorView
直接将该事件交给自己处理,这也造成了 CustomViewGroup
CustomView
无法接受后续事件。action_up
事件与 action_move
事件处理流程相同。
测试二
修改 CustomView
onTouchEvent
方法, action_down
时返回 true
,其他都返回父类调用。
注意,CustomView 不消费 action_move
事件,但是该事件并没有传递给 CustomViewGroup
的 onTouchEvent
。
因为当 action_move
发生时,mFirstTouchTarget
不为空,保存的就是 CustomView
。哪怕 CustomView
对 action_move
返回 false
, CustomViewGroup
也无法再获得处理权。
如果 CustomViewGroup
想处理 action_move
,必须使用 onInterceptTouchEvent
方法。
测试三
修改 CustonViewGroup
onInterceptTouchEvent
方法,action_down
时返回 true
,其他都返回父类调用。
因为 CustonViewGroup
中断了 action_down
事件的传递,该事件直接传递给 CustomViewGroup
的 onTouchEnent
。CustonViewGroup
和 CustonView
都没有成为 mFirstTouchTarget
,但是原因不同。 CustomView
压根没收到 action_down
,而 CutstonViewGroup
是收到了 action_down
事件但是不想消费(onTouchEvent
返回 false
)。
测试四
如果修改CustonViewGroup
onInterceptTouchEvent
方法,action_move
时返回 true
,其他都返回父类调用。 CustomView
onTouchEvent
方法, action_down
时返回 true
,其他都返回父类调用。action_down
事件传递流程与之前保持一致,但是 action_move
事件就比较复杂,而且注意到前后两次的 action_move
事件传递流程竟然也不一样。第一次没有调用 CustomViewGroup
的 onTouchEvent
,而第二次没有调用 onInterceptTouchEvent
- 第一次
action_move
CustonViewGroup
收到action_move
时,会中断该事件传递。此时, 不需要进行寻找目标view
操作,mFirstTouchTarget
不为空。
之后,会调用dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits)
。其中,event
的action
为move
,cancel
值为true
,child
值为CustomView
,desiredPointerIdBits
值为TouchTarget.ALL_POINTER_IDS
(不考虑多点触摸)。
在dispatchTransformedTouchEvent
方法内部,发送action_cancel
给目标view
,即CustonView
。CustonView
收到action_cancel
返回false
。最后,该方法返回false
。dispatchTransformedTouchEvent
返回后,dispatchTouchEvent
返回值仍为false
,之后销毁mFirstTouchTarget
。action_move
一直没有被消费,最终传递到Activity
的onTouchEvent
方法中。第一次action_move
事件到此结束。
可以看出,因为之前存在mFirstTouchTarget
,所以在CustomViewGroup
拦截事件后,并没有把事件交给自己处理。 - 第二次
action_move
DecorView
收到action_move
事件时,其内部的mFirstTouchTarget
不为空,保存的是CustomViewGroup
。
注意,DecorView
的mFirstTouchTarget
没有被销毁,之前CustomViewGroup
对action_move
返回的false
也并不影响mFirstTouchTarget
。
此时,不需要寻找目标View
。DecorView
把事件直接传递给CustomViewGroup
处理。CustonViewGroup
收到action_move
时,仍然中断事件传递。但此时mFirstTouchTarget
为空,CustomViewGroup
只好把事件交给自己处理。action_move
依然没有被消费,最终传递到Activity
的onTouchEvent
方法中。
两次 action_move
传递流程的区别在于:第一次决定中断事件操作是因为 onInterceptTouchEvent
返回 true
。第二次决定中断事件是因为 满足actionMasked != MotionEvent.ACTION_DOWN && mFirstTouchTarget == null
,即 “不是 action_down
事件且没有目标 View
”,ViewGroup
只好交给自己处理。
参考链接
http://blog.csdn.net/lfdfhl/article/details/42241253
https://github.com/CharonChui/AndroidNote/blob/master/Android加强/Android%20Touch事件分发详解.md
http://wangkuiwu.github.io/2015/01/04/TouchEvent-ViewGroup/