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事件的传递机制。


MacDown logo

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事件第二阶段

第二阶段流程图如下所示

此时可以分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为null,所以会执行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,会再9设置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处理,其他就不一定了,还是看mFirstTouchTarget的值。
3、如果一个down事件,大家都不处理,会怎么样?
这个文中说的很详细了,不停往上抛直到DecorView,返回false,然后MOVE和UP给了DecorView处理,DecorView拦下来返回false。相当于所有事件都丢弃了。
4、parent把事件传递给哪个子view呢?是根据位置查一遍的吗?
down是根据位置查的,move和up是根据mFirstTouchTarget来处理的
5、假设子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条规则,画了三幅流程图,写了一段伪代码,希望以后我遇到事件分发的问题,都能从这里找到答案。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容