通过流程图来分析Android事件分发

发表于:2019-3-15 11:11

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:Android开发架构    来源:简书

  前言
  今天有时间,把最近学习并实践于项目当中的知识点给梳理了,这就是今天要聊的话题——Android 事件分发
  如题目所示,这篇文章的侧重点在于通过流程图来分析事件分发,为什么要强调通过流程图来分析呢?要理解系统的流程,自然是要回归源码,但是站在工程应用的角度,我们不单止要弄懂事件分发的流程,还要能够应用于项目当中:也就是编码。所以本文的目的不是要通过阅读源码梳理事件分发流程,而是以流程图的形式,直接给出笔者阅读源码后得出的结论,并且讲述事件分发之所以要这样设计的原因(笔者自己理解的原因),以及在项目当中如何根据实际需要编码。
  本文脉络如下:
  1.关于触摸事件的基本知识
  2.为什么要有事件分发
  3.View 的事件分发
  4.ViewGroup 的事件分发
  5.事件分发的应用套路
  基本知识
  什么是触摸事件
  触摸事件,是 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 事件分发的特点(规律)
  相对于View的事件分发,ViewGroup的就明显复杂多了。在说明这幅图之前,笔者先给出这幅图里面的特点,有了这些特点,你就不会觉得混乱。就笔者的梳理而言,ViewGroup的事件分发可以总结有这样几个特点:
  ViewGroup 的事件分发“基于”责任链模式,同时是不纯的责任链模式。之所以说是“基于”是因为事件分发其实和责任链模式不完全相同,应当说事件分发的流程更加复杂一些。
  同一个事件序列里面的不同触摸事件之间的影响,通过变量target、disallowIntercept 来实现,所以这两个变量的情况会影响事件的传递流程,这也是和一般的责任链模式不同的地方。
  ACTION_DOWN事件在传递的同时,执行着“寻找target”的任务。而非ACTION_DOWN 事件则在使用target(如果有 target),即根据 target 以及和target 相关的东西(disallowIntercept)控制事件传递流程。
  事件的传递有“抛回来”的行为,就像是两个相邻的节点之间推卸责任。ViewGroup和它的 target(如果有)之间就像在对话:“这锅你背”、“你背你背,不用客气”。
  为什么要梳理这些规律?我们从事件分发的流程总结出规律,再通过这些规律倒回去理解事件分发的流程为什么要这样设计。在接下来的流程说明里面,我们通过具体流程来理解这几个特点。对于上图我们采取自上而下,从左向右的顺序来看。每一个触摸事件相当于责任链模式里面的一个请求,每一个 ViewGroup 或者是子 View 相当于责任链模式里面的一个处理器(处理者),在流程说明当中笔者会不断地对流程图、责任链模式、上面说的几个特点这三者进行对比,以加深理解。
  流程说明: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 事件负责寻找 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 明确需要一切触摸事件,而且不打算传递给任何子 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 的事件分发流程,还要说清楚这个流程有什么规律、为什么要这么设计、流程上的每个节点都在干什么。最后,站在实践的角度,说明在面对不同需求的时候,该如何选择流程图里的不同路线,实现需求。

      上文内容不用于商业目的,如涉及知识产权问题,请权利人联系博为峰小编(021-64471599-8017),我们将立即处理。
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号