事件分发机制(三)—— 顶级 View 的事件分发机制之源码分析

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


猜你喜欢

转载自blog.csdn.net/sinat_29874521/article/details/79568377
今日推荐