目录
  1. 1. 1. MotionEvent
  2. 2. 2. 事件分发传递规则
  3. 3. 3. 事件分发的核心方法
  4. 4. 4. 源码分析
    1. 4.1. 4.1 Activity事件分发
      1. 4.1.1. 源码
      2. 4.1.2. 流程
    2. 4.2. 4.2 ViewGroup事件分发
      1. 4.2.1. 源码
        1. 4.2.1.1. part1
        2. 4.2.1.2. part2
        3. 4.2.1.3. part3
      2. 4.2.2. 流程
    3. 4.3. 4.3 View事件分发
      1. 4.3.1. 源码
        1. 4.3.1.1. dispatchTouchEvent
        2. 4.3.1.2. onTouchEvent
      2. 4.3.2. 流程
View的事件分发

从我刚进实验室的时候,学长学姐就说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;
}

流程

文章作者: littlecorgi
文章链接: https://a1203991686.github.io/posts/bfb9e8a9.html
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 littlecorgiの小站
打赏
  • 微信
  • 支付寶

评论