我无法忘却 3 年前备受折磨的那个夜晚 —— 在我第一次学习 View 事件分发,却被网文折磨的那个夜晚。
是网上介绍 View 事件分发的文章不够多吗?
不是的,恰恰相反,网上的爆款文章不计其数,待你仔细阅读,却 颇有一种“外地人上了黑车”的感觉 —— 一言不合先上 30 张图表,带你在城市外围饶个上百圈,就是不直奔主题 解释一个现象为什么会存在、造成它存在的缘由为何、它如此设计是为了解决什么问题 ……
比起 拨开迷雾、明确状况、建立感性认识,他们更热衷于自我包装。 —— 有没有帮助我不管,先唬住人再说。 为了唬人,就算给他人徒添困扰、白费大量时间,也在所不惜!
正是对那次痛苦经历的念念不忘,于是我 破例 将这篇文章分享给大家。
在此,我向 3 年前的那个自己发誓,我必在 结尾 200 字 就讲明白,别人非要绕个 3000、5000 字都讲不明白的事件分发。
不仅如此,我还要额外地帮助大家理解,事件分发流程中的 3 个小细节:之所以如此设计,是出于什么考虑。通过“知其所以然”,来方便大家更好地加深印象。
?
此外,已经订阅专栏的小伙伴请不要担心,本文仅仅是介绍 View 事件分发机制的基础。至于滑动冲突等现实问题的解决,好戏还在后头 ~
还没阅读的小伙伴也请不要着急,正因为今天讲的是基础,光是看了这一篇,你也没白来!
什么是递归呢?递归的本质是什么呢?
顾名思义,递归是一种包含 “递” 流程和 “归” 流程的算法。当我们在找寻目标时,便是处于 “递” 流程,当我们找到目标,打算从目标开始来执行事务时,我们便开启了 “归” 流程。
如果这么说有点抽象的话,不妨结合现实中的实例来理解下递归:
案例:职场任务的下发和上报,就是典型的递归
领导 自上而下、逐级地下达任务、寻找目标执行者,这就是 “递” 流程。
直到找到合适的执行者时,便开启了 自下而上 的 “归”流程。若当前执行者无法让结果 OK,那么上报给他的上级,由他的上级来执行,如果上级也不 OK,那么继续向上,直到结果 OK 为止。
伪代码来表示,即:
boolean dispatch(){
if (hasTargetChild) {
return child.dispatch();
} else {
return executeByMySelf();
}
}
如此设计,是为了与 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 事件分发的递和归流程。
如图所示:???
事先分发包含 3 个重要方法:
dispatchTouchEvent,onInterceptTouchEvent,onTouchEvent。
因而首先,在递的过程中,当前层级是执行 child.dispatchTouchEvent:
此外,在 onTouchEvent 中如果 clickable 并且实现了 onClickListener 或 onLongClickListener,那么会执行 onClick 或 onLongClick。
总之,走到没有 child 的层级,即意味着步入“归”流程,如果该层级的 super.dispatchTouchEvent 没有返回 true,那么将继续执行上一级的 super.dispatchTouchEvent,直到被某一级消费,也即返回 true 了为止。
上面我们介绍了正常流程下,所会执行到的方法,包括 View 实现的 dispatchTouchEvent,ViewGroup 重写的 dispatchTouchEvent,以及 onTouchEvent。
如图。???
其实在事件的 “递” 流程中,ViewGroup 可以在当前层级,通过设置 onInterceptTouchEvent 方法返回 true,来拦截事件的下发,而直接步入“归”流程。
正所谓 “上有正策、下有对策”。在 ViewGroup 可以拦截事件下发的同时,child 也可以通过 getParent.requestDisallowInterceptTouchEvent 方法,来阻止上一级的下发拦截。
要将 “消费” 和 “执行” 这两个概念明确区分开。
网上的内容总让人误以为,当前层级不消费,就是不执行 super.dispatchTouchEvent 了。
事实上,不消费,简单地理解就是,“事情做了、只是结果不 OK” —— 在归流程中,如果当前层级的 super.dispatchTouchEvent return true 了,那么再往上的层级都不再执行自己的 super.dispatchTouchEvent,而是直接 return true。并且,当前层级的下级,都执行过 super.dispatchTouchEvent,只是结果返回了 false 而已。
网上的内容总是让人误以为,当前层级拦截了,就直接在当前层级消费了。
实际上,当前层级拦截了,只是提前结束了 “递” 流程,并从当前层级步入 “归” 流程而已。具体判定是在哪个层级被消费,还是根据 <细节1> 的指标:看在哪个层级的 super.dispatchTouchEvent return true。
网上的内容总是让人误以为,本次 ACTION_DOWN 被拦截了,那么往后的 ACTION_MOVE 和 ACTION_UP 都不被拦截了。
实际上,是 onInterceptTouchEvent 方法只走一次,一旦走过,就会留下记号(mFirstTouchTarget == null)那么下一次直接根据这个记号来判断拦不拦截。
为什么这么设计呢?因为一连串的事件序列,要求在几百微秒内完成。如果每次都完整走一遍方法,那岂不耽误事?所以本着 “能省即省” 的原则,凡是已确认会拦截的,后续就不再走方法判断,而是直接走变量标记来判断。
到此已经讲完 3 个细节了,要不要再讲 2 个呢?
讲?不讲?讲?不讲? 好嘛,再讲 2 个 ~
这个很好理解,和 <细节3> 同理。
连事件序列的第一个事件都不接了(父容器走后续事件的分发时发现 mFirstTouchTarget == null),那就意味着不接了呗 —— 那后续的活就不会交给你了(不会再走你的 super.dispatchTouchEvent 来试探),直接根据变量标记(mFirstTouchTarget == null)做出判断,“能省即省”。
也即在 child 的 onTouch、onTouchEvent 中调用 getParent.requestDisallowInterceptTouchEvent 时,被设计为对父容器的 ACTION_DOWN 无效 —— 在父容器 dispatchTouchEvent 时,会首先重置 mGroupFlags。( ViewGroup 正是根据 mGroupFlags 是否包含 FLAG_DISALLOW_INTERCEPT 来判断是否不拦截的)
为什么这么设计呢? 这个问题读者可以想一想,欢迎在评论区留言 ~
这样说,你理解了吗?
如果你觉得文章写得不错就给个赞呗?如果你觉得那里值得改进的,请给我留言。一定会认真查询,修正不足。谢谢。
希望读到这的您能转发分享和关注一下我,以后还会更新技术干货,谢谢您的支持!
转发+点赞+关注,第一时间获取最新知识点
Android架构师之路很漫长,一起共勉吧!
最后祝大家生活愉快~