android事件分发

事件分发在Android中非常重要,在滑动冲突,下拉刷新,嵌套滑动的时候都需要非常清楚事件分发的机制,才能写好对应的处理代码。曾经以为我对事件分发已经很清楚了,也写过几篇文章,但是总感觉没有完全说清楚,今天再从代码的角度分析一遍事件分发机制,希望以后遇到所有事件分发的问题,都能在这里找到答案。
先看几个问题,如果这些问题你都知道答案,那本篇文章就不用看了。

问题

1、如果拦截了某个事件,是否就会交由本view的View:dispatchTouchEvent处理?
2、一个事件,如果子view处理失败,是否就交还给父view处理?
3、如果一个down事件,大家都不处理,会怎么样?
4、parent把事件传递给哪个子view呢?是根据位置查一遍的吗?
5、某个view成功处理down事件,但是处理move事件失败,会如何?

神圣的规则

规则1:事件传递由父控件传递到子控件,事件消费是子控件优先。
规则2:down事件,子控件如果不消费,就还给父控件。
规则3:我是一个坏父亲,父亲吃到肉了,绝不会再给儿子,儿子吃到肉了,父亲还可能抢。
手指从按下到抬起,我们称为一个cycle,以DOWN事件开始,UP事件结束,里面有若干个MOVE事件。一个cycle内,v1处理了某事件,后边的事件绝不会被v1的child处理,v1肯定会拦下来。

很多文章在介绍事件分发的时候,都会提到onTouch或者onTouchEvent,本文不会说这2个,因为这2个都是View的dispatchTouchEvent方法内,本文只会提到dispatchTouchEvent方法,这样更准确一点。当然,其实大部分情况下,View的dispatchTouchEvent就是调用onTouchEvent,一般onTouch是没有的,这块的逻辑如果不清楚的话,可以看android点击事件(View)

down事件分发

在讲述事件分发的流程前,先定义三个角色,p,pp,c其中p为主角ViewGroup,pp是p的parent,c为p的child。

手指按下就会触发down事件。例如我们点击了一个TextView,down事件会从activity开始传递,然后传递给DecorView,接着往下传递给对应的ViewGroup,一层层传下来直到TextView。

触摸了任何一个ViewGroup都会调用ViewGroup的dispatchTouchEvent。首先会先进入onInterceptTouchEvent,如果返回true的话,就拦截了,交由本viewgroup的View::dispatchTouchEvent方法,注意这里和前面的dispatchTouchEvent方法不一样,一个是View的dispatchTouchEvent,一个是Viewgroup的dispatchTouchEvent。View的dispatchTouchEvent我们在android点击事件(View)详细说过了,而Viewgroup的dispatchTouchEvent就是负责事件分发的核心代码,也就是我们这篇文章的主要内容,看明白了这个函数的200多行代码,事件分发的所有问题都能明白。

结合下边的图,我们可以明白down事件的传递机制。
在这里插入图片描述

1、如果p的onInterceptTouchEvent返回true,那就直接拦截,交给p的View::dispatchTouchEvent处理,流程图中的super.dispatchTouchEvent就是指View::dispatchTouchEvent,后面的流程暂时不说;

2、如果p的onInterceptTouchEvent返回false,那就不拦截,继续查点击到了哪个child,如果查不到,那就还是交给p的View::dispatchTouchEvent处理。如果查到了,那就交给这个child(简称c)的dispatchTouchEvent处理。c的dispatchTouchEvent有2种结果,true和false,如果返回false,那还是交给p的View::dispatchTouchEvent处理;如果返回了true表示c已经处理好了这个事件,那p就很开心了,小弟帮我完成了一件事,记下他的功劳,把mFirstTouchTarget进行赋值,指向c,然后p的dispatchTouchEvent返回true。

刚才交给p的View::dispatchTouchEvent处理,后面的流程还没说。从前面的流程可以看到,走到这里有3种原因。1、onInterceptTouchEvent返回了true,拦截了;2、点击的这个点没有对应的child;3、c没有处理好,返回了false。p的View::dispatchTouchEvent也只有2种结果,true或者false,如果返回了true,那整个p的dispatchTouchEvent就返回了true,但是此时mFirstTouchTarget为null,因为是自己处理的,不是child处理的。如果p的View::dispatchTouchEvent返回了false,那整个p的dispatchTouchEvent就返回false。

此时p的dispatchTouchEvent结束了,结束的时候会返回true或者false,那后面会发生什么呢?我们看这个流程图,要有递归的思想。此时p: dispatchTouchEvent完成,其实和c:dispatchTouchEvent()是一样的,要把结果告诉pp(p的parent)。

