Android事件分发你真的掌握了吗

引子

Android事件分发其实是老生常谈了,但是说实话,我觉得很多人都只是懂其大概,模棱两可。不信我可以先抛出几个问题:

  • ACTION_DOWN和其他触摸事件的处理方式一样吗?如果不,有什么不同之处?
  • 为什么如果所有子View都不消费ACTION_DOWN事件,后续的MOVE和UP事件就被拦截了?
  • 子View能干预父View对ACTION_DOWN事件的拦截吗?
  • 如果有多个View都包含触摸坐标,它们都能接收到事件分发吗?如果不是,谁会接受?原理?
  • 多指操作怎么处理的?

相比很少能完全答上来吧。多数文章都在讲事件分发的递归调用链,确实那个很重要,但不够深入,比如都没有把ACTION_DOWN事件单独拿出来讲。如果真想彻底弄懂分发原理,碰到分发难题时想办法解决,必须得把源码的思路理清楚,然后才能对证下药。本文的目的就是从源码层次梳理一下,重点放在ViewGroup的dispatchTouchEvent方法上,这个方法是事件分发的核心中的核心!我们借此以小见大,理解事件分发的机制。ps,本文着重在源码和分析,就不怎么画图了(其实是懒),大家可以看网上相关图片,随便一搜很多。本文力求深入浅出,我来深入源码,然后尽量用浅显的语言讲出来。

先简单讲一下事件分发的源头

很多人讲事件分发,都说其开始是从Activity的dispatchTouchEvent开始的,大家可以简单这么理解,但是肯定会有人疑问,Activity的这个方法从哪儿调用的呢?我写了一个简单的Demo,然后在Activity的dispatchTouchEvent方法里加了一个断点得到其函数调用栈,看下图:

stack.png

好家伙,原来Activity分发之前还有这么多过程,简单梳理了一下:大概是从InputEventReceiver开始,经过ViewRootImpl,里面各种InputStage调用之后,最后给了DecorView,然后DecorView传给的Activity。其实这里挺有意思的,本来DecorView先获取到事件的,但是后来它又分配给了Activity,Activity之后又通过phoneWindow把事件传回给了DecorView,一来一回,就是为了让Activity去处理一下事件而已。Activity传给DecorView之后,DecorView会调用superDispatchTouchEvent方法:

    public boolean superDispatchTouchEvent(MotionEvent event){
        return super.dispatchTouchEvent(event);
    }

因为DecorView是一个FrameLayout,它最终还是调用了我们熟悉的ViewGroup的dispatchTouchEvent(),这也是本文的主角。所谓的事件分发,本质上就是一个递归函数的调用,这个递归函数就是dispatchTouchEvent,至于onIntercepterTouchEvent,onTouchEvent,OnTouchListener,onClickListener…balabala都是在这个递归函数里面的操作而已,最核心,最骨干的还是dispatchTouchEvent,所以我们来分析它:

ViewGroup的事件分发

大家应该或多或少读过其源码,源码虽然不是太长,但乍一看还是会头大的,我想大多数人可能大概看懂了其逻辑,对于里面很多东西不明所以。比如mFirstTouchTarget是干嘛的?临时变量alreadyDispatchedToNewTouchTarget是干嘛的?里面好像有链表啊,干嘛使的?

这里稍微补充一句,对于事件分发来说,从用户按下到抬起,这是一组事件,以ACTION_DOWN为开头,UP或CANCEL结束。我们后面分析的也是这一组事件。

源码较长,我写了伪代码给大家看看,说是伪代码,其实还是比较全面详细的,省略了部分函数参数,但重点的代码都包含了,重点看注释。如果嫌长,可以直接先看后面的结论,再回头看伪代码。

