从责任链模式看Android事件分发


/   今日科技快讯   /

昨日,作为世界上最大的存储芯片制造商,三星电子表示,尽管人们仍对新冠肺炎疫情感到担忧,但随着经济复苏,预计各种芯片产品的需求将会增长。三星设备解决方案(DS)部门负责人金基南表示,今年的“不确定性”将持续,但全球芯片需求将会增加。

/   作者简介   /

本篇文章来自伤心的猪大肠同学投稿,剖析了Android事件分发机制,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章!

伤心的猪大肠的博客地址:

https://juejin.cn/user/4441682709326958/posts

/   背景   /

事件分发机制在 Android 开发领域是非常重要的一个环节,本文将从事件的起源,分发流程,逐步深入分析事件分发机制。本文代码基于 Android 8.1。

/   正文   /

首先我们先从 Activity 启动流程 和 系统启动流程 讲起。

  • Activity 启动流程

熟悉 Activity启动流程的朋友们知道(不知道也不影响,跟着我的描述走),handleLaunchActivity 到 performLaunchActivity,performLaunchActiviy 中通过反射创建 Activity,同时进行 activity.attch 方法为其关联运行过程中需要的一系列上下文对象,同时在 attach 方法中创建 PhoneWindow 对象,然后执行 onCreate 方法中的 setContentView 方法,该方法中创建 DecorView,设置 contentView 并通过 onContentChange 来回调;

在 handleResumeActivity 中先执行 onResume 方法,然后再执行 r.activity.makeVisible方法,此时DecorView和Activity进行关联,然后设置DecorView可见。

   void makeVisible() {
        if (!mWindowAdded) {
            ViewManager wm = getWindowManager();
              //DecorView和WindowManager进行关联。
            wm.addView(mDecor, getWindow().getAttributes());
            mWindowAdded = true;
        }
              //设置DecorView可见
        mDecor.setVisibility(View.VISIBLE);
    }

可以看到会执行 wm.addView方法,而 WindowManagerGlobal 才是真正的实现者, 在该方法中会创建 ViewRootImpl,通过 ViewRootImpl.setView 方法来更新界面并完成 window 的添加过程。

//ViewRootImpl.java
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
···
//执行measure、layout、draw等, onDraw不一定会执行
requestLayout();
···  
mInputChannel = new InputChannel();  
···  
  // mWindowSession的类型是IWindowSession,它是一个Binder对象,真正的实现类是Session,也就是Window的添加过程是一次IPC调用。
  //addToDisplay() 将调用 WMS  mService.addWindow()
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                            getHostVisibility(), mDisplay.getDisplayId(),
                            mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
···
//输入事件接收器初始化                                  
mInputEventReceiver = new WindowInputEventReceiver(mInputChannel,
                            Looper.myLooper());
  ···                          
}                            

这里我们需要知道 Android 将输入事件 InputEvent 分为 KeyEvent 和 MotionEvent,我们的屏幕触摸事件是 MotionEvent ,MotionEvent类有多个 obtain()方法,因为MotionEvent存在大量的创建与释放,所以里面构建了一个大小为MAX_RECYCLED (10)的池,以便于复用。这些事件统一由系统输入管理器 InputManager 进行分发。

  • 系统启动流程

在 SystemServer.startOtherServices中 会启动 IMS(InputManagerService),让 InputReaderThread InputDispatcherThread 两个线程跑起来,最终调用 startDispatchCycleLocked 通过 socket 把事件发出去,WindowInputEventReceiver 通过 socket 来接收,通过查看源码我们可以看到会有一个 InputChannel,InputChannel 是一个管道(pipe),底层实现也就是我们前面说的 socket。所以 Window 和 IMS 之间的通信是通过 InputChannel(socket)。

