The Triggering Mechanism of ACTION_CANCEL in Android and the Sliding out Case of view
- 2021-12-19 06:46:36
- OfStack
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 () eventFirst 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
:
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 callViewParent#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 receiveViewParent#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.