//本源码来自 api 28,不同版本略有不同。
public boolean dispatchTouchEvent(MotionEvent ev) {
    // 第一步:处理拦截
   boolean intercepted;  
     // 注意这个条件,后者代表着有子view消费事件。后面会讲
   if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
    // 子view调用了parent.requestDisallowInterceptTouchEvent干预父布局的拦截,不让它爸拦截它
       final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
       if (!disallowIntercept) {
             intercepted = onInterceptTouchEvent(ev);
             ev.setAction(action); 
         } else {
             intercepted = false;
         }
     } else {
        //既不是DOWN事件,mFirstTouchTarget还是null,这种情况挺常见:如果ViewGroup的所有的子View都不消费				//事件,那么当ACTION_MOVE等非DOWN事件到来时,都被拦截了。
         intercepted = true;
     }

    // 第二步,分发ACTION_DOWN
    boolean handled = false;
    boolean alreadyDispatchedToNewTouchTarget = false; //注意这个变量,会用到
   // 不拦截才会分发它,如果拦截了,就不分发ACTION_DOWN了
    if (!intercepted) {
        //处理DOWN事件,捕获第一个被触摸的mFirstTouchTarget,mFirstTouchTarget很重要,
        保存了消费了ACTION_DOWN事件的子view
        if (ev.getAction == MotionEvent.ACTION_DOWN) {
            //遍历所有子view(看源码知子View是按照Z轴排好序的)
            for (int i = childrenCount - 1; i >= 0; i--) {
                //子view如果:1.不包含事件坐标 2. 在动画  则跳过
                if (!isTransformedTouchPointInView() || !canViewReceivePointerEvents()) {
                    continue;
                }
                //将事件传递给子view的坐标空间,并且判断该子view是否消费这个触摸事件(分发Down事件)
                if (dispatchTransformedTouchEvent()) {
                    //将该view加入头节点,并且赋值给mFirstTouchTarget
                    newTouchTarget = addTouchTarget(child, idBitsToAssign);
                    alreadyDispatchedToNewTouchTarget = true;
                    break;
                }

            }
        }
    }

        //第三步:分发非DOWN事件
        //如果没有子view捕获ACTION_DOWN,则交给本ViewGroup处理这个事件。我们看到,这里并没有判断是否拦截,
        //为什么呢?因为如果拦截的话,上面的代码不会执行,就会导致mFirstTouchTarget== null,于是就走下面第一         				//个条件里的逻辑了
        if (mFirstTouchTarget == null) {
            super.dispatchTouchEvent(ev); //调用View的dispatchTouchEvent,也就是自己处理
        } else {
            //遍历touchTargets链表,依次分发事件
            TouchTarget target = mFirstTouchTarget;
            while (target != null) {
              	if (alreadyDispatchedToNewTouchTarget) {
                  handled = true
                } else {
                  	if (dispatchTransformedTouchEvent()) {
                      handled = true;
                    }
                  target = target.next;
                }
            }
        }

        //处理ACTION_UP和CANCEL,手指抬起来以后相关变量重置
        if (ev.getAction == MotionEvent.ACTION_UP) {
            reset();
        }
    }
    return handled;
}

总结一下:ViewGroup事件分发分为三步

  1. 第一步:判断要不要拦截:这里的条件分支要看清,外层的判断语句意思是,要么肯定会拦截,要么可能不拦截,可能不拦截的话需要满足以下两个条件之一:

    1. 事件是DOWN事件。

    2. 非DOWN事件也可以,但是需要满足mFirstTouchTarget != null 。这个条件意味着什么呢?意味着在之前的DOWN事件中,至少有一个子View捕获(消费)了DOWN事件,也就是意味着对于这一组分发事件来说,有子View愿意处理这个事件。

    在可能拦截的情况下,我们进入拦截判断流程,很简单: 先看子view有没有调parent.requestDisallowIntercept,如果调用了,不拦截,没有的话走到onIntercepteTouchEvent方法,根据其返回值决定是否拦截。

  2. 第二步:如果没有拦截,分发DOWN事件:遍历所有子View,查看触摸区域是否有子view有资格消费这个事件,判断依据有二:子View不能在动画?触摸点坐标得落在子View的范围内。如果前两者都满足,则将DOWN事件分发给子View,这一步引出了一个重要的方法:dispatchTransformedTouchEvent ,这个方法干的活就是最重要的事情:分发给子view,也就是说,这个方法进行了递归的调用,感兴趣的同学可以自己阅读其源码。遍历的范围是什么呢?源码中告诉我们是按照z轴顺序排列好的一个view list,这里的z轴顺序保证了我们先把事件分发给z轴坐标大的值,也就是更靠外层的view。另外,这个分发方法有个返回值,如果为true,则为mFirstTouchTarget赋值,否则其值仍为null。最后有个方法,addTouchTarget,这个方法一方面为mFirstTouchTarget赋值,另外也构建了一个链表,链表保存的什么呢?其实这个跟多指操作有关,它保存了所有“mFirstTouchTarget”,mFirstTouchTarget是啥呢?其实字面意思很明白了,就是手指碰触的位置最外层的View,对于多个手指来说,每个手指都会有一个mFirstTouchTarget,于是就保存到了这个链表中了。为什么要保存mFirstTouchTarget呢?很简单,为了让后续的该事件组的其他事件知道谁要处理事件啊!要不然总不能每个事件都要判断一下,那效率就低多了。这里就得出来一个结论,Down事件的分发决定了那个view要捕获事件,如果捕获了,后续的事件就直接分发给它,也就是说move up等事件的分发交给谁,取决于它们的起始事件Down由谁捕获

  3. 第三步:分发其他事件:首先判断mFirstTouchTarget,如果为null,说明前一步的DOWN事件没有子view消费掉,这种情况表示该ViewGroup的孩子View都不打算处理事件,这种情况自然要交给ViewGroup自身处理,代码里交给了super.dispatchTouchEvent,也就是调用了ViewGroup的父类View处理(onTouchEvent)。如果不为null,说明有子View要处理事件,进入else语句里,把事件分发下去。 这里眼尖的读者应该看到了,第二步不会已经分发了DOWN事件了吗,这里为啥还要再分发一次呢?不重复了吗,这里就到了前面讲的另外一个变量出场了,alreadyDispatchedToNewTouchTarget,这个变量在伪代码里第二步的开头提到了,当第二步里有子View消费了事件后,该变量会变成true,此时第三步会判断该值,如果为true,就直接返回handle=true,不再分发事件了。这就避免了DOWN事件被两次分发。对于其他事件,这个变量肯定是false,所以一定会走else的逻辑,进行分发。

