触摸事件,主要有三种类型,ACTION_DOWN、ACTION_MOVE和ACTION_UP,分别指点击、移动和抬起。这篇文章主要来说一说一个事件产生之后,在控件间的传递。
与其说是事件传递,不如说是事件序列的传递。一个事件序列,是由一个ACTION_DOWN和一个ACTION_UP组成,中间可能包含0个至多个ACTION_MOVE事件。手指按下时,会产生一个ACTION_DOWN事件,手指抬起时,会产生一个ACTION_UP事件,如果按下和抬起的位置偏移超过系统设定的阈值,便会产生一个或多个ACTION_MOVE事件,这便是一个事件序列。
事件序列,是按照视图树由根到枝叶的顺序传递的,意思就是说,最外层的View或ViewGroup最先接收到事件,然后向内传递。其中事件流经的控件主要分为两种:ViewGroup和View。
ViewGroup
ViewGroup是View的子类,其中有3个和事件相关的方法:dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent,分别是分发事件、拦截事件和处理事件。
简单来说就是,系统通过dispatchTouchEvent方法将事件传入,在dispatchTouchEvent内根据onInterceptTouchEvent来判断是否拦截该事件,如果拦截了,则不再将其沿着视图树向内传递;如果拦截,则根据onTouchEvent方法来判读是否处理,如果处理了,则表示消费了该事件。
View
View没有onInterceptTouchEvent,因为View无子View,是在视图树的最内层了,不需要拦截。当事件通过dispatchTouchEvent传入,根据onTouchEvent方法来判断是否消费。
事件流转
因为事件都是以事件序列的形式传递的,单独看某个事件意义不大,这里分析事件序列,先看ACTION_DOWN事件,这里的向内和向外都是针对视图树的结构而言的。
1 | // 视图树 |
- 向内分发ACTION_DOWN
- ViewGroup接收到事件后,会根据onInterceptTouchEvent方法来判断是否拦截,如果拦截,则根据onTouchEvent方法来判断是否消费事件。
- View接收到事件后,根据onTouchEvent方法判断是否消费事件。
- 如果ViewGroup没有拦截事件,则根据坐标,依次遍历该事件命中的子View/ViewGroup,重复上面这个过程
- 当事件被拦截,或者传递到视图树最内层时,便不会继续向内传递,转而开始回传。
- 向外回传ACTION_DOWN
- 事件回传,传递的是dispatchTouchEvent方法的返回值,true/false
- 如果回传的false,父ViewGroup的onTouchEvent方法会被调用,可理解为本着事件不浪费的原则,既然子视图都没消费,那么就看看自己是否可以消费掉
- 如果回传到视图根时,依然是false,表示没有子视图消费ACTION_DOWN事件,则该序列的后续事件不再向内传递,都由根视图处理
- 事件序列的后续事件
- 系统本着一个事件序列只由一个视图处理的原则
- 后续事件都将传递至消费掉ACTION_DOWN事件的ViewGroup/View,且在dispatchTouchEvent内直接调用onTouchEvent,不经过onInterceptTouchEvent方法,如果有的话
- 当然,后续事件在传递过程中也可以被拦截和消费,但不建议这么做,比如一个按钮只接收了一个按下事件,则在逻辑上可能会不完整
- 后续事件在回传时,即便是false,父ViewGroup的onTouchEvent也不会被调用,因这与第一点相悖
根视图
根据上一篇文章的介绍可以知道,DecorView是我们看到的视图的根,它继承自FrameLayout,是一个ViewGroup,所以它有3个和事件相关的方法。此外,Activity对应一个PhoneWindow,PhoneWindow对应一个DecorView,所以在事件序列产生的时候,最新接收到的是DecorView。
根据源码,DecorView将事件转给了Window.Callback,这个Callback是Window在Activity中初始化时传入的,就是Activity本身,Activity实现了Callback接口,即,在DecorView中调用了Activity的dispatchTouchEvent。
Activity的dispatchTouchEvent方法中,先将事件传给了Window的superDispatchTouchEvent方法,若没消费,再调用自己的onTouchEvent。而Window的superDispatchTouchEvent很简单,就是直接调用了DecorView的superDispatchTouchEvent,最终调用了ViewGroup的dispatchTouchEvent方法。
总结下来就是,DecorView将事件转给了Activity,在Activity的dispatchTouchEvent方法中判断,若DecorView没有消费,则交给Activity的onTouchEvent。
所以,从处理事件顺序来看,真实的事件流转顺序应该是:Activity -> DecorView -> … -> DecorView -> Activity。
1 | // DecorView.java |
总结
- 将事件传递分为向内分发和向外回传两个过程更便于理解,这里的向内和向外都是针对视图树的结构而言的。
- 向内分发过程中,事件一旦被拦截,或者传到了最内层,便会开始回传事件的消费结果,也就是入口dispatchTouchEvent方法的结果
- 当回传结果是false,且事件是序列事件的头事件ACTION_DOWN时,会调用父ViewGroup的onTouchEvent来尝试消费事件
- 系统本着一个事件序列只交给一个View/ViewGroup来处理的原则
扩展之滑动冲突
当在列表内部内嵌可滑动视图时,内部的可滑动视图常常监听不到滑动事件,这是因为列表作为内嵌视图的父ViewGroup会拦截滑动事件,所以内部自然感知不到。比较好的体验是,内部可滑动视图向上滑动到顶时,后续的滑动事件交给父列表处理,让父列表滑动来响应用户的滑动操作;当内部视图向下滑动到底时,后续的滑动事件也是交给父列表处理,让父列表滑动个来响应用户怼滑动操作。
为了解决这个问题,就必须要让事件序列传到最内层,根据上面内容可以知道,要想让事件序列传到内部,那么ACTION_DOWN事件就要交给内部视图来处理。
再看后续事件,作为内部视图的父视图,后续的每一个事件在传到内部视图之前,都会流经外部的列表,因此,就有两种方式来处理后续事件
- 由父视图来判断当前事件是由自己消费,还是交给内部视图消费
- 由内部试图来判断当前事件是由自己消费,还是让父视图消费
第一种比较简单,在父视图内对ACTION_DOWN之外的后续事件判断,需要则拦截,不需要则通过;
通过源码可以看到,在子视图中,是可以控制父视图是否可以拦截事件的,所以上述的第二种方式也是可行的。内部视图在收到ACTION_DOWN事件后,通过parent.requestDisallowInterceptTouchEvent(true)
来禁止父视图拦截事件,当自己不再需要后续事件时,通过parent.requestDisallowInterceptTouchEvent(false)
来让父视图拦截处理后面的滑动事件,来响应用户的操作即可。
1 | // ViewGroup.java |