此时的状态有3种:
状态1:p: dispatchTouchEvent()返回true,并且p的mFirstTouchTarget空,代表是p处理了事件down
状态 2:p: dispatchTouchEvent()返回false
状态3:p: dispatchTouchEvent()返回true,并且p的mFirstTouchTarget非空,代表p的child处理了事件。

用mFirstTouchTarget记录有什么好处呢?想想,如果很上层的view想知道到底谁立下了如此大功,处理了事件,顺着mFirstTouchTarget找过来就行了,其实只有在down事件的时候会根据按下的位置来查找对应的子view,后面的事件都是根据mFirstTouchTarget来查找的,这样明显提高效率。

MOVE的事件分发

我们先回头看下,down事件结束之后的三种状态,其实可以合并成2种状态。
先看状态1,p: dispatchTouchEvent()返回true,那么pp的dispatchTouchEvent()肯定也返回true,并且pp的mFirstTouchTarget指向p,看看这个是不是和状态3类似的,只是p换成了pp。 这种情况我们称为case1,case1的本质是什么?有人成功处理了down事件。

再看状态2,p: dispatchTouchEvent()返回false,会来到pp的dispatchTouchEvent()代码内,pp的dispatchTouchEvent()可能返回true或者false,如果返回了true,那其实和case1类似了。如果还是返回false,那就继续往上传,只要祖宗有一个返回了true,那就掉入了case1.如果大家坚持返回false,那就会一直传到DecorView。这种情况我们称为case2,本质就是无人成功处理down事件。

无人成功处理down事件

无人处理down事件,比较简单,我们先说,发生的概率也很小。没有人处理down事件,这个事件就会一直往上抛,直到PhoneWindow$DecorView。而DecorView的onTouchEvent一般返回false,DecorView的mFirstTouchTarget为null。下一次move事件来了,直接拦截并且自己处理。所以结果就是后面的所有事件都停在了DecorView,不会下传,而DecorView的处理结果就是false。所以这种情况下,后面的事件都不会被处理,可以认为被丢弃了。

有人成功处理down事件

假设有view族谱p1,p2,…pn,后面一个是前面一个的parent。假设p2处理了down事件,那么我们根据规则3,move事件不可能给p1,所以我们不用考虑p1。此时p2的mFirstTouchTarget为null,p3,p4等的mFirstTouchTarget非空。所以此时有2种类型的view要考虑,第一种是p2类型的,mFirstTouchTarget为null;第二种是p3,p4类型的,mFirstTouchTarget非空。
move事件的传递,可以分为2个阶段,第一阶段就是决定intecepted的值,第二阶段就是根据intecepted的值进行事件分发.

MOVE事件第一阶段

第一阶段流程图如下所示。
在这里插入图片描述

第一种情况,mFirstTouchTarget为null,intecepted直接会变为true,拦截所有事件,这就是规则3的来源。
第二种情况,mFirstTouchTarget非空,会根据disallowIntercept标志和onInterceptTouchEvent()来决定intecepted的值。

move事件第二阶段

第二阶段流程图如下所示

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D2OlzNZC-1586358050316)(http://obbna3lzo.bkt.clouddn.com/%E4%BA%8B%E4%BB%B6%E5%88%86%E5%8F%91MOVE%E7%AC%AC%E4%BA%8C%E9%98%B6%E6%AE%B5%20.png)]

此时可以分3个case来看

case1

先看mFirstTouchTarget为空的情况,那么他的intecepted必定是true,会调用View:dispatchTouchEvent()作为返回值

case2

若mFirstTouchTarget非空,intecepted为false,此时按理说会去找对应位置的child,NONONO。这里的逻辑和down事件不一样,这里不会根据位置去找,而是根据mFirstTouchTarget去找,因为我们down事件的child已经记录在mFirstTouchTarget内了,所以直接找mFirstTouchTarget就行。(其实mFirstTouchTarget其实是个链表,跟着链表爬一遍)。mFirstTouchTarget的处理结果就作为整个dispatchTouchEvent的返回结果。

case3

若mFirstTouchTarget非空,intecepted为true,他会给mFirstTouchTarget指向的view发一个cancel事件,然后mFirstTouchTarget置null,然后返回true。啊??居然不调用自己的View:dispatchTouchEvent吗?确实是的。***本次move事件,实际上不会调用自己的View:dispatchTouchEvent。但是此时view mFirstTouchTarget已经为null了,所以下一次move来的时候,走的是case2,View:dispatchTouchEvent.***这一点我之前是理解错误的。其实这里损失了一个MOVE事件,这个MOVE事件虽然返回了true,但是其实没有任何人处理他。

case2源码分析

由于case2和case3的代码我不太熟悉,所以抓出来分析一下。
先看case2,此时mFirstTouchTarget非空,在L13把事件发给child

       // Dispatch to touch targets, excluding the new touch target if we already
                // dispatched to it.  Cancel touch targets if necessary.
                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;
                                //在这里把事件发给child
                        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;
                }
case3源码分析

再来看case3,mFirstTouchTarget非空,intecepted为true

来看这段代码,L12因为intercepted为true,所以cancelChild为true,会走到dispatchTransformedTouchEvent,dispatchTransformedTouchEvent内部会发一个cancel事件出去,然后返回true(后边会详细说)。然后L18,因为cancelChild为true,所以会执行L21,把mFirstTouchTarget置null。

 {
                // Dispatch to touch targets, excluding the new touch target if we already
                // dispatched to it.  Cancel touch targets if necessary.
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while (target != null) {
                    final TouchTarget next = target.next;
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {
                    //注意这里,因为intercepted为true,所以cancelChild也会为true
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                        if (cancelChild) {
                            if (predecessor == null) {
                            //mFirstTouchTarget置null
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            target.recycle();
                            target = next;
                            continue;
                        }
                    }
                    predecessor = target;
                    target = next;
                }
            }

我们再看看dispatchTransformedTouchEvent的流程,此时传进来的cancel为true,会再L9设置CANCEL事件,在L14由child发出去。

   private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;

        // Canceling motions is a special case.  We don't need to perform any transformations
        // or filtering.  The important part is the action, not the contents.
        final int oldAction = event.getAction();
        if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
            event.setAction(MotionEvent.ACTION_CANCEL);
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
            //走这里,发一个cancel消息出去
                handled = child.dispatchTouchEvent(event);
            }
            event.setAction(oldAction);
            //返回true
            return handled;
        }

UP的事件分发

UP事件其实和MOVE事件基本一致,UP事件一般不拦截。即使拦截了UP事件,也不会调用自己的View:dispatchTouchEvent.为什么?可以参考 move事件第二阶段的case3,简单来说如果拦截UP事件,此时mFirstTouchTarget非空的话,此次dispatchTouchEvent会让child发一个cancel出去,把自己的mFirstTouchTarget置空,然后返回true,不会调用View:dispatchTouchEvent。因为只有下一个事件来临的时候才调用View:dispatchTouchEvent,可是UP已经是最后一个事件了,所以不会发生后面的事。

伪代码

讲了这么多,我尝试着用伪代码写ViewGroup的dispatchTouchEvent,其实也不麻烦,40行代码说明了一切。

            // Check for interception.
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                } else {
                    intercepted = false;
                }
            } else {

                intercepted = true;
            }

          
            //查找合适的子view
            if(down事件&&!intercepted){
                handled=某个child.dispatchTouchEvent();
                if(handled){
                    mFirstTouchTarget赋值
                }
           
            }
            

            if(mFirstTouchTarget==null){
                //只有这种情况,parent亲自处理
                handled=super.dispatchTouchEvent()
            }else{
               if(intercepted){
                    mFirstTouchTarget发一个cancel事件
                    mFirstTouchTarget=null;
               }else{
                    return mFirstTouchTarget.dispatchTouchEvent();
               }
               
            }
            return handled;

问题答案

1、如果拦截了某个事件,是否就会交由本view的View:dispatchTouchEvent处理?
这里的拦截的意思是指在onInterceptTouchEvent里返回了true,如果拦截的只是down事件,那么必然会交给View:dispatchTouchEvent处理。如果拦截的只是MOVE事件,那么是不会交给View:dispatchTouchEvent处理的,此时只是把mFirstTouchTarget置null,下一个MOVE才会交由View:dispatchTouchEvent处理。如果拦截的只是UP事件,那就更加不可能交给View:dispatchTouchEvent处理了。
2、一个事件,如果子view处理失败,是否就交还给父view处理?
只有mFirstTouchTarget为null,才交由parent处理。所以只有down事件肯定会给parent处理。
3、如果一个down事件,大家都不处理,会怎么样?
这个文中说的很详细了,不停往上抛直到DecorView,返回false,然后MOVE和UP给了DecorView处理,DecorView拦下来返回false。相当于所有事件都丢弃了。
4、parent把事件传递给哪个子view呢?是根据位置查一遍的吗?
down是根据位置查的,move和up是根据mFirstTouchTarget来处理的
5、某个view成功处理down事件,但是处理move事件失败,会如何?
假设子view为c,c的parent为p,p的parent为pp。c成功处理了down事件,所以p的mFirstTouchTarget指向c,在p的dispatchTouchEvent过程里,c处理move失败,参考MOVE第二阶段的流程图,可以知道p的dispatchTouchEvent返回false,然后接着pp的dispatchTouchEvent也返回false,直到DecorView,这个和第三个问题有点像

总结

本文提了3条规则,画了三幅流程图,写了一段伪代码,希望以后我遇到事件分发的问题,都能从这里找到答案。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值