再简化一下,加点大白话:

  public boolean dispatchTouchEvent(MotionEvent event) {

        boolean intercepted = false;
        if (DOWN 或者 DOWN的时候没有孩子想处理) {
            if (孩子不让拦截?) {
                intercepted = false;
            } else {
                intercepted = onIntercept();
            }
        } else {
          intercepted = true;
        }

        if (DOWN && !intercepted) {
            for (遍历孩子View) {
                if(如果该孩子能消费就给分发给它,如果它真消费了DOWN事件){
                    给mFirstTouchTarget赋值 ;
                    Down事件已经分发了;
                    跳出循环;
                }
            }
        }

        if (mFirstTouchTarget == null) {
            孩子都不想消费,交给我自己处理吧;
        } else {
            while(遍历所有孩子,将事件分发下去) {
                if (DOWN事件已经分发了) {
                    return true;
                }else {
                    分发给之前保存的mFirstTouchTarget对应的子View;
                }
            }
        }

    }

到这里,我们就把ViewGroup的事件分发讲完了,接下来分析一下View的dispatchTouchEvent

View的事件分发

View的非常简单

public boolean dispatchTouchEvent(MotionEvent event) {
    boolean result = false;
    if (onTouchListener.onTouch()) {
        result = true;
    }
    if (!result && onTouchEvent()) {
        result = true;
    }
    return result;
}

可见,先判断listener,如果listener返回true了,onTouchEvent就不进入了,否则,走onTouchEvent方法。

View的复杂点的地方在onTouchEvent方法的默认实现里,里面处理了很多onClick,onLongclick事件的逻辑,感兴趣的同学可以自行阅读源码,这里只说一点,一旦设置了onClickListener或者onLongclickListener,那么onTouchEvent就会返回true,也就是消费,其他情况下默认不消费,源码里这么写的

        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

clickable为true,则返回true,否则返回false。

举个例子练习一下

问题很简单,一个FrameLayout中间放了一个按钮,Framelayout和按钮都添加了点击事件,那么,请问点击按钮和点击按钮之外的区域事件分发过程是怎样的?

先看按钮之外: FrameLayout是一个ViewGroup,而且没有重写dispatchTouchEvent方法。根据以上分析:

  • 第一步,down来了以后,进入拦截逻辑,framelayout不拦截,所以intercepted == false
  • 第二步,处理down事件,发现触摸点没有子view,所以不会有人处理这个事件的,mFirstTouchTarget == null
  • 第三步,交给自身处理,自身会调用onTouchEvent,在这里由于设置了clickListener,返回true,消费了事件。
  • 后续move和up,由于mFirstTouchTarget == null,第一步会拦截,所以直接交给自身处理,同上面的第三步,同时,up的时候会响应click事件。

按钮内:

  • 第一步,同上
  • 第二步, 发现触摸点有子view,mFirstTouchTarget != null,且将DOWN事件分发给了子View。
  • 第三步,mFirstTouchTarget非null,但alreadyDispatchedToNewTouchTarget这个变量为true,所以直接返回true。
  • 后续move和up,第一步不会拦截,因为不是down事件所以第二步跳过,第三步将事件分发给了子View,子View响应了点击事件,返回true,而这个过程中,ViewGroup没有消费任何事件,所以自然不会响应onClick事件。

这样,是不是就解释了两层View都添加click事件时的响应结果了~

总结

总的来说,事件分发分两步,拦截和分发,其中分发有两种情况,Down事件和非Down事件,down事件是事件链的起点,决定了要不要消费事件,而且将消费的子View保存下来给后面使用。如果所有的子View都不消费down事件或者压根没有子View,会使得mFirstTouchTarget为null,后面的所有事件就不再分发给子view了,直接由本view group处理。当然这里的交给本人处理,实际上可能它也不消费,会继续往上传,最终“归”到Activity处理。
越来越感到读源码的重要性,Let’s read the fucking sourceCode!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值