写在前面
距离前一次写博客,已经过去接近一年,回想起来这一年确实很忙,否则不会连写博客都耽误了。然而忙归忙,也并没有停下学习的脚步,趁着今天有时间,把最近学习并实践于项目当中的知识点给梳理了,这就是今天要聊的话题——Android事件分发。
如题目所示,这篇文章的侧重点在于通过流程图来分析事件分发,为什么要强调通过流程图来分析呢?要理解系统的流程,自然是要回归源码,但是站在工程应用的角度,我们不单止要弄懂事件分发的流程,还要能够应用于项目当中:也就是编码。所以本文的目的不是要通过阅读源码梳理事件分发流程,而是以流程图的形式,直接给出笔者阅读源码后得出的结论,并且讲述事件分发之所以要这样设计的原因(笔者自己理解的原因),以及在项目当中如何根据实际需要编码。
本文脉络如下:
关于触摸事件的基本知识
为什么要有事件分发
View的事件分发
ViewGroup的事件分发
事件分发的应用套路
基本知识
什么是触摸事件
触摸事件,是Android用来描述你的手对屏幕做的事情的最小单元。关键词有两个:手势(你的手对屏幕做的事情)、最小单元。
所谓手势,就是比如按下、移动、抬起、点击事件、长按事件、滑动事件。
所谓最小单元,就是不可再拆分的,比如按下(down)、移动(move)、抬起(up)就是不可再拆分的。而点击事件、长按事件、滑动事件则是由那些不可再拆分的事件组合起来的。比如点击事件是由按下、抬起组成的。长按事件是按下并保持一段时间不动。注意一下滑动事件和移动(move)的区别,滑动事件可以理解为是由若干个“move”组合起来的,系统对触摸的捕捉是很灵敏的,手指放在屏幕上稍微抖一下,都会产生“move”事件。用手指在屏幕上滑一段距离,我们感觉是一次滑动,但是系统已经产生了很多个“move”事件。
我们会遇到的触摸事件,大多就是按下(down)、移动(move)、抬起(up),有时候你可能会遇到取消(cancel)事件,但是事件分发重点关注的大多都是前三者。
MotionEvent对象
一个触摸事件包括事件类型和坐标两类信息:
事件类型(包括但不局限于):ACTION_DOWN(按下)、ACTION_ MOVE(移动)、ACTION_ UP(抬起)。
坐标:坐标还分参考系,有以屏幕作为参考系的坐标值,也有以被触摸的View作为参考系的坐标值。
站在面向对象的角度,MotionEvent对象就是对触摸事件相关信息的封装,这两类信息在MotionEvent对象里面都能找到。motionEvent.getAction()可以获得事件类型,每个类型在MotionEvent里都有对应的常量,motionEvent.getRawX()、motionEvent.getRawY()可以获得以屏幕作为参考系的坐标值,motionEvent.getX()、motionEvent.getY()可以获得以被触摸的View作为参考系的坐标值。
一个完整的事件序列
注意区分一个触摸事件和一个完整的事件序列两个概念。当手指在屏幕上按下、抬起,会产生一系列事件:ACTION_DOWN(一个)–>ACTION_MOVE(数量不定)–>ACTION_UP(一个),从ACTION_DOWN到ACTION_UP,称为一个完整的事件序列,即某一次手指按下到手指抬起算是一个完整的事件序列,下一次手指按下到抬起是另一个完整的事件序列。
笔者没有真的考究过Android官方是不是真的有“事件序列”这么一个概念性的名词,只是这个名词在一些讲解事件分发的博客里面有出现过。但是在ViewGroup的源码里面,确实是可以找到在遇到ACTION_DOWN时清除某些标记位(以避免受到前一个事件序列的影响),在非ACTION_DOWN的情况下,某次触摸事件传递受前一次触摸事件处理结果的影响之类的行为,确实反映了两个相邻的ACTION_DOWN和ACTION_UP之间的事件处理是有关联,下一个ACTION_DOWN会尽量和前一个ACTION_UP相互独立的情况。所以我们是可以认为确实有“事件序列”这么一个逻辑存在。
那么为什么要有事件序列这种处理逻辑呢,在搞懂了事件分发的流程之后,笔者个人的看法是,这是为了让Android在处理触摸事件的时候尽量“拟人化”。怎么就拟人化了呢,触摸事件是站在计算机的角度来看手势的,而事件序列是站在人(严格来讲是用户那种人,不是程序员那种人)的角度来看待手势的。比如我们点击一个按钮,人的反应应该是“我就点了一下”,而不是“我先按下、然后抬起”。比如刷新闻、刷资讯的动作,人的反应应该是“我向上滑了一下”,而不是“我先按下、然后向上移动了若干次(若干个ACTION_ MOVE事件),然后我抬起手”。将两个相邻的ACTION_DOWN和ACTION_UP及其中间所有的事件联系在一起,在需要的时候关联处理,这种结果更加符合人的感受,有更好的用户体验。
为什么要有事件分发
简单地讲,事件分发就是为了解决“谁来干活”的问题。当一个事件发生,有超过一个View(或者ViewGroup)能够对它进行响应(处理)时,就要确定到底谁来处理这个事件。比如下图:
如果触摸事件发生在点A,那么毫无疑问是由ViewGroup来处理的。如果触摸事件发生在点B,那就要确定到底是ViewGourp还是View来处理这个事件。
View事件分发
这里要讲的View的事件分发,是指View.java源码里对触摸事件进行传递的流程。
流程图
声明,以下关于View事件分发的流程图是根据Android源码2.3.7版本绘制的。流程如下图所示:
流程说明
View没有子View,不存在拦截行为,所以View的事件分发比较简单。
从dispatchTouchEvent()开始,进入当前View的事件分发流程,该方法只负责分发事件,没有实际进行事件处理。
有可能处理事件的有两个地方,一个是外部设置的OnTouchListener监听器,即OnTouchListener的onTouch(),一个是View内部的处理方法,即onTouchEvent()。而且外部设置的监听器优先获取事件。
当外部设置的监听器处理了事件(即有设置监听器、View处于ENABLE状态、监听器在onTouch()里面返回true三者均成立),dispatchTouchEvent()返回true,当前View的onTouchEvent()不会触发。
如果外部的监听器不处理事件,则dispatchTouchEvent()的返回值由View自己的onTouchEvent()决定。
注意一下,对于上级ViewGroup(也就是当前View的父容器)而言,它只认识子View的dispatchTouchEvent()方法,不认识另外两个处理事件的方法。子View的OnTouchListener和onTouchEvent()都是在自己的dispatchTouchEvent()里面调用的,他们两个会影响dispatchTouchEvent()的返回值,但是对于上级ViewGroup而言,它只认识dispatchTouchEvent()的返回值就足够了。
ViewGroup事件分发
这里要讲的ViewGroup的事件分发,是指ViewGroup.java源码里对触摸事件进行传递的流程。
流程图
声明,以下关于ViewGroup事件分发的流程图是根据Android源码2.3.7版本绘制的。这个流程图只是描述了事件分发的主要流程,言下之意如果你拿源码过来对照,你会发现有些东西这个流程图里面没有体现。比如对于ACTION_UP和ACTION_CANCEL事件有些清空标志位的逻辑在流程图里是没有的,比如把触摸事件传给子View时对触摸坐标进行转变的逻辑也没体现。笔者认为相对于事件传递本身而言,这些细枝末节的东西画出来既没有加大我们对事件传递的理解,还增加了理解复杂度,所以省略。
流程如下图所示:
ViewGroup事件分发
ViewGroup事件分发的特点(规律)
相对于View的事件分发,ViewGroup的就明显复杂多了。在说明这幅图之前,笔者先给出这幅图里面的特点,有了这些特点,你就不会觉得混乱。就笔者的梳理而言,ViewGroup的事件分发可以总结有这样几个特点:
ViewGroup的事件分发“基于”责任链模式,同时是不纯的责任链模式。之所以说是“基于”是因为事件分发其实和责任链模式不完全相同,应当说事件分发的流程更加复杂一些。
同一个事件序列里面的不同触摸事件之间的影响,通过变量target、disallowIntercept来实现,所以这两个变量的情况会影响事件的传递流程,这也是和一般的责任链模式不同的地方。
ACTION_DOWN事件在传递的同时,执行着“寻找target”的任务。而非ACTION_DOWN事件则在使用target(如果有target),即根据target以及和target相关的东西(disallowIntercept)控制事件传递流程。
事件的传递有“抛回来”的行为,就像是两个相邻的节点之间推卸责任。ViewGroup和它的target(如果有)之间就像在对话:“这锅你背”、“你背你背,不用客气”。
为什么要梳理这些规律?我们从事件分发的流程总结出规律,再通过这些规律倒回去理解事件分发的流程为什么要这样设计。在接下来的流程说明里面,我们通过具体流程来理解这几个特点。对于上图我们采取自上而下,从左向右的顺序来看。每一个触摸事件相当于责任链模式里面的一个请求,每一个ViewGroup或者是子View相当于责任链模式里面的一个处理器(处理者),在流程说明当中笔者会不断地对流程图、责任链模式、上面说的几个特点这三者进行对比,以加深理解。
流程说明:ACTION_DOWN事件
我们先来看ACTION_DOWN事件的流程:
ACTION_DOWN事件流程图
流程说明:
从ViewGroup的dispatchTouchEvent()开始,进入当前ViewGroup的事件分发流程。
首先要做的事情,自然是判断是不是迎来了一个新的事件序列,所以要判断该事件是否是ACTION_DOWN事件。
如果是ACTION_DOWN事件,作为一个事件序列的开头,应当要消除前面的事件序列可能留下的影响,所以接下来做的事情就是清空target。既然没有了target,自然也就不需要考虑disallowIntercept的影响,所以整个ACTION_DOWN事件里面没有去根据target和disallowIntercept的情况进行事件传递的。
接下来的事情就和一般的责任链模式类似,onInterceptTouchEvent()就像在责任链模式里面某个处理器判断是否处理当前请求,如果不处理(即不拦截),就沿着责任链往后传。
事件传给了子View,如果子View处理了,ViewGroup就找到target了,同时由于返回true,当前ViewGroup成了上级ViewGroup的target,找到了target就相当于找到了同个事件序列的后续触摸事件的“背锅侠”。
现在我们回退一步,如果子View不处理,这个“锅”就得ViewGroup自己担着,这就是我们前面说的“推卸责任”的特点。所以事件会传递到super.dispatchTouchEvent()。ViewGroup类继承自View类,也就是进入了前文说的View事件分发流程,就相当于询问当前ViewGroup自己是否处理这个事件,细节这里就不重复了。如果当前ViewGroup自己处理了,对于上级ViewGroup而言,还是找到了target,如果当前ViewGroup不处理,这个“锅”继续抛给上级ViewGroup。
接着继续回退一步,如果拦截了,就和一般的责任链模式类似,所以就不需要经过子View,直接由ViewGroup自己处理该事件。如果处理了,则当前ViewGroup就是上级ViewGroup的target,否则,事件还是抛回给上级ViewGroup。
以上,ACTION_DOWN事件的传递就说完了。简单地讲,ACTION_DOWN事件传递的最终结果,就是为ViewGroup找到target,如果找到了,target就是后续触摸事件的“背锅侠”,如果找不到,当前ViewGroup选择是否成为上级ViewGroup的target。
流程说明:除了ACTION_DOWN以外的事件
接下来看非ACTION_DOWN事件的流程:
非ACTION_DOWN事件流程图
流程说明:
既然ACTION_DOWN事件负责寻找target,那么非ACTION_DOWN事件做的第一件事,就是看看有没有target。这里也体现了同个事件序列里面某个触摸事件受前面的触摸事件的影响。
如果没有“背锅侠”(没有target),那这个锅就得当前ViewGroup自己担着,所以直接传递给了super.dispatchTouchEvent()。从这里你可以看出,如果在ACTION_DOWN里面没有找到target,那么同个事件序列后续的所有事件,都不会再有“背锅侠”,都是当前ViewGroup自己担着(如果ViewGroup能收到这些事件的话)。将target指向某个子View这个行为,只在ACTION_DOWN里面才有可能发生。
对于有target的情况,你从图里可以看到,往下总共分了三条路线:右边的路线是禁止拦截、左边的路线是不禁止拦截然后拦截、中间的路线是不禁止拦截然后不拦截。接下来我们单独对这共计三个情况进行说明,因为从有target开始往下走,才是我们在处理事件冲突的时候最经常遇到的情况。
流程说明:对于有target情况下的事件传递
接下来看在非ACTION_DOWN事件时,对于有target情况下的事件传递流程说明:
首先说明一下disallowIntercept变量,它是一个boolean类型的变量,表示子View是否禁止拦截触摸事件。它的取值,可以通过ViewGroup的requestDisallowInterceptTouchEvent(boolean)来修改,这个方法是public修饰的,如果子View在前一个触摸事件里面通过调用该方法设置了禁止拦截标识,后续的事件就会直接传给子View。注意一下,如果这个标识为禁止拦截,说明子View必须要这个事件,但是如果标识为不禁止拦截,并不是说明子View就不要这个事件了,只能认为子View“还没确定”要它。什么叫做“还没确定”要?我们举个例子来谈这个问题。
如果现在要你自己处理类似ViewPager嵌套ListView(即ViewGroup要横向滑动,里面的子View要竖向滑动)的冲突,基本思路是比较简单的,ViewGroup和子View均捕获触摸事件,如果ViewGroup发现是横向滑动,就拦截并且处理。如果子View发现是竖向滑动,就设置disallowInterceptTouchEvent为禁止拦截,然后自己处理事件。那么紧接着的问题就是,如何发现横/竖向滑动?自然是收集若干个触摸事件,然后根据这几个连续的触摸事件分析手指到底是朝哪个方向滑动。那么在你能够确定用户在朝哪个方向滑动之前,ViewGroup和子View都处于“迷茫”状态:它们两者均不知道手指到底朝哪滑动,也就是它们均不知道自己到底要不要处理这些事件。我们之前讨论事件传递过程的时候总在说如果ViewGroup要处理(或者不要处理),如果子View要处理(或者不要处理),而事实是存在这样一个情况:有段时间它们两者均不知道自己要不要处理这个事件。这个时候怎么办呢?合理的作法应当是让事件继续流经ViewGroup和子View,直到其中一方做出处理。此时,子View不应该禁止ViewGroup拦截事件,同时,ViewGroup也不应该拦截事件。这样,触摸事件就会经过中间这条路线:不禁止拦截然后不拦截。所以这条路线,是用来服务于ViewGroup和子View都还没确定要不要处理事件的情况。
如果子View认为手指是在竖向滑动,它决定要处理事件,它就会将diallowInterceptTouchEvent设置为禁止拦截,事件的传递就会走向右边的路线。自此,同一个事件序列后面的所有触摸事件,都会交给子View处理。
如果ViewGroup认为手指是在横向滑动,它就要拦截并处理后续事件,事件的传递就会走向左边的路线,于是后续事件不再需要传给子View,所以会将target置空。接着要告诉上级ViewGroup,自己需要接手这些事件,于是返回true。接下来的行为和ACTION_DOWN事件在拦截之后的行为有些不同,刚刚被拦截下来的事件并不传给super.dispatchTouchEvent(),后续的事件才会传给 super.dispatchTouchEvent()。这个行为笔者是这样理解的,从ACTION_DOWN开始,到最后一个流经onInterceptTouchEvent()事件之间的所有事件,构成了一个判断条件(类似开关),这个判断条件供onInterceptTouchEvent()用来判断是否拦截后续事件,这些判断条件本身是不需要被处理的,它们只是用来提供判断的,在这些判断条件之后的事件,才是需要处理的。所以是在被拦截的事件之后的事件,才开始流向super.dispatchTouchEvent()。
事件分发的应用套路
这里主要说的是两级View(一个ViewGroup和里面一个子View)的情况下,如何根据实际需要控制事件传递。所谓的套路,其实还是说规律,但是我们前面已经说了事件传递的规律了,所以现在说的是实际操作时的使用规律。为了方便阅读,重新把图再贴一次:
ViewGroup事件分发重复贴图
我们分几大类情况来说,旨在说明在不同的情况下,应该如何选择不同支路,也就是控制事件传递的流程。
ViewGroup需要一切事件:只有你的ViewGroup明确需要一切触摸事件,而且不打算传递给任何子View的时候,会在ACTION_DOWN事件里面拦截事件,这样所有的事件都会传给ViewGroup自己,并且不会下发给任何子View。此时从一开始就要选择的路线是:ACTION_DOWN事件–>onInterceptTouchEvent()拦截–>super.dispatchTouchEvent()处理。后续事件全部传给super.dispatchTouchEvent()。
子View需要一切事件:只有你的子View明确需要一切事件,而且不打算让ViewGroup处理任何事件的时候,要求ViewGroup不要拦截ACTION_DOWN事件,子View必须处理ACTION_DOWN事件,并且设置disallowIntercept标记为禁止拦截。此时从一开始需要走的路线是:ACTION_DOWN事件–>onInterceptTouchEvent()不拦截–>子View处理并且设置disallowIntercept为禁止拦截。后续事件全部传给子View,ViewGroup不会再有机会拦截。
ViewGroup和子View只需要特定情况下的事件(就好比前面我们说的例子,横竖两个方向的事件冲突):此时对于ACTION_DOWN事件,ViewGroup不能拦截,子View必须处理,这样才能保证两者均能收到后续事件。对于ACTION_DOWN以外的事件,在两者都还没能决定是否要处理事件的时候,需要走的路线是:有target–>不禁止拦截–>不拦截–>传给子View的dispatchTouchEvent()。这样能够让ViewGroup和子View均能不断收集事件,以便进行是否处理的判断。一旦子View决定要处理事件,设置disallowIntercept为禁止拦截,避免后续事件被ViewGroup拦截。一旦ViewGroup决定要处理事件,在onInterceptTouchEvent()里面进行拦截,避免后续事件被子View抢走。
至此,我们已经分析完了常见的事件处理需要流经的路线,从实际需求的角度出发反过去看事件传递,你是否能更深入地理解ViewGroup的事件分发为什么要设计成这样呢?
总结
正如本文开头所说,本文的侧重点是通过流程图来分析Android事件分发,除了文章开头介绍一些基本知识,全文基本都在说明流程图。
ViewGroup的事件分发是Android事件分发的重点,也是本文花最大篇幅描述的内容。不单止要说清楚ViewGroup的事件分发流程,还要说清楚这个流程有什么规律、为什么要这么设计、流程上的每个节点都在干什么。最后,站在实践的角度,说明在面对不同需求的时候,该如何选择流程图里的不同路线,实现需求。