到现在我们理清了输入管理器 InputManager 是什么时候和 Window,ViewRootImpl 进行关联的。用户输入事件后,通过 InputChannel(socket)将事件发出来,Window 通过 ViewRootImpl(里面有 WindowInputEventReceiver)交给应用层 UI 进行处理。

    final class WindowInputEventReceiver extends InputEventReceiver {
        public WindowInputEventReceiver(InputChannel inputChannel, Looper looper) {
            super(inputChannel, looper);
        }
        @Override
        public void onInputEvent(InputEvent event, int displayId) {
            enqueueInputEvent(event, this, 0, true); //将事件加入队列
        }
        @Override
        public void onBatchedInputEventPending() {···}
        @Override
        public void dispose() {···}
    }                      

走到这里,突然感觉 Window 只是起个关联的作用,当 ViewRootImpl 里面的 WindowInputEventReceiver 初始化后,WindowInputEventReceiver 自己会接收用户的输入事件,其他的好像就和 Window 没什么关系了。

事件分发中的责任链模式(外层责任链)

由于事件分发具有不同的阶段,在 Android 系统中使用责任链模式为输入事件按顺序分阶段进行分发处理,我们先来讲解外层责任链,这里的外层责任链指的是实现了 InputStage 模板的责任链,更多的是一种事件传递总体上的责任链。

    abstract class InputStage {
        private final InputStage mNext;
          ···
    }            

类InputStage提供了职责链的模板,也提供了一系列onProcess、forward、finish、apply方法,其目的也不言而喻:提供子类进行扩展的便捷。

// ----------------- InputStage的子类 ----------------------------
// 将预先输入事件提供给视图层次结构。视图预处理输入法事件阶段,将输入法的事件派发到视图的树。
final class ViewPreImeInputStage extends InputStage {}
// 执行事后输入事件的早期处理。输入法早期处理阶段。
final class EarlyPostImeInputStage extends InputStage {}
// 将后期输入事件提供给视图层次结构。视图输入处理阶段,主要处理按键、轨迹球、手指触摸及一般性的运动事件,触摸事件的分发对象是View。
final class ViewPostImeInputStage extends InputStage {}
// 从未处理的输入事件执行新输入事件的合成。综合性的事件处理阶段,该类主要轨迹球、操作杆、导航面板及未捕获的事件使用键盘进行处理。
final class SyntheticInputStage extends InputStage {}
// 用于实现支持输入事件的异步和无序处理的输入流水线级的基类。
abstract class AsyncInputStage extends InputStage {}
// ----------------- AsyncInputStage的子类----------------------------
// 将预先输入事件提供给 NativeActivity。本地方法预处理输入法事件阶段,可用于实现类似adb 输入的功能。
final class NativePreImeInputStage extends AsyncInputStage
            implements InputQueue.FinishedInputEventCallback {}
// 将预先输入事件提供给视图层次结构。输入法事件处理阶段,处理一些输入法字符等。如果对输入的内容无法识别,则继续往下转发。
final class ImeInputStage extends AsyncInputStage
            implements InputMethodManager.FinishedInputEventCallback {}
// 将事后输入事件提交到 NativeActivity 本地方法处理阶段,则构建可延迟的重用队列,此时执行操作将会异步回调结果。
final class NativePostImeInputStage extends AsyncInputStage
            implements InputQueue.FinishedInputEventCallback {}    

然后下面就是责任链模式的拼装

//ViewRootImpl#setView()
// Set up the input pipeline.
CharSequence counterSuffix = attrs.getTitle();
mSyntheticInputStage = new SyntheticInputStage();
InputStage viewPostImeStage = new ViewPostImeInputStage(mSyntheticInputStage);
InputStage nativePostImeStage = new NativePostImeInputStage(viewPostImeStage,
"aq:native-post-ime:" + counterSuffix);
InputStage earlyPostImeStage = new EarlyPostImeInputStage(nativePostImeStage);
InputStage imeStage = new ImeInputStage(earlyPostImeStage,
"aq:ime:" + counterSuffix);
InputStage viewPreImeStage = new ViewPreImeInputStage(imeStage);
InputStage nativePreImeStage = new NativePreImeInputStage(viewPreImeStage,
"aq:native-pre-ime:" + counterSuffix);

