重学安卓:学习 View 事件分发,就像外地人上了黑车!

前言

很高兴见到你!java

今天我要和你们分享的是 View 事件分发。算法

本来这是 《重学安卓》专栏原定的第 8 篇(鉴于读者的强烈要求,暂时插播了 Jetpack 系列),可没想到的是,我没法忘却 3 年前备受折磨的那个夜晚 —— 在我第一次学习 View 事件分发,却被网文折磨的那个夜晚。学习

是网上介绍 View 事件分发的文章不够多吗?spa

不是的,偏偏相反,网上的爆款文章不可胜数,待你仔细阅读,却 很有一种“外地人上了黑车”的感受 —— 一言不合先上 30 张图表,带你在城市外围饶个上百圈,就是不直奔主题 解释一个现象为何会存在、形成它存在的原因为什么、它如此设计是为了解决什么问题 ……设计

比起 拨开迷雾、明确情况、创建感性认识,他们更热衷于自我包装。3d

—— 有没有帮助我无论,先唬住人再说。code

为了唬人,就算给他人徒添困扰、白费大量时间,也在所不惜!cdn

正是对那次痛苦经历的念念不忘,因而我 破例 将这篇文章分享给你们。xml

在此,我向 3 年前的那个本身发誓,我必在 结尾 200 字 就讲明白,别人非要绕个 3000、5000 字都讲不明白的事件分发。对象

不只如此,我还要额外地帮助你们理解,事件分发流程中的 3 个小细节:之因此如此设计,是出于什么考虑。经过“知其因此然”,来方便你们更好地加深印象。

😉

此外,已经订阅专栏的小伙伴请不要担忧,本文仅仅是介绍 View 事件分发机制的基础。至于滑动冲突等现实问题的解决,好戏还在后头 ~

还没阅读的小伙伴也请不要着急,正由于今天讲的是基础,光是看了这一篇,你也没白来

View 事件分发的本质是递归

什么是递归呢?递归的本质是什么呢?

顾名思义,递归是一种包含 “递” 流程和 “归” 流程的算法。当咱们在找寻目标时,即是处于 “递” 流程,当咱们找到目标,打算从目标开始来执行事务时,咱们便开启了 “归” 流程。

若是这么说有点抽象的话,不妨结合现实中的实例来理解下递归:

案例:职场任务的下发和上报,就是典型的递归

领导 自上而下、逐级地下达任务、寻找目标执行者,这就是 “递” 流程。

直到找到合适的执行者时,便开启了 自下而上 的 “归”流程。若当前执行者没法让结果 OK,那么上报给他的上级,由他的上级来执行,若是上级也不 OK,那么继续向上,直到结果 OK 为止。

伪代码来表示,即:

boolean dispatch() {
    if (hasTargetChild && child.dispatch()) {
        return true;
    } else {
        return executeByMySelf();
    }
}
复制代码

View 事件分发为什么要设计成递归呢?

如此设计,是为了与 View 的排版相呼应。

View 的排版规则是:嵌套越深的,显示层级越高。而显示层级越高,就越容易覆盖层级低的、被用户看见。

再加上,“所见即所得”,要求 “用户看到了什么,触控到的也该是什么”(简言之,操做要符合用户直觉)。

所以,正是考虑到嵌套越深,层级越高,触摸也一般会是交给层级高的来处理,于是也将事件分发设计成递归。

View 排版规则为什么设计为“嵌套越深,显示层级越高”呢?

由于这符合常理。越外层的,做为父容器而充当背景,越里层的,做为子控件而至于前景。

<LinearLayout>
	<ScrollView>
		<TextView/>
	</ScrollView>
</LinearLayout>
复制代码

因此,整个流程大体是怎样的呢?

首先咱们要明确的 3 点是:

1.每次完整的事件分发流程,都包含自上而下的 “递”,和自下而上的 “归” 2 个流程。

2.每次完整的事件分发流程,都是针对一个事件(MotionEvent)完成的递归,而一个事件只对应着一个 ACTION,例如 ACTION_DOWN。

3.一次用户触摸操做,咱们称之为一个事件序列。一个事件序列会包含 ACTION_DOWN、ACTION_MOVE ... ACTION_MOVE、ACTION_UP 等多个事件。(其中 ACTION_MOVE 的数量是从 0 到多个不等)

也即一个事件序列,包含从 ACTION_DOWN 到 ACTION_UP 的屡次事件分发流程。

下面我用一张图归纳 View 事件分发的递和归流程。

事件分发流程.png

如图所示:👆👆👆

事先分发包含 3 个重要方法:

dispatchTouchEventonInterceptTouchEventonTouchEvent

经过前面的 《重学安卓:Activity 的快乐你不懂!》 咱们知道,View 和 ViewGroup 是组合模式的关系,于是 ViewGroup 为了分发的须要,会重写一些 View 的方法,就包括这里的 dispatchTouchEvent。

于是首先,在递的过程当中,当前层级是执行 child.dispatchTouchEvent:

  • 若是 child 是 ViewGroup,那么实际执行的就是 ViewGroup 重写的 dispatchTouchEvent 方法。该方法内能够判断,是否在当前层级拦截当前事件、或是递给下一级。
  • 若是 child 是再也不有 child 的 View 或 ViewGroup,那么实际执行的就是 View 类实现的 super.dispatchTouchEvent 方法。该方法内能够判断,若是 View enabled 而且实现了 onTouchListener,且 onTouch 返回 true,那么不执行 onTouchEvent,并直接返回结果。不然执行 onTouchEvent。

