什么是事件分发
当手指触摸屏幕的过程中,系统会创建一系列的MotionEvent
对象,并将此对象传递给一个具体的View
进行处理,这个传递过程即为分发过程,MotionEvent
即为分发的事件。典型的事件分为三个类型:
ACTION_DOWN
—— 表示手指按下屏幕ACTION_MOVE
—— 表示手指在屏幕上滑动ACTION_UP
—— 表示手指离开屏幕
一个完整的事件序列以ACTION_DOWN
开始,到ACTION_UP
结束。传递时涉及三个重要的方法:
public boolean dispatchTouchEvent(MotionEvent event)
:用来进行事件的分发,返回结果表示是否消耗当前事件public boolean onInterceptTouchEvent(MotionEvent event)
:用于判断是否拦截某个事件public boolean onTouchEvent(MotionEvent event)
用于处理事件,返回结果表示是否消耗当前事件
通过伪代码表示事件分发过程:1
2
3
4
5
6
7
8
9public boolean dispatchTouchEvent(MotionEvent event) {
boolean consume = false;
if (onInterceptTouchEvent(event)) {
consume = onTouchEvent(event);
} else {
consume = child.dispatchTouchEvent(event);
}
return consume;
}
Activity与Window的事件分发过程
当一个事件产生时,事件最先传递给当前Activity
的dispatchTouchEvent()
进行分发:1
2
3
4
5
6
7
8
9public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
Activity只是简单的将事件传递给了当前Activity
的Window
进行分发,当Window
的superDispatchTouchEvent()
方法返回true时,事件分发完成,否则交给当前Activity
的onTouchEvent()
分发处理。接着看PhoneWindow
的源码:1
2
3
4
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
这里直接传递给了DecorView
的superDispatchTouchEvent()
方法进行分发,再接着往下看:1
2
3public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
DecorView
将事件传递给了ViewGroup
的dispatchTouchEvent()
方法进行分发,至此事件传递到了View
。
ViewGroup的事件分发
对于ViewGroup
的点击事件,分发过程如下:
- 初始化:当事件是
ACTION_DOWN
时,清除之前事件序列的所有状态以及mFirstTouchTarget
- 判断是否拦截事件:
- 当事件是
ACTION_DOWN
时,如果没有设置FLAG_DISALLOW_INTERCEPT
标记位,是否拦截事件由ViewGroup
的onInterceptTouchEvent()
方法决定,否则ViewGroup
不拦截事件 - 当事件不是
ACTION_DOWN
时,如果mFirstTouchTarget != null
,那么与上一条类似,是否拦截事件由FLAG_DISALLOW_INTERCEPT
标记位与onInterceptTouchEvent()
方法决定。否则ViewGroup
将拦截事件
- 当事件是
- 当
ViewGroup
不拦截事件且事件为ACTION_DOWN
时,调用removePointersFromTouchTargets()
移除之前事件序列的TouchTarget
,并跳过遍历寻找能够处理该事件的View,寻找过程如下:- 从后往前遍历,使上层的View能优先收到事件
- 寻找能够接收事件的View,判断条件为View可见或处于动画中,并且事件坐标在View上
- 如果该View已经在
mFirstTouchTarget
的单链表上,那么结束寻找 - 如果该View不在
mFirstTouchTarget
的单链表上,通过dispatchTransformedTouchEvent()
对调用child的dispatchTouchEvent()
方法对事件进行分发,如果child消费此事件,则将此View添加到mFirstTouchTarget
的单链表上并设置alreadyDispatchedToNewTouchTarget
为true,结束寻找 - 重复,直到遍历完所有child或者找到符合条件的view
- 如果没有找到满足条件的View且
mFirstTouchTarget != null
,将最新添加的TouchTarget
作为找到的view
- 判断
mFirstTouchTarget
是否为空,即是否有child消费了此事件序列的ACTION_DOWN
事件,如果没有则自己处理该事件。 - 如果
mFirstTouchTarget
不为空,即有child消费了此事件序列的ACTION_DOWN
事件,则将此事件交给该view处理
1 |
|
从上面的代码中可以发现,在对事件进行分发时,都是调用的:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits)
看一下此方法的源码,虽然源码较长,但是可以发现,当child == null
时将调用super.dispatchTouchEvent()
即View类的dispatchTouchEvent()
方法,否则将调用child.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
71private 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) {
handled = super.dispatchTouchEvent(event);
} else {
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) {
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;
}
View(不包含ViewGroup)的事件分发
由于View
不再包含child所以不会进一步向下传递事件,当View
处于可用状态且设置了OnTouchListener
时,优先将事件传递给OnTouchListener
处理。当不满足条件或OnTouchListener
不消费当前事件(返回了false)时,事件将传递给onTouchEvent()
方法处理。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;
... ...
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;
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
... ...
return result;
}
对于onTouchEvent()
方法,主要分为几个部分:
- 当View处于不可用状态时,如果当前View可点击(
CLICKABLE
或LONG_CLICKABLE
,下同)则会消耗当前事件,但是不会响应事件。 - 当View设置有
TouchDelegate
(用于扩大View的可点击范围),将事件传递给TouchDelegate
的onTouchEvent()
方法处理 - 当View处于可点击状态时,所有事件都将被消费掉,并且在
ACTION_DOWN
时,发送延迟消息(默认500ms)来执行长按事件。在ACTION_UP
时,判断长按事件是否已执行,如果没有执行则移除长按事件的回调,并调用performClick()
方法执行点击事件。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
165public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
//1. 当View不可用时的处理过程
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;
}
//2. 当View设置有代理时的处理过程
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
//3. 对View事件的具体处理
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)) {
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;
}