通过与 Jira 对比,让您更全面了解 PingCode

  • 首页
  • 需求与产品管理
  • 项目管理
  • 测试与缺陷管理
  • 知识管理
  • 效能度量
        • 更多产品

          客户为中心的产品管理工具

          专业的软件研发项目管理工具

          简单易用的团队知识库管理

          可量化的研发效能度量工具

          测试用例维护与计划执行

          以团队为中心的协作沟通

          研发工作流自动化工具

          账号认证与安全管理工具

          Why PingCode
          为什么选择 PingCode ?

          6000+企业信赖之选,为研发团队降本增效

        • 行业解决方案
          先进制造(即将上线)
        • 解决方案1
        • 解决方案2
  • Jira替代方案

25人以下免费

目录

ACTION_CANCEL到底何时触发,滑出子View范围会发生什么

ACTION_CANCEL在这些时候会触发:1、父view拦截事件;2、ACTION_DOWN初始化操作;3、在子View处理事件的过程中被从父View中移除时等。父view拦截事件是指在子View处理事件的过程中,父View对事件拦截。

一、ACTION_CANCEL在这些时候会触发

1、父view拦截事件

首先要了解ViewGroup什么情况下会拦截事件,请看下面一段代码:

@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范围会发生什么

通常来说,滑出子View范围什么也不会发生。如果手指移出了子View之外,从而导致事件序列被取消,那么通常不会有太多事情发生。您的应用程序将会收到一个ACTION_CANCEL事件,但是由于事件已经被取消,您无法执行任何进一步的操作。如果您希望避免这种情况发生,您可以尝试使用requestDisallowInterceptTouchEvent()方法来防止触摸事件序列被拦截,或者重新设计您的UI以确保用户不会意外地移动手指到View的范围外。

延伸阅读1:ACTION_CANCEL作用

我们知道如果某一个子View处理了Down事件,那么随之而来的Move和Up事件也会交给它处理。但是交给它处理之前,父View还是可以拦截事件的,如果拦截了事件,那么子View就会收到一个Cancel事件,并且不会收到后续的Move和Up事件。

相关文章