学习Android Touch事件分发时序
关于事件分发主要有3个方向可以展开深入分析:
其中与上层开发相关的就是第3条,也是本次总结学习的重点内容,基于Android-28源码阅读学习
首先需要弄清楚2个概念
ViewGroup是一组View的组合,在其内部有可能包含多个子View,当手指触摸屏幕上时手指所在的区域既能在ViewGroup显示范围内,也可能在其内部View控件上
因此它内部的事件分发的**重心是处理当前Group和子View之间的逻辑关系 ** :
View是一个单纯的控件, 不能再被细分 ,内部也并不会存在子View,所以它的事件分发的重点在于当前View如何去处理touch事件 ,并根据相应的手势逻辑进行一系列的效果展示(比如滑动,放大,点击,长按)
整个View之间的事件分发, 实质上就是一个大的递归函数 ,而这个递归函数就是dispatchTouchEvent方法。在这个递归的过程中会适时调用onInterceptTouchEvent来拦截事件,或调用onTouchEvent方法来处理事件
先从宏观角度,纵览整个dispatch的源码:
如注释所言,dispatch主要分为3大步骤:
接下来具体看下各个步骤细节
红框标出了是否需要拦截的条件:
如果在1中,当前ViewGroup并没有对事件进行拦截,则执行步骤2
步骤3有2个分支判断:
定义如下布局文件
DownInterceptedGroup和CaptureTouchView是两个自定义View,源码如下:
手指触摸CaptureTouchView并滑动一段距离后抬起,最终打印log如下:
上图中在Down事件中,DownInterceptedGroup的onInterceptTouchEvent被触发一次;然后在子View CaptureTouchView的dispatchTouchEvent中返回true,代表它捕获消费了这个Down事件。这种情况下CaptureTouchView会被添加到父视图(DownInterceptedGroup)中的mFirstTouchTarget中。因此后续的Move和Up事件都会经过DownInterceptedGroup的onInterceptTouchEvent进行拦截判断。
所有touch事件都是从DOWN事件开始的,这是DOWN事件比较特殊的原因之一。另一个原因是DOWN事件的处理结果会直接影响后续MOVE,UP事件的逻辑
在步骤2(将事件分发给子View)中,只有DOWN事件会传递给子View进行捕获判断,一旦子View捕获成功,后续的MOVE和UP事件是通过遍历mFirstTouchTarget链表,查找之前接受ACTION_DOWN的子View,并将触摸事件分配给这些子View。
也就是说后续的MOVE,UP等事件的分发交给谁,取决于他们的起始事件Down是由谁捕获的。
mFirstTouchTarget部分源码:
可以看出其实mFirstTouchTarget是一个TouchTarget类型的链表结构。而这个TouchTarget的作用就是用来记录捕获了DOWN事件的View,具体保存在上图中的child变量。
为什么是链表结构呢?因为Android设备是支持多指操作的,每一个手指的Down事件都可以当作一个TouchTarget保存起来。在步骤3(根据mFirstTouchTarget再次分发事件)中判断如果mFirstTouchTarget不为null,则再次将事件分发给相应的TouchTarget
在上面的步骤3(根据mFirstTouchTarget再次分发事件)中,继续向子View分发事件的代码中,有一段比较有趣的逻辑:
上图红框表示已经有子View捕获了touch事件,但是蓝色框中的intercepted boolean变量又是true。这种情况下,事件主导权会重新回到父视图ViewGroup中,并传递给子View的分发事件中传入一个cancelChild == true。
看一下dispatchTransformedTouchEvent方法的部分源码:
因为之前传入参数cancel为true,并且child不为Null, 最终这个事件会被包装为一个ACTION_CANCEL事件传给child
什么情况下会触发这段逻辑呢?
当父视图的onInterceptTouchEvent 先返回false,然后在子View的dispatchTouchEvent中返回true(表示子View捕获事件),关键步骤就是在接下来的MOVE的过程中,父视图的onInterceptTouchEvent又返回true,intercepted被重新置为true,此时上述逻辑就会被触发,子控件就会收到ACTION_CANCEL的touch事件
有个经典的例子可以用来演示这个情况:
当在Scrollview中添加自定义View时,Scrollview默认在DOWN事件中并不会进行拦截,事件会被传递给Scrollview内的子控件。只有当手指进行滑动并达到一定的距离之后,onInterceptTouchEvent方法返回true,并触发Scrollview的滚动效果。当Scrollview进行滚动的瞬间,内部的子View会接收到一个CANCEL事件,并丢失touch焦点。
比如以下代码:
CaptureTouchView是一个自定义的View,源码如下:
CaptureTouchView的onTouchEvent返回true,表示它会将接收到的touch事件进行捕获消费
上述代码执行后,当手指点击屏幕时DOWN事件会被传递给CaptureTouchView,手指滚动屏幕将Scrollview上下滑动,刚开始MOVE事件还是由CaptureTouchView来消费处理,但是当Scrollview开始滚动时,CaptureTouchView会接收一个CANCEL事件,并不再接受后续的touch事件
因此,平时自定义View时,尤其是有可能被Scrollview或ViewPager嵌套使用的控件,不要遗漏对CANCEL事件的处理,否则可能引起UI显示异常!!!
主要分析了dispatchTouchEvent的事件的流程机制,这一过程分3部分:
介绍下整个事件分发中的几个特殊点: