上一篇文章我们主要介绍了Android UI事件处理机制-基于监听器方式、基于回调方法,同时从View的角度分析了Touch事件分发流程。这一篇文章我们将从ViewGroup角度来分析Touch事件分发流程,之前计划的关于Robolectric如何测试相关事件分发流程将会在后续『单元测试之-自定义View』中进行介绍。
事件传递流程
Touch事件在Android中对应的就是MotionEvent类。对Touch事件的分发其实就是对MotionEvent对象进行分发。当我们在屏幕上进行一次点击操作时,MotionEvent就产生了,Android系统会将这个MotionEvent对象传递到一个具体的View进行处理,这个传递过程就是事件分发。Android事件分发是一种委托思想:上层委托下层,父容器委托子元素处理。
下图是Android系统的界面结构图:最顶层为Activity的ViewGroup,下面有若干的ViewGroup节点,每个节点之下又有若干的ViewGroup节点或者View节点,依次类推。当一个Touch事件到达根节点,即Acitivty的ViewGroup时,该Touch事件会被依次分发,分发过程就是调用子View(ViewGroup)的dispatchTouchEvent方法实现的。简单来说,就是ViewGroup遍历它包含着的子View,调用每个View的dispatchTouchEvent方法,而当子View为ViewGroup时,又会通过调用ViwGroup的dispatchTouchEvent方法继续调用其内部的View的dispatchTouchEvent方法。下图中Touch事件下发顺序是这样的:①-②-⑤-⑥-⑦-③-④。dispatchTouchEvent方法只负责事件的分发,它拥有boolean类型的返回值,当返回为true时,顺序下发会中断。在上述例子中如果⑤的dispatchTouchEvent返回结果为true,那么⑥-⑦-③-④将都接收不到本次Touch事件。(这段节选自:Android:30分钟弄明白Touch事件分发机制)
ViewGroup继承自View,ViewGroup中对触摸事件的处理,很多也都继承于View。但是,ViewGroup又有自己对触摸事件的特定处理。ViewGroup重载了dispatchTouchEvent()方法,新增了onInterceptTouchEvent()方法。上一篇文章中我们已经分析过View类的事件处理机制-dispatchTouchEvent、onTouchEvent,本篇将会分析ViewGroup的dispatchTouchEvent分发流程。
ViewGroup->dispatchTouchEvent流程
关于ViewGroup->dispatchTouchEvent已经有很多大神们进行了详细的源码分析,我这里给出一些链接,方便大家去参考
1、Android 触摸事件机制(四) ViewGroup中触摸事件详解)
2、 13.View的事件分发机制——dispatchTouchEvent详解
为节约篇幅这篇文章就不贴源码了,将以精心烹制的『流程图』作为�主菜,另配上香甜可口的甜点『解析』为大家呈现ViewGroup中dispatchTouchEvent这道饕餮盛宴。
- Down操作:首先会通过拦截机制判断是否需要拦截,如果拦截,则不进行子View的Down操作分发,直接由当前ViewGroup处理;反之则循环遍历子View,进行事件分发。当然循环遍历所有子View之后也可能存在没有子View处理该Down操作,这个时候会继续交给当前ViewGroup处理。
- Up、Move操作:也是先通过拦截机制判断是否需要拦截,如果拦截,则由当前ViewGroup直接处理;反之则分发给处理Down操作的子View进行处理。
流程图中涉及到的拦截机制、TouchTarget、MotionEvent、Down操作分发流程等等,会在下面的内容中为大家一一讲解。
拦截机制
在自定义ViewGroup中,有时候需要实现Touch事件拦截,比如ListView下拉刷新就是典型的Touch事件拦截的例子。Touch事件拦截就是在Touch事件被父 view拦截,不会分发给其child,即使触摸发生在该child身上。被拦截的事件会转到parent view的onTouchEvent方法中进行处理。
那么ViewGroup的拦截机制具体原则是什么呢?我们先来看下流程图,方便大家理解。
这里根据流程图总结下具体原则:
- ViewGroup有一个禁止拦截的标志位:FLAG_DISALLOW_INTERCEPT,如果调用requestDisallowInterceptTouchEvent(),该标志位为True,则禁止该ViewGroup拦截事件。
- ViewGroup新增的接口onInterceptTouchEvent(),默认是不拦截的,即返回false;如果你需要拦截,只要return true就行了,这样该事件就不会往子View传递了。
- Down操作-Touch事件的开始,此时ViewGroup会先根据FLAG_DISALLOW_INTERCEPT标志位判断,如果允许拦截,则进一步调用新增接口onInterceptTouchEvent()来确定Down操作是否继续传递给子View。
- Move、UP操作-如果mFirstTouchTarget != null(Down操作已经被某个子View消费掉了),此时,才有必要再进一步判断当前ViewGroup是否需要对Move、UP进行拦截,具体如何判断同Down操作。
- Move、UP操作-如果mFirstTouchTarget == null(Down操作已经被当前ViewGroup拦截了,或者遍历了所有子View 但都没有对Down操作进行处理),此时,完全没必要进一步判断当前ViewGroup是否要拦截,因为这种情况Down操作肯定已经由该ViewGroup了,后续的Move、UP自然也由该ViewGroup处理。
TouchTarget
TouchTarget是ViewGroup的一个内部类,是一个触摸对象的链表类。ViewGroup类中mFirstTouchTarget就是当前ViewGroup中触摸对象链表的头节点,用于记录处理某Down操作的所有子View和触摸点(对于多点触摸,需要记录每次的触摸点)信息。下面给出了这个类的源码,方便大家理解。
/* Describes a touched view and the ids of the pointers that it has captured.
*
* This code assumes that pointer ids are always in the range 0..31 such that
* it can use a bitfield to track which pointer ids are present.
* As it happens, the lower layers of the input dispatch pipeline also use the
* same trick so the assumption should be safe here...
*/
private static final class TouchTarget {
private static final int MAX_RECYCLED = 32;
private static final Object sRecycleLock = new Object[0];
private static TouchTarget sRecycleBin;
private static int sRecycledCount;
public static final int ALL_POINTER_IDS = -1; // all ones
// The touched child view.
public View child;
// The combined bit mask of pointer ids for all pointers captured by the target.
public int pointerIdBits;
// The next target in the target list.
public TouchTarget next;
private TouchTarget() {
}
public static TouchTarget obtain(View child, int pointerIdBits) {
final TouchTarget target;
synchronized (sRecycleLock) {
if (sRecycleBin == null) {
target = new TouchTarget();
} else {
target = sRecycleBin;
sRecycleBin = target.next;
sRecycledCount--;
target.next = null;
}
}
target.child = child;
target.pointerIdBits = pointerIdBits;
return target;
}
public void recycle() {
synchronized (sRecycleLock) {
if (sRecycledCount < MAX_RECYCLED) {
next = sRecycleBin;
sRecycleBin = this;
sRecycledCount += 1;
} else {
next = null;
}
child = null;
}
}
}
MotionEvent
这篇文章一开始就介绍了MotionEvent是事件源(Button、CheckBox、EditText等)产生的Touch事件。我们需要从MotionEvent中获取哪些信息呢?
-
事件类型
- 常见的事件类型有: ACTION_DOWN: 表示用户开始触摸、 ACTION_MOVE: 表示用户手指移动、ACTION_UP:表示用户抬起了手指 、ACTION_POINTER_DOWN:有一个非主要的手指按下、ACTION_POINTER_UP:一个非主要的手指抬起来。后两者是在Android 2.2支持多点触摸之后增加的事件类型。
- 获取事件类型的方法:getActionMasked()
-
事件触摸点索引信息
- Android是支持多点触控,通过触摸点索引信息可以得知一个MotionEvent事件类型是哪个触摸点触发。
- 获取触摸点索引信息的方法:getActionIndex()
-
事件发生的位置信息
- getX()方法获得事件发生时,触摸的中间区域在屏幕的X轴
- getY() 获得事件发生时,触摸的中间区域在屏幕的Y轴
- 多点触摸还可以通过getX(int pointerIndex) 和 getY(int pointerIndex)来获取对应手指事件的X、Y轴信息
- 事件发生的位置信息的坐标系是Android系统坐标系(这个概念可以参考文章:Android中的坐标系以及获取坐标的方法)
Down操作处理流程
当Touch事件MotionEvent中的ACTION_DOWN、ACTION_POINTER_DOWN操作来临时ViewGroup的分发流程是如何的呢?请先看流程图,我们再来分析。
- 首先我们需要判断当前事件是否是取消事件、是否已经被ViewGroup拦截,如果是取消事件或者已经被拦截,那么该Down操作是没有必要在子View中进行事件分发,则直接跳出该流程。
- 核心是循环遍历当前ViewGroup的所有子View。
- 循环过程中第一步是判断子View是否可接受Touch事件,同时当前的Touch事件的位置是否位于子View中。如果满足这两个条件才会进一步对该子View和Touch事件进行关联、将Touch事件分发给该子View;如果不满足这两个条件,则说明当前Touch事件和该子View没有任何联系,直接退出当前循环,进行下一个子View的处理。
- 如何将子View和当前Touch事件进行关联呢?TouchTarget链表粉墨登场(噔噔噔。。。)通过mFirstTouchTarget链表中获取和当前子View相关的TouchTarget,如果已经存在该子View相关的TouchTarget,直接更新该TouchTarget的pointerIdBits属性,让其包含当前触摸点信息,同时退出循环遍历子View的流程。(例如:第一个手指触摸在View - A上,这个时候第二个手指也触摸在View - A上)如果不存在该子View相关的TouchTarget,则新建一个并插入到mFirstTouchTarget链表中,同时调用该子View 的dispatchTouchTarget方法,这里的流程就回归到我们上一篇中关于View的事件处理流程。根据子View的dispatchTouchTarget返回值判断子View是否消费了当前Touch事件,若消费则退出循环遍历子View的流程,反之则继续遍历。
Up、Move操作处理流程
ViewGroup主要是先通过对Down操作进行分发,记录处理Down操作的子View链表,然后循环遍历该链表,完成Up、Move操作的分发处理。
总结
- ViewGroup的dispatchTouchEvent负责事件分发,由上至下,有父容器至子View依次分发
- 如果ViewGroup找到了能够处理该事件的子View,则直接交给子View处理,自己的onTouchEvent不会被触发
- 可以通过复写onInterceptTouchEvent(ev)方法,拦截子View的事件(即return true),把事件交给自己处理,则会执行自己对应的onTouchEvent方法
- 子View可以通过调用getParent().requestDisallowInterceptTouchEvent(true); 阻止ViewGroup对其事件的拦截