美文网首页Android自定义ViewAndroid开发Android开发经验谈
为啥还在聊:事件分发?还不是因为不会!

为啥还在聊:事件分发?还不是因为不会!

作者: 咸鱼正翻身 | 来源:发表于2019-08-04 23:46 被阅读6次

    前言

    事件分发是一个老生常谈的话题,既然是一个“冷饭”,那为什么今天又开始“炒冷饭”了呢?说白了,还是自己高估了对事件分发的理解。

    这里抛出几个问题:

    • 1、对一个View进行setOnTouchListener操作,并且onTouch()返回true,为啥它的onTouchEvent()不会被响应? -> 答案在:方法展开2部分。
    • 2、一个View的onTouchEvent()返回了true,为啥它下层的View就再也不会响应任何事件回调了? -> 答案在:方法展开1部分
    • 3、如果一个ViewGroup只重写了onTouchEvent()并返回了true,那么它的onInterceptTouchEvent()还会被回调吗? -> 答案在:1.2、部分总结部分。
    • 4、重写dispatchTouchEvent()并直接返回true,会怎么样?-> 答案在:方法展开2部分。

    如果各位小伙伴可以非常清晰的回答这些问题,那么这篇文章就不用看了,左上角点X,唱、跳、Rap、打会篮球什么的...当然如果你愿意留下来点点广告,那也是极好的~哈哈

    正文

    既然叫做事件分发,那么本质其实就是分发。我猜大家刚开始了解这一块内容时,肯定绕不开三个方法:dispatchTouchEvent()onInterceptTouchEvent()onTouchEvent()。不过我真觉得,扯上后边俩个方法,反而把问题复杂化。

    对于事件分发来说,核心就是dispatchTouchEvent()的实现,onInterceptTouchEvent()、和onTouchEvent()只是让我们参与到分发流程当中来的接口而已。

    因此,这篇文章的核心就在于梳理、阅读ViewGroup和View的dispatchTouchEvent()方法实现。相信我,阅读完这篇文章绝对有收获~~

    一、ViewGroup中的dispatchTouchEvent()

    源码基于api-28

    关于dispatchTouchEvent()的逻辑,这里主要分为俩个大部分,前半部分侧重于事件消费对象的确定(1.1部分);后半部分侧重于对事件消费对象的后续分发(1.2部分)。

    1.1、mFirstTouchTarget的首次赋值

    这部分代码逻辑主要为了:

    • 1、找到并记录命中消费事件的View
    • 2、对各层View的DOWN事件分发
    public boolean dispatchTouchEvent(MotionEvent ev){
        // 记住这个mFirstTouchTarget,很关键
        if(actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null){
            // 如果子View没有调用requestDisallowInterceptTouchEvent(true),则调用自身的onInterceptTouchEvent()
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {
                intercepted = onInterceptTouchEvent(ev);
            } else {
                intercepted = false;
            }
        } else {
            // 如果不是DOWN事件,并且mFirstTouchTarget == null,那么就直接认定当前View拦截
            intercepted = true;
        }
        TouchTarget newTouchTarget = null;
        boolean alreadyDispatchedToNewTouchTarget = false; // 注意一下这个局部变量,会用到
        if (!canceled && !intercepted) {
            // 省略部分代码
            if (newTouchTarget == null && childrenCount != 0) {
                // 遍历View(这里的顺序可以通过重写setChildrenDrawingOrderEnabled() + getChildDrawingOrder()自定义顺序)
                for (int i = childrenCount - 1; i >= 0; i--) {
                    final int childIndex = getAndVerifyPreorderedIndex(
                            childrenCount, i, customOrder);
                    final View child = getAndVerifyPreorderedView(
                            preorderedList, children, childIndex);
                    // 如果当前的View出在动画;或者x、y不在View区域内直接continue
                    if (!canViewReceivePointerEvents(child)
                            || !isTransformedTouchPointInView(x, y, child, null)) {
                        continue;
                    }
                    // 该方法会遍历TouchTarget,但是初始的target需要通过mFirstTouchTarget进行赋值,此时为null。具体实现细节可查看:方法展开4
                    newTouchTarget = getTouchTarget(child);
                    if (newTouchTarget != null) {
                        // 多指操作,暂时忽略
                        break;
                    }
                    // DOWN事件一定会走到此,因为newTouchTarget == null,此方法逻辑见:方法展开1
                    // 此方法便开始向其他层级的View进行分发事件,此方法的返回值决定了是否走if的逻辑。
                    if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                        // 当子View选择消费这个事件时,那么将会走接下来的代码。这里主要的内容就是给newTouchTarget和mFirstTouchTarget进行赋值。(此方法逻辑见:方法展开3)
                        // 也就是说,如果代码走到这,那么mFirstTouchTarget将不再为null
                        newTouchTarget = addTouchTarget(child, idBitsToAssign);
                        alreadyDispatchedToNewTouchTarget = true;
                    }
                }
            }
        }
        // 截止到此是intercepted位false的逻辑
    }
    

    1.2、部分总结

    此时总结并解释一下开头写的:1、找到并记录命中消费事件的View;2、对各层View的DOWN事件分发。
    1、找到并记录命中消费事件的View:
    当DOWN来到ViewGroup的时候,如果自身不拦截,那么就会尝试分发。最终将根据命中View是否消费(重写onTouchEvent()/onTouch()/重写dispatchTouchEvent())来决定是否对mFirstTouchTarget进行赋值(记录命中消费事件的View)。
    2、对各层View的DOWN事件分发:
    这部分代码里,我们第一个遇到了dispatchTransformedTouchEvent()方法,这个方法会调用child或者super的dispatchTouchEvent(),最终通过View的onTouchEvent()/onTouch()等方法的返回值来决定dispatchTransformedTouchEvent()的返回值。因此拿到返回值的时候,其实这个事件已经在所有的View中分发了一遍。

    此时如果mFirstTouchTarget不为null,那么后续的MOVE和UP事件将重走这一套流程(if(actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null))。
    或者intercepted直接为true;直接交给自己处理。

    这里解答开篇的第三个问题,通过代码我们可以看到只要mFirstTouchTarget不为null,并且子View不调用requestDisallowInterceptTouchEvent(true),那么当前ViewGroup的onInterceptTouchEvent()一定会调用,它和onTouchEvent()的返回值没有任何关系。

    解答完这个问题,不知道有没有小伙伴想到一个点:那就是如果ViewGroup的onInterceptTouchEvent()在满足条件下,一定会调用。那么我是不是可以在某一层View消费了一定的事件后,然后再通过一些条件判断让ViewGroup中的onInterceptTouchEvent()返回true。这样就可以做到事件没消费完继续分发给其他View,那这种想法能不能实现呢?答案是不能,为什么请阅读:事件分发额外阅读

    1.3、MOVE/UP事件分发的关键

    此部分逻辑DOWN也会触发,但更多的是为了分发MOVE/UP

    public boolean dispatchTouchEvent(MotionEvent ev){
        // 此逻辑分析承接上半部分
        // 如果mFirstTouchTarget == null有俩种可能,一个是的确没有找到能够命中的View,另一个是自己直接拦截
        if (mFirstTouchTarget == null) {
            // 此时child这个字段传null,也就是说直接调自己的super.dispatchTouchEvent()分发给了自己。
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
        } else {
            // 能走到此方法说明mFirstTouchTarget已经不会null,也就是找个了可以去分发的View
            // 省略部分条件
            TouchTarget target = mFirstTouchTarget;
            while (target != null) {
                // 这里用到了alreadyDispatchedToNewTouchTarget,很简单对于DOWN事件来说,其实分发已经走了一遍,并且为mFirstTouchTarget赋了值,如果此处不过滤掉那么分发流程就会走俩遍。
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    handled = true;
                } else {
                    // 否则向其他View分发事件,其实我猜大家应该都明白了,MOVE/UP事件会通过此逻辑完成分发
                    if (dispatchTransformedTouchEvent(ev, cancelChild,
                            target.child, target.pointerIdBits)) {
                        handled = true;
                    }
                    // 取消的逻辑暂时不做考虑
                }
        }
    }
    

    1.4、部分总结

    此部分代码较少,而且逻辑清晰。主要就在于俩个分支,一个是没有找到能够消费的View,那么分发给自己,直接super.dispatchTouchEvent()。自己的onTouchEvent()处理。否则通过mFirstTouchTarget,分发后续产生的事件。

    二、方法展开

    此部分内容,请结合一、ViewGroup中的dispatchTouchEvent()“食用”

    2.1、方法展开1:

    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
                View child, int desiredPointerIdBits) {
        final boolean handled;
        // 省略部分代码
        if (child == null) {
            handled = super.dispatchTouchEvent(event);
        } else {
            // 如果事件命中了某个View,此时将调用这个View的dispatchTouchEvent()。当然如果此时的View是一个ViewGroup那么会不断进行上述的过程,此时的返回值就是super.dispatchTouchEvent(event),也就是View的dispatchTouchEvent(此方法逻辑见:方法展开2)。
            // 不过这里肯定有同学会问如果我当前的View重写了```dispatchTouchEvent()```,并return true会怎么样?-> 看一下 方法展开2 就会明白
            handled = child.dispatchTouchEvent(event);
        }
        return handled;
    }
    

    对于此方法来说,一旦handled返回了true,那么对于ViewGroup的dispatchTouchEvent()来说就可以确定mFirstTouchTarget。有了mFirstTouchTarget,意味着消费的View已经被确定,无需要在将事件往下分发。(这也就解答了开篇抛出来的第2个问题)白话文:背锅的已经找到,此事无序再追查。哈哈~

    2.2、方法展开2:View中的dispatchTouchEvent()

    // 可以看到,对于View来说dispatchTouchEvent()的返回值,依赖onTouchEvent()的返回值、onTouch()返回值。
    // 并且这也说明了一个严重的问题:那就是onTouchEvent等事件的调用是在View的dispatchTouchEvent之中,如果我们重写了某个View的dispatchTouchEvent直接return会了true,那么就意味着onTouchEvent等方法将再也没有机会执行了。(这也就解答了开篇抛出来的第4个问题)
    public boolean dispatchTouchEvent(MotionEvent event) {
        // 省略 
        if (onFilterTouchEventForSecurity(event)) {
            // 省略
            ListenerInfo li = mListenerInfo;
            // 此处可以看到,如果listener不为null,并且onTouch()返回true,那么result这个局部变量就会为true。那么就对于下边的判断条件来说第一个条件就不满足,因此就不会再调用onTouchEvent()了。(这也就解答了开篇抛出来的第1个问题)
            if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }
        return result;
    }
    

    总结方法展开1 + 方法展开2
    如果我们某个View重写了dispatchTouchEvent()并且直接返回true,那么对于dispatchTransformedTouchEvent()这个方法来说,将直接得到true;否则将依赖View中
    onTouchEvent()的返回值、onTouch()返回值。

    2.3、方法展开3:

    private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
        final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;
        return target;
    }
    

    2.3、方法展开4:

    private TouchTarget getTouchTarget(@NonNull View child) {
        // 因为mFirstTouchTarget的默认值是null,因此首次调用此方法一定return null。也就是DOWN来的时候,此方法return null。
        for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
            if (target.child == child) {
                return target;
            }
        }
        return null;
    }
    

    三、事件分发额外阅读

    上文产生的这个问题,首先明确答案是不行。因为我们已经看罢了通篇的源码。当事件已经开始被某个View消费,那么就意味着mFirstTouchTarget不为null,那么```getTouchTarget(child)``````也将不为null,因此将不会重新分发此事件。同一个事件序列只会继续分发给mFirstTouchTarget。

    对于当前的dispatchTouchEvent()来说。事件已经被其他View消费,木已成舟。此时再想改变onInterceptTouchEvent()为true,已经“无力回天”。

    尾声

    本篇文章到此就结束了,可能有朋友会问,关于CANCEL事件还没讲!没错,为啥没聊呢?因为我还没看。有机会的话,会把关于CANCEL事件的部分补上。

    不着急,咱先把今天的文章唠明白。

    我是一个应届生,最近和朋友们维护了一个公众号,内容是我们在从应届生过渡到开发这一路所踩过的坑,以及我们一步步学习的记录,如果感兴趣的朋友可以关注一下,一同加油~

    个人公众号:咸鱼正翻身

    相关文章

      网友评论

        本文标题:为啥还在聊:事件分发?还不是因为不会!

        本文链接:https://www.haomeiwen.com/subject/yubudctx.html