ACTION_CANCEL,ACTION_OUTSIDE 的触发条件与使用

5,414 阅读5分钟

前言

关于MotionEvent的其他知识可以阅读:GcsSloop 的文章,本文主要解释ACTION_CANCEL,ACTION_OUTSIDE 这两个概念 并看看源码中相关的使用例子。

一. ACTION_CANCEL 的触发条件

ChildView原先拥有事件处理权,后面由于某些原因,该处理权需要交回给上层去处理,ChildView便会收到ACTION_CANCEL事件(代码逻辑上是:上层判断之前交给ChildView的事件处理权需要收回来了,便会做事件的拦截处理,拦截时给ChildView发一个ACTION_CANCEL事件)。

举个例子:上层 View 是一个 RecyclerView,它收到了一个 ACTION_DOWN 事件,由于这个可能是点击事件,所以它先传递给对应 ItemView,询问 ItemView 是否需要这个事件,然而接下来又传递过来了一个 ACTION_MOVE 事件,且移动的方向和 RecyclerView 的可滑动方向一致,所以 RecyclerView 判断这个事件是滚动事件,于是要收回事件处理权,这时候对应的 ItemView 会收到一个 ACTION_CANCEL ,并且不会再收到后续事件。

一个简单场景即可验证:自定义两个View:Parent、Son,在Parent的onInterceptTouchEvent中,放行ACTION_DOWN事件(Down事件是一个事件序列的开始,如果Down事件都拦截了那么后续所有事件都将不再往下分发),拦截ACTION_MOVE事件,其他事件不做处理只打印log,验证log如下:

D/LinearParent: onInterceptTouchEvent: down
D/LinearSon: onTouchEvent: down
D/LinearParent: onInterceptTouchEvent: move
// 父类做了拦截,子类将不再收到move事件以及up事件,而是收到cancel事件
D/LinearSon: onTouchEvent: cancel
D/LinearParent: onTouchEvent: move
D/LinearParent: onTouchEvent: move
D/LinearParent: onTouchEvent: move
D/LinearParent: onTouchEvent: move
D/LinearParent: onTouchEvent: up

了解完该事件的出现时机我们来看看这个事件一般要怎么处理做什么操作,看看源码中的说明:

     * 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.

意思是:当前手势已经被终止了,后续将不再收到任何其他事件,所以应该把改时间当做一个up事件来看待,但是不应该再执行任何其他我们常做的操作

下面简单总结下:

  • 首先cancel事件的触发时机是特殊的,当子View至始至终没有事件处理权时,该事件是不会发生的;当子View的事件处理权从有到无时,在失去处理权的那一刻子View会被告知,也就是收到cancel事件
  • 其次,我们应该像对待up事件一样对待cancel事件,因为cancel事件的出现意味着子View至少是收到了某些事件的,比如down,又因为子View收到cancel事件后将不再收到其他事件了,所以需要在cancel事件中给当前阶段的事件处理做一个了结。
  • 另外:cancel事件不管返回 true/false 父View都是不会再收到任何事件的

说明另外一点:当子View的onTouchEvent事件返回false时要将事件抛回给父View做处理,这里抛回是有条件的:

  • 当子View在down事件中return false后,那后续子View将不再收到其他事件,此时父View拿到该down事件的处理权;

  • 当子View在down中没将事件抛回给父View(return false),而父View也没拦截其他事件(move/up...)时,则子View在后续的其他事件中做的抛回操作是没用的,例如在子View的move事件中去执行rerurn false并不能触发父View的move事件,这个也很好理解,父类没拦截表明不想处理,这才给了子View,这时候你子View再丢回来也没意思了,我要处理刚才就给你拦截下来了

二. ACTION_OUTSIDE 的触发条件

看下源码说明:

     * Constant for {@link #getActionMasked}: A movement has happened outside of the
     * normal bounds of the UI element.  This does not provide a full gesture,
     * but only the initial location of the movement/touch.
   
   也就是说:当一个事件发生在当前视图的范围之外时,该事件会被触发  ,而且不再提供完整的手势,只提供 运动/触摸 的初始位置。

你可能有些疑惑,如果初始事件发生在视图范围外,那该视图压根就不会收到任何事件回调,怎么会有上面说的这种情况呢?其实确实是有一些特殊情景的,Dialog就是该事件的典型应用场景。常见的操作是我们在Dialog的视图范围外做点击操作时,将该Dialog隐藏调,这个时候就是利用了该事件来处理的。

需要注意的是,这里DialogonTouchEvent函数中要收到ACTION_OUTSIDE事件也是有条件的,我们看一下WindowManager中的FLAG_WATCH_OUTSIDE_TOUCH的说明:

frameworks/base/core/java/android/view/WindowManager.java$LayoutParams

        /** Window flag: if you have set {@link #FLAG_NOT_TOUCH_MODAL}, you
         * can set this flag to receive a single special MotionEvent with
         * the action
         * {@link MotionEvent#ACTION_OUTSIDE MotionEvent.ACTION_OUTSIDE} for
         * touches that occur outside of your window.  Note that you will not
         * receive the full down/move/up gesture, only the location of the
         * first down as an ACTION_OUTSIDE.
         */
        public static final int FLAG_WATCH_OUTSIDE_TOUCH = 0x00040000;

也就是Dialog(本质上Dialog就是一个Window)如果想要收到ACTION_OUTSIDE事件,还需要设置FLAG_WATCH_OUTSIDE_TOUCH这个Flag。

典型应用场景:

设置了FLAG_WATCH_OUTSIDE_TOUCH这个Flag的Dialog 中重写onTouchEvent,当收到ACTION_OUTSIDE事件时表明用户点击了当前Dialog之外的区域,这个时候我们可以在这里做隐藏操作或者其他操作。

当然了,如果只是点击Dialog之外时隐藏Dialog,那可以通过dialog.setCanceledOnTouchOutside(true);简单处理(其实这个接口的实现方式就是通过ACTION_OUTSIDE事件来做处理的,实现代码在Window.java中,这里不做分析),这里只是举例该事件的使用场景之一。

Android源码应用场景

再看个Android源码应用场景:Android原生音量键的实现是一个Dialog(frameworks/base/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java):

        // 给音量键Window设置FLAG_WATCH_OUTSIDE_TOUCH 
        mDialog = new CustomDialog(mContext);
        ......
        mWindow = mDialog.getWindow();
        ......
        mWindow.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
                | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
                | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
                | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH //设置Window Flag
                | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
        ......

        // 重写onTouchEvent并处理ACTION_OUTSIDE事件
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            if (mShowing) {
                if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
                    dismissH(Events.DISMISS_REASON_TOUCH_OUTSIDE);
                    return true;
                }
            }
            return false;
        }

从而实现了点击音量键之外的区域取消音量键显示的功能。