面试中的ACTION_CANCEL到底何时触发,滑出子View返回会触发吗?

192 阅读5分钟

首先看一下官方的注释:

/**
 * Constant for {@link #getActionMasked}: The current gesture has been aborted.
 * You will not receive any more points in it.  You should treat this as
 * an up event, but not perform any action that you normally would.
 */
public static final int ACTION_CANCEL           = 3;

当前的手势被中支,你将不会收到任何事件了,可以把它当做一个ACTION_UP事件,但不要执行正常情况下的逻辑。

ACTION_CANCEL的触发时机分别是: 1 在子View处理事件的过程中,父View对事件拦截,

// Dispatch to touch targets.
if (mFirstTouchTarget == null) {//1
    // 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 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; //2
            if (dispatchTransformedTouchEvent(ev, cancelChild, //3 
                    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;
    }
}

第一处代码注释地方,当有子View处理事件时,那么mFirstTouchTarget这个就不为null的。所以符合在子View处理事件的过程中这个前置条件了。 第二处代码注释地方,当父View拦截时,那么intercepted这个值就是为ture的,那么cancelChild就为true了,接着执行了第三处代码注释dispatchTransformedTouchEvent方法,根据上述的变量值可以看出

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {
    final boolean handled;

    // Canceling motions is a special case.  We don't need to perform any transformations
    // or filtering.  The important part is the action, not the contents.
    final int oldAction = event.getAction();
    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
        event.setAction(MotionEvent.ACTION_CANCEL);//5
        if (child == null) {
            handled = super.dispatchTouchEvent(event);
        } else {
            handled = child.dispatchTouchEvent(event);//4
        }
        event.setAction(oldAction);
        return handled;
    }

第四处代码注释:就执行了child的dispatchTouchEvent的方法来自第五处代码注释的CANCEL_ACTION。 小结: 当父View不拦截DOWN事件时,拦截MOVE事件时,此次它的子View会收到CANCEl事件,可以自行验证下。 image.png

2 ACTION_DOWN初始化操作时

public boolean dispatchTouchEvent(MotionEvent ev) {
    if (mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
    }

    // If the event targets the accessibility focused view and this is it, start
    // normal event dispatch. Maybe a descendant is what will handle the click.
    if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
        ev.setTargetAccessibilityFocus(false);
    }

    boolean handled = false;
    if (onFilterTouchEventForSecurity(ev)) {
        final int action = ev.getAction();
        final int actionMasked = action & MotionEvent.ACTION_MASK;

        // 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. //1
            cancelAndClearTouchTargets(ev);
            resetTouchState();
        }

这个源码第1处注释写的很清楚,系统可能由于切换APP切换,ANR,或者其他一些状态改变导致丢失up,或者cancel事件。 3 在子View处理事件的过程中被父View中移除时

public void removeViews(int start, int count) {
    removeViewsInternal(start, count);
    requestLayout();
    invalidate(true);
}

private boolean removeViewInternal(View view) {
    final int index = indexOfChild(view);
    if (index >= 0) {
        removeViewInternal(index, view);
        return true;
    }
    return false;
}

private void removeViewInternal(int index, View view) {
    if (mTransition != null) {
        mTransition.removeChild(this, view);
    }

    boolean clearChildFocus = false;
    if (view == mFocused) {
        view.unFocus(null);
        clearChildFocus = true;
    }
    if (view == mFocusedInCluster) {
        clearFocusedInCluster(view);
    }

    view.clearAccessibilityFocus();

    cancelTouchTarget(view);//3.1处
    cancelHoverTarget(view);

再看这个3.1处这个方法

@UnsupportedAppUsage
private void cancelTouchTarget(View view) {
    TouchTarget predecessor = null;
    TouchTarget target = mFirstTouchTarget;
    while (target != null) {
        final TouchTarget next = target.next;
        if (target.child == view) {
            if (predecessor == null) {
                mFirstTouchTarget = next;
            } else {
                predecessor.next = next;
            }
            target.recycle();

            final long now = SystemClock.uptimeMillis();
            MotionEvent event = MotionEvent.obtain(now, now,
                    MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
            event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
            view.dispatchTouchEvent(event);
            event.recycle();
            return;
        }
        predecessor = target;
        target = next;
    }
}

构造了一个CANCEL事件的event,然后view去分发这个事件。 4 子View设置了PFLAG_CANCEL_NEXT_UP_EVENT标志位时。 View#dispatchTouchEvent方法片段截取

// 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 { //4.1处
    // Dispatch to touch targets, excluding the new touch target if we already
    // dispatched to it.  Cancel touch targets if necessary.
    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)//4.2处
                    || 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;
    }
}

