手势冲突是 android 开发中经常遇到的一类问题了,网上讲解此问题的文章也很多,但是大都浅显地过一遍事件分发的调用栈,然后给出一个调用栈流程图;要不就是使用日志大法,用日志来验证自己的想法,完全没有参考价值;这里根据事件分发相关源码,记录下我的理解

MotionEvent 里定义的 ACTION_XXX 还不少有 10 多个,看起来情况很复杂的样子,实际上只需要关注三个:ACTION_DOWNACTION_MOVEACTION_UP,而且在一个手势里它们的顺序是:

ACTION_DOWN → ACTION_MOVE → ACTION_MOVE → ... → ACTION_UP

跟踪源码的调用栈

网上大部分文章到此就结束了,实际上重点应该在 ViewGroup.dispatchTouchEvent,里面是事件分发的核心逻辑,我把它切分为三个阶段:

拦截

// DOWN 会被拦截,后续的 MOVE 和 UP 如果有 touch target 也会被拦截
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
    // 可以通过 requestDisallowInterceptTouchEvent 跳过此阶段,
    // 一般是 child 调用 parent.requestDisallowInterceptTouchEvent 来阻止 parent 拦截 touch event
    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 {
    // 后续的 MOVE 和 UP 没有 touch target 则直接走向 onTouchEvent 也就不需要拦截了
    // There are no touch targets and this action is not an initial down
    // so this view group continues to intercept touches.
    intercepted = true;
}

分发

// 收到 CANCEL 或者 onInterceptTouchEvent 返回 true,则不分发 DOWN 给 children
// 导致 children 收不到 DOWN 以及没有 touch target
final boolean canceled = resetCancelNextUpFlag(this) || actionMasked == MotionEvent.ACTION_CANCEL;
// ...
if (!canceled && !intercepted) {
    // ...
    if (actionMasked == MotionEvent.ACTION_DOWN
        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
        // ... 按顺序分发 ACTION_DOWN,child index(in children array) 越大优先级越高,child z value 越大优先级越高
        final ArrayList<View> preorderedList = buildTouchDispatchChildList();
        for (int i = childrenCount - 1; i >= 0; i--) {
            final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
            final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
            // ... touch 是否落在 child 的矩形区域内
            if (!child.canReceivePointerEvents()
                || !isTransformedTouchPointInView(x, y, child, null)) {
                ev.setTargetAccessibilityFocus(false);
                continue;
            }
            // ... 将 touch event 坐标转换为 child 区域坐标,分发给 child;当有第一个 child 消费时,记录起来并中断剩下的分发过程
            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                // ...
                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                alreadyDispatchedToNewTouchTarget = true;
                break;
            }
        // ...
}

处理

// 如果没有 touch target,则走自己的 View.dispatchTouchEvent 流程(相当于流向 onTouchEvent)
// 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);
} else {
    // 分发给 touch target
    // 但如果 onInterceptTouchEvent 返回 true,则发送 CANCEL 给 touch target,后续将不再流向 touch target,而是直接流向 onTouchEvent
    // onInterceptTouchEvent 拦截的那个 touch 不会流向 onTouchEvent
    TouchTarget predecessor = null;
    TouchTarget target = mFirstTouchTarget;
    while (target != null) {
        final TouchTarget next = target.next;
        if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
            handled = true;
        } else {
            final boolean cancelChild = resetCancelNextUpFlag(target.child)
                    || intercepted;
            if (dispatchTransformedTouchEvent(ev, cancelChild,
                    target.child, target.pointerIdBits)) {
                handled = true;
            }
            if (cancelChild) {
                if (predecessor == null) {
                    mFirstTouchTarget = next;
                } else {
                    predecessor.next = next;
                }
                target.recycle();
                target = next;
                continue;
            }
        }
        predecessor = target;
        target = next;
    }
}