mFirstInputStage = nativePreImeStage;
mFirstPostImeInputStage = earlyPostImeStage;
···  

最终构造成如下的职责链:mSyntheticInputStage --> viewPostImeStage --> nativePostImeStage --> earlyPostImeStage --> imeStage --> viewPreImeStage --> nativePreImeStage。

我们再来看看前面 WindowInputEventReceiver.onInputEvent()中的 enqueueInputEvent的代码。

    void enqueueInputEvent(InputEvent event,
            InputEventReceiver receiver, int flags, boolean processImmediately) {
        adjustInputEventForCompatibility(event);
        QueuedInputEvent q = obtainQueuedInputEvent(event, receiver, flags);

        // Always enqueue the input event in order, regardless of its time stamp.
        // We do this because the application or the IME may inject key events
        // in response to touch events and we want to ensure that the injected keys
        // are processed in the order they were received and we cannot trust that
        // the time stamp of injected events are monotonic.
                ···
    }

可以看到这段注释,输入的事件始终按照顺序处理,而不是根据时间。同时也按照责任链模式的顺序来处理。

那我们再来看看责任链模式中这些 XxxInputStage 都对应处理些什么内容,总得来说是经过{综合性处理阶段}到{视图处理阶段}到{本地处理阶段},接着从{早期输入法处理阶段}到{输入法阶段}到{输入法视图预处理阶段}最后到{输入法本地预处理阶段}。我们平时常讲的事件分发阶段就在视图处理阶段

事件在 DecorView,Activity,Window 的传递顺序

视图处理阶段对应 ViewPostImeInputStage 这个类,事件的分发在 processPointerEvent 方法中

        private int processPointerEvent(QueuedInputEvent q) {
            final MotionEvent event = (MotionEvent)q.mEvent;

            mAttachInfo.mUnbufferedDispatchRequested = false;
            mAttachInfo.mHandlingPointerEvent = true;
              //分发事件,mView 是DecorView(FrameLayout)
            boolean handled = mView.dispatchPointerEvent(event);
            ···
        }

熟悉的朋友都知道 Activity 的根 View 是个是 DecorView,看到这里你可能以为用户触摸事件直接通过 DecorView 一直传递到最下面的 View,实际上不是这样的。从上文中责任链模式来看,在 ViewPostImeInputStage 接受到事件后,事件传递给 DecorView,我们再来看看 DecorView 是如何传递的。

//DecorView.java
          @Override
407    public boolean dispatchTouchEvent(MotionEvent ev) {
408        final Window.Callback cb = mWindow.getCallback();
409        return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
410                ? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);

DecorView.dispatchTouchEvent 中会调用 Window.Callback 的 dispatchTouchEvent 方法, 我们再来看看这个 Window.Callback 实例是什么,在 Activity.attach 方法中可以看到

//Activity#attach
mWindow = new PhoneWindow(this, window, activityConfigCallback);  
···
mWindow.setCallback(this);  //Activity实现了Window.Callback接口

//Activity#dispatchTouchEvent
3303    public boolean dispatchTouchEvent(MotionEvent ev) {
3304        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
3305            onUserInteraction();
3306        }
3307        if (getWindow().superDispatchTouchEvent(ev)) {
3308            return true;
3309        }
3310        return onTouchEvent(ev);
3311    }

Window 是一个抽象类,真正的实现类是 PhoneWindow。

