关于点击事件如何在 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 的分发过程。