你真的看懂Android事件分发了吗?
引子
Android事件分发其实是老生常谈了,但是说实话,我觉得很多人都只是懂其大概,模棱两可。本文的目的就是再次从源码层次梳理一下,重点放在ViewGroup的dispatchTouchEvent方法上,这个方法是事件分发的核心中的核心!我们借此以小见大,理解事件分发的机制。ps,本文着重在源码和分析,就不怎么画图了(其实是懒),大家可以看网上相关图片,随便一搜很多。
先简单讲一下事件分发的源头
很多人讲事件分发,都说其开始是从Activity的dispatchTouchEvent开始的,大家可以简单这么理解,但是肯定会有人疑问,Activity的这个方法从哪儿调用的呢?我写了一个简单的Demo,然后在Activity的dispatchTouchEvent方法里加了一个断点得到其函数调用栈,看下图:
好家伙,原来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; // 注意这个条件,后面会讲 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; } } } } //第三步:分发非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) {