CrazyDailyQuestion icon indicating copy to clipboard operation
CrazyDailyQuestion copied to clipboard

mico: View的事件分发机制

Open MicroKibaco opened this issue 4 years ago • 0 comments

View 事件分发

在最新的 Android 系统中,事件的处理者不再由 InputEventReceiver 独自承担,而是通过多种形式的 InputStage 来分别处理,它们都有一个回调接口 onProcess 函数,这些都声明在 ViewRootImpl 内部类里面,并且在 setView 里面进行注册,比如有 ViewPreImeInputStage 用于分发 KeyEvent,这里我们重点关注与 MotionEvent 事件分发相关的 ViewPostImeInputStage。在它的 onProcess 函数中,如果判断事件类型是 SOURCE_CLASS_POINTER,即触摸屏的 MotionEvent 事件,就会调用 mView 的 dispatchPointerEvent 方法处理。也就是说事件分发最开始是传递给 DecorView 的,DecorView 的 dispatchTouchEvent 是传给 Window Callback 接口方法 dispatchTouchEvent,而 Activity 实现了 Window Callback 接口,在 Activity 的 dispatchTouchEvent 方法里,是调到 Window 的 dispatchTouchEvent,Window 的唯一实现类 PhoneWindow 又会把这个事件回传给 DecorView,DecorView 在它的 superDispatchTouchEvent 把事件转交给了 ViewGroup。

所以,事件分发的流程是:

DecorView -> Activity -> PhoneWindow -> DecorView -> ViewGroup -> View

这里面涉及了三个元素,Activity、ViewGroup 和 View。Activity 的 dispatchTouchEvent 前面说过,它的 dispatchTouchEvent 一般都是返回 false 不消费往下传;在说 View 的 dispatchTouchEvent,如果注册了 OnTouchListener 就调用其 onTouch 方法,如果 onTouch 返回 false 还会接着调用 onTouchEvent 函数,onTouchEvent 作为一种兜底方案,它在内部会根据 MotionEvent 的不同类型做相应处理,比如是 ACTION_UP 就需要执行 performClick 函数。ViewGroup 因为涉及对子 View 的处理,其派发流程没有 View 那么简单直接,它重写了 dispatchTouchEvent 方法,如果 ViewGroup 允许拦截,就调用其 onInterceptTouchEvent 来判断是否要真正执行拦截了,如果拦截了就交由自己的 onTouchEvent 处理,如果不拦截,就从后遍历子 View 处理,它有两个函数可以过滤子 View,一个是判断这个子 View 是否接受 Pointer Events 事件,另一个是判断落点有没有落在子 View 范围内。如果都满足,则调用其 dispatchTouchEvent 处理。如果该子 View 是一个 ViewGroup 就继续调用其 dispatchTouchEvent,否则就是 View 的 dispatchTouchEvent 方法,如此循环往复,直到事件真正被处理。

伪代码表示为:

public boolean dispatchTouchEvent(MotionEvent event) {
    boolean consume = false;
    if (onInterceptTouchEvent(event)) {
        consume = onTouchEvent(event);
    } else {
        consume = child.dispatchTouchEvent(event);
    }
    return consume;
}

最后可以画一下这个图:

到这里基本上事件分发就讲完了,但是还可以扩展一下,事件到底是从哪里来的?

这就要涉及 ImputManagerService 相关的知识了。IMS 的创建过程和 WMS 类似,都是由 SystemServer 统一启动,在创建时 IMS 会把自己的实例传给 WMS,也就是表明 WMS 是 InputEvent 的派发者,这样的设计是自然而然的,因为 WMS 记录了当前系统中所有窗口的完整状态信息,所以也只有它才能判断应该把事件投递给哪一个具体的应用程序处理。

IMS 在 Native 层创建了两个线程,InputReaderThread 和 InputDispatcherThread,前者负责从驱动节点中读取 Event,后者专责与分发事件。InputReaderThread 中的实现核心是 InputReader 类,但在 InputReader 实际上并不直接去访问设备节点,而是通过 EventHub 来完成这一工作。EventHub 通过读取 /dev/input 下的相关文件来判断是否有新事件,并通知 InputReader。InputDispatcherThread 在创建之初,就把自己的实例传给了 InputReaderThread,这样便可以源源不断的获取事件进行分发了。

分发时是如何找到投递目标呢?也就是 findFocusedWindowTargetsLocked 方法的实现。也就是通过 InputMonitor 来找到 “最前端” 的窗口即可。这个 InputMonitor 是 WMS 提供的,而 IMS 实现了 WindowManagerCallbacks 接口,并把 InputMonitor 作为参数传递进去。在获知 InputTarget 之后,InputDispatcher 就需要和窗口建立连接,是通过 InputChannel,这也是一个跨进程通信,但是并不是采用 Binder,而是 Unix Domain Socket 实现。

在 Java 层,InputEventReceiver 对 InputChannel 进行包装,它是一个抽象类,它唯一的实现 WindowInputEventReceiver 就是在 ViewRootImpl 中,这样 ViewRootImpl 就可以获取到事件了。在 ViewRootImpl 中,一旦获知 InputEvent,就会进行入队操作,如果是紧急事件就直接调用 doProcessInputEvent 处理,如果不是紧急事件,就会把这个 InputEvent 推送到消息队列,然后按顺序处理,此时需要注意 Message 为异步消息。

至此,事件就流向了 ViewRootImpl 了。

事件分发

熟悉事件分发是处理嵌套滑动的基础。

在项目中,我们遇到了一个 ViewPager2 嵌套 RecyclerView 滑动过于灵敏的问题,即稍微滑动一点 RecyclerView 就会导致 ViewPager2 切换,这在使用 ViewPager 是没啥问题,但是 ViewPager2 内部使用的也是 RecyclerView + SnapHelper 做横向滑动,导致了滑动灵敏问题。又因为 ViewPager2 是 final 的,所以只能使用内部拦截法。

解决办法是:重写 ViewPager2 的根布局 FrameLayout 的 dispatchTouchEvent,判断如果 dx>dy+30,即表明是横向滑动,requestDisallowInterceptTouchEvent(false) 允许 ViewPager2 去拦截处理。详细代码如下:

    int dx = 0;
    int dy = 0;

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {

        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                dx = (int) ev.getRawX();
                dy = (int) ev.getRawY();
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:
                if (Math.abs(dx - ev.getRawX()) > Math.abs(dy - ev.getRawY()) + 30) {
                    getParent().requestDisallowInterceptTouchEvent(false);
                } else {
                    getParent().requestDisallowInterceptTouchEvent(true);
                }
                break;
            case MotionEvent.ACTION_UP:
                dx = 0;
                dy = 0;
                getParent().requestDisallowInterceptTouchEvent(false);
                break;
            default:
                break;
        }
        return super.dispatchTouchEvent(ev);
    }

MicroKibaco avatar Nov 15 '20 13:11 MicroKibaco