//PhoneWindow.java
1827    @Override
1828    public boolean superDispatchTouchEvent(MotionEvent event) {
1829        return mDecor.superDispatchTouchEvent(event);

所以事件最终又传递到了 DecorView 中,WindowInputEventReceiver 接受到事件后交给 DecorView 处理,DecorView.dispatchTouchEvent -> Activity -> PhoneWindow -> DecorView.superDispatchTouchEvent ,**系统经过这么一层处理,可以将用户的触摸事件优先交给 Activity 进行处理,开发者可以第一时间对事件进行对应的处理。Window 实际是 View 的直接管理者,单击事件流程会从 PhoneWindow 传递到 DecorView **。

事件在 Activity,ViewGroup,View 的传递顺序

Android 中的事件分发,我们知道触摸事件时从外层传递到内层 Acitivty -> ViewGroup -> ViewGroup -> ···View,最终再有由内层传递到外层。对于 Activity 来说:

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return super.onTouchEvent(event);
    }

对于 ViewGroup 来说,还有 onInterceptTouchEvent 方法。

        @Override
        public boolean dispatchTouchEvent(MotionEvent ev) {
            return super.dispatchTouchEvent(ev);
        }

        @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            return super.onInterceptTouchEvent(ev);
        }

        @Override
        public boolean onTouchEvent(MotionEvent event) {
            return super.onTouchEvent(event);
        }

对于 View 来说和 Activity 一样,只有 dispatchTouchEvent 和 onTouchEvent 方法。

        @Override
        public boolean dispatchTouchEvent(MotionEvent event) {
            return super.dispatchTouchEvent(event);
        }

        @Override
        public boolean onTouchEvent(MotionEvent event) {
            return super.onTouchEvent(event);
        }

我在 MainActivity 的布局设置为自定义 MyLinearlayout 包含 MyTextView,点击 MyTextView,打印出来的日志如下:

com.jackie.algorithmdemo I/MainActivity: dispatchTouchEvent: 
com.jackie.algorithmdemo I/MyLinearLayout: dispatchTouchEvent: 
com.jackie.algorithmdemo I/MyLinearLayout: onInterceptTouchEvent: 
com.jackie.algorithmdemo I/MyTextView: dispatchTouchEvent: 
com.jackie.algorithmdemo I/MyTextView: onTouchEvent: 
com.jackie.algorithmdemo I/MyLinearLayout: onTouchEvent: 
com.jackie.algorithmdemo I/MainActivity: onTouchEvent: 

如果都不进行拦截,都不消费的基本流程图

如果在 MainActivity.dispatchTouchEvent 直接 返回 true,那么最后打印出来的只有它的 dispatchTouchEvent 方法

com.jackie.algorithmdemo I/MainActivity: dispatchTouchEvent: 

后续就不再做任何处理了。

返回 true 表示拦截该流程,后面的流程就不继续走下去了,这一点非常重要。View 的 onTouchEvent 默认都会消耗事件(返回 true),除非它是不可点击的(clickable 和 longClickable 同时为 false)如果我的内层控件是 Button,它执行到 Button.onTouchEvent就结束了(clickable 为 true,longClickable 为 false;只要有一个为 true,就为 true)。但是上面的控件是 TextView(clickable 和 longClickable 同时为 false),所以它的 onTouchEvent 返回的是 false。

对于 onTouch,onTouchEvent 这两者优先级依次降低,如果设置了前者 return true,后者就不被调用了。onClick 是肯定会被执行的。

事件处理中的责任链模式(内层责任链)

很多人会将 View 事件传递分发归结为责任链模式,我们再来看看责任链模式的定义:责任链模式是一种对象的行为模式。在责任链模式里,很多对象由每一个对象对其下家的引用而连接起来形成一条链。请求在这个链上传递,直到链上的某一个对象决定处理此请求。发出这个请求的客户端并不知道链上的哪一个对象最终处理这个请求,这使得系统可以在不影响客户端的情况下动态地重新组织和分配责任。这里的责任链可能是一条直线,一个环链或者一个树结构的一部分

所以我们看上面的传递也挺像这么回事的,Activity -> PhoneWindow -> DecorView -> ViewGroup -> …View,然后 View -> ViewGroup -> DecorView -> PhoneWindow -> Activity。这其实和我们常见的责任链模式不太一样,一来一回的链条,其实很多设计模式都是这样,无定法。

