一、概述
在 Android UI 开发中,经常涉及与 touch(触摸)事件和手势,最经常使用的点击事件(OnClickListener)也与 touch 事件相关。因此,理解 touch 事件在 View 层级中的传递机制尤为重要。然而,onInterceptTouchEvent、onTouchEvent、onTouchListener 等一系列接口方法很容易让人混淆。
本文将介绍 touch 事件的一些基础知识,并通过分析 Android FrameWork 源码来深入理解 touch 事件的分发机制。
注:
- 本文的源码分析基于 Android API Level 21,并省略掉部分与本文关系不大的代码。
- 在代码中加入了个人对源码的理解,以注释形式呈现。
二、基础知识
首先介绍几个相关的类和方法:
MotionEvent 类:
该类封装了一个 Touch 事件的相关参数,我们通常所说的一个 Touch 事件,就是指一个MotionEvent类的实例。一个MotionEvent可以分为多种类型,即ACTION_DOWN(按下)、ACTION_MOVE(移动)、ACTION_UP(抬起)和ACTION_CANCEL(取消)等。ACTION_DOWN:
按照常规的操作顺序,通常的 Touch 事件触发的流程都是 DOWN → UP,或者 DOWN → MOVE → UP。所以ACTION_DOWN事件通常都是一系列连续操作事件的起点,也因此它通常在处理程序中被作为一个特殊的标识。ACTION_MOVE:
当手指按下后在屏幕上移动,就会产生ACTION_MOVE事件,并且通常会随着手指移动而连续产生很多个。在移动过程中,可以根据MotionEvent类的坐标信息,得到手指在屏幕上移动的位置。ACTION_UP:
UP 是一系列手势操作的结束点,程序会在收到ACTION_UP事件时做一些收尾性的工作,例如恢复 View 的点击状态,值得一提的是,View 的 click 事件就是在ACTION_UP时加以判断满足其他条件之后被触发的。ACTION_CANCEL:
CANCEL 事件不是由用户触发的,而是系统经过逻辑判断后对某个 View 发送“取消”消息时产生的。收到 CANCEL 事件时,View 应该负责将自己的状态恢复。事件分发方法
public boolean dispatchTouchEvent(MotionEvent ev):
事件由上一层的 View 传递到下一层 View 的过程称为事件分发。dispatchTouchEvent方法负责事件分发。Activity、ViewGroup、View类中都定义了该方法,所以它们都具有事件分发的能力。Activity.dispatchTouchEvent实际上是调用了DecorView的dispatchTouchEvent方法,而DecorView实际上是一个 FrameLayout,因此 Activity 的dispatchTouchEvent最终也是调用到了 ViewGroup 的dispatchTouchEvent方法。
另外,由于 View 没有管理子 View 的能力,所以View.dispatchTouchEvent方法实际上不是用来向下分发事件,而是将事件分发给自己,调用了自己的事件响应方法去响应事件。事件响应方法
public boolean onTouchEvent(MotionEvent event):
该方法负责响应事件,并且返回一个 boolean 型,表示是否消费掉事件,返回 true 表示消费,false 表示不消费。Activity、View、ViewGroup 都有这个方法,所以它们都具有事件响应的能力,并且通过返回值来表示事件是否已经消费。事件拦截方法
public boolean onInterceptTouchEvent(MotionEvent ev):
事件在 ViewGroup 的分发过程中,ViewGroup 可以决定是否拦截事件而不对子 View 分发。该方法的返回值决定是否需要拦截的,返回 true 表示拦截,false 表示不拦截。该方法只定义在 ViewGroup 类中,所以只有 ViewGroup 有权拦截事件不对子View 分发。
小结:上述几个方法和类的关系如下:
三、View 中 Touch 事件的分发逻辑
先来看 View.dispatchTouchEvent 的源码:
|
|
可以看出,View 的事件分发过程主要涉及两个方法:mOnTouchListener.onTouch 和 onTouchEvent,并且当 mOnTouchListener 存在时,mOnTouchListener.onTouch 调用的优先级比较高。
什么时候 mOnTouchListener 会存在?通过 View 的源码可看到 mOnTouchListener 是在 View 的 setOnTouchListener(OnTouchListener l) 方法中被设置的。所以,当我们通过 setOnTouchListener(OnTouchListener l) 方法设置了 onClickListener,并在 onClickListener.onTouch 方法中返回 true 消费了事件之后,onTouchEvent 将不会再被调用。
可见,mOnTouchListener.onTouch 是由外部 set 到 View 里去的,而 onTouchEvent 只能通过 Override 去重写自己的逻辑,且 View 的 onTouchEvent 方法自身已经有不少逻辑。所以 mOnTouchListener.onTouch 适用于添加不太复杂的 touch 逻辑,并且可以不妨碍 onTouchEvent 的正常调用;而 onTouchEvent 更适用于用 Override 的形式来改变 View 本身 touch 逻辑。
四、ViewGroup 中 Touch 事件的分发逻辑
虽然 ViewGroup 是 View 的子类,但是因为 ViewGroup 涉及对子 View 的处理,所以其事件分发逻辑比 View 的分发逻辑会复杂许多。ViewGroup 中重载了 dispatchTouchEvent 方法,逻辑也完全与之前不一样。
看 ViewGroup.dispatchTouchEvent 的源码:
|
|
ViewGroup 的 dispatchTouchEvent 逻辑显然比 View 的逻辑复杂得多,主要分为以下 4 步:
- Step1. 如果是 DOWN 事件,则清理之前的变量和状态
- Step2. 检查拦截的情况
- Step3. 分发 DOWN 事件或其他初始事件(例如多点触摸的 DOWN 事件)
- Step4. 接下来将事件分发到 touchTarget 中或分发到自己身上。
我们从以下几点来总结一下 ViewGroup 的事件分发逻辑:
ViewGroup 在什么情况下可以拦截事件?
我们知道,拦截是由onInterceptTouchEvent方法的返回值决定的。假设该 ViewGroup 没有被设置为不允许拦截(即正常情况下),那么对于 DOWN 事件,onInterceptTouchEvent方法肯定会被调用。另外,如果是 MOVE、UP 或其他事件类型,只要满足mFirstTouchTarget != null时也会调用onInterceptTouchEvent。mFirstTouchTarget变量会在什么时候被赋值?它的作用是什么?mFirstTouchTarget是用来记录在 DOWN 事件中消费了事件的子 View,它以链表的形式存在,通过 next 变量串起来。在 DOWN 事件中,如果通过点击的坐标找到了某个子 View,且该子 View 消费了事件,那么链表中就将这个子 View 记录了下来。这样在后续的 MOVE、UP 事件中,能直接根据这个链表,将事件分发给目标子 View,而无需重复再遍历子 View 去寻找事件的消费者。onInterceptTouchEvent方法针对不同类型的事件进行拦截,会有什么影响?
从上面的源码可知,如果在onInterceptTouchEvent方法中拦截了非 DOWN 的事件,那么只会影响本次事件的分发流程,把事件分发到自己的onTouchEvent方法去处理。而如果onInterceptTouchEvent方法中拦截的是 DOWN 事件,那么将导致在 dispatch 过程中找不到事件的消费者(即mFirstTouchTarget == null),那么后续的 MOVE、UP 事件将不会再询问是否需要拦截,而是直接分发到自己的onTouchEvent方法去处理。
因此,DOWN 事件在 ViewGroup 的事件拦截、分发过程中是一个特殊的角色,对其处理的结果将直接影响后续事件的分发流程。
五、Activity 中 Touch 事件的分发逻辑
了解完 View 和 ViewGroup 的事件分发逻辑后,再来看 Activity 的分发逻辑就简单多了。
看 Activity.dispatchTouchEvent 的源码:
|
|
非常简单,先尝试调用 window.superDispatchTouchEvent 方法,改方法返回 false 时才调用 onTouchEvent 方法。而 window.superDispatchTouchEvent 方法,实际上是调用了 Window 的 DecorView 的 dispatchTouchEvent 方法,由于 DecorView 是 FrameLayout 的子类,当然也就是一个 ViewGroup,所以归根到底 Activity.dispatchTouchEvent 方法最终也是调用了 ViewGroup.dispatchTouchEvent 方法。
至此为止,我们将 View、ViewGroup、Activity 的事件分发流程都了解完了。可以想象,当用户触发了一个触摸事件,Android 系统会将其传递到当前触摸的 Activity.dispatchTouchEvent 方法中,接着,就由 Activity、ViewGroup、View 的 dispatchTouchEvent 方法不断递归调用,把事件传递给某个目标 View,然后再逐层返回。
六、例子
最后,我们再通过一个例子来回顾一下整个分发过程。
假设有一个 Activity,他的界面内容是一个 ViewGroup,ViewGroup 内还有一个 Button。当点击 Button 的位置时,会产生一连串事件,像 DOWN → UP 或者 DOWN → MOVE → MOVE → UP,这些事件分发过程的时序图如下: