事件分发机制源码分析

11/22/2019 源码分析

# 分发流程

Activity.dispatchTouchEvent --> PhoneWindow.superDispatchTouchEvent --> DecorView.superDispatchTouchEvent --> ViewGroup.dispatchTouchEvent

# ACTION_DOWN

每次事件都从MotionEvent.ACTION_DOWN开始

ACTION_DOWN到达时会清空mFirstTouchTarget和mGroupFlags,并检测onInterceptTouchEvent(ev)判断当前viewGroup是否需要拦截,

if (actionMasked == MotionEvent.ACTION_DOWN) {
    // 清空标记
    cancelAndClearTouchTargets(ev);
    resetTouchState();
}

// 检查是否需要拦截
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
        || mFirstTouchTarget != null) {
    //由于前面已经重置了标记,这里disallowIntercept一定会为false
    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
    if (!disallowIntercept) {
        intercepted = onInterceptTouchEvent(ev);
        ...
    }
    ...
}

拦截的话由于mFirstTouchTarget为null,将调用dispathTransformedTouchEvent并传递child为null,由于child为null将调用本身的dispatchTouchEvent(此方法为父类view所有)

private boolean dispatchTransformedTouchEvent(...) {
    ...
    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
        event.setAction(MotionEvent.ACTION_CANCEL);
        // child为null,调用父类View的dispatchTouchEvent方法
        if (child == null) {
            handled = super.dispatchTouchEvent(event);
        } else {
            handled = child.dispatchTouchEvent(event);
        }
        ...
    }

不拦截的话将ACTION_DOWN的分发给合适的子类(子类根据Z轴和定义的顺序递归、判断是否可获取焦点,是否在子类的坐标内等),同样调用dispathTransformedTouchEvent并传递该子view后,调用child.dispatchTouchEvent,如果子View消费了该事件,viewGroup将把该view存入mFirstTouchTarget

public boolean dispatchTouchEvent(MotionEvent ev) {
    ...
    if (!canceled && !intercepted) {
        ...
        for (int i = childrenCount - 1; i >= 0; i--) {
            ...
            // 经过一系列的判断后找到可以处理事件的子view
            if (childWithAccessibilityFocus != null) {
                if (childWithAccessibilityFocus != child) {
                    continue;
                }
                childWithAccessibilityFocus = null;
                i = childrenCount - 1;
            }
            if (!child.canReceivePointerEvents()
                    || !isTransformedTouchPointInView(x, y, child, null)) {
                ev.setTargetAccessibilityFocus(false);
                continue;
            }
            newTouchTarget = getTouchTarget(child);
            if (newTouchTarget != null) {
                newTouchTarget.pointerIdBits |= idBitsToAssign;
                break;
            }
            ...

            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                ...
                // 子view成功消费事件后把该view存入mFirstTouchTarget
                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                alreadyDispatchedToNewTouchTarget = true;
                break;
            }

# ACTION_MOVE、ACTION_UP

后面ACTION_MOVE、ACTION_UP到达时,先判断FLAG_DISALLOW_INTERCEPT是否被设置,被设置将跳过拦截,当跳过拦截或不拦截时,此时由于mFirstTouchTarget不为null,直接将该事件分派给该子view,如果此时事件被拦截,mFirstTouchTarget将再次被置为null,且mFirstTouchTarget保存的子view将收到ACTION_CANCEL的事件(后面的事件只能viewgroup自行处理)

//事件被viewGroup拦截后,此时mFirstTouchTarget不为null
if (mFirstTouchTarget == null) {
    ...
} else {
    // 走这里
    TouchTarget predecessor = null;
    TouchTarget target = mFirstTouchTarget;
    while (target != null) {
        ...
        // intercepted为true,所以cancelChild为true
        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                || intercepted;
        //还是在这个方法内,给已保存的子view发送一个ACTION_CANCEL的事件
        if (dispatchTransformedTouchEvent(ev, cancelChild,
                target.child, target.pointerIdBits)) {
            handled = true;
        }
        ...
    }
}

# 解决事件冲突

针对事件冲突,可使用外部拦截法和内部拦截法(都是已经分发完ACTION_DOWN后)

# 外部拦截法

由viewgroup的onInterceptTouchEvent内判断是否拦截,拦截后mFirstTouchTarget被置为null,将保存的子view将收到ACTION_CANCEL的事件,接下来的事件都只能自己处理。

# 内部拦截法

稍微复杂一点,viewgroup除了ACTION_DOWN不拦截(ACTION_DOWN如果拦截,子view将不再接收到后面的事件)其他都拦截,子view在收到ACTION_DOWN时调用parent的requestDisallowInterceptTouchEvent(true),让parent不拦截事件,dispatchTouchEvent中判断是否需要继续消费事件,不消费则再调用parent的requestDisallowInterceptTouchEvent(false)将接下来的事件都交给parent。