关于点击事件如何在 View 中进行分发,上一篇文章已经做了详细的介绍,这里就不做过多的解释了,下边我们来看顶级 View 是如何进行事件的分发的。
首先看 ViewGroup 的点击事件分发过程,其主要实现在 ViewGroup 的 dispatchTouchEvent 方法中。这个方法比较长,我们分段说明。先看下边的代码,很显然,它描述的是当前 View 是否拦截点击事件这个逻辑。
// 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;
}
从上边的代码我们可以看出,ViewGroup 在两种情况下会判断是否拦截当前事件,事件类型为 ACTION_DOWN 或者 mFirstTouchTarget 不为空的时候。ACTION_DOWN 时间好理解,那么 mFirstTouchTarget != null 是个什么意思呢?这个先不讲,后边的逻辑会说明。当事件由 ViewGroup 的子元素处理时,mFirstTouchTarget 会被赋值并指向子元素,换种方式说,当 ViewGroup 不拦截事件,并且将事件交给子元素处理的时候,mFirstTouchTarget 不是空值。反过来,一旦事件交由 ViewGroup 拦截时,mFirstTouchTarget != null 就不成立。那么当 ACTION_MOVE 和 ACTION_UP 来的时候,由于 (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null)这个条件返回的是 false,将导致 ViewGroup 的 onInterceptTouchEvent 不会被调用,并且同一序列中的其他事件都会交给它处理。
当然,还有一种特殊情况,那就是FLAG_DISALLOW_INTERCEPT标记位,这个标记是通过 requestDisallowInterceptTouchEvent 方法来设置的,一般用于子 View 中,FLAG_DISALLOW_INTERCEPT 一旦设置后,ViewGroup 将无法拦截除了 ACTION_DOWN 以外的事件。为什么这么说呢?这是因为 ViewGroup 在 ACTION_DOWN 事件中会重置 FLAG_DISALLOW_INTERCEPT 这个标记位,将导致子 View 中设置这个标记位无效。因此,当面对 ACTION_DOWN 事件时,ViewGroup 总是会调用自己的 onInterceptTouchEvent 方法来询问自己是否要拦截事件。
在下边的代码中,ViewGroup 会在 ACTION_DOWN 事件到来的时候做重置的操作,而在 resetTouchState 方法中 会对 FLAG_DISALLOW_INTERCEPT 进行重置,因此子 View 调用 requestDisallowInterceptTouchEvent 方法并不能影响 ViewGroup 对 ACTION_DOWN 事件的处理。
// 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 方法,这就证实了第一篇文章说的第三条结论了。FLAG_DISALLOW_INTERCEPT 这个标记的作用是让 ViewGroup 不在拦截事件,当然前提是 ViewGroup 不拦截 ACTION_DOWN 事件,因为 FLAG_DISALLOW_INTERCEPT 是在 requestDisallowInterceptTouchEvent 方法中设置的,而 FLAG_DISALLOW_INTERCEPT 可以判断 ViewGroup 是否拦截除 ACTION_DOWN 以外的事件,这就证实了第一篇文章第十一条结论。
那么这段分析对我们有什么价值呢?总结起来两点,
第一点:onInterceptTouchEvent 方法不是每次事件都会调用的,如果我们想要提前处理所有的点击事件,要选择 dispatchTouchEvent 方法,只有这个方法能确保每次事件都会调用,当然前提是事件能够传递到当前的 ViewGroup,
第二点:FLAG_DISALLOW_INTERCEPT 标记位的作用为我们提供了一个思路,当面对滑动冲突的时候,我们是不是可以考虑用这种方法去解决问题。后续文章会讲解。
下边接着来看当 ViewGroup 不拦截事件的时候,事件会分发交由他的子 View 来处理,如下代码:
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;
}
上边代码的逻辑很清晰,首先遍历 ViewGroup 的所有子元素,然后判断子元素是否能接收到点击事件,是否能接受点击事件主要由两点来衡量,一个是子元素是否在播放动画,一个是点击事件的坐标是否在子元素的做区域内。如果子元素满足这两个条件,那么事件就会交给它来处理。可以看到,dispatchTransformedTouchEvent 实际上调用的就是子元素的 dispatchTouchEvent 方法。在它的内部有如下一段代码,因为上边的代码中 child 传递的不为空,因此它会直接调用子元素的 dispatchTouchEvent 方法,这件事件就交给子元素处理了。从而完成一轮事件分发。
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
如果子元素的 dispatchTouchEvent 返回 true,那么 mFirstTouchTarget 会被赋值,还记得刚才我们说过 ViewGroup 拦截事件的条件吗,其中一个是 mFirstTouchTarget,同时跳出 for 循环,如下代码:
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
这几行代码完成了对 mFirstTouchTarget 的赋值并终止了子元素的遍历,如果子元素的 dispatchTouchEvent 返回 false,ViewGroup 就会把事件分发给下一个子 View (如果还存在下一个子 View 的话)。
其实 mFirstTouchTarget 真正的赋值过程是在 addTouchTarget 方法的内部完成的,从下边 addTouchTarget 方法的内部结构可以看出, mFirstTouchTarget 其实是一种单链表结构,mFirstTouchTarget 是否被赋值,将直接影响到 ViewGroup 对事件的拦截策略,如果 mFirstTouchTarget == null,那么 ViewGroup 将默认拦截接下来同一序列中所有的事件。
/**
* Adds a touch target for specified child to the beginning of the list.
* Assumes the target child is not already present.
*/
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
如果遍历所有的子元素后都没有找到合适的处理,这包含了两种情况:
1. ViewGroup 没有子元素;
2. 子元素处理了点击事件,但是在 dispatchTouchEvent 中返回了 false,这一般是因为在 onTouchEvent 方法中返回了 false,ViewGroup 将事件分发给下一个子元素了。
在上边两种情况下,ViewGroup 会自己处理点击事件,这就证实了第一篇文章第四条结论,代码如下:
// 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);
注意上边这段代码,这里 dispatchTransformedTouchEvent 方法中第三个参数 child 为null,他会调用 super.dispatchTouchEvent(event),很显然,这就跳转到了 View 的 dispatchTouchEvent 方法,即点击事件交给 View 来处理。至此,ViewGroup 事件的分发过程已经完成,接下来将继续讲解 View 的分发过程。