**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:
- 如果 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 了为止。
上面我们介绍了正常流程下,所会执行到的方法,包括 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。被消费后,上级都不需要干活,直接向上传达消费者的功。
这样说,你理解了吗?
最后
如果你觉得文章写得不错就给个赞呗?如果你觉得那里值得改进的,请给我留言。一定会认真查询,修正不足。谢谢。
希望读到这的您能转发分享和关注一下我,以后还会更新技术干货,谢谢您的支持!
转发+点赞+关注,第一时间获取最新知识点
Android架构师之路很漫长,一起共勉吧!
以下墙裂推荐阅读!!!
- Android学习笔记参考(敲黑板!!)
- “寒冬未过”,阿里P9架构分享Android必备技术点,让你offer拿到手软!
- 毕业3年,我是如何从年薪10W的拖拽工程师成为30W资深Android开发者!
- 腾讯T3大牛带你了解 2019 Android开发趋势及必备技术点!
- 八年Android开发,从码农到架构师分享我的技术成长之路,共勉!
最后祝大家生活愉快~
技术点!]( )
- 八年Android开发,从码农到架构师分享我的技术成长之路,共勉!
最后祝大家生活愉快~
[外链图片转存中…(img-DkPKUZin-1630941615066)]