MotionEvent事件分发机制解读

阅读提示

开篇强调,事件手势动作(点击、滑动等)是有区别,例如:一个滑动事件以ACTION_DOWN事件开始,经历多个ACTION_MOVE事件,最后以ACTION_UP事件结束。而分发机制讲述的主体是事件,没这点的意识的话,理解起来估计就没完没了。还有要理解super关键字并不是父ViewGroup(大多时候ViewGroup中的super很有可能指的就是View这个java类)。

关键函数

说到点击事件的分发怎么能不提到这三个函数:

public boolean dispatchTouchEvent(MotionEvent ev)

public boolean onInterceptTouchEvent(MotionEvent ev)

public boolean onTouchEvent(MotionEvent ev)

主体内容

dispatchTouchEvent来源

其中最难理解的也就是dispatchTouchEvent了,而且dispachTouchEvent描述了整个事件的分发与处理流程。

当点击屏幕时,事件最先传递给ActivitydispatchTouchEvent(MotionEvent ev),下面我们开始分析源码:

源码:Activity#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

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


......
/**
* 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() {
}

显然,从内容上看onUserInteraction();并不影响事件的传递,它只是可以在一个新手势开始(ACTION_DOWN事件发生)时,在事件传递开始前做最优先的事情。

下面会执行getWindow().superDispatchTouchEvent(ev)操作,那么getWindow()又是啥?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
......
mWindow = new PhoneWindow(this, window, activityConfigCallback);
......

/**
* Retrieve the current {@link android.view.Window} for the activity.
* This can be used to directly access parts of the Window API that
* are not available through Activity/Screen.
*
* @return Window The current window, or null if the activity is not
* visual.
*/
public Window getWindow() {
return mWindow;
}

再进入AndroidXRef,找到PhoneWindow.java:

1
2
3
4
5
6
7
// This is the top-level view of the window, containing the window decor.
private DecorView mDecor;
....
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}

咋样?是不是想到了传说种的Activity->PhoneWindow->DecorView。且慢,我们的目的时“事件分发机制”。那么重新找到DecorView.java:

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

DecorView extends FrameLayout,现在知道为啥有的文章说“所有的布局都在一个FrameLayout里面”了吧?其实就是DecorView,FrameLayout extends ViewGroup,简单梳理就会知道这个dispatchTouchEvent(event)就是在ViewGroup种实现的。

最终还是看ViewGroup怎么实现这个dispatchTouchEvent(event)ViewGroup#dispatchTouchEvent(MotionEvent ev),最终返回true的话,Activity#dispatchTouchEvent(MotionEvent ev)函数就直接返回了,Activity#onTouchEvent(ev)也不会被调用,如果最终返回为false的话则Activity#onTouchEvent(ev)执行,为什么要加“最终”二字。请听我娓娓道来,前方高能。

源码解析

dispatchTouchEvent源码

没错,源码(为了便于理解,这段源码有一点点的删除,阅读的时候可直接跳过这一段源码,因为我会在后面具体分析):ViewGroup#dispatchTouchEvent(MotionEvent ev):

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

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

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
//第一段-----------------------------------------------------------START
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;

// 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.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
//第一段-----------------------------------------------------------END
//第二段-----------------------------------------------------------START
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;

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;
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = 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;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);

// 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)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}

newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// 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 (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();
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();
}
}
}
//第二段-----------------------------------------------------------END
//第三段-----------------------------------------------------------START
// 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 {
// 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 {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}

}
predecessor = target;
target = next;
}
}
//第三段-----------------------------------------------------------END
return handled;
}

年轻人,我知道你不想看源码,我也很无奈。

第一段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}

final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}

从备注与函数名上理解,开始只是做一些重置初始工作(99.9%情况下,我们要对Google的工程师足够的相信,不然你会越陷越深)。

然后,会在判断里面看到onInterceptTouchEvent(ev),嗯,终于看到第二个函数了。那么什么情况下会调用这个函数呢?disallowIntercept从字面和后面的赋值理解,这是通过mGroupFlags字段,不允许ViewGroup拦截,通过语句判断如果ACTION_DOWN不予许拦截成立的话(!disallowIntercept == false),所以intercepted = false,(分析时避免情况复杂化,我们要猜一些变量或者函数的意义,比如:!canceled应该就时动作没被取消,正常情况下该条件是成立的)所以,会进入执行第二段。

第二段

接下来分析一下这个”mChildrenCount”:

1
2
3
4
5
// Child views of this ViewGroup
private View[] mChildren;
// Number of valid children in the mChildren array, the rest should be null or not
// considered as children
private int mChildrenCount;

现在,大概可以看出什么意思了吧?mChildren是指ViewGroup里面的所有子View,而mChildrenCount指的是能所有有效的ViewGroup里子View的数量,好了,我们接着执行第二段。可以看出,如果没有有效的子View,则会直接进入第三段。如果有有效的子View,且满足传递的要求(这一点可以通过备注看出,各种函数名也表面),则会在该子View执行dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign),再看看这个函数名:

1
2
3
4
5
6
7
8
9
10
11

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits){
//各种判断各种处理以后都会成为这种结构
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
return hanled;
}

啥意思?子View不为空,则子View继续分发事件,否则执行super(赌一包辣条,这里的super就是View)的dispatchTouchEvent,所以接下来看看ViewdispatchTouchEvent函数:
源码: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
/**
* 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) {

if (onFilterTouchEventForSecurity(event)) {
//noinspection SimplifiableIfStatement
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;
}
}

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

熟悉不?你有没有OnTouchListener啊,要是有的话,你onTouch返回了啥东东啊?给我看看,我好决定怎么处理onTouchEvent,这里就清楚onTouchonTouchEvent了吧!!!关于传递—>处理的流程就到这里了。接下来看看返回值对后续的过程影响。

很明显,onTouchonTouchEvent任意一个返回true(从代码可见得:onTouch返回true后,onTouchEvent是没有机会执行的),都将成为其所在的ViewGroupdispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)的返回值,如果返回false则,会使得mFirstTouchTarget == null,直接执行第三段的handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);。明显进入[onTouch], onTouchEvent过程handle的返回值将直接依赖这两个(或没有设置onTouch监听就是一个)的返回值,从这里我们知道父ViewGroup中的[onTouch], onTouchEvent也调用了。如果返回true,将会走进addTouchTarget(child, idBitsToAssign)

源码:ViewGroup#addTouchTarget

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(View child, int pointerIdBits) {
TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}

第三段

mFirstTouchTarget == null时,上面已经分析。

但是在上述条件不成立的情况下这里mFirstTouchTarget被赋值后,其他非ACTION_DOWN事件将继续通过dispatchTransformedTouchEvent传递到子View,并且。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 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 {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}

}
predecessor = target;
target = next;
}

大致流程就是这样分析的,未完待续。。。。。。

小结

这篇文章是看完《Android开发艺术探索》后自己跟着源码一步步来走的,看完书以后其实只是一个轮廓,然后自己看源码后映像深刻了些,但总是感觉不放心,编程路上一位导师曾经跟我说过:当你觉得你懂了得时候,你就该写下来。当你觉得你能写明白得时候,去找一个人然后把他教会。这时候你才是真正得懂。

遗憾由于水平有限,连写都没能写得明白,但是写得过程中确实也有了不少得收获,但也不该只满足于此。

有人说对于源码得追究其实不该太过钻牛角,然而还是希望尽量满足自己得好奇心,待续。。。