终于搞懂android事件分发啦!

816 阅读13分钟

总览

这篇文章从源码角度着重讲述事件处理机制原理,会从以下几个方面(但不仅限于)进行深入讲解:

1.dispatchTouchEvent() onInterceptTouchEvent() onTouchEvent()详解

2.onTouch() 和 onClick() 有什么关系?

3.为什么手指按下 button 后再移出去 onClick() 不执行?

4.如何解决事件冲突?

⚡ 本篇文章有 一定难度,会将事件分发和消费讲透彻,请做好心理准备。

✔️ 本文阅读时间约为:15+分钟

由于涉及到的源码很多,为了便于理解我只展示核心代码,其它与重点内容不相关的在这里就省略了,建议大家看懂整个流程可以自己去翻翻源码看看细节。本篇文章是在 API 31 基础下创作的。

分发机制

在讲第一个问题前我们首先要清楚这个事件是从哪里来的。在 Android驱动层 捕获到消息后,它会将消息发送给当前的 app,然后再由 ViewRootImplmInputEventReceiver 接收并层层分发下去,之后到我们的 Activity 中,再由 Activity 进行分发,我们的重点就是从这里开始的。

接下来就是带大家粗略看一看事件是怎么到 view 手中的。

// Activity.java
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    // 关键代码,继续调用 PhoneWindow 的 superDispatchTouchEvent()
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}

Activity 接着调用 PhoneWindowsuperDispatchTouchEvent() 方法。

// PhoneWindow.java
public boolean superDispatchTouchEvent(MotionEvent event) {
    // 关键代码,继续调用 DecorView 的 superDispatchTouchEvent()
    return mDecor.superDispatchTouchEvent(event);
}

PhoneWindow 接着调用 DecorViewsuperDispatchTouchEvent() 方法。

// DecorView.java
public boolean superDispatchTouchEvent(MotionEvent event) {
    // 调用 super 的 dispatchTouchEvent()
    // 实际上调用的是 ViewGroup 的 dispatchTouchEvent()
    return super.dispatchTouchEvent(event);
}

接着 DecorView 调用 superdispatchTouchEvent(),我们知道 DecorView 的父类是 FrameLayout ,但是它没用重写该方法,因此就调用 ViewGroupdispatchTouchEvent()。至此,事件终于和 View 产生联系了。

对上面的过程做一个总结,我们的事件分发是从 Activity -> PhoneWindow -> DecorView -> ViewGroup,最后由我们的 View 去消费事件。

image.png

我们要很清晰的认识一点,ViewGroup 中的 dispatchTouchEvent() 是用来 分发事件 的,而 View 中的 dispatchTouchEvent() 才是真正用来 消费事件 的。

View 的事件消费

说到事件的分发与消费,那必然涉及到最重要的三个方法 dispatchTouchEvent()onInterceptTouchEvent()onTouchEvent() ,我们一点一点来说。

事件都是从 父容器 -> 子view ,从 ACTION_DOWN -> ACTION_MOVE -> ACTION_UP 这么一个过程,也就是说会先经历 ViewGroup.dispatchTouchEvent() 分发事件,再走 View.dispatchTouchEvent()去消费事件。但是为了便于大家对整个流程的理解,这里我先讲 View.dispatchTouchEvent() 事件消费

View.dispatchTouchEvent()

// View.java
public boolean dispatchTouchEvent(MotionEvent event) {
    ...
    // 关键变量,决定此事件是否被消费掉
    // 如果被消费掉就不会再往下分发此事件
    boolean result = false;
    ...
    if (onFilterTouchEventForSecurity(event)) {
        if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
            result = true;
        }
        // 如果设置了 setOnTouchListener
        // 并且实现了onTouch() 方法且返回值是 true
        // 那么就会执行 onTouch() 里面的内容
        // 这时返回值 result 改为 true,事件被消费掉
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            result = true;
        }

        // 如果没有设置setOnTouchListener
        // result 也还是 false
        // 就会走 onTouchEvent()
        // 相反如果设置了监听,实现了onTouch()并返回true 就会执行 onTouch()
        // result 就会为 true
        // 短路与 就不会执行 onTouchEvent()
        if (!result && onTouchEvent(event)) {
            result = true;
        }
    }

    if (!result && mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
    }
    ...
    // 如果到这里 result 还是 false 说明该事件未被此 view 消费 返回false
    // 如果返回 true 代表已消费,不再将此事件继续向下分发
    return result;
}

在这里就出现了和我们经常打交道的 onTouch()onTouchEvent()

我们先来看看 onTouch()onTouch() 的意思是当事件到达 view 时应该处理的逻辑。onTouch() 其实就是一个接口方法,等待着我们去实现,当事件到达 view 层时会回调我们在 onTouch() 中写的逻辑。

public interface OnTouchListener {
    boolean onTouch(View v, MotionEvent event);
}

现在我们再来看看 onTouchEvent()

View.onTouchEvent()

// View.java
public boolean onTouchEvent(MotionEvent event) {
    // 获取当前手指 x 坐标
    final float x = event.getX();
    // 获取当前手指 y 坐标
    final float y = event.getY();
    final int viewFlags = mViewFlags;
    final int action = event.getAction();
    ...
    if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
        switch (action) {
            case MotionEvent.ACTION_UP:
            ...
                boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                // 如果在 ACTION_UP 中修改掉了 mPrivateFlags,那么 if 就不会执行
                // 因此 onClick 就不会执行
                // 这就做到了手指按下 view 再移出去 onClick事件 不会响应
                if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                    boolean focusTaken = false;
                    if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                        focusTaken = requestFocus();
                    }
                    if (prepressed) {
                        setPressed(true, x, y);
                    }
                    if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                        removeLongPressCallback();
                        if (!focusTaken) {
                            if (mPerformClick == null) {
                                mPerformClick = new PerformClick();
                            }
                            if (!post(mPerformClick)) {
                                // 这里进去就会调用 li.mOnClickListener.onClick(this);
                                // 紧接着下一行就将返回值 result 改成 true
                                // 如果我们实现了 onClick 方法,就会回调我们自己写的逻辑
                                performClickInternal();
                            }
                        }
                    }
                    removeTapCallback();
                }
                mIgnoreNextUpEvent = false;
                break;
            ...

            case MotionEvent.ACTION_MOVE:
                ...
                int touchSlop = mTouchSlop;
                // 当前的手指移出此 view 时会走这段代码
                if (!pointInView(x, y, touchSlop)) {
                    // 执行removeTapCallback() 内部会修改 mPrivateFlags
                    // 导致 ACTION_UP 中的 prepressed 条件不成立 不会执行 performClickInternal()
                    removeTapCallback();
                    removeLongPressCallback();
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                        setPressed(false);
                    }
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                }
                ...
                break;
        }

        return true;
    }

    return false;
}

onTouch() 和 onClick() 的关系

至此,我们知道 1️⃣如果实现了 onTouch() 且返回值是 false 并且也实现了 onClick() ,那么当事件到达此 view 的时候会先经历 onTouch() 再执行 onClick,执行完 onClick() 后这个事件就被消费了(因为调用 onClick() 后,result 就会改成 true)。2️⃣如果实现了 onTouch() 且返回值是 true,那么 result 就会为 trueonClick() 就不会执行,只会执行 onTouch()

❤️ 此外,当手指按在 view 上再移出时,会执行 removeTapCallback() 修改 mPrivateFlags 的值,导致 performClickInternal() 外层的 if 条件不满足,因此不会执行 performClickInternal() ,进而 onClick() 也不会响应了。

这就是整个 view 的事件处理。接下来我们讲讲 事件分发

ViewGroup 的事件分发

搞懂 View 的事件消费,我们需要明白一点,如果此事件被消费,则返回 true,如果没有消费就返回 false,此事件继续向下分发。接下来让我们来看看 ViewGroup.dispatchTouchEvent()

ViewGroup 的事件分发是一个递归的过程,先来看一遍正常的分发流程。

ViewGroup.dispatchTouchEvent()

// ViewGroup.java
public boolean dispatchTouchEvent(MotionEvent ev) {
    // 表示此事件是否分发处理完成
    boolean handled = false;
    if (onFilterTouchEventForSecurity(ev)) {
        final int action = ev.getAction();
        final int actionMasked = action & MotionEvent.ACTION_MASK;

        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // 每次 down 都是一个新的动作
            // 因此这里会将所有的旧状态全部清除
            cancelAndClearTouchTargets(ev);
            resetTouchState();
        }

        // 这个变量代表 ViewGroup 是否拦截此事件
        // 正常的流程 ACTION_DOWN , ViewGroup 都是不会拦截的
        // intercepted 为 false
        final boolean intercepted;
        // mFirstTouchTarget 是一个链表的第一个结点,代表按下的第一根手指和消费事件的 view
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {
                // onInterceptTouchEvent() 是暴露给外部调用
                // 用来设置 intercepted 值的方法
                intercepted = onInterceptTouchEvent(ev);
                ev.setAction(action); 
            } else {
                intercepted = false;
            }
        } else {
            // 如果此事件不是 ACTION_DOWN 并且 mFirstTouchTarget 为空,父容器就拦截掉
            intercepted = true;
        }

        // 检查是否取消
        final boolean canceled = resetCancelNextUpFlag(this)
                || actionMasked == MotionEvent.ACTION_CANCEL;
                
        TouchTarget newTouchTarget = null;
        // 重要变量,表示是否有 view 处理了 DOWN 事件
        boolean alreadyDispatchedToNewTouchTarget = false;
        if (!canceled && !intercepted) {
            View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                    ? findChildWithAccessibilityFocus() : null;
            // 注意这里只有 DOWN 事件才能进来,MOVE 是不会走这段代码的
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                final int actionIndex = ev.getActionIndex(); 
                final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                        : TouchTarget.ALL_POINTER_IDS;
                        
                ...

                final int childrenCount = mChildrenCount;
                if (newTouchTarget == null && childrenCount != 0) {
                    final float x =
                            isMouseEvent ? ev.getXCursorPosition() : ev.getX(actionIndex);
                    final float y =
                            isMouseEvent ? ev.getYCursorPosition() : ev.getY(actionIndex);
                    final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                    final boolean customOrder = preorderedList == null
                            && isChildrenDrawingOrderEnabled()
                    // 拿到此父容器的所有 子view
                    final View[] children = mChildren
                    // 倒序遍历所有 子view,询问其是否处理 ACTION_DOWN 事件
                    // 为什么倒序?
                    // 最后面的 view 在屏幕的最顶层
                    // 我们总是希望点击最顶层的 view
                    for (int i = childrenCount - 1; i >= 0; i--) {
                        final int childIndex = getAndVerifyPreorderedIndex(
                                childrenCount, i, customOrder);
                        final View child = getAndVerifyPreorderedView(
                                preorderedList, children, childIndex);
                        ...
                        // dispatchTransformedTouchEvent() 内部会调用 View.dispatchTouchEvent()
                        // 如果 dispatchTransformedTouchEvent() 返回 true 代表消费此事件
                        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                            ...
                            mLastTouchDownX = ev.getX();
                            mLastTouchDownY = ev.getY();
                            // 这里会调用 addTouchTarget()
                            // 会给 mFirstTouchTarget 赋值,并将当前的 view 添加进 TouchTarget 中
                            // 为分发 ACTION_MOVE 和 ACTION_UP 事件做准备 
                            // 谁处理了 ACTION_DOWN 就会将剩下的事件分发给谁
                            newTouchTarget = addTouchTarget(child, idBitsToAssign);
                            // 置为 true 表示 DOWN 事件有 view 处理过
                            alreadyDispatchedToNewTouchTarget = true;
                            break;
                        }

                        ev.setTargetAccessibilityFocus(false);
                    }
                    if (preorderedList != null) preorderedList.clear();
                }
                ...
        }

        if (mFirstTouchTarget == null) {
            // 如果 子view 都没有处理当前事件 
            // mFirstTouchTarget 就会为空
            // 父容器自己就来处理当前事件
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
        } else {
            TouchTarget predecessor = null;
            TouchTarget target = mFirstTouchTarget;
            while (target != null) {
                final TouchTarget next = target.next;
                // alreadyDispatchedToNewTouchTarget 表示有无 view 处理此事件
                // 如果处理过就直接放回 true
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    handled = true;
                } else {
                    // 如果没有 view 处理过此事件就会进入这里
                    // 正常流程 intercepted 为 false
                    // 因此 cancelChild 也是false
                    final boolean cancelChild = resetCancelNextUpFlag(target.child)
                            || intercepted;
                    // target.child 就是 消费 ACTION_DOWN 的 子view
                    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;
            }
        }
        ...
    return handled;
}

