Android 的 View 的事件分发一直是老生常谈的问题,市面上的所有文章资料都在通过 dispatchTouchEvent、onInterceptTouchEvent 和 onTouchEvent 讲事件分发的原理,对于一些细节没有涉及,本文带你一窥源码,搞清楚 View 是如何选择事件处理的。
MotionEvent
MotionEvent 定义了用于报告移动(鼠标、笔、手指、轨迹球)事件的对象。运动事件可以保存绝对或相对运动和其他数据,这取决于设备的类型。
用户触摸一次屏幕将会产生 MotionEvent 事件,并将事件传递给合适的 View 。MotionEvent 会包含一些触摸行为的相关信息,例如事件的类型、触摸的X和Y坐标、接触区域的大小和方向的信息等等。有些设备可以同时报告多条移动轨迹。多点触控屏幕为每个手指发出一个运动轨迹。单个的手指或其他产生运动轨迹的对象称为指针。
通常情况下,用户的触摸事件类型包括:按下(ACTION_DOWN)、移动(ACTION_MOVE)、抬起(ACTION_UP)、取消(ACTION_CANCEL)。
用户的一次触摸通常以 ACTION_DOWN 事件开始,在屏幕上移动时产生多个 ACTION_MOVE 事件,最后抬起手指触发 ACTION_UP 或 ACTION_CANCEL 事件。
View 和 ViewGroup
在讲解事件分发机制之前,要区分 View 和 ViewGroup ,尽管 ViewGroup 继承自 View ,但是它们代表了不容的概念。 Android 的视图结构是树结构,View 代表的是叶子节点,而 ViewGroup 则表示可以拥有子节点的非叶子节点。它们的事件分发逻辑是有区别的。
简述事件分发
下文的分析可能比较复杂,这里总结一下事件分发涉及核心方法的流程:
View 的事件分发流程
当触摸事件来临时,会触发 View 的 dispatchTouchEvent(event)
将事件进行分发。如果 View 设置了 onTouchListener ,会优先回调 onTouch(view, event)
,onTouch 方法的返回值表示是否处理事件。
如果 onTouch 不处理事件,会进入 View 自己的 onTouchEvent ,在没有覆盖的情况下,该方法内部会识别该事件是点击、长按或其他类型,点击会调用 performClick()
,perfromClick 方法会调用 onClickListener 的 onClick(View)
。
onTouchEvent 方法用户可以自己来实现,从而实现 View 的拖拽等能力。
如果 onTouchEvent 方法也返回 false ,说明当前 View 不消耗这个事件,dispatchTouchEvent 返回 false 。
ViewGroup 的事件分发流程
当触摸事件来临时,会触发 ViewGroup 的 dispatchTouchEvent 将事件进行分发。
ViewGroup 会优先处理事件的分发逻辑,经过窗口的检查后,首先会通过通过 Flag 检查是否需要拦截,并调用 onInterceptTouchEvent(event)
确定真正是否进行拦截的结果。然后进行取消的 Flag 检查。
在不拦截不取消的情况下,通过 View 的渲染顺序和 Z 轴顺序会生成一个有序的 View 列表,根据列表顺序获取符合事件坐标位置的 View ,然后通过 dispatchTransformedTouchEvent(view, ...)
来分发给这个 View ,dispatchTransformedTouchEvent 方法内部调用 view.dispatchTouchEvent(event)
,如果 view 为空的情况,直接调用 super.dispatchTouchEvent(event)
,也就是调用 ViewGroup 的父类 View 的 dispatchTouchEvent 方法。
如果子 View 不消耗事件(dispatchTouchEvent 方法返回了 false),ViewGroup 会调用 dispatchTransformedTouchEvent(view = null, ...)
的形式,调用到自己的 super.dispatchTouchEvent(event)
,也就是将自己视为最后 View 来检查是否消耗。
View dispatchTouchEvent
View 的 dispatchTouchEvent 方法会将触摸屏运动事件传递到目标视图,如果是目标,则传递给这个视图。并返回当前 View