也有看到网上有人说,事件分发的本质原理就是递归,这里的递归更多仅仅是纯粹的事件分发层面,很难将 Activity 、PhoneWindow、ViewGroup、View 这些传递和责任关系更形象的表达出来,我觉得用责任链模式可以更加形象的表达出这种责任关系。想想如果有很多个 ViewGroup/View 的嵌套,大多数设计者都会用递归来处理事件分发,一旦涉及到嵌套复杂,具有一定规律性,递归机会几乎都是第一选择方案,所以事件分发的本质原理就是递归没有问题,但很难将责任关系表达清楚。

ACTION_DOWN、ACTION_MOVE、ACTION_UP 事件的拦截和分发

我们再来看看 MotionEvent 中的一些重要的事件。

public static final int ACTION_DOWN = 0;
public static final int ACTION_MOVE = 1;
public static final int ACTION_MOVE = 2;
public static final int ACTION_CANCEL = 3; 

通常来说,用户点击屏幕到离开屏幕会经历 1 个 ACTION_DOWN -> n 个 ACTION_MOVE -> 1 个 ACTION_UP,对于 ACTION_CANCEL,当用户保持按下操作,并从你的控件转移到外层控件时,会触发 ACTION_CANCEL,推荐将这个事件作为 ACTION_UP 来看待,但是要区别于普通的 ACTION_UP。

下面我们继续以 MyTextView 做实验,它的 onTouchEvent 方法默认返回 false

        @Override
        public boolean onTouchEvent(MotionEvent event) {

            switch (event.getAction()){
                case MotionEvent.ACTION_DOWN:
                    Log.i(TAG, "onTouchEvent: =====down====");
                    break;
                case MotionEvent.ACTION_MOVE:
                    Log.i(TAG, "onTouchEvent: =====move====");
                    break;
                case MotionEvent.ACTION_UP:
                    Log.i(TAG, "onTouchEvent: =====up====");
                    break;
                default:
                    break;
            }
            return super.onTouchEvent(event); //MyTextView 默认 return false
        }

这个时候点击 TextView,我们只会收到 ACTION_DOWN 这一事件。

com.jackie.algorithmdemo I/MyTextView: onTouchEvent: =====down====

整体的事件传递如下:

com.jackie.algorithmdemo I/MainActivity: dispatchTouchEvent: 
com.jackie.algorithmdemo I/MyLinearLayout: dispatchTouchEvent: 
com.jackie.algorithmdemo I/MyLinearLayout: onInterceptTouchEvent: 
com.jackie.algorithmdemo I/MyTextView: dispatchTouchEvent: 
com.jackie.algorithmdemo I/MyTextView: onTouchEvent: =====down==== //return false
com.jackie.algorithmdemo I/MyLinearLayout: onTouchEvent:  //注意这里是父元素马上处理了
com.jackie.algorithmdemo I/MainActivity: onTouchEvent: 
com.jackie.algorithmdemo I/MainActivity: dispatchTouchEvent: 
com.jackie.algorithmdemo I/MainActivity: onTouchEvent: 

而如果我将 return super.onTouchEvent(event) 改为 return true,或者改成 Button,那么它就会返回:

com.jackie.algorithmdemo I/MyTextView: onTouchEvent: =====down====
com.jackie.algorithmdemo I/MyTextView: onTouchEvent: =====move====
com.jackie.algorithmdemo I/MyTextView: onTouchEvent: =====move====
com.jackie.algorithmdemo I/MyTextView: onTouchEvent: =====move====
com.jackie.algorithmdemo I/MyTextView: onTouchEvent: =====up====

整体的事件传递如下:

