Android必知必会——事件分发机制

1,551 阅读6分钟

关于事件分发

无非就是当用户触摸屏幕或者按键操作,首次触发的是硬件驱动,驱动收到事件后,将该相应事件写入到输入设备节点, 这便产生了最原生态的内核事件。接着,输入系统取出原生态的事件,经过层层封装后成为KeyEvent或者MotionEvent ;最后,交付给相应的目标窗口(Window)来消费该输入事件。

一组事件:从手指触摸屏幕开始,到手指离开屏幕结束。

从进程层面来看事件分发

早在16年,Gityuan大神就有写过关于Input系统的一些底层源码文章,详细的介绍了Android系统输入事件处理机制。

很明显,输入事件的产生和最终的消费,是分别存在于系统进程system_server和具体消费事件的App进程:

InputManagerService

系统进程SysterServer启动的时候,会开启核心服务以及其他各种服务,其中就包含InputManagerService,用来处理各种输入事件,比如触摸事件和按键事件,以及外设键入事件等。

可见,我们所关心触摸事件,只是众多输入事件的一部分。

InputChannle

InputChannle是指定用于在另一个进程中将输入事件发送到窗口的文件描述符。

InputChannle会创建socket pair,用于两个进程的线程间相互通信。

Activity启动流程中,ActivityThread的handleResumeActivity会创建ViewRootImpl,并调用其setView方法。

ViewRootImpl.setView会创建InputChannle,并通过WindowInputEvent在native层初始化channle,同时注册监听事件。

ViewRootImpl中的InputChannle是socket的客户端,而系统服务IMS中获取到的,则是socket的服务端。

从应用内视图来看事件分发

对于应用内视图来说,事件分发的起点在Activity,并且经过一个递归循环之后,又在Activity中将最终事件处理结果返回。

由于这里已经属于老生常谈的点,也就不再进行大篇幅的代码分析,详细内容可移步Carson_Ho大神的Android事件分发机制详解:史上最全面、最易懂,这里仅引用几张图片,对Activity、ViewGroup和View各个层级,以及整体的工作流程进行一个总结:

Activity层面

ViewGroup层面

View层面

整体工作流程

从具体事件ACTION来看事件分发

在进行触摸事件分发时,针对不同的ACTION,会有不同的逻辑路线,大概可以拆分为三大类型:ACTION_DOWN、ACTION_MOVE&ACTION_UP以及ACTION_CANCLE。这三种类型的事件,会有不同的逻辑走线:

ViewGroup中分发逻辑走线

View中分发逻辑走线

事件分发关键点

ACTION_DOWN的时候不举手,这组事件就没机会了

从上图中可以看出,ViewGroup在分发当前这组事件时,只有在ACTION_DOWN的时候,才会去寻找TouchTarget,也就是想要处理这组事件的子View。所以如果子View不在ACTION_DOWN的时候“举手”,那么这组事件它就没有机会处理了。

ViewGroup寻找TouchTarget

在一组事件开始分发时,ViewGroup会首先寻找TouchTarget,如果找到了, 那么这组事件就交个这个TouchTarget处理。ViewGroup在寻找TouchTarget时,通过如下几个环节来筛选最终的子View:

这里需要注意两点:

  • 后添加的子View会先收到事件,也就是越上层的View越先收到

  • 变更子View的Z轴偏移,会影响接收事件的先后顺序,Z值越大越先接收

关于FLAG_DISALLOW_INTERCEPT

  1. 每当ACTION_DOWN触发时,会reset此flag,保证ViewGroup的onInterceptTouchEvent可以在每一组事件中都有机会被调用到
  2. 子View在处理当前这组事件时,可以适时的关闭父View对当前这组事件后续事件的拦截:
//禁止拦截
getParent().requestDisallowInterceptTouchEvent(true)
//打开拦截
getParent().requestDisallowInterceptTouchEvent(false)

ViewGroup中的super.dispatchTouchEvent()

