软件编程
位置:首页>> 软件编程>> Android编程>> Android中ACTION_CANCEL的触发机制与滑出子view的情况

Android中ACTION_CANCEL的触发机制与滑出子view的情况

作者:涂程  发布时间:2023-08-01 14:39:09 

标签:Android,ACTION,CANCEL,触发时机

看完本文你将了解:

  • ACTION_CANCEL的触发时机

  • 滑出子View区域会发生什么?为什么不响应onClick()事件

首先看一下官方的解释:


/**
* 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的触发时机

有四种情况会触发ACTION_CANCEL:

  • 在子View处理事件的过程中,父View对事件拦截

  • ACTION_DOWN初始化操作

  • 在子View处理事件的过程中被从父View中移除时

  • 子View被设置了PFLAG_CANCEL_NEXT_UP_EVENT标记时

1,父view拦截事件

首先要了解ViewGroup什么情况下会拦截事件,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;
       // 判断条件一
       if (actionMasked == MotionEvent.ACTION_DOWN
               || mFirstTouchTarget != null) {
           final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
           // 判断条件二
           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;
       }
       ...
   }
   ...
}

有两个条件

  • MotionEvent.ACTION_DOWN事件或者mFirstTouchTarget非空也就是有子view在处理事件

  • 子view没有做拦截,也就是没有调用ViewParent#requestDisallowInterceptTouchEvent(true)

如果满足上面的两个条件才会执行onInterceptTouchEvent(ev)
如果ViewGroup拦截了事件,则intercepted变量为true,接着往下看:


@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) {
               // 当mFirstTouchTarget != null,也就是子view处理了事件
               // 此时如果父ViewGroup拦截了事件,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 {
                   // 判断一:此时cancelChild == true
                   final boolean cancelChild = resetCancelNextUpFlag(target.child)
                           || intercepted;

// 判断二:给child发送cancel事件
                   if (dispatchTransformedTouchEvent(ev, cancelChild,
                           target.child, target.pointerIdBits)) {
                       handled = true;
                   }
                   ...
               }
               ...
           }
       }
       ...
   }
   ...
   return handled;
}

以上判断一处cancelChild为true,然后进入判断二中一看究竟:


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设置成ACTION_CANCEL
       event.setAction(MotionEvent.ACTION_CANCEL);
       if (child == null) {
           ...
       } else {
           // 分发给child
           handled = child.dispatchTouchEvent(event);
       }
       event.setAction(oldAction);
       return handled;
   }
   ...
}

当参数cancel为ture时会将event设置为MotionEvent.ACTION_CANCEL,然后分发给child。

2,ACTION_DOWN初始化操作


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.
           // 取消并清除所有的Touch目标
           cancelAndClearTouchTargets(ev);
           resetTouchState();
   }
   ...
   }
   ...
}

系统可能会由于App切换、ANR等原因丢失了up,cancel事件。

因此需要在ACTION_DOWN时丢弃掉所有前面的状态,具体代码如下:


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);
           // 分发事件同情况一
           dispatchTransformedTouchEvent(event, true, target.child, target.pointerIdBits);
       }
       ...
   }
}

PS:在dispatchDetachedFromWindow()中也会调用cancelAndClearTouchTargets()

3,在子View处理事件的过程中被从父View中移除时


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) {
           ...
           // 创建ACTION_CANCEL事件
           MotionEvent event = MotionEvent.obtain(now, now,
                   MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
           event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
           分发给目标view
           view.dispatchTouchEvent(event);
           event.recycle();
           return;
       }
       predecessor = target;
       target = next;
   }
}

4,子View被设置了PFLAG_CANCEL_NEXT_UP_EVENT标记时

在情况一种的两个判断处:


// 判断一:此时cancelChild == true
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;

// 判断二:给child发送cancel事件
if (dispatchTransformedTouchEvent(ev, cancelChild,
   target.child, target.pointerIdBits)) {
   handled = true;
}

resetCancelNextUpFlag(target.child) 为true时同样也会导致cancel,查看代码:


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

根据注释大概意思是,该view暂时detached,detached是什么意思?就是和attached相反的那个,具体什么时候打了这个标记,我觉得没必要深究。

以上四种情况最重要的就是第一种,后面的只需了解即可。

滑出子View区域会发生什么?

了解了什么情况下会触发ACTION_CANCEL,那么针对问题:滑出子View区域会触发ACTION_CANCEL吗?这个问题就很明确了:不会。

实践是检验真理的唯一标准,代码撸起来:


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);
   }
}

一波操作以后日志如下:

(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

滑出view后依然可以收到ACTION_MOVEACTION_UP事件。

为什么有人会认为滑出view后会收到ACTION_CANCEL呢?

我想是因为滑出view后,view的onClick()不会触发了,所以有人就以为是触发了ACTION_CANCEL

那么为什么滑出view后不会触发onClick呢?再来看看View的源码:

在view的onTouchEvent()中:


case MotionEvent.ACTION_MOVE:
   // Be lenient about moving outside of buttons
// 判断是否超出view的边界
   if (!pointInView(x, y, mTouchSlop)) {
       // Outside button
       if ((mPrivateFlags & PRESSED) != 0) {
           // 这里改变状态为 not PRESSED
           // Need to switch from pressed to not pressed
           mPrivateFlags &= ~PRESSED;
       }
   }
   break;

case MotionEvent.ACTION_UP:
   boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
   // 可以看到当move出view范围后,这里走不进去了
   if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
       ...
       performClick();
       ...
   }
   mIgnoreNextUpEvent = false;
   break;

1,在ACTION_MOVE中会判断事件的位置是否超出view的边界,如果超出边界则将mPrivateFlags置为not PRESSED状态。
2,在ACTION_UP中判断只有当mPrivateFlags包含PRESSED状态时才会执行performClick()等。
因此滑出view后不会执行onClick()

结论:

  • 滑出view范围后,如果父view没有拦截事件,则会继续受到ACTION_MOVEACTION_UP等事件。

  • 一旦滑出view范围,view会被移除PRESSED标记,这个是不可逆的,然后在ACTION_UP中不会执行performClick()等逻辑。

来源:https://blog.csdn.net/u012165769/article/details/120162218

0
投稿

猜你喜欢

手机版 软件编程 asp之家 www.aspxhome.com