从我刚进实验室的时候,学长学姐就说View的事件分发机制是Android里面一个很重要的内容,要我们好好学。

但是随着自己对Android了解的深入,越发觉得这个东西很有必要了解下,正好Android艺术开发探索也看到了View这块,也看了郭霖大神的博客另一位大神的博客,所以就好好学习了一番,并写了此博客。

1. MotionEvent

在开始讲View事件分发之前,我们先来了解下MotionEvent。

这个就是手指解除到屏幕后所产生的一系列事件,主要为一下三个典型事件:

  • ACTION_DOWM——手指刚接触屏幕
  • ACTION_MOVE——手指在屏幕上移动
  • ACTION_UP——手指从屏幕上松开
  • ACTION_CANCEL——结束事件(非人为)

正常情况下,一次手指触摸屏幕然后离开可能触发一下两种情况:

  • 点击屏幕然后松开:ACTION_DOWN -> ACTION_UP
  • 点击屏幕然后滑动再松开:ACTION_DOWN -> ACTION_MOVE -> ACTION_UP

下面一个图片来概括下:
MotionEvent

2. 事件分发传递规则

众所周知,AndroidUI是由Activity、ViewGroup、View及其派生类组成的。

大致示意图如下:
AndroidUI

其中:

  • Activity:控制生命周期或者处理事件
  • ViewGroup:一组View或者多个View的集合。也是布局Layout的基类。但是特别的是,他也集成自View。
  • View:所有UI组件的基类

从上图我们就可以看出来,事件分发的顺序就是:Activity -> ViewGroup -> View。也就是说一个点击事件产生,先交由Activity,再传到ViewGroup,再传到View。这个过程中只要有一个部分说要拦截,就不会再继续往下传递。

3. 事件分发的核心方法

其实时间分发的核心方法很简单,就由三个方法组成:

  • public boolean dispatchTouchEvent(MotionEvent ev)
    用来进行事件的分发。如果事件能够传递给当前View,那么此方法一定会被调用,返回结果受当前View的onTouchEvent和下级View的dispatchTouchEvent方法的影响,表示是否消耗当前事件。
  • public boolean onInterceptTouchEvent(MotionEvent event)
    在dispatchTouchEvent方法内部调用,用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么在同一事件序列当中,此方法不会再次被调用,返回结果表示是否拦截当前事件。(只有ViewGroup中才有此方法,View中没有)

  • public boolean onTouchEvent(MotionEvent event)
    在dispatchTouchEvent方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前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

/**
* 点击事件产生后
*/
// 步骤1:调用dispatchTouchEvent()
public boolean dispatchTouchEvent(MotionEvent ev) {

boolean consume = false; //代表 是否会消费事件

// 步骤2:判断是否拦截事件
if (onInterceptTouchEvent(ev)) {
// a. 若拦截,则将该事件交给当前View进行处理
// 即调用onTouchEvent ()方法去处理点击事件
consume = onTouchEvent (ev) ;

} else {

// b. 若不拦截,则将该事件传递到下层
// 即 下层元素的dispatchTouchEvent()就会被调用,重复上述过程
// 直到点击事件被最终处理为止
consume = child.dispatchTouchEvent (ev) ;
}

// 步骤3:最终返回通知 该事件是否被消费(接收 & 处理)
return consume;
}

对于一个根ViewGroup,点击事件产生后,就会传递给他,这时他的dispatchTouchEvent就会被调用,然后就开始判断他是否拦截。如果拦截,那么点击事件就会给ViewGroup去处理,如果不拦截,就调用child.dispatchTouchEvent (ev) 传给子控件的dispatchTouchEvent方法。然后继续循环,直到到最底层view,也就是没有child的时候,或者直到事件被拦截。

4. 源码分析

4.1 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
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
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
// ->> 分析1
}
if (getWindow().superDispatchTouchEvent(ev)) {
// ->> 分析2
return true;
}
return onTouchEvent(ev);
// ->> 分析5
}

/**
* 分析1:onUserInteraction()
* 这个方法就是个空方法,但是我是在没搞懂这个方法是干嘛的,所以就直接粘贴了carson这位大神的说明
* 作用:实现屏保功能
* 注:
* a. 该方法为空方法
* b. 当此activity在栈顶时,触屏点击按home,back,menu键等都会触发此方法
*/
public void onUserInteraction() {
}

/**
* 分析2:getWindow().superDispatchTouchEvent(ev)
* 点开之后进入Window类,
* 但是发现这个类是个抽象类,这个方法是个抽象方法。
*
* 了解过View的同学都知道,Window的唯一实现类就是PhoneWindow
* 那我们进入PhoneWindow看下他的superDispatchTouchEvent方法
*/
public abstract boolean superDispatchTouchEvent(MotionEvent event);
// ->> 分析3

/**
* 分析3:PhoneWindow.superDispatchTouchEvent()
* 实际上又调用了DecorView的superDispatchTouchEvent方法,DecorView是最顶层的View
*/
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
// ->> 分析4
}

/**
* 分析4:DecorView.superDispatchTouchEvent()
* 注:
* a. DecorView继承自FrameLayout,而FrameLayout继承自ViewGroup,也就是说DecorView就是ViewGroup。
* b. DecorView调用了父类的dispatchTouchEvent方法,也就相当于ViewGroup的dispatchTouchEvent方法,就把事件交给了ViewGroup去处理,这块后面再说
*/
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}

/**
* 分析5:return onTouchEvent(ev)
* 当触摸屏事件未由其下的任何视图处理时调用
*
*/
public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
// ->> 分析6
finish();
return true;
}

return false;
// 只有在点击事件在Window边界外才会返回true,一般情况都返回false,分析完毕
}

/**
* 分析6:Window.shouldCloseOnTouch()
*
* 返回:
* 返回true:说明事件在边界外,即 消费事件
* 返回false:未消费(默认)
*/
public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
final boolean isOutside =
event.getAction() == MotionEvent.ACTION_DOWN && isOutOfBounds(context, event)
|| event.getAction() == MotionEvent.ACTION_OUTSIDE;
// 主要是对于处理边界外点击事件的判断:是否是DOWN事件,event的坐标是否在边界内等
if (mCloseOnTouchOutside && peekDecorView() != null && isOutside) {
// peekDecorView是返回PhoneWindow的mDecor
return true;
}
return false;
}

流程

Activity事件分发流程

4.2 ViewGroup事件分发

源码

由于此部分代码过长,我们将代码拆分成两部分:

part1

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
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...// 仅贴出关键代码

boolean handled = false;
/**
* onFilterTouchEventForSecurity(ev)
* 筛选touch事件,进去之后的判断核心就是当前视图是否被其它窗口遮挡或者隐藏
*/
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;

// Handle an initial down.
// ->> 分析1
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;
/**
* 分析2
* 可以看到会有两种情况下拦截事件:事件类型为DOWN,或者mFirstTouchTarget != null。
* 那么这个mFirstTouchTarget是什么呢?
* 从后面的代码我们可以得知,当ViewGroup不拦截事件交给子元素处理的时候,mFirstTouchTarget不为null。
* 所以,也就是说当MotionEvent为UP或者MOVE的时候,都进不去这个方法,也就是不调用ViewGroup的onInterceptTouchEvent,他不拦截事件
*/
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;
}

/**
* 分析1
* 事件开始时会调用resetTouchState()来清空mFirstAndClearTouchTarget
*/
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
// 核心目的就是清空mFirstAndClearTouchTarget
resetTouchState();
}

part2

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
        ...//省略中间代码