此外,在 onTouchEvent 中若是 clickable 而且实现了 onClickListener 或 onLongClickListener,那么会执行 onClick 或 onLongClick。

总之,走到没有 child 的层级,即意味着步入“归”流程,若是该层级的 super.dispatchTouchEvent 没有返回 true,那么将继续执行上一级的 super.dispatchTouchEvent,直到被某一级消费,也即返回 true 了为止。

事件分发流程.png

上面咱们介绍了正常流程下,所会执行到的方法,包括 View 实现的 dispatchTouchEvent,ViewGroup 重写的 dispatchTouchEvent,以及 onTouchEvent。

如图。👆👆👆

其实在事件的 “递” 流程中,ViewGroup 能够在当前层级,经过设置 onInterceptTouchEvent 方法返回 true,来拦截事件的下发,而直接步入“归”流程。

正所谓 “上有正策、下有对策”。在 ViewGroup 能够拦截事件下发的同时,child 也能够经过 getParent.requestDisallowInterceptTouchEvent 方法,来阻止上一级的下发拦截。

(具体会在下一篇《滑动冲突处理》中介绍)

额外须要明确的 3 个小细节

细节1:明确消费的概念

要将 “消费” 和 “执行” 这两个概念明确区分开。

网上的内容总让人误觉得,当前层级不消费,就是不执行 super.dispatchTouchEvent 了。

事实上,不消费,简单地理解就是,“事情作了、只是结果不 OK” —— 在归流程中,若是当前层级的 super.dispatchTouchEvent return true 了,那么再往上的层级都再也不执行本身的 super.dispatchTouchEvent,而是直接 return true。而且,当前层级的下级,都执行过 super.dispatchTouchEvent,只是结果返回了 false 而已。

细节2:明确拦截的做用

网上的内容老是让人误觉得,当前层级拦截了,就直接在当前层级消费了。

实际上,当前层级拦截了,只是提早结束了 “递” 流程,并从当前层级步入 “归” 流程而已。具体断定是在哪一个层级被消费,仍是根据 <细节1> 的指标:看在哪一个层级的 super.dispatchTouchEvent return true。

细节3:拦截方法只走一次,不表明拦截只走一次

网上的内容老是让人误觉得,本次 ACTION_DOWN 被拦截了,那么日后的 ACTION_MOVE 和 ACTION_UP 都不被拦截了。

实际上,是 onInterceptTouchEvent 方法只走一次,一旦走过,就会留下记号(mFirstTouchTarget == null)那么下一次直接根据这个记号来判断拦不拦截。

为何这么设计呢?由于一连串的事件序列,要求在几百微秒内完成。若是每次都完整走一遍方法,那岂不耽误事?因此本着 “能省即省” 的原则,凡是已确认会拦截的,后续就再也不走方法判断,而是直接走变量标记来判断。

到此已经讲完 3 个细节了,要不要再讲 2 个呢?

讲?不讲?讲?不讲?

好嘛,再讲 2 个 ~

细节4:ACTION_DOWN 不执行,那么没下次了

这个很好理解,和 <细节3> 同理。

连事件序列的第一个事件都不接了(父容器走后续事件的分发时发现 mFirstTouchTarget == null),那就意味着不接了呗 —— 那后续的活就不会交给你了(不会再走你的 super.dispatchTouchEvent 来试探),直接根据变量标记(mFirstTouchTarget == null)作出判断,“能省即省”。

细节5:内部拦截并不能阻止父容器对 ACTION_DOWN 的处理

也即在 child 的 onTouch、onTouchEvent 中调用 getParent.requestDisallowInterceptTouchEvent 时,被设计为对父容器的 ACTION_DOWN 无效 —— 在父容器 dispatchTouchEvent 时,会首先重置 mGroupFlags。( ViewGroup 正是根据 mGroupFlags 是否包含 FLAG_DISALLOW_INTERCEPT 来判断是否不拦截的)

为何这么设计呢?

这个问题读者能够想想,欢迎在评论区留言 ~

咱们会在下一篇,滑动冲突实战中介绍。

综上

  • View 事件分发的本质是递归。
  • 递归的本质是,任务的下发和结果的上报。
  • View 事件分发设计成递归,是为了配合 View 的排版规则,造成符合用户直觉的触控体验。
  • View 事件分发的对象是一个 MotionEvent。
  • 一次用户触控操做包含多个 MotionEvent(例如从 ACTION_DOWN 到 ACTION_UP ),也即会走屡次事件分发流程。
  • 一次 View 事件分发流程包含 “递” 流程和 “归” 流程,“递” 流程能够因 ViewGroup 的拦截而提早步入 “归” 流程。
  • child 能够经过 getParent.requestDisallowInterceptTouchEvent 阻止父容器的拦截。于是须要差别化地配置阈值,来确保 child 执行 getParent.requestDisallowInterceptTouchEvent 优先于父容器 onInterceptTouchEvent 返回 true(否则都先被拦截了,child 哪有机会阻止?)
  • 在“归”流程中,惟有当前层级的 super.dispatchTouchEvent 返回了 true,才认定被消费,被消费前,下级都有干活,只是结果不 OK。被消费后,上级都不须要干活,直接向上传达消费者的功。

这样说,你理解了吗?

xzl短

看不过瘾?这里只为你 而准备了一份 简洁有力的 《重学安卓》认知地图 😉

相关文章
相关标签/搜索