0%

View事件分发机制

一、先看ViewGroup类中的dispatchTouchEvent方法,其中关于拦截事件的代码:

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

可以看到,// Check for interception. 这句注释说明是处理拦截逻辑的代码。先定义一个布尔型的变量,我们看到,ViewGroup会在两种情况下判断是否要拦截当前事件:1,actionMasked == MotionEvent.ACTION_DOWN(手指刚触摸屏幕事件)。2,mFirstTouchTarget != null()。

MotionEvent.ACTION_DOWN好理解,就是触摸的一瞬间。mFirstTouchTarget != null()是什么意思呢?

1
2
// First touch target in the linked list of touch targets.
private TouchTarget mFirstTouchTarget;

源码如上,这个mFirstTouchTarget是一个TouchTarget变量。从后面的代码可以看到:当ViewGroup的子元素成功处理时,mFirstTouchTarget会被赋值并指向子元素,也就是说,当ViewGroup不拦截事件并将事件交由子元素处理时mFirstTouchTarget != null成立。反之,一旦事件由当前ViewGroup拦截,mFirstTouchTarget != null就不成立。这时候ACTION_DOWN和ACTION_UP事件到来时,由于(actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null)这个条件就为false,将导致ViewGroup的onInterceptTouchEvent不会被调用,并且同一事件序列的其他事件也会默认交由他处理。

有一种特殊情况,就是FLAG_DISALLOW_INTERCEPT标记位,它是通过requestDisallowInterceptTouchEvent方法来设置的,一般用于子View中。FLAG_DISALLOW_INTERCEPT一旦被设置,ViewGroup将无法拦截除了ACTION_DOWN以外的其他点击事件。为什么是除了ACTION_DOWN点击事件以外的其他事件?因为在ViewGroup分发事件中,如果是ACTION_DOWN事件就会重置FLAG_DISALLOW_INTERCEPT这个标记位,导致子View标记位无效,因此,当面对当前ACTION_DOWN事件时,ViewGroup总是会调用自己的onInterceptTouchEvent方法来询问自己是否需要拦截事件,从源码可以看出来。

下面代码中,ViewGroup会在ACTION_DOWN事件到来的时候做重置状态的操作,而在resetTouchState();方法中,会对FLAG_DISALLOW_INTERCEPT进行重置,因此子View调用requestDisallowInterceptedTouchEvent方法并不会影响ViewGroup对ACTION_DOWN的处理。

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

从上面的源码分析,当ViewGroup决定拦截事件后,后续的点击事件将会默认交给它处理而不是调用它的onInterceptTouchEvent方法,这证实了:当某一个View开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent返回false),那么同一事件序列的其他事件也不会再交由它处理,并且将事件重新交由它的父元素处理,即父元素的onTouchEvent会被调用。也就是说,一旦事件交由一个View处理,那么它就必须消耗掉,否则同一事件序列的其他事件就不会再交由它处理了。FLAG_DISALLOW_INTERCEPT的作用是让ViewGroup不再拦截事件,当然前提是ViewGroup不拦截ACTION_DOWN事件,这证实了:事件传递过程是由外向内的,即事件总是先传给父元素,然后再由父元素分发给子View,通过requestDisallowInterceptedTouchEvent方法可以在子元素中干预父元素的事件分发过程,但是ACTION_DOWN事件除外。这意味着什么?这证明了两点:1,onInterceptTouchEvent不是每次事件都会被调用,如果我们想提前处理所有点击事件,要选择dispatchTouchEvent方法,只有这个方法能确保每次会被调用,当然前提是事件能够传递到当前的ViewGroup,2,FLAG_DISALLOW_INTERCEPT的作用给我们提供了一个思路,当面对滑动冲突时,我们可以考虑用这种方法去解决。

接着再看ViewGroup不拦截事件的时候,事件会向下分发交由它的子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
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
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);
}

View先遍历所有的子元素,然后判断子元素是否能够接收到点击事件,能否接收点击事件由两个指标决定:1,子元素是否在播放动画和点击事件的坐标是否落在子元素的区域内。如果某个子元素满足这个条件,那么事件就会由它来处理。dispatchTransformedTouchEvent实际上调用的是子元素的dispatchTouchEvent方法,在它的内部有如下代码,而在上面的代码中child传递的不是null,因此它会直接调用子元素的dispatchTouchEvent方法,这样事件就交由子元素处理了,从而完成了一轮事件分发。

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

如果子元素的dispatchTouchEvent返回true,这时不考虑事件在子元素内部是怎么分发的,那么mFirstTouchTarget就会被赋值的同时跳出for循环,如下所示:

1
2
3
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;