❄️ 将整个 ViewGroup.dispatchTouchEvent() 分为三块代码,让我们来看看下面这张图,重新梳理一下思路:

image.png

请大家根据这幅图去理解上面的代码,搞懂正常的分发流程后才能清楚如何去解决事件冲突。

这是 ACTION_DOWN 的分发流程,前面说过 ACTION_MOVE 是不会走进第二块代码的。事实上,谁处理了 ACTION_DOWN 谁才有资格处理 ACTION_MOVEACTION_UP

现在继续对上面的过程进行解释说明,dispatchTransformedTouchEvent()addTouchTarget() 出现很多次,对于我们理解 ViewGroup 的分发流程也很重要。

dispatchTransformedTouchEvent()内部调用的是 View.dispatchTouchEvent()

/*
* 重要方法 dispatchTransformedTouchEvent()
* 用来处理 子view 或者 父容器 的事件消费的
*/
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {
    // 老朋友了,代表此事件是否被消费
    final boolean handled;

    // 这里是后面解决事件冲突的重要方法
    // 继续埋个坑,后面再讲
    final int oldAction = event.getAction();
    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
        event.setAction(MotionEvent.ACTION_CANCEL);
        if (child == null) {
            handled = super.dispatchTouchEvent(event);
        } else {
            handled = child.dispatchTouchEvent(event);
        }
        event.setAction(oldAction);
        return handled;
    }

    ...
    
    // child 为 null 表示父容器亲自来处理此事件
    // 对应前面 mFirstTouchTarget==null 时,无子view处理,父容器自己处理
    if (child == null) {
        // 调用 View.dispatchTouchEvent()
        handled = super.dispatchTouchEvent(transformedEvent);
    } else {
        final float offsetX = mScrollX - child.mLeft;
        final float offsetY = mScrollY - child.mTop;
        transformedEvent.offsetLocation(offsetX, offsetY);
        if (! child.hasIdentityMatrix()) {
            transformedEvent.transform(child.getInverseMatrix());
        }
        // child 不为空
        // 这里就是 子view 处理事件
        // 调用 View.dispatchTouchEvent() 进行事件消费
        // 这里忘记的同学去看看前面的View.dispatchTouchEvent()
        handled = child.dispatchTouchEvent(transformedEvent);
    }

    transformedEvent.recycle();
    // 返回我们的老朋友
    return handled;
}

这里有一个链表结构,主要是为了用来保存多根手指操作(用int来保存的),一根手指就用一位 比特位 来储存,因此理论上来讲最多能识别 32 根手指的操作。