com.jackie.algorithmdemo I/MainActivity: dispatchTouchEvent: 
com.jackie.algorithmdemo I/MyLinearLayout: dispatchTouchEvent: 
com.jackie.algorithmdemo I/MyLinearLayout: onInterceptTouchEvent: 
com.jackie.algorithmdemo I/MyTextView: dispatchTouchEvent: 
com.jackie.algorithmdemo I/MyTextView: onTouchEvent: =====down====  //return true
com.jackie.algorithmdemo I/MainActivity: dispatchTouchEvent: //交给最顶层的Activity,move开始了
com.jackie.algorithmdemo I/MyLinearLayout: dispatchTouchEvent: 
com.jackie.algorithmdemo I/MyLinearLayout: onInterceptTouchEvent: 
com.jackie.algorithmdemo I/MyTextView: dispatchTouchEvent: 
com.jackie.algorithmdemo I/MyTextView: onTouchEvent: =====move====
com.jackie.algorithmdemo I/MainActivity: dispatchTouchEvent: 
com.jackie.algorithmdemo I/MyLinearLayout: dispatchTouchEvent: 
com.jackie.algorithmdemo I/MyLinearLayout: onInterceptTouchEvent: 
com.jackie.algorithmdemo I/MyTextView: dispatchTouchEvent: 
com.jackie.algorithmdemo I/MyTextView: onTouchEvent: =====move====
com.jackie.algorithmdemo I/MainActivity: dispatchTouchEvent: 
com.jackie.algorithmdemo I/MyLinearLayout: dispatchTouchEvent: 
com.jackie.algorithmdemo I/MyLinearLayout: onInterceptTouchEvent: 
com.jackie.algorithmdemo I/MyTextView: dispatchTouchEvent: 
com.jackie.algorithmdemo I/MyTextView: onTouchEvent: =====move====
com.jackie.algorithmdemo I/MainActivity: dispatchTouchEvent: 
com.jackie.algorithmdemo I/MyLinearLayout: dispatchTouchEvent: 
com.jackie.algorithmdemo I/MyLinearLayout: onInterceptTouchEvent: 
com.jackie.algorithmdemo I/MyTextView: dispatchTouchEvent: 
com.jackie.algorithmdemo I/MyTextView: onTouchEvent: =====up====

从上面我们可以得到如果某个 View 开始处理事件,如果它不消耗 ACTION_DOWN 事件(onTouchEvent 返回了 false),那么同一事件序列中的其他事件都不会交给它来处理,并且事件将重新交给它的父元素去处理。

事件传递中的深度优先遍历

深度优先遍历机制在安卓中有很多运用,比如在 findViewById 的时候,我们的的系统会找到 TextView 的 id,而不是 ImageView 的 id。

<LinearLayout>
    <TextView
        android:id="@+id/text"/>
    <ImageView
        android:id="@+id/text"/> 
</LinearLayout>

可能你觉得我举的这个例子太简单,那么我们来看看源码。

//View.java
    @Nullable
    public final <T extends View> T findViewById(@IdRes int id) {
        if (id == NO_ID) {
            return null;
        }
        return findViewTraversal(id);
    }

//ViewGroup.java  
/**
     * {@hide}
     */
    @Override
    protected <T extends View> T findViewTraversal(@IdRes int id) {
        if (id == mID) {
            return (T) this;
        }

        final View[] where = mChildren;
        final int len = mChildrenCount;

        for (int i = 0; i < len; i++) {
            View v = where[i];

            if ((v.mPrivateFlags & PFLAG_IS_ROOT_NAMESPACE) == 0) {
                v = v.findViewById(id);

                if (v != null) {
                    return (T) v;
                }
            }
        }

        return null;
    }

可以看到 ViewGroup 在查找的时候,先把遍历自己的子元素,如果子元素是个 ViewGroup,那么就会自己调用 ViewGroup 的这个方法,显然这是一个深度优先遍历。

再来看看我们的事件分发机制,每当用户触摸了屏幕,会发出一系列的事件 ACTION_DOWN、ACTION_MOVE、ACTION_UP,如果用户点击后触发了 ACTION_DOWN,后续的事件难道每次都要一个个遍历去寻找目标 View 吗,系统肯定有个机制可以保证快速的找到目标 View。在 ViewGroup 中有个 mFirstTouchTarget 字段就是用来做这个的。

//ViewGroup.java
// First touch target in the linked list of touch targets.
    @UnsupportedAppUsage
    private TouchTarget mFirstTouchTarget;
        //TouchTarget.java
    private static final class TouchTarget {
          ···
        public View child;
        // The next target in the target list.
        public TouchTarget next;
    }

这里也是使用深度优先遍历,每个ViewGroup都持有一个mFirstTouchTarget, 当接收到一个ACTION_DOWN时,通过递归遍历找到View树中真正对事件进行消费的Child,并保存在mFirstTouchTarget属性中,依此类推组成一个完整的分发链。这里 TouchTarget 其实是个单链表,责任链模式的最常见实现方式就是使用链表存储处理器,链表也方便管理。

举个例子:事件从 A -> B -> E -> H 进行分发,但是整个布局中有多个 ViewGroup 和 View,如何最快的找到目标 View,这里 mFirstTouchTarget 就起到作用了。

在源码中的显示如下:

    @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) {
                                ···
                cancelAndClearTouchTargets(ev);
                resetTouchState(); //ACTION_DOWN 时重置状态
            }
                     ···
            // Dispatch to touch targets.
            if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                // 不是目标 target,说明没有子 View 消费
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } 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) {
                        handled = true;
                    } else {
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                          // 有消费该事件的 View/ViewGroup,继续分发事件
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                        ···
                    }
                    predecessor = target;
                    target = next;
                }
            }
            ···
        return handled;
    }

事件拦截机制

在安卓开发中我们常常会遇到滑动冲突,处理滑动冲突我们常用外部拦截法和内部拦截法,关于他们可以参考这篇文章(https://www.cnblogs.com/andy-songwei/p/11076612.html),总的来说是重写 onInterceptTouchEvent 和 getParent().requestDisallowInterceptTouchEvent(true) 来进行处理,我们通过源码来查看实现原理:

//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;

      // Handle an initial down.
      //重置状态 ACTION_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.
        cancelAndClearTouchTargets(ev);
        resetTouchState();
      }

        // 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;
        }

    return handled;
}

通过重写 onInterceptTouchEvent,然后根据具体调节来决定是否拦截,而对于 requestDisallowInterceptTouchEvent 方法会影响上面的 FLAG_DISALLOW_INTERCEPT 这个标志位,一般用在子 View 当中。

FLAG_DISALLOW_INTERCEPT 一旦设置后,ViewGroup 将无法拦截除了 ACTION_DOWN 以外的其他点击事件。这是因为 ViewGroup 在分发事件时,如果是 ACTION_DOWN 就会重置 FLAG_DISALLOW_INTERCEPT 这个标志位,将导致子 View 中设置的这个标记位无效。因此,当面对 ACTION_DOWN 事件时,ViewGroup 总是会调用自己的 onInterceptTouchEvent 方法来询问自己是否要拦截事件。

也就是说 onInterceptTouchEvent 可能在子 View 设置 requestDisallowInterceptTouchEvent 后不一定被调用到,所以当我们想提前处理所有的点击事件,要选择 dispatchTouchEvent,只有这个方法才能确保每次都被调用。

/   总结   /

我们从事件相关类的初始化流程,事件的起源到分发,其中涉及外层责任链和内层责任链,最后我们讲了事件的拦截机制,如果你都掌握了,相信你对事件分发的理解会更加深刻。

推荐阅读:

我的新书,《第一行代码 第3版》已出版!

用Jetpack Compose写一个玩安卓App

Android 11新特性,Scoped Storage又有了新花样

欢迎关注我的公众号

学习技术或投稿

长按上图,识别图中二维码即可关注

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值