一、ACTION_CANCEL在这些时候会触发
1、父view拦截事件
首先要了解ViewGroup什么情况下会拦截事件,请看下面一段代码:
@Overridepublic 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,接着往下看:
@Overridepublic 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 == truefinal 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范围会发生什么
通常来说,滑出子View范围什么也不会发生。如果手指移出了子View之外,从而导致事件序列被取消,那么通常不会有太多事情发生。您的应用程序将会收到一个ACTION_CANCEL事件,但是由于事件已经被取消,您无法执行任何进一步的操作。如果您希望避免这种情况发生,您可以尝试使用requestDisallowInterceptTouchEvent()方法来防止触摸事件序列被拦截,或者重新设计您的UI以确保用户不会意外地移动手指到View的范围外。
延伸阅读1:ACTION_CANCEL作用
我们知道如果某一个子View处理了Down事件,那么随之而来的Move和Up事件也会交给它处理。但是交给它处理之前,父View还是可以拦截事件的,如果拦截了事件,那么子View就会收到一个Cancel事件,并且不会收到后续的Move和Up事件。