本文的主题是了解View的事件分发整体流程,然后重点实现京东淘宝首页二级联动,解决滑动冲突。
根据面向对象思想,事件被封装成 MotionEvent 对象
多点触控 ( Multitouch,也称 Multi-touch ),即同时接受屏幕上多个点的人机交互操作,多点触 控是从 Android 2.0 开始引入的功能
上图的注意点:
1.事件返回时 dispatchTouchEvent 直接指向了父View的 onTouchEvent 这一部分是不合理的,实际上它仅仅是给了父View的 dispatchTouchEvent 一个 false 返回值, 父View根据返回值来调用自身的 onTouchEvent。
2.ViewGroup的dispatchTouchEvent是根据 onInterceptTouchEvent 的返回值来确定是调用子View的 dispatchTouchEvent 还是调用自身的 onTouchEvent, 并没有将调用交给onInterceptTouchEvent
ViewGroup的dispatchTouchEvent事件分发简化流程:
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean result = false; // 默认状态为没有消费过
if (!onInterceptTouchEvent(ev)) {
// 如果没有拦截,则交给子View
result = child.dispatchTouchEvent(ev);
}
if (!result) {
// 如果事件没有被消费,则询问自身onTouchEvent
result = onTouchEvent(ev);
}
return result;
}
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean result = false; // 默认状态为没有消费过
if (!onInterceptTouchEvent(ev)) {
// 如果没有拦截,则交给子View
result = child.dispatchTouchEvent(ev);
}
if (!result) {
// 如果事件没有被消费,则询问自身onTouchEvent
result = onTouchEvent(ev);
}
return result;
}
public boolean dispatchTouchEvent(MotionEvent ev) {
// 默认状态为没有消费过
boolean result = false;
//决定是否拦截
final boolean intercepted = false;
if (!requestDisallowInterceptTouchEvent()) {
intercepted = onInterceptTouchEvent(ev);
}
//找出最适合接收的孩子
if (!intercepted && (DOWN || POINTER_DOWN || HOVER_MOVE)) {
// 如果没有拦截交给子View
for (int i = childrenCount - 1; i >= 0; i--) {
mFirstTouchTarget = child.dispatchTouchEvent(ev);
}
}
//分发事件
if (mFirstTouchTarget == null) {
// 如果事件没有被消费,询问自身onTouchEvent
result = onTouchEvent(ev);
} else {
for(TouchTarget touchTarget : mFirstTouchTarget) {
result = touchTarget.child.dispatchTouchEvent(ev);
}
}
return result;
}
/**
整体流程就是分3步:
步骤1:判断事件是否拦截
步骤2:遍历所有的子View,寻找targets
步骤3: 将事件分发给targets
*/
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// ACTION_DOWN事件是一个手势的开始,所以这里会清空之前的手势的所有状态
// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Throw away all previous state when starting a new touch gesture.
// The framework may have dropped the up or cancel event for the previous gesture
// due to an app switch, ANR, or some other state change.
cancelAndClearTouchTargets(ev);
resetTouchState();
}
//步骤1:判断事件是否拦截
// Check for interception.
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 {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
...
//步骤2:遍历所有的子View,寻找targets
if (!canceled && !intercepted) {
...
//如果是一个手势的开始事件,MotionEvent.ACTION_DOWN、 MotionEvent.ACTION_POINTER_DOWN、MotionEvent.ACTION_HOVER_MOVE都是一个手势的开始事件
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
...
for (int i = childrenCount - 1; i >= 0; i--) {
//遍历所有的子View,找到TouchTarget
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
...
//找到TouchTarget后,加入mFirstTouchTarget链表
newTouchTarget = addTouchTarget(child, idBitsToAssign);
...
}
...
}
...
}
// 步骤3: 将事件分发给targets
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
...
}
}
}
所有的TouchTarget构成一个链表,mFirstTouchTarget指向的是链表的头节点:
// First touch target in the linked list of touch targets.
@UnsupportedAppUsage
private TouchTarget mFirstTouchTarget;
/* 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.
@UnsupportedAppUsage
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;
@UnsupportedAppUsage
private TouchTarget() {
}
public static TouchTarget obtain(@NonNull View child, int pointerIdBits) {
if (child == null) {
throw new IllegalArgumentException("child must be non-null");
}
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() {
if (child == null) {
throw new IllegalStateException("already recycled once");
}
synchronized (sRecycleLock) {
if (sRecycledCount < MAX_RECYCLED) {
next = sRecycleBin;
sRecycleBin = this;
sRecycledCount += 1;
} else {
next = null;
}
child = null;
}
}
}
记录着每个手指触摸屏幕时的接收down事件的那些View,所以用链表。
如果多个手指跨越了多个View,则mFirstTouchTarget指向的链表就有多个节点;如果多个手指都在一个View上,则mFirstTouchTarget指向的链表只有一个结点。
我们知道 View 可以注册很多事件监听器,例如:单击事件(onClick)、长按事件(onLongClick)、触摸事件(onTouch),并且View自身也有 onTouchEvent() 方法,那么问题来了,这么多与事件相关的方法应该由谁管理?毋庸置疑就是 dispatchTouchEvent(),所以 View 也会有事件分发。
•单击事件(onClickListener) 需要两个两个事件(ACTION_DOWN 和 ACTION_UP )才能触发,如果先分配给onClick判断,等它判断完,用户手指已经离开屏幕,黄花菜都凉了,定然造成 View 无法响应其他事件,应该最后调用。(最后)
•长按事件(onLongClickListener) 同理,也是需要长时间等待才能出结果,肯定不能排到前面,但因为不需要ACTION_UP,应该排在 onClick 前面。(onLongClickListener > onClickListener)
•触摸事件(onTouchListener) 如果用户注册了触摸事件,说明用户要自己处理触摸事件了,这个应该排在最前面。(最前)
•View自身处理(onTouchEvent()方法)
View提供了一种默认的处理方式,如果用户已经处理好了,也就不需要了,所以应该排在 onTouchListener 后面。(onTouchListener > onTouchEvent())
所以调用顺序是:onTouchListener > onTouchEvent() > onLongClickListener > onClickListener
/*
* ....
*
* @param ev The motion event being dispatched down the hierarchy.
* @return Return true to steal motion events from the children and have
* them dispatched to this ViewGroup through onTouchEvent().
* The current target will receive an ACTION_CANCEL event, and no further
* messages will be delivered here.
*/
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
&& ev.getAction() == MotionEvent.ACTION_DOWN
&& ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
&& isOnScrollbarThumb(ev.getX(), ev.getY())) {
return true;
}
return false;
}
TouchTarget和mFirstTouchTarget, 这个变量是给手势设置的,跨越事件保留的。
/* 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.
@UnsupportedAppUsage
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;
@UnsupportedAppUsage
private TouchTarget() {
}
public static TouchTarget obtain(@NonNull View child, int pointerIdBits) {
if (child == null) {
throw new IllegalArgumentException("child must be non-null");
}
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() {
if (child == null) {
throw new IllegalStateException("already recycled once");
}
synchronized (sRecycleLock) {
if (sRecycledCount < MAX_RECYCLED) {
next = sRecycleBin;
sRecycleBin = this;
sRecycledCount += 1;
} else {
next = null;
}
child = null;
}
}
}
1、如果现在分发的这个事件就是分发给我这个辅助功能获焦的view的,那么我们立即进行正常的分发,同时清除这个MotionEvent事件FLAG_TARGET_ACCESSIBILITY_FOCUS标志, 请注意这个标志是事件的,而不是view的
2、如果这个事件被该ViewGroup拦截,或者已经有子view正在处理这个手势,我们清除这个事件的FLAG_TARGET_ACCESSIBILITY_FOCUS标志,进行正常的分发
•当ViewPager接收到DOWN事件,ViewPager默认不拦截DOWN事件,DOWN事件交由ListView处理,由于ListView可以滚动,即可以消费事件,则ViewPager的 mFirstTouchTarget会被赋值,即找到处理事件的子View。然后ViewPager接收到MOVE事件,
•若此事件是ViewPager不需要,则同样会将事件交由ListView去处理,然后ListView处理事件; •若此事件ViewGroup需要,因为DOWN事件被ListView处理,mFirstTouchEventTarget会被赋值,也就会调用onInterceptedTouchEvent,此时由于ViewPager对此事件感兴趣 ,则onInterceptedTouchEvent方法会返回true,表示ViewPager会拦截事件,此时当前的MOVE事件会消失,变为CANCEL事件,往下传递或者自己处理,同时 mFirstTouchTarget被重置为null。
•当MOVE事件再次来到时,由于mFristTouchTarget为null,所以接下来的事件都交给了ViewPager。
嵌套滑动有两个角色,一个是父亲,一个是孩子,这里的父亲是ScrollView,孩子是RecyclerView。
在滑动ViewPager里面的RecyclerView时,需要先判断ScrollView需不需要滑动。
Android事件分发之ACTION_CANCEL机制及作用
深入理解事件分发 ViewGroup.mFirstTouchTarget的设计
Android事件分发mFirstTouchTarget的思考