private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
    /*
    * 处理了 DOWN 事件后会执行此方法 忘记的同学再去看看前面的分发流程
    * TouchTarget 是一个结点,用来储存 按下屏幕的手指 和 处理事件的view
    * 在 MOVE 事件处理时,就是从这里拿 view
    * 如果是单指操作就不用在意其是否是链表
    */
    final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
    // 这两行代码将target的next域 和 mFirstTouchTarget赋值
    // 因为是在 DOWN 之后调用的此方法
    // 在这里其实就是将链表的第一个结点值域赋值为 mFirstTouchTarget
    target.next = mFirstTouchTarget;
    mFirstTouchTarget = target;
    return target;
}

ViewGroup 的正常的分发流程就讲完了,总的来说就是当事件传到 ViewGroup 后,它会倒序遍历自己的所有孩子,询问它们有没有要处理此事件的。

1️⃣ 如果有,就走 View.dispatchTouchEvent() 去消费事件并执行消费事件的逻辑。

2️⃣ 如果没有,父容器自己就会亲自执行 View.dispatchTouchEvent() 去消费事件。

一旦此事件被消费,也就是会返回 true,此事件便不会继续向下分发了。否则一直向下分发事件直到被消费。

如何解决事件冲突?

大家在开发中肯定会遇到事件冲突,只要两个 view 叠在一起,那冲突是必然的,那怎么解决呢?如何做到像 抖音 那样,上下左右 都能滑动呢?接下来,我们亲自来实现一下。

❗❗ 解决事件冲突前,建议先明白上面的 ViewGroup 正常事件分发流程。

❓❓ 有的同学可能会说,我写了那么多的 ViewPager 嵌套 RecyclerView 为什么没有出现过事件冲突啊?那是因为 google工程师 在内部帮我们处理了,任何叠在一起的 view,肯定是会产生事件冲突的。

⭐ 我们首先来看看 onInterceptTouchEvent(),这个大家应该很熟悉了吧。这是我们上面 ViewGroup.dispatchTouchEvent()第一块代码 里出现过的,用来设置 intercepted 的值,决定父容器是否拦截此事件。

⭐ 而 requestDisallowInterceptTouchEvent() ,其内部是通过修改 mGroupFlags 的值来间接修改 intercepted 的值。

好了,现在我们来当一把 google工程师 吧!

这里为了方便,我自定义两个 View 继承自 RecyclerViewViewPager,因为这样自己就不用再单独写事件的消费了😍。

// MyViewPager extends ViewPager
override fun onInterceptTouchEvent(e: MotionEvent?): Boolean {
    // 我们不能让父容器拦截 ACTION_DOWN 事件 
    // 因为如果 子view 连 DOWN 事件都接收不到
    // 那么肯定是无法接收其它事件的
    if (e?.action == MotionEvent.ACTION_DOWN) {
        super.onInterceptTouchEvent(e)
        return false
    }
    // 拦截除 DOWN 事件的其他事件
    return true
}
private var lastX = 0f
private var lastY = 0f
// MyRecyclerView extends RecyclerView
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
    val dx = ev!!.x
    val dy = ev.y
    when(ev.action) {

        MotionEvent.ACTION_DOWN -> {
            // 请求父容器不要拦截子view接收事件
            parent.requestDisallowInterceptTouchEvent(true)
        }
        MotionEvent.ACTION_MOVE -> {
            // 如果 横向滑动的距离 大于 纵向滑动的距离
            // 就请求父容器拦截此事件 不让子view接收到
            if (abs(lastX - dx) > abs(lastY - dy)) {
                parent.requestDisallowInterceptTouchEvent(false)
            }
        }
        MotionEvent.ACTION_UP -> {}
    }
    lastX = dx
    lastY = dy
    return super.dispatchTouchEvent(ev)
}

那么究竟为什么写了这区区几行代码就能解决掉事件冲突,达到能够 上下左右 滑动的目的呢?

我之前是不是还留了个坑?现在是时候将它填上了,我们来看看 ViewGroup.dispatchTouchEvent() 最后那段代码。

// View.dispatchTouchEvent()
if (mFirstTouchTarget == null) {
    // 别忘了,这是父容器自己处理事件
    // 标记一下 这是父容器把处理事件的权利从子view手中抢过来的关键
    handled = dispatchTransformedTouchEvent(ev, canceled, null,
            TouchTarget.ALL_POINTER_IDS);
} else {
    TouchTarget predecessor = null;
    TouchTarget target = mFirstTouchTarget;
    while (target != null) {
        final TouchTarget next = target.next;
        if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
            handled = true;
        } else {
            // 重点关注的地方
            // 之前的正常流程 cancelChild 是 false
            // 现在因为我们在外部让父容器拦截了事件 intercepted 变为 true
            // 因此 cancelChild 也变成true
            // 接下来就是执行 dispatchTransformedTouchEvent()
            final boolean cancelChild = resetCancelNextUpFlag(target.child)
                    || intercepted;
            if (dispatchTransformedTouchEvent(ev, cancelChild,
                    target.child, target.pointerIdBits)) {
                handled = true;
            }
            // 如果cancelChild 为true
            // 将 mFirstTouchTarget 置空,target 置空
            if (cancelChild) {
                if (predecessor == null) {
                    // next 是链表的下一个结点 MOVE时后面没有结点了 其实就是 null
                    // 这个变量置为 null
                    // 是父容器把处理事件的权利从子view手中抢过来的关键
                    mFirstTouchTarget = next;
                } else {
                    predecessor.next = next;
                }
                target.recycle();
                target = next;
                continue;
            }
        }

✔️ ACTION_DOWN 事件和正常的流程一样,分发给了 子view,也就是 RecyclerView

✔️ 当 ViewPager 第一次分发到 ACTION_MOVE 事件时,这里因为我们在外部通过调用 parent.requestDisallowInterceptTouchEvent(false) ,让父容器拦截了 MOVE 事件,因此这时候 interceptedtrue,导致 cancelChildtrue

// 还记得这个方法吗,我在这里也留了个坑
// ViewGroup.java
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {
    final boolean handled;

    final int oldAction = event.getAction();
    // 之前正常的流程,cancel 是 false
    // 现在父容器拦截了事件,cancel 是 true
    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
        event.setAction(MotionEvent.ACTION_CANCEL);
        if (child == null) {
            handled = super.dispatchTouchEvent(event);
        } else {
            // 执行 View.dispatchTouchEvent()
            handled = child.dispatchTouchEvent(event);
        }
        event.setAction(oldAction);
        return handled;
    }
    ...
}

✔️ 现在,当 ACTION_MOVE 事件再次分发到 ViewPager 时,因为前面已经将 mFirstTouchTarget 置为空,因此 if 条件满足,这里就让父容器(ViewPager)亲自去处理事件了。

至此成功将事件从 子view 手中抢过来,解决了滑动事件冲突。

我们再来对上面 ViewPager 将事件从 RecyclerView 手中抢过来的过程总结一下,当我们手指开始左右滑动时:

1️⃣ 事件 ACTION_DOWN 初次传递到 ViewPager 时,会走正常流程,询问子view(RecyclerView) 是否处理,紧接着RecyclerView 处理掉,ACTION_DOWN 事件被消费。

2️⃣ 紧接着事件 ACTION_MOVE 传递给 ViewPager ,我们在外部将 intercepted 置为 true,导致 cnacelChildtrue,在 RcyclerView 消费掉此事件后,mFirstTouchTarget 置为空。

3️⃣ 最后事件 ACTION_MOVE 再次传递到 ViewPager,父容器依然拦截着此事件,但由于 mFirstTouchTarget 为空,所以满足 if 条件,轮到父容器(ViewPager)去处理事件。

写在最后

✈️✈️ 篇幅稍稍过长,但是耐心看完的同学一定会有收获。这篇文章多看两遍,大概率是能填补关于 View 这块的事件分发和消费的知识点的空缺的。

欢迎大家在评论区提出问题或者自己的观点。