Skip to content

事件分发

输入事件的分发是个经典的话题。我将它分为“应用进程内”和“应用进程外”来说明。

应用进程内

概述

由主线程的looper收到事件后,逐步交由ViewRootImplinputStage处理链处理。一般情况下,会由ViewPostImeInputStage此节点来处理:

        private int processPointerEvent(QueuedInputEvent q) {
            final MotionEvent event = (MotionEvent)q.mEvent;
            ...
            boolean handled = mView.dispatchPointerEvent(event);
            ...
            return handled ? FINISH_HANDLED : FORWARD;
        }
之后会由DecorViewdispatchTouchEvent来处理:
    public boolean dispatchTouchEvent(MotionEvent ev) {
        final Window.Callback cb = mWindow.getCallback();
        return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
                ? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
    }
一般情况下,Activity, Dialog在构造时会window.setCallback由此可以在activity, dialog的子类里复写dispatchXxxEvent
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }
如果不考虑子类覆盖,Activity会先遵从Window的意愿:
    @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }
即会进行ViewGroup的触摸事件分发:
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }
最后ActivityonTouchEvent是:
    public boolean onTouchEvent(MotionEvent event) {
        if (mWindow.shouldCloseOnTouch(this, event)) {
            finish();
            return true;
        }

        return false;
    }
由此来总结一下:事件分发时,优先交给window#callback#dispatchTouchEvent(可以复写Activity, Dialog此方法),其次在ViewGroup中分发(前提取决于DispatchTouchEvent的逻辑)。

View的Touch事件分发框架概述

在View的层级结构里,会从上到下,即从父View到子View依次分发触摸事件。一般按ViewGroup的行为来讲,会检测可见的子View的边界是否和触摸事件坐标相交,在相交的前提下才会继续分发。

事件分发过程主要涉及的方法包含:

分发处理相关:

  • View#dispatchTouchEvent
  • ViewGroup#dispathTransformedTouchEvent
  • View#onTouchListner.onTouch
  • View#onTouchEvent,可能继而触发clickListener.onClick等

拦截相关:

  • ViewGroup#onInterceptTouchEvent
  • ViewGroup#requestDisallowInterceptTouchEvent

嵌套滚动相关:

  • View#startNestedScroll
  • ViewGroup#onStartNestedScroll

典型的触摸事件流(即一系列要分发的触摸事件)是(*代表可选):

  • ACTION_DOWN
  • ACTION_MOVE / ACTION_DOWN(多点触摸,可根据pointerIndex判断触摸点编号)
  • ACTION_CANCEL / ACTION_UP(若是多点触摸则会有多次)

ACTION_DOWN作为起始,ACTION_UP作为结束,如果有意外发生,则会以一个ACTION_CANCEL作为结束。之间可以出现多次ACTION_MOVE以及子序列(除了ACTION_CANCEL的情况)

若需要保留MotionEvent实例,请使用MotionEvent.obtain(MotionEvent)方法,而不要作为成员变量,因为MotionEvent实例是被高度复用的。

分发的机制如下:

  • 对于Down事件,父View按默认的行为,在dispatchTouchEvent中分发给可以获取焦点、可见、且触摸位于其边界内、层级更高的子View。若返回了true则代表消费此事件,并会继续消费后续事件。若返回false,则后续事件将不再分发至此。即,任何View必须在Down事件决定是否接收后续事件流。
  • 对于确认消费事件流的子View,被认定为事件分发目标。在它层级之上的父View在后续触摸事件中,dispatchTouchEvent依然会触发(因为它在Down事件返回的值就是目标子View返回的true),此外,它的onInterceptTouchEvent也会被调用,除非分发目标View正是自己。在onInterceptTouchEvent中若返回了true,则代表父View拦截了后续所有事件。子View将无法收到此次事件,转而会收到ACTION_CANCEL事件(即拦截判断发生在分发给目标View之前)。
  • 子View可以通过requestDisallowInterceptTouchEvent来阻止父View对触摸事件拦截。当然,此调用必须发生地尽可能早。若父View已经开始拦截时此调用无意义。
  • 此外,子View可以通过startNestedScroll表示自己进行了嵌套滚动。这更类似一种hint机制,父View的onStartNestedScroll会按层级得到回调。若返回true表示接受,false表示拒绝。若接受了,一般隐含意思是,将不再拦截。
  • View可以添加onTouchListneronClickListener,以及onLongClickListener等,View也有onTouchEvent回调。它们的关系是,优先由onTouchListener判断是否处理触摸事件。若不处理,则再交给View#onTouchEvent回调,而这个回调中会包含一系列的手势检测,对于ACTION_UP时,则会试图调用onClickListener。它也会记录按压时长,从而在释放时根据需要调用onLongClickListener

