简洁的事件分发

说在前面的话

事件分发是一个重点也是难点,所以,本篇幅有点长,如果耐心看完本篇,相信读者会有收获的。同时,读者也可以自己写例子测试,毕竟,纸上得来终觉浅,绝知此事要躬行。但是,对于水平高的读者,其实最好的方式是看源代码,因为一切原因都可以从源头找到答案

关于事件分发

关于事件分发,其实主要就是理解三个函数,这三个函数分别是dispatchTouchEvent(MotionEvent ev)onInterceptTouchEvent(MotionEvent ev)以及onTouchEvent(MotionEvent ev),这里直接上一张图:

伪代码-20161116
伪代码-20161116

这张图将分发事件中的重要的三个函数之间的关系表达的比较清晰,这里再简单的解释一下上面的伪代码:一个点击事件产生后,会从外到内传递,传递到根ViewGroup后,会调用根ViewGroup的dispatchTouchEvent方法,然后如果自己的onInterceptTouchEvent方法返回true,表示拦截事件,那么这个事件就会自己处理,也就是调用自己的onTouchEvent方法,如果返回false,就表示自己不拦截这个事件,事件就会传递给子元素,也就是调用子元素的dispatchTouchEvent方法,如此传递下去,直到事件被处理。

知道了关系以后,我们还需要了解这三个函数,了解一个函数,其实无非就是理解它的作用,传入的参数以及返回值。
先说dispatchTouchEvent(MotionEvent ev):
这个函数的作用是分发事件,不管是ViewGroup还是View都有这个方法,它的返回值受本身的onInterceptTouchEvent(MotionEvent ev)child.dispatchTouchEvent(MotionEvent ev)共同影响(从上面那张图就可以看出来),返回的各个值的意义如下:
return true :表示该View内部消化掉了所有事件。

return false :事件在本层不再继续进行分发,这个false也就是本身的dispatchTouchEvent(MotionEvent ev)返回值,而这个返回值会回溯给上层控件的dispatchTouchEvent(MotionEvent ev),表示自己没有接受这个事件,不管上层控件是view还是viewGroup,都是交由上层控件的onTouchEvent(MotionEvent ev)方法进行消费(如果本层控件已经是Activity,那么事件将被系统消费或处理)。

如果事件分发返回系统默认的 super.dispatchTouchEvent(ev),事件将分发给本层的事件拦截onInterceptTouchEvent(MotionEvent ev)方法进行处理,而不是super的onInterceptTouchEvent。因为return super.dispatchTouchEvent(ev)会去运行父viewGroup的dispatchTouchEvent(ev),然后运行onInterceptTouchEvent,那么这个onInterceptTouchEvent是谁的呢?根据方法是基于对象的,所以就会运行child的onInterceptTouchEvent(MotionEvent ev)也就是本层的事件拦截器,而不是super的onInterceptTouchEvent。详情可以参考规则中的第十条。

然后是onInterceptTouchEvent(MotionEvent ev):
这个函数的作用是拦截事件,只有ViewGroup有这个方法,返回的各个值的意义如下:
return true :表示将事件进行拦截,并将拦截到的事件交由本层控件 的 onTouchEvent 进行处理;
return false :则表示不对事件进行拦截,事件得以成功分发到子View。并由子View的dispatchTouchEvent进行处理。 
如果返回super.onInterceptTouchEvent(ev),默认false,即表示不拦截该事件,这样事件才能以分发下去。
最后是onTouchEvent(MotionEvent ev):
这个函数的作用是处理触摸事件,ViewGroupView都有这个方法,返回值的意义如下:
如果return true,表示onTouchEvent处理完事件后消费了此次事件。此时事件终结;

如果return fasle,则表示不响应事件,如果是ACTION_DOWN事件,那么该事件将会不断向上层View的onTouchEvent方法传递,直到某个View的onTouchEvent方法返回true,如果到了最顶层View还是返回false,那么事件就会交给Activity处理。且在同一个事件系列中,当前View无法再次接收到该事件序列,如果不是ACTION_DOWN事件,那么不会返回给父view的onTouchEvent处理,而是给Activity处理,并且该view可以继续接收该事件序列;

如果return super.onTouchEvent(event);,默认是true,即表示处理事件。那这个和return true有什么区别呢?从代码就可以看出来,return super.onTouchEvent(event)会执行super.onTouchEvent(event)这个方法。比如,当你继承EditText后,重写onTouchEvent(MotionEvent event)方法,如果你将return super.onTouchEvent(event);换成return true,就会发现当你按返回取消输入框,再次点击自定义EditText时就会无法弹出输入框,解决办法可以是将return true修改成return super.onTouchEvent(event),或者是在之前调用一次super.onTouchEvent(event)方法,弹出输入框是在action为ACTION_UP的时候弹出的。

