Android中事件分发机制
首先了解下Android中View的结构:
Android 中View是树形结构的,
可以看到view顶层出现两个吧:
简单来说,Window是一个抽象类,是所有视图的最顶层容器,视图的外观和行为都归他管,不论是背景显示,标题栏还是事件处理都是他管理的范畴,它其实就像是View界的太上皇(虽然能管的事情看似很多,但是没实权,因为抽象类不能直接使用)。而 PhoneWindow 作为 Window 的唯一亲儿子(唯一实现类),自然就是 View 界的皇帝了,PhoneWindow 的权利可是非常大大,不过对于我们来说用处并不大,因为皇帝平时都是躲在深宫里面的,虽然偶尔用特殊方法能见上一面,但想要完全指挥 PhoneWindow 为你工作是很困难的。而 DecorView 是 PhoneWindow 的一个内部类,其职位相当于小太监,就是跟在 PhoneWindow 身边专业为 PhoneWindow 服务的,除了自己要干活之外,也负责消息的传递,PhoneWindow 的指示通过 DecorView 传递给下面的 View,而下面 View 的信息也通过 DecorView 回传给 PhoneWindow。
触摸事件的相关方法:
一、ViewGroup:
①、dispatchTouchEvent(MotionEvent)用于分发Touch事件
②、onInterceptTouchEvent( MotionEvent ) 用于是否中断touch事件
③、onTouchEvent( MotionEvent ) 用于处理Touch事件
二、View 和 Activity:
①、dispatchTouchEvent(MotionEvent) 事件的分发
②、onTouchEvent(MotionEvent) onTouch的处理
接下来用代码诠释下问题,至于源码的有大神写的更详细:
郭大神的:android事件分发机制完全解析(上)
本试验有5个类:
Utils用于获取MotionEvent的事件名称
MainActivity用于显示界面
ViewGroupA继承于LinearLayout
ViewGroupB继承于LinearLayout
ViewC继承于TextView
Utils:
2、ViewGroupA.java 代码如下:
3、ViewGroupB.java代码与ViewGroupA.java类似,只是类名不同,还有输出Log的时候是“ViewGroupB"的Log
4、ViewC.java 代码与ViewGroupA.java类似,只是类名不同,还有输出Log的时候是“ViewC"的Log,还有ViewC是直接继承于TextView,它没有onInterceptTouchEvent(MotionEvent)方法
5、MainActivity.java代码与ViewGroupA.java类似,只是类名不同,还有输出Log的时候在分行方法之前先打印一行分隔线,还有Log是“Activity"的Log,还有MainActivity是直接继承于Activity,它没有onInterceptTouchEvent(MotionEvent)方法
6、activity_main.xml 代码如下:
运行代码,在蓝色的View上进行按下、移动、抬起,输出的Log如下:
---------------------------------------------
DOWN , Activity.dispatch
DOWN ,ViewGroupA.dispatch
DOWN ,ViewGroupA.intercept = false
DOWN ,ViewGroupB.dispatch
DOWN ,ViewGroupB.intercept = false
DOWN ,ViewC.dispatch
DOWN ,ViewC.touch = false
DOWN ,ViewC.dispatch = false
DOWN ,ViewGroupB.touch = false
DOWN ,ViewGroupB.dispatch = false
DOWN ,ViewGroupA.touch = false
DOWN ,ViewGroupA.dispatch = false
DOWN ,Activity.touch = false
DOWN ,Activity.dispatch = false
---------------------------------------------
MOVE ,Activity.dispatch
MOVE ,Activity.touch = false
MOVE ,Activity.dispatch = false
---------------------------------------------
UP ,Activity.dispatch
UP ,Activity.touch = false
UP ,Activity.dispatch = false
---------------------------------------------
从上面的Log可分析出:
①、触摸事件最先是由Activity获得,然后是ViewGroupA、ViewGroupB、ViewC
②、如果Down事件没有人处理,则事件丢失,后续的事件(如Move、Up)不再传递,直接由Activity进行处理
③、所有的事件分发都是调用super.dispatchTouchEvent(ev)完成的,所以如果不调用这句代码则事件中止传递。但是要中止事件传递一般不会这么做,一般是在onInterceptTouchEvent(MotionEvent) 方法中处理,如果该方法返回true则中止。既然dispatchTouchEvent(ev)方法可以中止事件传递,为什么还要设计一个onInterceptTouchEvent(MotionEvent) 方法呢? 因为子View可以请求父View不要拦截事件,如ListView是可以上下滑动的,当处于滑动状态时候就会请求禁止父View的拦截触摸事件方法,让ListView可以一直获取到touch事件进行滚动。假设这个时候父View又想响应触摸事件怎么办?可以写到dispatchTouchEvent方法中,因为事件是先传到这个方法,然后再传递给ListView的。
④、既然Activity最先获得事件,则可在Activity的dispatchTouchEvent方法不调用super.dispatchTouchEvent(ev);代码,则事件就不会进行分发了,可在此方法中调用onTouchEvent(ev)让它去处理。
⑤、应用:假如在Activity中要一定要响应一些触摸事件,又怕事件传递后被消费了,也是相同道理,直接在Activity的dispatchTouchEvent方法中处理即可)
三、修改ViewGroupA的onInterceptTouchEvent让其返回true,代表拦截事件。
运行,然后按下、移动、抬起,Log如下:
---------------------------------------------
DOWN , Activity.dispatch
DOWN , ViewGroupA.dispatch
DOWN , ViewGroupA.intercept = true
DOWN , ViewGroupA.touch = false
DOWN , ViewGroupA.dispatch = false
DOWN , Activity.touch = false
DOWN , Activity.dispatch = false
---------------------------------------------
MOVE , Activity.dispatch
MOVE , Activity.touch = false
MOVE , Activity.dispatch = false
---------------------------------------------
UP , Activity.dispatch
UP , Activity.touch = false
UP , Activity.dispatch = false
从上面的Log可分析出:
①、ViewGroupA在Down事件的时候中断事件传递后直接把Touch事件交由自己的onTouchEvent方法去处理
②、ViewGroupA中断触摸事件后只是子View(ViewGroupB和ViewC)不再获得touch事件,而父View(Activity)可以。
③、虽然ViewGroupA中断触摸事件传递,由于它的onTouchEvent方法也没有处理Down事件,所以事件也丢失了,后续事件(如Move、Up)不再传给ViewGroupA,由Activity中行处理。所以,如果你想要获取后续事件,在处理了Down事件后一定要记得返回true。这里说的后续事件是指一次整体的操作,一般是由一个Down和多个Move和一个Up组成。按下然后移动然后弹起,这样的操作称为一次整体的操作。
修改ViewGroupA的onInterceptTouchEvent让其返回false,代表不拦截事件
修改ViewGroupB的onInterceptTouchEvent让其返回true,代表拦截事件,并修改onTouchEvent方法如下:
运行,然后按下、移动、抬起,Log如下:
---------------------------------------------
DOWN ,Activity.dispatch
DOWN , ViewGroupA.dispatch
DOWN , ViewGroupA.intercept = false
DOWN , ViewGroupB.dispatch
DOWN , ViewGroupB.intercept = true
DOWN , ViewGroupB.touch = true
DOWN , ViewGroupB.dispatch = true
DOWN , ViewGroupA.dispatch = true
DOWN , Activity.dispatch = true
---------------------------------------------
MOVE , Activity.dispatch
MOVE , ViewGroupA.dispatch
MOVE , ViewGroupA.intercept = false
MOVE , ViewGroupB.dispatch
MOVE , ViewGroupB.touch = false
MOVE , ViewGroupB.dispatch = false
MOVE , ViewGroupA.dispatch = false
MOVE , Activity.touch = false
MOVE , Activity.dispatch = false
---------------------------------------------
MOVE , Activity.dispatch
MOVE , ViewGroupA.dispatch
MOVE , ViewGroupA.intercept = false
MOVE , ViewGroupB.dispatch
MOVE , ViewGroupB.touch = false
MOVE , ViewGroupB.dispatch = false
MOVE , ViewGroupA.dispatch = false
MOVE , Activity.touch = false
MOVE , Activity.dispatch = false
---------------------------------------------
UP , Activity.dispatch
UP , ViewGroupA.dispatch
UP , ViewGroupA.intercept = false
UP , ViewGroupB.dispatch
UP , ViewGroupB.touch = false
UP , ViewGroupB.dispatch = false
UP , ViewGroupA.dispatch = false
UP , Activity.touch = false
UP , Activity.dispatch = false
从上面的Log可分析出:
①、ViewGroupB中断了触摸事件的传递,并把事件交给自己的onTouchEvent方法处理。它的孩子ViewC就接收不到touch事件了
②、ViewGroupB中消费了Down事件(返回true),所以可以接收后续的Move、Up事件,dispatch方法接收到Move事件后并没有去调用onInterceptTouchEvent方法了,因为在接收到Down事件时候已经成为了目标View,非目标View不能接收后续事件,所以后续事件(Move、Up)就不需要再调用了onInterceptTouchEvent,因为不需要再传递了,所以直接把事件交给自己的onTouchEvent方法进行处理。
③、虽然ViewGroupB在处理Move事件时返回了false,但是父View(ViewGroupA)的onTouchEvent方法并没有执行,这说明,谁消费了Down事件(返回true)谁就能接收后续事件(Move、Up),没有消费Down事件的其它View则接收不到(Activity例外)。
④、虽然ViewGroupB在处理Move事件时返回了false,但是还是依旧可以接收后续事件的(Move、Up),那在处理Move和UP时返回false或返回true有什么区别吗?答:返回true则消费此事件,Activity的onTouchEvent方法就不会接收到了。
通过这个Demo可以模拟任何的不同情况,如处理Down事件返回true,Move事件返回false,事件拦截,请求禁止拦截等等。
总结:
①、touch事件传递其实就是一连串的方法调用,由Activity的dispatchTouchEvent方法开始调用,当这个方法调用结束时,这个touch事件就结束了。
②、 一个整体的touch事件由1个Donw和0 ~ n个Move和1个Up事件组成
③、 消费了Down事件的View称为目标View,目标View可接收后续事件(非目标View不接收后续事件)
④、 Down事件如果没有任何View消费,则后续事件不再传递,直接由Activity的onTouchEvent方法处理,所以,想要处理其它事件首先要消费Down事件,也就是在接收到Down事件的时候返回true。
二、拦截:
①、如果在Down事件拦截,则把当前事件交自己的onTouchEvnet方法处理,如果此方法不消费Down事件,则事件丢失,后续事件由Activity的onTouchEvent方法处理
②、如果在其它事件拦截,则不处理当前事件,且传一个Cancel事件给目标View,后续的事件就会交给自己的onTouchEvent方法处理(此时拦截了事件的View变成了目标View,虽然它没有消费Down事件)
③、如果在Down事件不拦截,在事件分发返回后,如果Down事件没被消费,则会把事件交给自己的onTouchEvent方法处理。
④、如果在其它事件不拦截(能接收其它事件说明有目标View),在事件分发返回后,不会把事件交给自己的onTouchEvent,即使目标View在处理(Move、Up)事件时返回false。
⑤、Activity不管有无目标View,也不管是Down还是Move、Up事件,只要分发调用返回时,如果事件没被消费,则交给自己的onTouchEvent方法处理
⑥、可以调用getParent().requestDisallowInterceptTouchEvent(true)方法请求父View禁止拦截事件,这个方法会递归的请求所有的父View禁止拦截事件。
三、注意:
①。如果想要获取到一个整体的touch事件,一定要消费Down事件,如果在Down事件的时候只是请求父View禁止拦截并不消费Down事件,虽然父View不再拦截了,但后续事件也接收不到了,哪个父View消费了Down事件哪个父View就可以接收到后续事件。
②、有两个View并没有包含关系,但是有重叠,则上面的View先拿到事件,如果消费了,则事件不会传给另一个View。
③、容器类一般都是调用ViewGroup的dispatchTouchEvent方法进行事件分发,其它类一般是调用View类的dispatchTouchEvent方法进行事件分发;通常默认的onInterceptTouchEvent、onTouchEvent方法都是返回false。
应用技巧:
①、父类的onTouchEvent方法要想执行,要么是等所有的子View都不消费Down事件,要么是父View把事件拦截。
②、如果子类消费了Down事件,而父View又不想拦截但是又想处理这个事件,则父View可以在onInterceptTouchEvent或dispatchTouchEvent方法处理touch事件
③、如果子View请求了禁止父View拦截,且父View还想要拦截的话,可在父View的dispatchTouchEvent方法中不调用super.dispatchTouchEvent则把事件拦截了。