滚动检测

一般会以GestureDetector实例,在onDispatchTouchEvent中为其传递触摸事件,并提供不同手势触发的回调实现,来完成手势检测。但有时候需要手动检测时,步骤一般如下:

  • 在ACTION_DOWN中返回true,从而可以得到后续事件,并记录DOWN事件的坐标。
  • 在ACTION_MOVE时,和DOWN事件坐标对比得出手指移动偏移量,和ViewConfiguration.TOUCH_SLOP或其他参考值对比,来确认是否可以认为发生了滚动。

嵌套滚动案例

下面来举一个嵌套滚动的场景。有一个可滚动容器A,内部包含了一个内部也可以上下滚动的view B。当View B内部上下滚动到边界时,外部容器A继续滚动。

解决此场景类似于NestedScrollView中对onInterceptTouchEvent的处理,即子View若判断出了是竖直滚动,若可以滚动,则调用startNestedScroll,而父View除了复写onNestedScroll以接受嵌套滚动外,在onInterceptTouchEvent中的ACTION_MOVE中需要通过getNestedScrollAxis来检查是否有竖直方向的嵌套滚动正在进行,若有,则不应该拦截,否则,可以在它也判断出是竖直方向滚动时,返回true拦截此事件。一旦拦截后,子View将不再收到事件。

应用进程外

这里主要讨论输入事件是如何分发到应用进程中

todo 待补充

dispatchTouchEvent调用栈

dispatchTouchEvent:53, MainActivity$TestView (org.tu.android)
dispatchTransformedTouchEvent:3131, ViewGroup (android.view)
dispatchTouchEvent:2731, ViewGroup (android.view)
dispatchTransformedTouchEvent:3131, ViewGroup (android.view)
dispatchTouchEvent:2731, ViewGroup (android.view)
superDispatchTouchEvent:528, DecorView (com.android.internal.policy)
superDispatchTouchEvent:1857, PhoneWindow (com.android.internal.policy)
dispatchTouchEvent:4105, Activity (android.app)
dispatchTouchEvent:69, WindowCallbackWrapper (androidx.appcompat.view)
dispatchTouchEvent:478, DecorView (com.android.internal.policy)
dispatchPointerEvent:13887, View (android.view)
processPointerEvent:6209, ViewRootImpl$ViewPostImeInputStage (android.view)
onProcess:5947, ViewRootImpl$ViewPostImeInputStage (android.view)
deliver:5400, ViewRootImpl$InputStage (android.view)
onDeliverToNext:5460, ViewRootImpl$InputStage (android.view)
forward:5419, ViewRootImpl$InputStage (android.view)
forward:5584, ViewRootImpl$AsyncInputStage (android.view)
apply:5427, ViewRootImpl$InputStage (android.view)
apply:5641, ViewRootImpl$AsyncInputStage (android.view)
deliver:5400, ViewRootImpl$InputStage (android.view)
onDeliverToNext:5460, ViewRootImpl$InputStage (android.view)
forward:5419, ViewRootImpl$InputStage (android.view)
apply:5427, ViewRootImpl$InputStage (android.view)
deliver:5400, ViewRootImpl$InputStage (android.view)
deliverInputEvent:8356, ViewRootImpl (android.view)
doProcessInputEvents:8325, ViewRootImpl (android.view)
enqueueInputEvent:8276, ViewRootImpl (android.view)
onInputEvent:8495, ViewRootImpl$WindowInputEventReceiver (android.view)
dispatchInputEvent:188, InputEventReceiver (android.view)
nativePollOnce:-1, MessageQueue (android.os)
next:336, MessageQueue (android.os)
loop:184, Looper (android.os)
main:7830, ActivityThread (android.app)
invoke:-1, Method (java.lang.reflect)
run:492, RuntimeInit$MethodAndArgsCaller (com.android.internal.os)
main:1040, ZygoteInit (com.android.internal.os)