Jimmy Chen

A Programmer

(转载)Input系统: 触摸事件分发

Input系统: InputReader 处理触摸事件 分析了 InputReader 对触摸事件的处理流程,最终的结果是把触摸事件包装成 NotifyMotionArgs,然后分发给下一环。根据 Input系统: InputManagerService的创建与启动 可知,下一环是 InputClassifier。然而系统目前并不支持 InputClassifier 的功能,因此事件会被直接发送到 InputDispatcher

Input系统: 按键事件分发 分析了按键事件的分发流程,虽然分析的目标是按键事件,但是也从整体上,描绘了事件分发的框架。而本文分析触摸事件的分发流程,也会用到这个框架,因此重复内容不再赘述。

1. InputDispatcher 收到触摸事件

InputDispatcher 收到触摸事件后的处理流程,与收到按键事件的处理流程非常相似

  1. 对触摸事件进行截断策略查询。参考【1.1 截断策略查询
  2. 把触摸事件加入 InputDispatcher 收件箱,然后唤醒线程处理触摸事件。

1.1 截断策略查询

一个触摸事件,必须满足下面三种情况,才执行截断策略

  1. 触摸事件是受信任的。来自输入设备的触摸事件都是受信任的。
  2. 触摸事件是非注入的。monkey 的原理就是注入触摸事件,因此它的事件是不需要经过截断策略处理的。
  3. 设备处于非交互状态。一般来说,非交互状态指的就是显示屏处于灭屏状态。

另外还需要关注的是,事件在什么时候是不需要经过截断策略,有两种情况

  1. 对于受信任且非注入的触摸事件,如果设备处于交互状态,直接发送给用户。 也就是说,如果显示屏处于亮屏状态,输入设备产生的触摸事件一定会发送给窗口。
  2. 对于不受信任,或者注入的触摸事件,如果设备处于交互状态,也是直接发送给用户。也就是说,如果显示屏处于亮屏状态,monkey 注入的触摸事件,也是直接发送给窗口的。

最后还要注意一件事,如果一个触摸事件是不受信任的事件,或者是注入事件,当设备处于非交互状态下(通常指灭屏),那么它不经过截断策略,也不会发送给用户,也就是会被丢弃。

在实际工作中处理的触摸事件,通常都是来自输入设备,它肯定是受信任的,而且非注入的,因此它只有在设备处于非交互状态下(一般指灭屏)下,非会执行截断策略,而如果设备处于交互状态(通常指亮屏),会被直接分发给窗口。

现在来看下截断策略的具体实现

截断策略是否截断触摸事件,取决于策略的返回值,有两种情况

  1. 返回 0,表示截断触摸事件。
  2. 返回 ACTION_PASS_TO_USER ,表示不截断触摸事件,也就是把触摸事件分发给用户/窗口。

下面列举触摸事件截断与否的情况,但是要注意一个前提,设备处于非交互状态(一般就是指灭屏状态)

  1. 事件会被传递给用户,也就是不截断,情况如下
    • 有锁屏,并且显示屏处于非 off 状态。注意,非 off 状态,并不是表示屏幕处于 on(亮屏) 状态,也可能是 doze 状态(屏幕处于低电量状态),doze 状态屏幕也是黑的。
    • 梦境状态。因为梦境状态下会运行 doze 组件。
  2. 事件被截断,情况如下
    • 策略标志位包含 FLAG_WAKE ,它会导致屏幕被唤醒,因此需要截断触摸事件。FLAG_WAKE 一般来自于输入设备的配置文件。
    • 没有锁屏,没有梦境,也没有 FLAG_WAKE,默认就会截断。

从上面的分析可以总结出了两条结论

  1. 如果系统有组件在运行,例如,锁屏、doze组件,那么触摸事件需要分发到这些组件,因此不会被截断。
  2. 如果没有组件运行,触摸事件都会被截断。触摸事件由于需要唤醒屏幕,而导致被截断,只是其中一个特例。

2. InputDispatcher 分发触摸事件

Input系统: InputManagerService的创建与启动 可知,InputDispatcher 通过线程循环来处理收件箱中的事件,而且一次循环只能处理一个事件

一次线程循环处理触摸事件的过程如下

  1. 分发一个触摸事件。
  2. 当事件分发给窗口后,会计算一个窗口反馈的超时时间,利用这个时间,计算线程下次唤醒的时间点。
  3. 利用上一步计算出的线程唤醒的时间点,计算出线程最终需要休眠多长时间。当线程被唤醒后,会检查接收触摸时间的窗口,是否反馈超时,如果超时,会引发 ANR。

