手势冲突是 android 开发中经常遇到的一类问题了,网上讲解此问题的文章也很多,但是大都浅显地过一遍事件分发的调用栈,然后给出一个调用栈流程图;要不就是使用日志大法,用日志来验证自己的想法,完全没有参考价值;这里根据事件分发相关源码,记录下我的理解
MotionEvent 里定义的 ACTION_XXX 还不少有 10 多个,看起来情况很复杂的样子,实际上只需要关注三个:ACTION_DOWN,ACTION_MOVE 和 ACTION_UP,而且在一个手势里它们的顺序是:
ACTION_DOWN → ACTION_MOVE → ACTION_MOVE → ... → ACTION_UP
网上大部分文章到此就结束了,实际上重点应该在 ViewGroup.dispatchTouchEvent,里面是事件分发的核心逻辑,我把它切分为三个阶段:
// DOWN 会被拦截,后续的 MOVE 和 UP 如果有 touch target 也会被拦截
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
// 可以通过 requestDisallowInterceptTouchEvent 跳过此阶段,
// 一般是 child 调用 parent.requestDisallowInterceptTouchEvent 来阻止 parent 拦截 touch event
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 {
// 后续的 MOVE 和 UP 没有 touch target 则直接走向 onTouchEvent 也就不需要拦截了
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
// 收到 CANCEL 或者 onInterceptTouchEvent 返回 true,则不分发 DOWN 给 children
// 导致 children 收不到 DOWN 以及没有 touch target
final boolean canceled = resetCancelNextUpFlag(this) || actionMasked == MotionEvent.ACTION_CANCEL;
// ...
if (!canceled && !intercepted) {
// ...
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
// ... 按顺序分发 ACTION_DOWN,child index(in children array) 越大优先级越高,child z value 越大优先级越高
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
// ... touch 是否落在 child 的矩形区域内
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
// ... 将 touch event 坐标转换为 child 区域坐标,分发给 child;当有第一个 child 消费时,记录起来并中断剩下的分发过程
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// ...
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
// ...
}
// 如果没有 touch target,则走自己的 View.dispatchTouchEvent 流程(相当于流向 onTouchEvent)
// 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 {
// 分发给 touch target
// 但如果 onInterceptTouchEvent 返回 true,则发送 CANCEL 给 touch target,后续将不再流向 touch target,而是直接流向 onTouchEvent
// onInterceptTouchEvent 拦截的那个 touch 不会流向 onTouchEvent
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;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}