Android的MotionEvent事件分发机制


android事件的源头在哪里?

当用户触摸屏幕或者按键等时,形成事件,事件经过linux底层Event节点捕获之后,一直传到android应用层。中间传递的过程不是本文的重点,我也不是很清楚(哈哈哈)。本文的重点是事件在应用层的分发机制。

事件在View树中的分发过程

View树:
图片

在Android中,事件的分发过程就是MotionEvent在view树分发的过程。默认是中从上而下,然后从下而上的传递的,直到有view、viewgroup或者Activity处理事件为止。

为什么要先从上而下?是为了在默认情况下,屏幕上层叠的所有控件都有机会处理事件。这个阶段我们称为事件下发阶段。

为什么要从下而上?是为了在从上而下分发时,事件没有控件处理时,再从下而上冒泡事件,是否有控件愿意处理事件。如果中间没有控件处理,事件就只能由Acitivity处理了。这个阶段我们称为事件的冒泡阶段。

准备

事件序列:从用户手指触摸屏幕开始,经过滑动到手指离开屏幕。这个操作产生了一个dowm事件,一系列move事件,最后一个up事件结束。我们把这一个操作产生的事件称为一个事件序列。

Acitivity中和事件传递有关的函数
事件分发:dispatchTouchEvent
事件处理:onTouchEvent

ViewGrop中和事件传递有关的函数
事件分发:dispatchTouchEvent
事件拦截:onInterceptTouchEvent
事件处理:onTouchEvent

View中和事件传递有关的函数
事件分发:dispatchTouchEvent
事件处理:onTouchEvent

从上面可以看出,ViewGrop中多了事件拦截onInterceptTouchEvent函数,是为了询问自己是否拦截事件(在事件分发中询问),如果没有拦截就传递事件给直接子view,如果拦截就将事件交给自己的事件处理函数处理。View中没有事件拦截函数,因为view是在view树中的叶节点,已经没有子view。

下面是先进行源码分析,然后再验证得出一些结论。代码迟点上传github。
用图表示布局的层级关系:
图片

这里分析事件的分发过程,是从down事件的分发开始,以及分析它在两个阶段的传递过程:下发阶段和冒泡阶段。

事件下发阶段

(1)在Acitvity中的源码分析:

Activity#dispatchTouchEvent

1
2
3
4
5
6
7
8
9
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}

在第4行,Acivity将事件传递给了Window,Window是一个抽象类。在手机系统中它的实现是PhoneWindow.下面进入PhoneWindow中。

PhoneWindow#superDispatchTouchEvent

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

从上面可以看出,事件已经从Acitivity到PhoneWindow,再传到了DecorView。DecorView是一个继承FrameLayout的ViewGroup,从而事件进入了View树的传递。

重写在Acitvity中的事件传递方法

重写Activity#dispatchTouchEvent:
1、返回false,事件不分发,所有事件在Acitivity的分发函数中就中断(真的不见了),连Acitivity的事件处理函数都到达不了。
2、返回true,所有事件在Acitivity的分发函数中就中断,和false一样
3、返回父函数方法,事件就传给直接子view分发

进一步的,DecorView是一个FrameLayout,也即是一个ViewGruop。

(2)在ViewGruop中的源码分析:
ViewGruop#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
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;
}

在5-11行,是每个新的事件系列开始前,会重置事件相关的状态。这里我们关注两个地方。第一个是第17行的disallowIntercept标志,第二个是第19行调用了事件拦截函数,询问是否拦截事件。

ViewGruop#onInterceptTouchEvent

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

onInterceptTouchEvent的代码很简单。

重写在ViewGroup中的事件传递方法
重写ViewGroup#dispatchTouchEvent:
1、返回false,不分发,down事件给父ViewGroup处理,以后的事件全部直接通过父ViewGroup分发函数给父ViewGroup的事件处理函数处理。
2、返回true,则所有的事件都从头来到这里就中断,不见了。
3、返回父函数方法,看下面拦截函数

重写ViewGroup#onInterceptTouchEvent(询问是否拦截):
1、返回true,就调用处理函数,在处理函数中是否消耗down事件
2、返回false,是否是最后一个view?否,down事件就分发给子View;是,就调用一次它的处理函数,进入冒泡阶段(就是一寸事件处理函数调用)
3、返回父函数的方法,和返回false一样

重写ViewGroup的onTouchEvent,当down事件来到中onTouchEvent时,
1、返回true,就消耗down事件,后面全部事件从头分发到处理函数(不用再询问是否拦截)。后面的事件根据是否消耗而是否消失(不消耗就消失),消失的所有事件由Acitivity处理(注意消失的事件也是从头传递到这里再传给Acitivity的)。
2、返回false,将down事件冒泡回去,看谁会处理。
3、返回父函数方法,是默认不消耗。

(3)在View中的源码分析:
View#dispatchTouchEvent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;
}
}

这里关注的地方是,第9行和第13行。第9行是当前view如果设置了onTouch事件,并且它返回了true,那它就直接将result设置为true,事件就消耗了,不会再继续传递下去,只到达onTouch。第13行,是事件处理函数。可以看出onTouch是优先于onTouchEvent的。

View#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
....
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
...

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();
}
}
}
...
}
...
}
return true;
}

view根据是否可以点击等等一系列判断什么的。这里关注up事件中的第42-53行,有performClick。

View#performClick

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}

sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);

notifyEnterOrExitForAutoFillIfNeeded(true);

return result;
}

如果view设置了mOnClickListener,即点击事件,会调用view的点击事件。如果在父view中拦截了up事件,使up事件到达不了这里,会使view的点击事件失效。

可以知道,onTouch是优先于onTouchEvent,onTouchEvent优先于onclick。

事件冒泡阶段

当down事件到达了最后一个子view,如果仍然没有view愿意处理它,就调用一次最后一个子view的事件处理函数,是否处理dowm事件,如果不处理,就一直冒泡回去,直到有view的onTouchEvent处理为止。如果都不处理,就只有Acitivity自己处理了。整个事件冒泡阶段就是一串onTouchEvent的回溯过程,自下而上。

坚持原创技术分享,您的支持将鼓励我继续创作!
-------------本文结束感谢您的阅读-------------