当执行到4.1时代表有子View处理事件,4.2处即使父View在此过程中没有拦截,但是子View设置了PFLAG_CANCEL_NEXT_UP_EVENT标志位,同样cancelChild为ture,后续逻辑同1了。 现在可以得出结论:当一个按钮被按下时,滑出button区域时,是不会收到CANCEL事件的。只会收到DWON-MOVE-MOVE-MOVE-MOVE-MOVEz最后UP事件,滑出button区域依然可以收到MOVE和UP事件,这点可以自行验证。实践是检验真理的唯一标准。 而且UP事件后也不会触发onclick了, View#onTouchEvent()

case MotionEvent.ACTION_MOVE:
    if (clickable) {
        drawableHotspotChanged(x, y);
    }

    final int motionClassification = event.getClassification();
    final boolean ambiguousGesture =
            motionClassification == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE;
    int touchSlop = mTouchSlop;
    if (ambiguousGesture && hasPendingLongPressCallback()) {
        if (!pointInView(x, y, touchSlop)) {
            // The default action here is to cancel long press. But instead, we
            // just extend the timeout here, in case the classification
            // stays ambiguous.
            removeLongPressCallback();
            long delay = (long) (ViewConfiguration.getLongPressTimeout()
                    * mAmbiguousGestureMultiplier);
            // Subtract the time already spent
            delay -= event.getEventTime() - event.getDownTime();
            checkForLongClick(
                    delay,
                    x,
                    y,
                    TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
        }
        touchSlop *= mAmbiguousGestureMultiplier;
    }

    // Be lenient about moving outside of buttons
    if (!pointInView(x, y, touchSlop)) {//4.3处
        // Outside button
        // Remove any future long press/tap checks
        removeTapCallback();
        removeLongPressCallback();
        if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
            setPressed(false);4.4处
        }
        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
    }
public void setPressed(boolean pressed) {
    final boolean needsRefresh = pressed != ((mPrivateFlags & PFLAG_PRESSED) == PFLAG_PRESSED);

    if (pressed) {  
        mPrivateFlags |= PFLAG_PRESSED;
    } else { //4.5
        mPrivateFlags &= ~PFLAG_PRESSED;
    }

    if (needsRefresh) {
        refreshDrawableState();
    }
    dispatchSetPressed(pressed);
}

4.3处代码注释判断是否超过view的边界,就会调用4.4处设置setPressed(false),再执行4.5代码注释其实就是移除一个PFLAG_PREDDED标志位,

case MotionEvent.ACTION_UP:
    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
    if ((viewFlags & TOOLTIP) == TOOLTIP) {
        handleTooltipUp();
    }
    if (!clickable) {
        removeTapCallback();
        removeLongPressCallback();
        mInContextButtonPress = false;
        mHasPerformedLongPress = false;
        mIgnoreNextUpEvent = false;
        break;
    }
    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) { //4.6
        // take focus if we don't have it already and we should in
        // touch mode.
        boolean focusTaken = false;
        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
            focusTaken = requestFocus();
        }

        if (prepressed) {
            // The button is being released before we actually
            // showed it as pressed.  Make it show the pressed
            // state now (before scheduling the click) to ensure
            // the user sees it.
            setPressed(true, x, y);
        }

        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
            // This is a tap, so remove the longpress check
            removeLongPressCallback();

            // Only perform take click actions if we were in the pressed state
            if (!focusTaken) {
                // Use a Runnable and post this rather than calling
                // performClick directly. This lets other visual state
                // of the view update before click actions start.
                if (mPerformClick == null) {
                    mPerformClick = new PerformClick();
                }
                if (!post(mPerformClick)) {
                    performClickInternal(); //4.7处
                }
            }
        }

        if (mUnsetPressedState == null) {
            mUnsetPressedState = new UnsetPressedState();
        }

        if (prepressed) {
            postDelayed(mUnsetPressedState,
                    ViewConfiguration.getPressedStateDuration());
        } else if (!post(mUnsetPressedState)) {
            // If the post failed, unpress right now
            mUnsetPressedState.run();
        }

        removeTapCallback();
    }
    mIgnoreNextUpEvent = false;
    break;

当在MOVE事件了View超过了边界了就移除了PFLAG_PRESSED时,那么代码4.6处代码就判断条件进不去,此时就执行不大4.7代码注释处的performClickInternal()也就不会onClcik。 一旦滑出View范围,View就会被移除PRESSED标志位,这个是不可逆的(即使你再滑回来,再UP时候)也不会执行performClick后续逻辑。