Android MotionEvent 之ACTION_CANCEL

28
五月
2021

前言

对于Android MotionEvent,我们平时大多关注的是ACTION_DOWN、ACTION_UP、ACTION_MOVE,本篇将重点分析ACTION_CANCEL 产生的原因及其滑动事件的处理。
通过本篇文章,你将了解到:

1、ACTION_CANCEL 产生的原因
2、手指离开当前View时事件处理
3、手指离开屏幕时事件处理

1、ACTION_CANCEL 产生的原因

从ViewGroup 入手分析

事件分发是从ViewGroup-->View,因此想要知道View是否收到ACTION_CANCEL,需要从ViewGroup入手,而ViewGroup 分发的重点即在dispatchTouchEvent(xx)里。

先看看dispatchTouchEvent(xx) 代码,之前的文章有详细分析过,此次重点关注
ACTION_CANCEL的处理逻辑:

 

#ViewGroup.java
    @Override
    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) {
                //首次Down事件处理------------>(1)
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }
            //ViewGroup是否拦截事件
            ...
            //是否发送取消事件
            final boolean canceled = resetCancelNextUpFlag(this)
                    || actionMasked == MotionEvent.ACTION_CANCEL;

            //寻找接收了Down事件的子View(子布局)
            ...
            if (mFirstTouchTarget == null) {
                //没有子View(子布局)消费事件,于是事件流转到ViewGroup onTouchEvent
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                //有子View(子布局)消费事件
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while (target != null) {
                    //遍历消费链
                    final TouchTarget next = target.next;
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        //已经消费过,直接返回
                        handled = true;
                    } else {
                        //判断是否需要发送取消事件------------>(2)
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                        //分发事件------------>(3)
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                        if (cancelChild) {
                            //如果是取消事件,则子View(子布局)没必要消费了,因此从消费链里摘除
                            if (predecessor == null) {
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            target.recycle();
                            target = next;
                            continue;
                        }
                    }
                    predecessor = target;
                    target = next;
                }
            }
            ...
        }
        ...
        return handled;
    }

上面列出了三个重点,将一一分析:
(1)
主要是cancelAndClearTouchTargets(xx)方法:

 

#ViewGroup.java
    private void cancelAndClearTouchTargets(MotionEvent event) {
        //子View(子布局) 消费了Down事件
        if (mFirstTouchTarget != null) {
            boolean syntheticEvent = false;
            ...
            for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
                resetCancelNextUpFlag(target.child);
                //分发cancel 事件
                dispatchTransformedTouchEvent(event, true, target.child, target.pointerIdBits);
            }
            ...
        }
    }

可以看出,最终调用了dispatchTransformedTouchEvent(xx)发送cancel事件。
按照正常流程来说,因为收到Down事件时,mFirstTouchTarget==null,因此此处通常不会执行发送cancel事件。
(2)
resetCancelNextUpFlag(xx)方法,顾名思义:重置取消标记。

 

#ViewGroup.java
    private static boolean resetCancelNextUpFlag(@NonNull View view) {
        if ((view.mPrivateFlags & PFLAG_CANCEL_NEXT_UP_EVENT) != 0) {
            //之前设置过取消标记,此处重置
            view.mPrivateFlags &= ~PFLAG_CANCEL_NEXT_UP_EVENT;
            //返回true
            return true;
        }
        return false;
    }

PFLAG_CANCEL_NEXT_UP_EVENT 标记的作用是记录View是否被临时移出Window。比如View.performButtonActionOnTouchDown(xx)处理鼠标相关的问题,这个值平时也很少用到。

既要判断resetCancelNextUpFlag(xx)返回值,也要判断intercepted值:ViewGroup是否拦截并消费了事件。
若是两者之一有一者满足,则认为需要发送cancel事件。

(3)
(2)点仅仅是判断是否需要发送cancel事件,真正发送事件的方法是dispatchTransformedTouchEvent(xx):

 

#ViewGroup.java
    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;

        final int oldAction = event.getAction();
        //判断参数cancel 是否为true
        if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
            //将event 事件设置为cancel
            event.setAction(MotionEvent.ACTION_CANCEL);
            if (child == null) {
                //ViewGroup自己消费
                handled = super.dispatchTouchEvent(event);
            } else {
                //子View(子布局) 消费
                handled = child.dispatchTouchEvent(event);
            }
            //处理后,将action重置
            event.setAction(oldAction);
            return handled;
        }
        //正常事件流程
    }

结合上述3点可知,在ViewGroup.dispatchTouchEvent(xx)里发送cancel事件,常用的判断即是:

1、ViewGroup是否拦截消费了事件,若是则给子View(子布局)发送cancel事件。
2、发送给子View cancel事件后,后续的事件将不会发给子View。

那么除了ViewGroup 拦截消费事件,还有哪些地方触发发送cancel 事件呢?

ViewGroup 移除View

想象一种场景:

手指在View 上滑动,在此过程中,View 被移出ViewGroup。

来看ViewGroup.remove(xx)的实现:

removeView(view)-->removeViewInternal(view)-->removeViewInternal(index, view)

核心功能在removeViewInternal(index, view) 实现:

 

#ViewGroup.java
    private void removeViewInternal(int index, View view) {
        ...

        view.clearAccessibilityFocus();

        //发送cancel 事件
        cancelTouchTarget(view);
        cancelHoverTarget(view);

        if (view.getAnimation() != null ||
                (mTransitioningViews != null && mTransitioningViews.contains(view))) {
            addDisappearingView(view);
        } else if (view.mAttachInfo != null) {
           //调用detached
           view.dispatchDetachedFromWindow();
        }
        ...
    }

可以看出,当View 从ViewGroup 移除后,若是它已经消费了事件,那么将会收到cancel 事件。
使用如下代码测试:

 

        viewGroup.postDelayed(new Runnable() {
            @Override
            public void run() {
                viewGroup.removeAllViews();
            }
        }, 3000);

手指按在View 上,3s后将View 从ViewGroup里移除。

Window 移除View

当调用WindowManager.removew(view)方法时,最终会调用到ViewGroup.dispatchDetachedFromWindow(xx)方法:

 

##ViewGroup.java
    void dispatchDetachedFromWindow() {
        //发送 cancel 事件
        cancelAndClearTouchTargets(null);
        ...
    }

使用如下代码测试:

 

        viewGroup.postDelayed(new Runnable() {
            @Override
            public void run() {
                getWindowManager().removeView(getWindow().getDecorView());
            }
        }, 3000);

手指按在View 上,3s后将View 从Window里移除。

可以看出,触发发送cancel 事件常见的有三种场景:

2、手指离开当前View时事件处理
 

如上图,手指在View 上按下,View消费了Down事件,此时手指滑出View,并在ViewGroup上滑动,那么整个事件分发是怎么样的呢?
通常来说,单指滑动的时候事件分为三个过程:

1、按下事件--->Down 事件。
2、滑动事件--->Move 事件。
3、抬起事件--->Up 事件。

从事件分发流程可知,当手指按下的时候:

1、若View 消费了Down事件,那么之后的Move、Up事件都会传递给View(不考虑ViewGroup拦截情况)。
2、手指在滑动的过程中,滑出了View,在ViewGroup上滑动,此时Move、Up事件依然会传递给View。

而View 本身是支持响应长按与点击动作的,若是当前手指还在View 上,那么很好理解,若是当前手指已经滑动到ViewGroup上,长按与点击动作如何响应呢?
从直觉上来说,应该不响应的。
如何做到不响应的?理论上来说:因为可以知道每个MotionEvent的触摸坐标,通过该坐标就可以得知当前MotionEvent是否落在目标View 上,若坐标不在目标View(消费Down事件的View),那么就不响应长按与点击动作。
来看看源码里是如何处理的:

 