重要的知识点(大家拿本子记一下,高考必考啊)

1.一个viewGroup一旦决定拦截事件(这里分两种情况,一个是拦截了ACTION_DOWN事件,还有一个是没有子View满足分发事件的条件或者子view在ACTION_DOWN时返回了false),那么后面的事件序列都会交给它处理,并且不会再调用onInterceptTouchEvent(ev)方法, 当ACTION_DOWN事件成功传入子view的时候, 那么父ViewGroup在别的事件分发的时候,比如ACTION_MOVE,每次都会调用onInterceptTouchEvent来判断是否拦截当前事件。 也就是说,父ViewGroup的onInterceptTouchEvent不会再次调用的时机只是自己来处理这个事件,也就是自己的onTouchEvent被调用,只有这个时候才不会再次调用onInterceptTouchEvent,当事件传入子view来处理事件的时候,父ViewGroup都会每次都调用onInterceptTouchEvent来决定是否拦截当前事件。

2.dispatchTouchEvent无论返回true还是false,事件都不再进行分发,只有当其返回super.dispatchTouchEvent(ev),才表明其具有向下层分发的愿望,但是是否能够分发成功,则需要经过事件拦截onInterceptTouchEvent的审核。事件是否向上传递处理是由onTouchEvent的返回值决定的。

3.正常情况下,一个事件序列只能被一个view拦截且消耗,因为,一旦决定拦截事件,那么这个事件只能被这个view消耗,并且它的onInterceptTouchEvent(ev)方法也不会再次调用(这里的拦截和规则一中的拦截是一样的。这里的再次调用是指当确定拦截事件后,除了在ACTION_DOWN时调用onInterceptTouchEvent(ev),后面都不调用,其实跟规则一中说的一样),如果你想这个事件序列被多个view拦截消耗,那么你可以在拦截事件的那个view中的onTouchEvent()方法中调用你想让其拦截事件的那个view的onTouchEvent()方法来实现。

4.view一旦用onTouchEvent()开始处理事件,如果没有处理ATION_DOWN事件,那么同一个事件序列中的事件也不会交给他处理,会回溯给他的父控件,如果你处理了ACTION_DOWN但是没有处理ACTION_MOVE或者ACTION_UP,那么这个事件还是被你消耗,不会调用父控件的onTouchEvent方法,最后会是Activity处理,后面的事件还是继续交给你处理。其实,这就类似现实,如果别人第一次叫你做事,你没做好,那么后面就都不会放心叫你做了,如果你第一次做好了,后面没做好,别人还是会给你做的,所以,第一次很重要。

5.view的onTouchEvent默认都是消费事件的(返回true),除非是不可点击的,也就是longClickable和clickable都为false,只有这个属性会影响view的onTouchEvent的返回值,别的属性不会,比如,Enabled属性,就算是Enabled属性为false,也就是disable状态,view的onTouchEvent默认返回的还是true。

6.事件传递是由外向内的,即事件总是传递给父元素,再由父元素分发给子控件,通过requestDisallowInterceptTouchEvent();方法可以在子元素中干预父元素的事件分发过程,但是不能干预ACTION_DOWN事件,因为当时ACTION_DOWN事件的时候,父元素会重置FLAG_DISALLOW_INTERCEPT标志位。

7.使用内部拦截法的时候,为了弄清楚顺序,我就直接调试,结果,运行到父元素的dispatchTouchEvent后,不会去调用父元素的onInterceptTouchEvent方法,直接就到了子元素的dispatchTouchEvent,依然会运行到子view的onTouchEvent,等到ACTION_UP的时候才会又跑到父元素中的dispatchTouchEvent和onInterceptTouchEvent去判断是否拦截ACTION_UP事件。我倒腾了一天,才发现是需要移动,也就是让move多次调用才行,因为事件是由外向内的,当第一次ACTION_MOVE事件到的时候,先运行父ViewGroup的dispatchTouchEvent方法,此时FLAG_DISALLOW_INTERCEPT依然是设置成true,所以,不会运行父ViewGroup的onInterceptTouchEvent方法,直接就会运行子view的dispatchTouchEvent方法,然后FLAG_DISALLOW_INTERCEPT被设置成false,于是当第二次的ACTION_MOVE到来的的时候,才会去运行父viewGroup的onInterceptTouchEvent方法,然后子view收到ACTION_CANCEL事件,等到第三个ACTION_MOVE的时候父viewGroup才开始拦截事件。但是因为我之前是调试,所以都只有一次move事件,结果就不一样了。也是醉了。并且使用内部拦截法的时候,ACTION_UP事件也会被父view拦截,不会传递到子view中,也就意味着子view的onClick事件不会响应,这一点要记住。

8.内部拦截法和外部拦截法的区别:内部拦截法需要到该事件的第三个的时候才有用,也就是该事件的第一个依然被子view得到,外部拦截法则是到第二个就有用了,子view不会得到该事件的任何一个,比如,拦截ACTION_MOVE的时候,使用内部拦截法在拦截第三个ACTION_MOVE的时候才拦截了,因为第一个ACTION_MOVE会被子view得到,而使用外部拦截法则是第二个ACTION_MOVE的时候就拦截了,因为子view不会得到ACTION_MOVE中的任何一个。详情可以见9,10。所以,使用外部拦截法要好点。

9.当viewGroup没有拦截ACTION_DOWN而拦截了ACTION_MOVE或者ACTION_UP的时候,那么,第一个被拦截的动作不会在viewGroup中的onTouchEvent中触发,也不会在子view的onTouchEvent中触发,而是子view会受到ACTION_CANCEL事件。该事件序列后面的事件都会被拦截,并且下一个同类型的事件传来时,不会再调用viewGroup的onInterceptTouchEvent方法,直接就调用viewGroup的onTouchEvent方法,这里解释一下,什么是第一个被拦截的动作,比如,多个move的时候,第一个move就不会被父view或者子view执行,感觉是这个事件变成了ACTION_CANCEL事件传递到了子view。也就是说,一旦在这种情况下,ACTION_UP事件永远不会被子view接收。也就意味着,不管是使用外部拦截法还是内部拦截法,只要拦截了,那么子view就收不到ACTION_UP事件。还有就是ViewGroup就不要拦截ACTION_UP了,因为这样大家都得不到ACTION_UP事件,何必呢?

10.为了讲述方便,当从一个ViewGroup分发事件到子ViewGroup时,在子ViewGroup的dispatchTouchEvent方法中调用父类的dispatchTouchEvent,发现不会继续调用父类的onInterceprTouchEvent,而是直接调用子ViewGroup的onInterceptTouchEvent,为什么在这里调用父类的dispatchTouchEvent不会跟着调用父类的onInterceptTouchEvent?我调试和看源码发现当运行到findChildWithAccessibilityFocus()方法时,view会变成接受到事件的view,然后就不知道了。水平还是看不懂源代码。其实这是因为我对java的理解有错误,基于方法都是基于对象的,所以在子viewGroup中调用父类的dispatchTouchEvent,也就是super.dispatchTouchEvent()时,这时会运行到父viewGroup的dispatchTouchEvent里,会调用onInterceptTouchEvent方法,这时的onInterceptTouchEvent其实就已经是子viewGroup的onInterceptTouchEvent方法,而不是父ViewGroup的dispatchTouchEvent方法,因为方法是基于对象的。

11.onClick发生的前提就是可点击,并且收到了ACTION_DOWN和ACTION_UP事件。这里解释一下,这里的收到了用词不是那么准确,应该是能接收到事件,并且return super.onTouchEvent()了,记住是return super.onTouchEvent(),如果是return true都不行,因为return true没有执行view的onTouchEvent方法,而点击事件是在ACTION_UP中设置的。即在ACTION_UP的时候源码中调用了performClick()方法。这里贴一部分源代码

TextView的源代码:

    final boolean superResult = super.onTouchEvent(event);

View的源代码:

  case MotionEvent.ACTION_UP:
                boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                    // take focus if we don't have it already and we should in
                    // touch mode.
                    boolean focusTaken = false;
                    if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                        focusTaken = requestFocus();
                    }

                    if (prepressed) {
                        // The button is being released before we actually
                        // showed it as pressed.  Make it show the pressed
                        // state now (before scheduling the click) to ensure
                        // the user sees it.
                        setPressed(true, x, y);
                   }

                    if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                        // This is a tap, so remove the longpress check
                        removeLongPressCallback();

                        // Only perform take click actions if we were in the pressed state
                        if (!focusTaken) {
                            // Use a Runnable and post this rather than calling
                            // performClick directly. This lets other visual state
                            // of the view update before click actions start.
                            if (mPerformClick == null) {
                                mPerformClick = new PerformClick();
                            }
                            if (!post(mPerformClick)) {
                                performClick();
                            }
                        }
                    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容