通过之前的图解可以看出,当没有找到touchTarget或者ViewGroup决定拦截时,ViewGroup会从View的角度,去考虑要不要自己消费这个事件。

View.canReceivePointerEvents

在ViewGroup遍历寻找TouchTarget过程中,这个方法也起到了决定性的作用,具体来看一下这个方法:

/**
* @hide
*/
protected boolean canReceivePointerEvents() {
    //可见或当前执行动画Animation不为null
    return (mViewFlags & VISIBILITY_MASK) == VISIBLE || getAnimation() != null;
}

所以在动画执行完毕时,应该适时的清除view的动画view.clearAnimation()。

多点触控的处理

在某些场景下,用户可以通过多个手指进行交互操作,这就需要在这个场景下,合理的处理用户的多点触控行为。

TouchTarget

在进行多点触控分析前,首先来看一下TouchTarget:

private static final class TouchTarget {
    //省略部分代码
    
    // 保存的子View引用,
    @UnsupportedAppUsage
    public View child;

    // 当前子View追踪手指的掩码
    public int pointerIdBits;

    // 指向下一个TouchTarget
    public TouchTarget next;
    
    //省略部分代码

在TouchTarget中保存着消费事件的子View,以及这个子View目前追踪的手指ids(可为多个手指),还有一个next指向下一个TouchTarget,那么下一个TouchTarget又是什么意思呢?

我们知道,顶层ViewGroup会在手指按下的时候,reset一些属性同时还会寻找TouchTarget。reset属性时,需要事件只能是ACTION_DOWN,但是寻找TouchTarget却还允许触摸事件为ACTION_POINTER_DOWN。

ACTION_POINTER_DOWN,表示当手指按下时,已经存在其他手指在事件序列中,即还没有抬起。那么就可能存在这么一种情况,第一个手指按在了一个Button上,但是第二根手指按在了另一个Button上,那么当第二根手指按下时,就会寻找到第二个TouchTarget,这个时候就会将这个新的TouchTarget.next指向mFirstTouchTarget(即第一个),然后将mFirstTouchTarget指向这个新的TouchTarget。

如果你现在是使用的掘金APP,那么可以试一下长按右上角会弹出分享,然后别松开,再长按文章内容区域,会再弹出menu。

MotionEvent.getActionMask

MotionEvent就是事件分发时的主角,只有了解了它才能正确的处理触摸事件。

请先查看GcsSloop大佬的MotionEvent详解

  • 获取事件类型

    1.getAction:单点触控触摸事件

    2.getActionMask:多点触控必须使用此方法获取Action

  • 追踪事件时,应该瞅准PointerId

    int index = event.getActionIndex()
    int pointerId = event.getPointerId(index)
    

多指按下和抬起时,每个手指对应的pointerId是固定不变的,而对应的index会发生变化,变化的规则是保证index是连续的。

多指按下时:

多指抬起时:

多点触控处理方案

  • 接力型

    同一时刻只追踪一个手指(如始终追踪最新按下的手指)的运动轨迹。

  • 协作型

    同时追踪所有触摸手指运动轨迹,判断用户行为(捏撑缩放、多指平移等)。

  • 各自为栈

    同时追踪所有触摸手指运动轨迹,但是互不影响(如每个手指都能画画)。

ViewGroup.setMotionEventSplittingEnabled(false)

此方法可以关闭被调用ViewGroup对多点触控的支持,再来看一下ViewGroup的dispatchTouchEvent方法:

public boolean dispatchTouchEvent(MotionEvent ev) {
    //省略部分代码
    final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
    //省略部分代码
    if (actionMasked == MotionEvent.ACTION_DOWN
        //当ACTION_POINTER_DOWN发生时,必须split为true,才会发起对TouchTarget的寻找
        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
        //省略部分代码
        findTouchTarget
    }
}

GestureDetector更便捷的处理用户触摸事件

使用手势监听器,可以更加便捷的处理用户手势,详细的内容可以移步Carson_Ho大佬博文Android GestureDetector详解