The Triggering Mechanism of ACTION_CANCEL in Android and the Sliding out Case of view

  • 2021-12-19 06:46:36
  • OfStack

What happens when the directory ACTION_CANCEL is triggered 1, the parent view intercepts the event 2, the ACTION_DOWN initializes the operation 3, when the child View is removed from the parent View during the event processing 4, and when the child View slips out of the child View area when the PFLAG_CANCEL_NEXT_UP_EVENT flag is set? Conclusions:

After reading this article, you will know:

Triggering Timing of ACTION_CANCEL What happens when you slip out of the View region? Why not respond to the onClick () event

First of all, look at the official explanation:


/**
 * 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;

In human terms, the current gesture has been aborted, and you will not receive any more events. You can treat it as an ACTION_UP event, but do not perform normal logic.

Trigger Timing of ACTION_CANCEL

There are four conditions that trigger ACTION_CANCEL :

While the child View handles the event, the parent View intercepts the event ACTION_DOWN initialization operation When the child View is removed from the parent View during the event processing When the child View is set with the PFLAG_CANCEL_NEXT_UP_EVENT flag

1. Parent view intercepts events

First of all, we need to know when ViewGroup intercepts events. Look the Fuck Resource Code:


/**
 * {@inheritDoc}
 */
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
	...

    boolean handled = false;
    if (onFilterTouchEventForSecurity(ev)) {
        final int action = ev.getAction();
        final int actionMasked = action & MotionEvent.ACTION_MASK;
		...
        // Check for interception.
        final boolean intercepted;
        //  Judgement condition 1
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            //  Judgement condition 2
            if (!disallowIntercept) {
                intercepted = onInterceptTouchEvent(ev);
                ev.setAction(action); // restore action in case it was changed
            } else {
                intercepted = false;
            }
        } else {
            // There are no touch targets and this action is not an initial down
            // so this view group continues to intercept touches.
            intercepted = true;
        }
        ...
    }
    ...
}

There are two conditions

MotionEvent.ACTION_DOWN event or mFirstTouchTarget is not null, that is, there is a child view processing the event Subchild view did not intercept, that is, did not call ViewParent#requestDisallowInterceptTouchEvent(true)

If the above two conditions are met, it will be executed onInterceptTouchEvent(ev) .
If ViewGroup intercepts events, then intercepted The variable is true, and then look down:


@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    
    boolean handled = false;
    if (onFilterTouchEventForSecurity(ev)) {
        ...

        // Check for interception.
        final boolean intercepted;
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {
                //  When mFirstTouchTarget != null , that is, children view Handled the event 
                //  At this time, if the parent ViewGroup Intercepted the incident, intercepted==true
                intercepted = onInterceptTouchEvent(ev);
                ev.setAction(action); // restore action in case it was changed
            } else {
                intercepted = false;
            }
        } else {
            // There are no touch targets and this action is not an initial down
            // so this view group continues to intercept touches.
            intercepted = true;
        }

        ...

        // Dispatch to touch targets.
        if (mFirstTouchTarget == null) {
            ...
        } 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) {
                    ...
                } else {
                    //  Judge 1 : At this point cancelChild == true
                    final boolean cancelChild = resetCancelNextUpFlag(target.child)
                            || intercepted;

					//  Judge 2 Here child Send cancel Events 
                    if (dispatchTransformedTouchEvent(ev, cancelChild,
                            target.child, target.pointerIdBits)) {
                        handled = true;
                    }
                    ...
                }
                ...
            }
        }
        ...
    }
    ...
    return handled;
}

The above judgment is 1 cancelChild Is true, and then go to 1 in judgment 2 to see what is going on:


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) {
        //  Will event Set to ACTION_CANCEL
        event.setAction(MotionEvent.ACTION_CANCEL);
        if (child == null) {
            ...
        } else {
            //  Distribute to child
            handled = child.dispatchTouchEvent(event);
        }
        event.setAction(oldAction);
        return handled;
    }
    ...
}

When the parameter cancel is ture, event is set to MotionEvent.ACTION_CANCEL and distributed to child.

2. ACTION_DOWN initialization operation


public boolean dispatchTouchEvent(MotionEvent ev) {

    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.
            //  Cancel and clear all Touch Objectives 
            cancelAndClearTouchTargets(ev);
            resetTouchState();
    	}
    	...
    }
    ...
}

The system may lose up and cancel events due to App switching, ANR and other reasons.

Therefore, you need to discard all previous states at ACTION_DOWN, as follows:


private void cancelAndClearTouchTargets(MotionEvent event) {
    if (mFirstTouchTarget != null) {
        boolean syntheticEvent = false;
        if (event == null) {
            final long now = SystemClock.uptimeMillis();
            event = MotionEvent.obtain(now, now,
                    MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
            event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
            syntheticEvent = true;
        }

        for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
            resetCancelNextUpFlag(target.child);
            //  Distribute events and situations 1
            dispatchTransformedTouchEvent(event, true, target.child, target.pointerIdBits);
        }
        ...
    }
}