#View.java
    public boolean onTouchEvent(MotionEvent event) {
        //点击的坐标
        final float x = event.getX();
        final float y = event.getY();
        ...

        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    ...
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        //处在按下状态
                        ...

                        //没有发生长按动作
                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            //移除长按动作
                            removeLongPressCallback();

                            if (!focusTaken) {
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                //响应点击动作
                                if (!post(mPerformClick)) {
                                    performClickInternal();
                                }
                            }
                        }
                        ...
                    }
                    mIgnoreNextUpEvent = false;
                    break;

                case MotionEvent.ACTION_DOWN:
                    ...
                    break;

                case MotionEvent.ACTION_CANCEL:
                    if (clickable) {
                        //重置按下状态
                        setPressed(false);
                    }
                    //移除延时单击动作
                    removeTapCallback();
                    //移除长按动作
                    removeLongPressCallback();
                    ...
                    break;

                case MotionEvent.ACTION_MOVE:
                    ...

                    if (!pointInView(x, y, touchSlop)) {
                        //移除延时单击动作
                        removeTapCallback();
                        //移除长按动作
                        removeLongPressCallback();
                        if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                            重置按下状态
                            setPressed(false);
                        }
                        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    }
                    ...

                    break;
            }

            return true;
        }

        return false;
    }

A、正常响应点击/长按动作

对于正常情况来说,比如上面的对ACTION_UP 事件的处理。
先说单击动作:

1、首先需要当前View 处在按下状态。
2、其次长按动作没有发生。

再说长按动作:

长按动作是个延时执行任务,若是没被取消,则终将会被执行。

B、移动时处理点击/长按动作

由A点可知,若是手指没有滑出当前View,那么点击与长按按照正常流程走。若是滑动出了View,在ACTION_MOVE事件处理逻辑如下:

1、一直在监测当前的事件点坐标是否还在目标View里,若不在,则进行第2步处理。
2、发现事件点坐标已经不在目标View里,于是移除延时单击动作、移除长按动作、重置按下状态。
3、当目标View收到ACTION_UP事件后,发现按下状态为false,于是不响应单击动作,而延时长按动作已经被取消了,也不会响应长按动作。

C、总结

1、一旦View消费了Down事件,那么后续的Move、Up、Cancel等事件都会交给它处理(ViewGroup没有拦截消费的前提下)
2、即使滑动超出了当前View的范围,它依然能够收到上述事件。
3、若是最终的Up事件不是发生在当前View之上,那么该View不响应单击与长按动作。
4、若是收到Cancel事件,那么View就不会再响应单击与长按动作,并且后续的事件将不会再收到。

3、手指离开屏幕时事件处理

当手指滑动离开屏幕时,如下图:

其处理逻辑与手指离开当前View时事件处理是一致的。

本文基于Android 10。
各种事件Demo请移步:相关代码演示

您若喜欢,请点赞、关注,您的鼓励是我前进的动力

持续更新中,和我一起步步为营系统、深入学习Android/Java

1、Android各种Context的前世今生
2、Android DecorView 一窥全貌(上)
3、Android DecorView 一窥全貌(下)
4、Window/WindowManager 不可不知之事
5、View Measure/Layout/Draw 真明白了
6、Android事件分发全套服务
7、Android invalidate/postInvalidate/requestLayout 彻底厘清
8、Android Window 如何确定大小/onMeasure()多次执行原因
9、Android事件驱动Handler-Message-Looper解析
10、Android 键盘一招搞定
11、Android 各种坐标彻底明了
12、Android Activity/Window/View 的background
13、Android IPC 之Service 还可以这么理解
14、Android IPC 之Binder基础
15、Android IPC 之Binder应用
16、Android IPC 之AIDL应用(上)
17、Android IPC 之AIDL应用(下)
18、Android IPC 之Messenger 原理及应用
19、Android IPC 之获取服务(IBinder)
20、Android 存储基础
21、Android 10、11 存储完全适配(上)
22、Android 10、11 存储完全适配(下)
23、Java 并发系列不再疑惑

作者:fishforest
链接:https://www.jianshu.com/p/0a8ec531d5fb
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

 

 

 

 

 

 

 

 

TAG

网友评论

共有访客发表了评论
请登录后再发布评论,和谐社会,请文明发言,谢谢合作! 立即登录 注册会员