Android中ACTION_CANCEL的触发机制与滑出子view的情况
目录
- ACTION_CANCEL的触发时机
- 1,父view拦截事件
- 2,ACTION_DOWN初始化操作
- 3,在子View处理事件的过程中被从父View中移除时
- 4,子View被设置了PFLAG_CANCEL_NEXT_UP_EVENT标记时
- 滑出子View区域会发生什么?
- 结论:
- 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} */@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==trueintercepted = 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 == truefinal 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_CANCELevent.setAction(MotionEvent.ACTION_CANCEL); if (child == null) {...} else {// 分发给childhandled = 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); 分发给目标viewview.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暂时detached,detached是什么意思?就是和attached相反的那个,具体什么时候打了这个标记,我觉得没必要深究。
以上四种情况最重要的就是第一种,后面的只需了解即可。
滑出子View区域会发生什么? 了解了什么情况下会触发
ACTION_CANCEL
,那么针对问题:滑出子View区域会触发ACTION_CANCEL
吗?这个问题就很明确了:不会。实践是检验真理的唯一标准,代码撸起来:
public class MyButton extends androidx.appcompat.widget.AppCompatButton { @Overridepublic 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滑出view后依然可以收到
(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
ACTION_MOVE
和ACTION_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 buttonif ((mPrivateFlags & PRESSED) != 0) {// 这里改变状态为 not PRESSED// Need to switch from pressed to not pressedmPrivateFlags &= ~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()
。【Android中ACTION_CANCEL的触发机制与滑出子view的情况】
结论:
- 滑出view范围后,如果父view没有拦截事件,则会继续受到
ACTION_MOVE
和ACTION_UP
等事件。 - 一旦滑出view范围,view会被移除
PRESSED
标记,这个是不可逆的,然后在ACTION_UP
中不会执行performClick()
等逻辑。
推荐阅读
- 热闹中的孤独
- android第三方框架(五)ButterKnife
- Shell-Bash变量与运算符
- JS中的各种宽高度定义及其应用
- 2021-02-17|2021-02-17 小儿按摩膻中穴-舒缓咳嗽
- 深入理解Go之generate
- 异地恋中,逐渐适应一个人到底意味着什么()
- 我眼中的佛系经纪人
- 《魔法科高中的劣等生》第26卷(Invasion篇)发售
- “成长”读书社群招募