PS: In dispatchDetachedFromWindow() Will also be called in cancelAndClearTouchTargets()

3. When the child ES 120EN is removed from the parent ES 121EN during the processing of the event


public void removeView(View view) {
    if (removeViewInternal(view)) {
        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) {

    ...
    cancelTouchTarget(view);
	...
}

private void cancelTouchTarget(View view) {
    TouchTarget predecessor = null;
    TouchTarget target = mFirstTouchTarget;
    while (target != null) {
        final TouchTarget next = target.next;
        if (target.child == view) {
            ...
            //  Create ACTION_CANCEL Events 
            MotionEvent event = MotionEvent.obtain(now, now,
                    MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
            event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
             Distribute to targets view
            view.dispatchTouchEvent(event);
            event.recycle();
            return;
        }
        predecessor = target;
        target = next;
    }
}

4. When the child View is marked with PFLAG_CANCEL_NEXT_UP_EVENT

In the two judgments of Case 1:


//  Judge 1 : At this point cancelChild == true
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;

//  Judge 2 Here child Send cancel Events 
if (dispatchTransformedTouchEvent(ev, cancelChild,
    target.child, target.pointerIdBits)) {
    handled = true;
}

When resetCancelNextUpFlag(target.child) true also causes cancel, see the code:


/**
 * Indicates whether the view is temporarily detached.
 *
 * @hide
 */
static final int PFLAG_CANCEL_NEXT_UP_EVENT        = 0x04000000;

private static boolean resetCancelNextUpFlag(View view) {
    if ((view.mPrivateFlags & PFLAG_CANCEL_NEXT_UP_EVENT) != 0) {
        view.mPrivateFlags &= ~PFLAG_CANCEL_NEXT_UP_EVENT;
        return true;
    }
    return false;
}

According to the general meaning of the annotation, what is the meaning of view temporarily detached and detached? It is the one opposite to attached. I don't think it is necessary to delve into when this mark was made.

The most important of the above four situations is the first one, and the latter one only needs to be understood.

What happens when you slip out of the View region?

Know what will trigger ACTION_CANCEL For the problem: Sliding out of the View region will trigger ACTION_CANCEL Is it? The question is clear: No.

Practice is the only criterion for testing truth, and the code is rolled up:


public class MyButton extends androidx.appcompat.widget.AppCompatButton {

	@Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                LogUtil.d("ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                LogUtil.d("ACTION_MOVE");
                break;
            case MotionEvent.ACTION_UP:
                LogUtil.d("ACTION_UP");
                break;
            case MotionEvent.ACTION_CANCEL:
                LogUtil.d("ACTION_CANCEL");
                break;
        }
        return super.onTouchEvent(event);
    }
}

The log after 1 wave operation is as follows:

(MyButton.java:32) -- > ACTION_DOWN
(MyButton.java:36) -- > ACTION_MOVE
(MyButton.java:36) -- > ACTION_MOVE
(MyButton.java:36) -- > ACTION_MOVE
(MyButton.java:36) -- > ACTION_MOVE
(MyButton.java:36) -- > ACTION_MOVE
(MyButton.java:39) -- > ACTION_UP

You can still receive it after sliding out of view ViewParent#requestDisallowInterceptTouchEvent(true)0 And ACTION_UP Events.

Why would anyone think that after sliding out of view, they will receive ACTION_CANCEL What about?

I think it's because after sliding out of view, view's onClick() It won't trigger, so some people think it triggered ACTION_CANCEL .

Then why won't it trigger after sliding out of view onClick What about? Let's take a look at the source code of View:

In view onTouchEvent() Medium:


/**
 * {@inheritDoc}
 */
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
	...

    boolean handled = false;
    if (onFilterTouchEventForSecurity(ev)) {
        final int action = ev.getAction();
        final int actionMasked = action & MotionEvent.ACTION_MASK;
		...
        // Check for interception.
        final boolean intercepted;
        //  Judgement condition 1
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            //  Judgement condition 2
            if (!disallowIntercept) {
                intercepted = onInterceptTouchEvent(ev);
                ev.setAction(action); // restore action in case it was changed
            } else {
                intercepted = false;
            }
        } else {
            // There are no touch targets and this action is not an initial down
            // so this view group continues to intercept touches.
            intercepted = true;
        }
        ...
    }
    ...
}
0

1. In ViewParent#requestDisallowInterceptTouchEvent(true)0 Will determine whether the location of the event exceeds the boundary of view, and if it exceeds the boundary, it will mPrivateFlags Set to not PRESSED Status.
2. In ACTION_UP Judge only if mPrivateFlags Include PRESSED Status is executed only when performClick() Wait.
Therefore, it will not be executed after sliding out of view onClick() .

Conclusions:

After sliding out of the view range, if the parent view does not intercept events, it will continue to receive ViewParent#requestDisallowInterceptTouchEvent(true)0 And ACTION_UP Events such as. 1 Once you slip out of the view range, view will be removed PRESSED Mark, this is irreversible, and then in the ACTION_UP Will not be executed in performClick() Wait for logic.

Related articles: