Android事件传递、多点触控及滑动冲突的处理
基本概念
- 所有Touch事件都会被封装MotionEvent, 包括Touch的类型、位置(相对屏幕的绝对位置,相对View的相对位置)、时间、历史记录以及第几个手指(多点触控)等;
- 事件有多种类型,常用的事件类型有:ACTION_DOWN,ACTION_UP,ACTION_MOVE,ACTION_CANCEL 等;
- 对事件的处理包括三类:
事件传递,dispatchTouchEvent();
拦截,onInterceptTouchEvent();
消费,onTouchEvent()、OnTouchListener;
传递过程
网上有很多资料对事件的分发过程做了详尽的代码追踪,比如 https://www.jianshu.com/p/38015afcdb58
有兴趣的同学可以参考并去详细走一下,这里我做一个文字性描述:
传递细节描述
- 事件从 Activity.dispatchTouchEvent() 开始传递, 依次通过getWindow().superDispatchTouchEvent(event)、mDecor.superDispatchTouchEvent(event) 传递,即从Activity-> PhoneWindow ->DecorView, DecorView 是整个 ViewTree 的顶层 ViewGroup ;
- 在整个 ViewGroup 中,事件从顶层开始,依次往子View传递;
- 父 ViewGroup 可以通过 onInterceptTouchEvent() 对事件做拦截,阻止其往下传递;
- 如果未被拦截,则子 View 可以通过 onTouchEvent() 消费(处理)事件;
- 如果事件从上往下传递过程中一直没有被拦截,且最底层子 View 没有消费事件,事件会反向往上传递,这时父 ViewGroup 可以在 onTouchEvent() 中消费该事件,如果还是没有被消费的话,最后会到 Activity 的 onTouchEvent() 函数;
- 底层View是具有事件的优先消费权的;
- 如果View 没有对 ACTION_DOWN 进行消费,此次点击的后续事件不会传递过来;
- 如果 View 消费了 ACTION_DOWN ,此次点击的后续事件会直接给这个 View,这里的后续事件指的是 ACTION_MOVE 和 ACTION_UP 事件;此时,其父 ViewGroup 的 onIntercept 函数仍会被调用,仍能进行拦截,但它自己的 onIntercept 不会被调用了;
- 子 View 可以在 onTouchEvent 中调用 getParent().requestDisallowInterceptTouchEvent(true),这样父 ViewGroup 的 onIntercept 在后续的事件中就不会被调用了;
- 如果第一个事件即 ACTION_DOWN 就被父 ViewGroup 拦截了,子 View 将不会获取到消费事件的机会;
- OnTouchListener 优先于 onTouchEvent() 对事件进行消费;
- 消费指的是相应的函数返回 true ;
- ViewGroup 才有 onIntercept 方法,View 是没有的,即View不可以拦截事件;
- 所有的事件处理过程都是以 ACTION_DOWN 开始,ACTION_UP 或者 ACTION_CANCEL 结束,ACTION_UP 是事件正常处理逻辑的结束标志,ACTION_CANCEL 是由父 ViewGroup 主动发出,当父 ViewGroup 拦截了除 ACTION_DOWN 之外的事件,会给正在消费 ACTION_DOWN 并等待后续事件的子 View 发送一个 ACTION_CANCEL 事件,通知子 View 结束自己的事件等待;
TouchTarget
关于第7、8两点,ViewGroup是如何在 dispatchTouchEvent 过程中快速命中并分发到对应子 View 的呢?这里是通过 TouchTarget 这个结构来实现的。
private static final class TouchTarget {
private static final int MAX_RECYCLED = 32;
// 用于控制同步的锁
private static final Object sRecycleLock = new Object[0];
// 注意这是static类型的,内部可复用实例链表表头
private static TouchTarget sRecycleBin;
// 内部可复用的实例链表的长度
private static int sRecycledCount;
public static final int ALL_POINTER_IDS = -1; // all ones
// 当前被触摸的 View
public View child;
// 对目标捕获的所有指针的指针id的组合位掩码
public int pointerIdBits;
// 链表中指向的下一个目标
public TouchTarget next;
private TouchTarget() {
}
...
}
在ViewGroup中维护了一个变量:mFirstTouchTarget,这是在 ViewGroup 中维护的链表, 用于记录当前响应事件序列的子 View (一个事件序列对应一个响应它的子View),mFirstTouchTarget 指向链表首部。
先看一下 mFirstTouchTarget 的赋值:
// 这是发生在ViewGroup中的dispatchTouchEvent方法中
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
...
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
}
// 当响应事件的目标child View添加到链表中,同时让 mFirstTouchTarget 指向链表的表头
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
再看 mFirstTouchTarget 在 dispatchTouchEvent 方法中的使用:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
1、如果事件是 ACTION_DOWN 事件,重置 touchTargets 状态,在 cancelAndClearTouchTargets 方法中会发出 ACTION_CANCEL 事件
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
2、对于一个事件序列,当其中某一个事件成功拦截时,那么对于剩下的一系列事件也会被拦截,并且不会再次执行onInterceptTouchEvent方法。如果 ACTION_DOWN 事件被拦截了,即当前ViewGroup的 onInterceptTouchEvent(ev) return true;此时 mFirstTouchTarget 必然为null,后续的事件都会当前 ViewGroup 拦截不再传递
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
intercepted = true;
}
3、如果事件既没有cancel,也没有被 intercept,遍历子View进行事件分发
if (!canceled && !intercepted) {
...
}
4、事件分发过程中,如果dispatchTouchEvent返回了false,或者说当前的ViewGroup没有子元素的话,会走到这个逻辑。mFirstTouchTarget == null说明子View并没有消费事件,所以没有对mFirstTouchTarget进行赋值。这里child == null,代码会进一步执行super.dispatchTouchEvent(event),即 View 中的 dispatchTouchEvent 方法
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
5、mFirstTouchTarget != null, 说明事件被子View消费,此时会依次将事件分发到 mFirstTouchTarget 保存的链表 View中
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
...
target = next;
}
}
}
if (!handled && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
}
return handled;
}
这个地方重点关注一下1、2、3、4、5几个注释点。现在我们回到7 8两点。
如果View 没有对 ACTION_DOWN 进行消费,此次点击的后续事件不会传递过来。这个很显然,如果没有对 ACTION_DOWN 进行消费,就不会被保存到 TouchTarget 链表中,后续事件的分发是直接往这个链表中进行分发的。
如果 View 消费了 ACTION_DOWN ,此次点击的后续事件会直接给这个 View,这里的后续事件指的是 ACTION_MOVE 和 ACTION_UP 事件;此时,其父 ViewGroup 的 onIntercept 函数仍会被调用,仍能进行拦截,但它自己的 onIntercept 不会被调用了。这个可以从第2点注释中找到答案,如果事件被消费了,mFirstTouchTarget != null, 后续事件可以从mFirstTouchTarget 链表中直接分发,同时后续事件过来的时候会跳过intercepted 的判断,所以自己的 onIntercept 就不会调用了。
RecyclerView 的事件传递
这里以点击 RecyclerView 中的某个Item中的 Button 为例:
点下Button
- 产生了一个down事件,activity-->phoneWindow-->ViewGroup-->ListView-->botton,中间如果有重写了拦截方法,则事件被该view拦截可能消耗;
- 没拦截,事件到达了button,这个过程中建立了一条事件传递的view链表;
- 到button的dispatch方法-->onTouch-->view是否可用-->Touch代理;
移动点击按钮的时候
- 产生move事件,RecyclerView 中会对move事件做拦截;
- 此时 RecyclerView 会将该滑动事件消费掉;
- 后续的滑动事件都会被 RecyclerView 消费掉;
- Button之前已经处理了 down 事件,现在还在等着后续事件,这个时候 RecyclerView 就会发出 cancel 事件通知Button不要再等了
手指抬起
前面建立了一个view链表,RecyclerView 的父view在获取事件的时候,会直接取链表中的RecyclerView 让其进行事件消耗
有兴趣的同学可以带着这个步骤去追踪 RecyclerView 的源代码。
多点触控
多点触控涉及到了多个手指点击事件的处理,这里要增加两个额外的事件
- ACTION_POINTER_DOWN:额外⼿手指按下(按下之前已经有别的⼿手指触摸到 View)
- ACTION_POINTER_UP:有⼿手指抬起,但不不是最后⼀一个(抬起之后,仍然还有别的⼿手指在触摸着 View)
事件类型: ACTION_POINTER_UP;
active pointer index: 0;pointer: x: 200, y: 300, index: 0, id: 1;
pointer: x: 300, y: 500, index: 1, id: 2
多点触控触摸事件的结构
- 触摸事件是按序列列来分组的,每⼀一组事件必然以 ACTION_DOWN 开头,以 ACTION_UP 或 ACTION_CANCEL 结束;
- ACTION_POINTER_DOWN 和 ACTION_POINTER_UP 和 ACTION_MOVE ⼀一样,只是事件序列列中 的组成部分,并不不会单独分出新的事件序列列;
- 同⼀一时刻,⼀一个 View 要么没有事件序列列,要么只有⼀一个事件序列列;
- 多点触控要解决的问题之一是:手指触摸的顺序,手指的区分,这两个问题通过 index 和 id 来区分;
- 多点触控要解决的问题二:多点触控时滑动了一个手指,这时候要知道动的是哪个
多点触控的三种类型
- 接⼒力力型 同⼀一时刻只有⼀一个 pointer 起作⽤用,即最新的 pointer。 典型:ListView、 RecyclerView。 实现⽅方式:在 ACTION_POINTER_DOWN 和 ACTION_POINTER_UP 时记录下最 新的 pointer,在之后的 ACTION_MOVE 事件中使⽤用这个 pointer 来判断位置。
- 配合型 所有触摸到 View 的 pointer 共同起作⽤用。
典型:ScaleGestureDetector,以及 GestureDetector 的 onScroll() ⽅方法判断。 实现⽅方式:在 每个 DOWN、POINTER_DOWN、POINTER_UP、UP 事件中使⽤用所有 pointer 的坐标来共同更更新焦点坐标,并在 MOVE 事件中使⽤用所有 pointer 的坐标来判断位置。 - 各⾃自为战型 各个 pointer 做不不同的事,互不不影响。 典型:⽀支持多画笔的画板应⽤用。 实现⽅方式: 在每个 DOWN、POINTER_DOWN 事件中记录下每个 pointer 的 id,在 MOVE 事件中使⽤用 id 对 它们进⾏行行跟踪。
滑动冲突处理
什么是滑动冲突?就是父 View 和子 View 都需要处理滑动,例如父 View 需要左右滑动,子 View 需要上下滑动(ViewPager 嵌套 RecyclerView),一个点击事件,到底交给谁处理?
首先我们需要定义好处理规则,然后我们在父 View 的 onIntercept、子 View 的 onTouchEvent 以及父 View 的 onTouchEvent 函数中实现我们定义的规则即可。例如父 View 的 onIntercept 中,如果发现是左右滑动,那就拦截,否则不拦截。
NestedScrollView 嵌套 RecyclerView 也是一样的道理,NestedScrollView 发现是上下滑动,就直接拦截并处理,RecyclerView 就没有处理的机会了。
参考文章