现在来看看如何分发一个触摸事件

Input系统: 按键事件分发 已经分析过 InputDispatcher 的线程循环。而对于触摸事件,是通过 InputDispatcher::dispatchMotionLocked() 进行分发

一个触摸事件的分发过程,可以大致总结为以下几个过程

  1. 如果有原因表明触摸事件需要被丢弃,那么触摸事件不会走后面的分发流程,即被丢弃。
  2. 通常触摸事件是发送给窗口的,因此需要为触摸事件寻找触摸窗口。窗口最终被保存到 inputTargets 中。参考【2.1 寻找触摸的窗口
  3. inputTargets 保存触摸窗口后,还要保存 global monitor 窗口。例如开发者选项中的 Show taps 和 Pointer location,就是利用这个窗口实现的。
  4. 启动分发循环,把触摸事件分发给 inputTargets 保存的窗口。 由于 Input系统: 按键事件分发 已经分发过这个过程,本文不再分析。

2.1 寻找触摸的窗口

为触摸事件寻找触摸窗口的过程,极其复杂。虽然这段代码被我省略了很多过程,但是我估计读者也会看得头晕。

对于 DOWN 事件

  1. 根据 x,y 坐标寻找触摸的窗口。参考【2.1.1 根据坐标找到触摸窗口
  2. 获取所有的 gesture monitor 窗口 。
  3. 把触摸窗口保存到 tempTouchState 中。
  4. 把所有的 gesture monitor 窗口保存到 tempTouchState 中。
  5. 为 tempTouchState 保存所有窗口,创建 InputTarget 对象,并保存到参数 inputTargets 中。参考【2.1.2 保存窗口
  6. 使用 mTouchStatesByDisplay 缓存 tempTouchState。

gesture monitor 是为了实现手势功能而添加的一个窗口。什么是手势功能? 例如在屏幕的左边/右边,向屏幕中央滑动,会触发返回手势。这个手势功能用来替代导航键。在下一篇文章中,我会剖析这个手势功能的原理。

对于非 DOWN 事件,一般为 MOVE, UP 事件

  1. 获取 DOWN 事件缓存的 tempTouchState。 因为 tempTouchState 保存了处理 DOWN 事件的触摸窗口和 gesture monitor,非 DOWN 事件,也会发送给这些窗口。
  2. 重复 DOWN 事件的第5步。

当分析的代码量很大的时候,我们需要有一个整体的观念。为触摸事件寻找触摸窗口,最终的结果就是把找到的窗口保存到参数 inputTargets 中,后面会把事件分发给 inputTargets 保存的窗口。

2.1.1 根据坐标找到触摸窗口

这里涉及一个 portal window 的概念,由于我没有找到具体使用的地方,我大致猜测它的意思就是,设备外接一个屏幕,然后在主屏幕上显示一个窗口来操作这个外接屏幕。后面的分析,我将略过 portal window 的部分。当然,触摸掌握了触摸事件的分发流程,以后遇到了 portal window 的事情,再来分析,应该没问题的。

寻找触摸点所在的窗口,其实就是从上到下遍历所有窗口,然后找到满足条件的窗口。

窗口首先要满足前置条件

  1. 窗口要在指定屏幕上。
  2. 窗口要可见。
  3. 窗口要可触摸。

满足了所有的前置条件后,只要满足以下任意一个条件,那么就找到了触摸点所在的窗口

  1. 是触摸模型的窗口: 可获取焦点,并且不允许窗口之外的触摸事件发送到它后面的窗口。
  2. 触摸点的 x,y 坐标落在窗口坐标系中。

2.1.2 保存窗口

对于触摸事件,无论是触摸窗口,还是 gesture monitor,都会被转化为 InputTarget,然后保存到参数 inputTargets 中。当后面启动分发循环后,触摸事件就会发送到 inputTargets 保存的窗口中。

结束

本文从整体上分析了触摸事件的分发过程,很多细节并没有深入去分析,例如,当窗口无响应时,如何优化事件分发。但是,只要你掌握了基本的流程,这些细节你可以自行分析。

本文的某些分析过程,跨度可能很大,那是因为这些知识已经在前面的文章中讲过,如果你阅读本文,感觉有点困难,那么请先阅读前面的文章,打好基础。

理论的文章总有一些枯燥,但是不妨碍我继续向前,下一篇文章,将以此为基础,分析那个代替系统导航栏的手势功能是如何实现的,这也将作为 Input 系统的收官之作。

本文转自 https://juejin.cn/post/7202537103934177338,如有侵权,请联系删除。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注