Android触摸事件传递机制

触摸事件的类型

触摸事件对应的是Android中的MotionEvent类,主要分为三种

  • ACTION_DOWN:用户按下屏幕触发
  • ACTION_MOVE: 用户手指在屏幕移动一定距离
  • ACTION_UP:用户抬起手指,一般代表整个触发事件结束

事件传递的三个阶段

Android系统中拥有事件传递处理能力的只有三种:Activity、ViewGroup、View,其中ViewGroup处理较为特殊,多了拦截(Intercept)方法,Activity和View只具备分发和消费。结下来介绍事件传递的三个阶段分发、拦截和消费。

  • 分发(Dispatch):用户在按下屏幕,系统会首先调用 Activity#dispatchTouchEvent 方进行事件分发

    1
    public boolean dispatchTouchEvent(MotionEvent ev)
  • 拦截(Intercept): 对应方法 ViewGroup#onInterceptTouchEvent(MotionEvent ev) 可以设置拦截事件

    1
    public boolean onIterceptTouchEvent(MotionEvent ev)

    该方法的返回值如果是true表示拦截此事件,交由当前ViewGroup#onTouchEvent消费该事件,不传递给子视图,返回false表示不对事件进行拦截,继续传递给子视图。

  • 消费(Consume):事件的消费对应着 onTouchEvent 方法

    1
    public booelan onTouchEvent(MotionEvent ev)

View 中事件传递分析

截屏2019-11-21上午11.19.26

总结:用户点击View 首先会调用 Activity中的dispatchTouchEvent方法,如果返回 super.dispatchTouchEvent方法,用户点击事件会继续传递至 View#dispatchTouchEvent 方法,如果返回 true或false 方法是无法向下传递的,且不会触发 View#onClick 事件

View/ViewGroup 中 dispatchTouchEvent 方法返回值问题,当返回父类默认方法 super.dispatchTouchEvent 方法,事件正常向下传递。返回值 true 或者 false 区别:返回 true 时,代表 View/ViewGroup 的 dispatchTouchEvent 方法可以处理该事件可以响应 ACTION_DOWN、ACTION_MOVE、ACTION_UP等,返回 false 时,只响应 ACTION_DOWN事件。

ViewGroup 中事件传递

从Activity –> ViewGroup –> View 角度分析。用户触发 ACTION_DOWN 事件,首先调用 Activity#dispatchTouchEvent 方法,如果返回值为 true或false ,ACTION_DOWN事件将停止传递。如果返回 super.dispactchTouchEvent 默认值,将继续传递到 ViewGroup#dispatchTouchEvent , 如果返回 true 或 false,

截屏2019-11-22上午9.38.00

Android 事件分发机制源码分析

我们都知道分析事件传递是ACTION_DOWN、ACTION_MOVE和ACTION_UP事件在Activity、ViewGroup 和View 中的传递过程,主要是依赖下面的三个方法(分发、拦截、消费):

1
public boolean diapatchTouchEvent(MotionEvent event)

用来进行事件的分发。如果事件能够传递给当前View,那么此方法一定会被调用,返回结果受当前View 的 onTouchEvent 和下级 View 的 dispatchTouchEvent 方法的影响,表示是否消耗当前事件。

1
public boolean onIterceptTouchEvent(MotionEvent event)

只存在于 ViewGroup中,用于判断是否拦截某个事件,如果当前 ViewGroup 拦截了某个事件,那么在同一个事件序列当中,此方法不会被再次调用,返回结果表示是否拦截当前事件。

1
public boolean onTouchEvent(MotionEvent event)

用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前View无法再次收到事件。

我们先由 Activity#dispatchTouchEvent() 方法开始分析,如下所示:

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

我们可以看调用的是 Window#superDispatchTouchEvent() 方法,我们都知到Window接口的唯一实现是PhoneWindow类,方法如下所示:

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

其中mDecor 为 DecorView实例,Activity的结构是:Activity -> PhoneWindow -> DecorView -> titleView+contentView。DecorView#superDispatchTouchEvent 如下所示:

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

该方法最终是调用 ViewGroup#dispatchTouchEvent() ,所以我们最终是分析ViewGroup -> View 的事件传递过程。先看一下ViewGroup#dispatchTouchEvent() 中的部分代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// 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;
}
}

先分析一下不通过调用拦截方法直接设置 intercepted = true 的情况,mFirstTouchTarget == null 且 事件是除了ACTION_DOWN 以外的事件,从后续的代码中我们可以知道 mFirstTouchTarget 参数的赋值是在ViewGroup的子元素成功处理ACTION_DOWN的时候,所以一旦我们在ViewGroup中拦截此事件,那么ViewGroup 不会再调用拦截方法而直接设置 intercepted = true。

在此方法内部,还有一个特殊值 diallowIntercepte 参数,这个参数由 mGroupFlags这个参数决定,可以通过requestDisallowInterceptTouchEvent方法这个参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
...
if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
// We're already in this state, assume our ancestors are too
return;
}

if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}

// Pass it up to our parent
if (mParent != null) {
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
...
}

一般由子元素设置解决事件冲突的。可以认为默认为false,当子元素设置这个参数为true时,ViewGroup会默认不拦截除了ACTION_DOWN以外的其他事件。为什么说是除了ACTION_DOWN以外的事件,因为ACTION_DOWN事件会重置mGroupFlags 这个参数。

接下来看一下 mFirstTouchTarget的赋值:

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
// ViewGroup#requestDisallowInterceptTouchEvent(boolean disallowIntercept)
...
if (!canceled && !intercepted) {

// If the event is targeting accessibility 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();
}

if (newTouchTarget == null && mFirstTouchTarget != null) {
// Did not find a child to receive the event.
// Assign the pointer to the least recently added target.
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
}
...

此方法中遍历子元素,判断子元素是否能够接收到点击事件。是否能够接收点击事件主要由两点来衡量:子元素是否在播动画和点击事件的坐标是否落在子元素的区域,如果子元素满足这两个条件,那么事件就会传递给它来处理。子元素处理事件是通过dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign),而该方法的主要内容如下所示:

1
2
3
4
5
6
7
...
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
...

mFirstTouchTarget 的赋值是在 dispatchTransformedTouchEvent() 返回值为true,即子元素成功处理了这个事件之后调用 addTouchTarget(child, idBitsToAssign) 方法时赋值,addTouchTarget 方法如下所示:

1
2
3
4
5
6
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}

如果mFirstTarget 为 null ,这有两种可能,第一种是ViewGroup没有子元素,第二种是子元素没有处理这个事件(返回值 false),在这种情况下一般有ViewGroup自己的onTouchEvent() 方法来处理:

1
2
3
4
5
6
// 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);
}

View 对点击事件的处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public 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;
}

OnTouchListener 方法发生在 onTouchEvent之前,如果OnTouchListener#onTouch 方法返回值为true,将不会调用onTouchEvent() 方法,onClick事件是发生在onTouchEvent方法中的,因此也不会调用。

View 事件冲突的解决办法

外部拦截法

例如水平方向的ScrollView内部嵌套ListView,重写ScrollerView的onInterceptTouchEvent方法,

内部拦截法

ViewGroup#requestDisallowIntercept()

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×