// 进入if语句,判断条件为没有对事件进行拦截,同时事件没有结束。对ViewGroup的子元素进行遍历
if (!canceled && !intercepted) {

...//继续省略中间代码

// 得到所有的子View
final View[] children = mChildren;
// 对子View进行遍历
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.
// 该viewgroup设置事件了指向焦点View并且焦点View在前面已经找到了
if (childWithAccessibilityFocus != null) {
// 判断遍历的此View是否是焦点View,如果不是就直接下一遍循环
if (childWithAccessibilityFocus != child) {
continue;
}
// 如果是的话就将找到的焦点view置空
// i回到到数第一个下标
// 这样做的目的是先让该焦点view尝试进行下面的普通分发操作
// 如果成功了,会在下面跳出循环。
// 如果不成功,就将记录的焦点view置空,
// 从最后一个开始重新遍历,不再进入这个判断。
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}

// 判断当前子View是否能获取焦点或者是否正在做动画
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);
// ->> 分析1
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 (chifcldren[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
// ->> 分析2
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);
}

... //省略
}

/**
* 分析1 dispatchTransformedTouchEvent()
*
* 核心就是那个if (child == null)。
* 如果child不为空,那么事件就交给子View处理
*/
private 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;
}
...//省略
}

/**
* 分析2
* 如果子View能处理点击事件,那么就调用addTouchTarget方法,对mFirstTouchTarget方法进行复制,然后再进入part1中分析2。
*/
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}

part3

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
        // Dispatch to touch targets.
// 如果part2开头的那个if没有进,也就是对事件进行拦截的话,直接到这来
// 并且如果没有进入part2,那么mFirstTouchTarget仍然为空,那么就进入if
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
// 此处在上面分析过了,但是不同的是由于child直接传入了null,那么就执行super.dispatchTouchEven。
// 那么super是谁呢?我们在前面说过,ViewGroup是继承自View的,那么他就是执行View的dispatchTouchEvent。
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
... //省略
}

// Update list of touch targets for pointer up or cancel, if needed.
if (canceled
|| actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
// ->> 分析1
resetTouchState();
} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
final int actionIndex = ev.getActionIndex();
final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
removePointersFromTouchTargets(idBitsToRemove);
}
}

...//省略

}

/**
* 分析1
* 在同一事件系列结束后调用resetTouchState();
*/
private void resetTouchState() {
// 对mFirstTouchTarget清空还原
clearTouchTargets();
resetCancelNextUpFlag(this);
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
mNestedScrollAxes = SCROLL_AXIS_NONE;
}

流程

此图搬自Carson_Ho大佬的博客

4.3 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
public boolean dispatchTouchEvent(MotionEvent event) {
...//省略

boolean result = false;

...//省略

// 和ViewGroup一样的判断,判断当前视图是否被遮挡或者不可见
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
// 检测有没有设置OnTOuchListener,如果有,并且onTouch方法返回true那么进入if,结果导致onTouchEvent不被执行
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {

result = true;
}

// 如果没进入上面的if,也就相当于没有设置OnTouchListener,那么执行onTOuchEvent
if (!result && onTouchEvent(event)) {
result = true;
}
}

...//省略

return result;
}

从上面可以看出,onTouch的优先级高于onTouchEvent

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
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
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();

/**
* 注释1
* 只有在Click、LongClick、contextClick都不可用的时候才为false
*/
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

// 首先判断是不是不可用,如果是,则进入if
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.
// ->> 注释1(见上面👆)
return clickable;
}
// 如果View有代理会执行这个方法
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}

// 这块我们只调出ACTION_UP来看
// 只要clickable为true(见上面注释1👆)或者TOOLTIP(可能是Android8.0新出的提示功能吧)
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:

...//省略

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)) {
// ->> 分析1
performClickInternal();
}
}
}

... //省略

}
mIgnoreNextUpEvent = false;
break;

... //省略其它几种MotionEvent
}

return true;
}
// 由代码可知只要上面的if语句成立,不管进入switch中的任何ACTION或是都不进入,返回值都是true,即事件消费了。
return false;
}

/**
* 分析1:performClick()
*/
public boolean performClick() {

if (mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
mOnClickListener.onClick(this);
return true;
// 只要我们通过setOnClickListener()为控件cvView注册1个点击事件
// 那么就会给mOnClickListener变量赋值(即不为空)
// 则会往下回调onClick() & performClick()返回true
}
return false;
}

流程