一、概述
在 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.java
/**
* Pass the touch screen motion event down to the target view, or this
* view if it is the target.
*
* @param event The motion event to be dispatched.
* @return True if the event was handled by the view, false otherwise.
*/
public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;
// ...
if (onFilterTouchEventForSecurity(event)) {
ListenerInfo li = mListenerInfo;
// 只要该 View 设置了 onTouchListener,并且该 View 是 enabled,
// 则调用 onTouchListener.onTouch
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
// 只有 onClickListener.onTouch 返回 false,
// onTouchEvent 才会被调用,并将其返回值返回
if (!result && onTouchEvent(event)) {
result = true;
}
}
// ...
return result;
}
可以看出,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.java
/**
* {@inheritDoc}
*/
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// ...
// 该变量记录事件是否已被处理
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// Step1.如果是 DOWN 事件,则清理之前的变量和状态
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
// Step2.检查拦截的情况
final boolean intercepted;
// 只有满足以下两种情况,才可能去判断是否需要拦截,否则都当作拦截:
// 1.如果是 DOWN 事件
// 2.在之前的 DOWN 事件分发过程中已经找到并记录下了响应 touch 事件的目标 View
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
// 如果该 View 被设置为不允许拦截,则跳过拦截判断
// (注:调用 requestDisallowInterceptTouchEvent 方法可设置该变量)
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
// 允许拦截,则调用 onInterceptTouchEvent 判断是否需要拦截
intercepted = onInterceptTouchEvent(ev);
} else {
// 否则不允许拦截(注意此时不会调用 onInterceptTouchEvent)
intercepted = false;
}
} else {
// 如果不是 DOWN 事件,且之前没有找到响应 touch 事件的目标 View,
// 则该 View 继续拦截事件
intercepted = true;
}
// 该变量记录是否需要取消掉这次事件
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
// Step3.分发 DOWN 事件或其他初始事件(例如多点触摸的 DOWN 事件)
// 如果既不取消,又不拦截
if (!canceled && !intercepted) {
// 如果是 DOWN 事件或其他两种特殊事件(先只看 DOWN 事件)
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// 遍历所有子 View
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
// 找到事件的坐标(x,y)对应的子 View
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
continue;
}
// ...
// 调用 dispatchTransformedTouchEvent 方法将事件分发给子 View,
// 该方法会调用子 View 的 dispatchTouchEvent 方法继续分发事件
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// 如果该方法返回 true,代表子 View 消费了该事件
// 记录接受该事件的子 View,记录在以 mFirstTouchTarget 开头的链表中,具体看 addTouchTarget 方法的源码
newTouchTarget = addTouchTarget(child, idBitsToAssign);
// 标记已经成功分发了事件
alreadyDispatchedToNewTouchTarget = true;
// 退出循环
break;
}
}
}
}
}
// 到目前为止,在 (不拦截 && 不取消 && 是 DOWN 事件) 的前提下,已经在子 View 中寻找过一次事件的响应者。
// 如果有子 View 消费了事件,那么事件已经通过 dispatchTransformedTouchEvent 方法分发到了该子 View 中,
// 并且 alreadyDispatchedToNewTouchTarget = true,
// 并且将响应者记录在局部变量 newTouchTarget 和 成员变量 mFirstTouchTarget 链表中。
// Step4.接下来将事件分发到 touchTarget 中或分发到自己身上。
if (mFirstTouchTarget == null) {
// mFirstTouchTarget == null 意味着之前的程序没有找到事件的消费者,那么事件将传递给自己,
// 注意:是通过调用 dispatchTransformedTouchEvent 方法,并将该方法的第3个参数设为 null,代表传递给自己。
// 而该方法中,当第3个参数为 null 时,会调用了 super.dispatchTouchEvent 方法,而 ViewGroup 的父类就是 View,所以就是走了 View 的事件分发流程将事件传递给自己。
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// 接下来通过遍历 mFirstTouchTarget 链表,将事件分发到 touchTarget 中,
// 注意上面用 newTouchTarget 变量记录了已被分发的 View,这里不会重复分发。
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
}
target = next;
}
}
}
// ...
return handled;
}
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
的源码:
// Activity.java
/**
* Called to process touch screen events. You can override this to
* intercept all touch screen events before they are dispatched to the
* window. Be sure to call this implementation for touch screen events
* that should be handled normally.
*
* @param ev The touch screen event.
*
* @return boolean Return true if this event was consumed.
*/
public boolean dispatchTouchEvent(MotionEvent ev) {
// ...
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
非常简单,先尝试调用 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,这些事件分发过程的时序图如下: