我们知道,View
是 Android 的最基本控件,不能再细分。而ViewGroup
继承于 View,可以包含多个 View。手指触碰屏幕时,触摸事件可能由 ViewGroup 拦截处理了,也可能传递给 ViewGroup 内部的 Child View 去处理。
ViewGroup 的事件分发核心就是方法 dispatchTouchEvent(MotionEvent)
,主要分为几步:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// 1. 如果是 Down 事件,处理初始化,重置各种状态;
// 2. 检查是否拦截了事件
// 3. 检查是否取消了事件
// 4. 根据 intercepted 和 canceled 决定是否分发事件给 Child View
// 5. 根据 mFirstTouchTarget 再次分发事件
}
// 处理初始化的 Down 事件。
if (actionMasked == MotionEvent.ACTION_DOWN) {
/*
* 当开始新的手势时,放弃所有之前的状态。
* 框架可能由于应用程序切换,ANR 或其他一些状态更改而丢失了上一个手势的抬起或取消事件。
*/
cancelAndClearTouchTargets(ev);
resetTouchState();
}
步骤 1 说明事件分发的起始就是 Down 事件。当接收到 Down 事件,说明开始了一次新的触摸事件分发。
// 拦截检查
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); // 重新恢复 action 以防发生了改变
} else {
intercepted = false;
}
} else {
// 没有触摸目标并且不是初始化的 Down 事件,当前 ViewGroup 继续拦截触摸事件。
intercepted = true;
}
步骤 2 决定了 ViewGroup 是否需要拦截事件,如果拦截那么 Child View 不会接收到此次事件或者接收到 Cancel 事件。
主动判断是否拦截的条件有两个:
mFirstTouchTarget
是否为空。不为空说明上一次事件有 Child View 捕获,再一次进行拦截判断。被动判断是否拦截的条件有一个:
disallowIntercept
是否为true
。Child View 可以通过调用 ViewGroup
的方法requestDisallowInterceptTouchEvent(boolean)
来控制 ViewGroup 是否拦截 Child View 的事件。当该变量为true
,ViewGroup 不拦截 Child View 的事件。
// 取消检查
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
步骤 3 判断当前事件是否为取消,影响后面对 Child View 事件的分发。
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
···
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
···
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
···
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
···
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, chi
ev.setTargetAccessibilityFocus(false);
continue;
}
···
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
···
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
···
}
···
}
···
}
}
步骤 4 通过canceled
和intercepted
判断到如果不取消、不拦截当前事件,并且当前事件为 Down 事件(这里还有多指等其他情形,暂不讨论),对所有 Child View 进行遍历,找到位置处于事件范围内并且不处于动画状态的 Child View,调用dispatchTransformedTouchEvent(MotionEvent, boolean, View, int)
方法将事件分发给 Child View,如果该方法返回true
说明 Child View 处理了事件,在addTouchTarget(View, int)
方法内部赋值mFirstTouchTarget
。
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// 没有触摸目标,把当前 ViewGroup 当做一个 View
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// 分发事件给触摸目标,如果已经分发过给 newTouchTarget,则排除它。必要时则取消触摸目标。
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;
}
···
}
···
}
}
步骤 5 里判断到如果mFirstTouchTarget
为空,说明没有 Child View 处理事件,则把 ViewGroup 当做一个普通的 View,把事件分发给自身。否则将当前以及接下来的事件分发给mFirstTouchTarget
指向的 Child View 处理。handled
是dispatchTouchEvent(MotionEvent)
的返回值,说明当前 View 处理了该事件。