ViewGroup 的事件分发核心

439 阅读3分钟

我们知道,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 再次分发事件
}

步骤 1

// 处理初始化的 Down 事件。
if (actionMasked == MotionEvent.ACTION_DOWN) {
    /*
     * 当开始新的手势时,放弃所有之前的状态。
     * 框架可能由于应用程序切换,ANR 或其他一些状态更改而丢失了上一个手势的抬起或取消事件。
     */
    cancelAndClearTouchTargets(ev);
    resetTouchState();
}

步骤 1 说明事件分发的起始就是 Down 事件。当接收到 Down 事件,说明开始了一次新的触摸事件分发。

步骤 2

// 拦截检查
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 事件。

主动判断是否拦截的条件有两个:

  1. 当前事件是否为 Down 事件。如果是 Down 事件则进行第一次拦截判断;
  2. mFirstTouchTarget 是否为空。不为空说明上一次事件有 Child View 捕获,再一次进行拦截判断。

被动判断是否拦截的条件有一个:
disallowIntercept 是否为true。Child View 可以通过调用 ViewGroup 的方法requestDisallowInterceptTouchEvent(boolean)来控制 ViewGroup 是否拦截 Child View 的事件。当该变量为true,ViewGroup 不拦截 Child View 的事件。

步骤 3

// 取消检查
final boolean canceled = resetCancelNextUpFlag(this)
        || actionMasked == MotionEvent.ACTION_CANCEL;

步骤 3 判断当前事件是否为取消,影响后面对 Child View 事件的分发。

步骤 4

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 通过canceledintercepted判断到如果不取消、不拦截当前事件,并且当前事件为 Down 事件(这里还有多指等其他情形,暂不讨论),对所有 Child View 进行遍历,找到位置处于事件范围内并且不处于动画状态的 Child View,调用dispatchTransformedTouchEvent(MotionEvent, boolean, View, int)方法将事件分发给 Child View,如果该方法返回true说明 Child View 处理了事件,在addTouchTarget(View, int)方法内部赋值mFirstTouchTarget

步骤 5

// 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 处理。handleddispatchTouchEvent(MotionEvent)的返回值,说明当前 View 处理了该事件。