Jimmy Chen

A Programmer

(转载)Input系统:截断策略的分析与应用

上一篇文章 Input系统: 按键事件分发 分析了按键事件的分发过程,虽然分析的对象只是按键事件,但是也从整体上,描绘了事件分发的过程。其中比较有意思的一环是事件截断策略,本文就来分析它的原理以及应用。

其实这篇文章早已经写好,这几天在完善细节的时候,我突然发现了源码中的一个 bug,这让我开始对自己的分析产生质疑,最终我拿了一台公司的样机 debug,我发现这确实是源码的一个 bug。本文在分析的过程中,会逐步揭开这个 bug。

截断策略的原理

根据 Input系统: 按键事件分发 的分析,事件开始分发前,会执行截断策略,如下

根据 Input系统: InputManagerService的创建与启动 可知,底层的截断策略实现类是 NativeInputManager

首先调用上层来执行截断策略,然后根据执行的结果,再决定是否在参数 policyFlags 添加 POLICY_FLAG_PASS_TO_USER 标志位。这个标志位,就决定了事件是否能传递给用户。

截断策略经过 InputManagerService,最终是由上层的 PhoneWindowManager 实现,如下

这里以 power 按键为例,它的按键事件的截断策略的处理结果,是去掉了 ACTION_PASS_TO_USER 标志位,也即告诉底层不要把事件发送给用户,这就是为何窗口(例如 Activity)无法收到 power 按键事件的原因。

现在,如果项目的硬件上新增一个按键,并且不想这个按键事件被分发给用户,你会搞了吗?

截断策略的应用

根据 Input系统: 按键事件分发 可知,截断策略发生在事件分发之前,因此它能及时处理一些系统功能的事件,例如,power 按键亮/灭屏没有延时,挂断按键挂断电话也没有延时。

刚才,我们看到了截断策略的一个作用,阻塞事件发送给用户/窗口。然而,它还可以实现按键的手势,手势包括单击,多击,长按,组合键(例如截屏组合键)。

下面来分析截断策略是如何实现按键手势中的单击、多击、长按。至于组合键,由于涉及分发策略,留到下一篇文章分析。

初始化

按键的手势是用 SingleKeyGestureDetector 来管理的,它在 PhoneWindowManager 中的初始化如下

SingleKeyGestureDetector 根据配置,为 Power 键和 Back 键保存了规则(rule)。所谓的规则,就是如何实现单个按键的手势。

所有的规则的基类都是 SingleKeyGestureDetector.SingleKeyRule,它的使用方式用下面一段代码解释

  • getMaxMultiPressCount() 表示支持的按键的最大点击次数。如果返回1,表示只支持单击,如果返回3,表示支持双击和三击,and so on…
  • 单击按键会调用 onPress()
  • 多击按键会调用 onMultiPress(long downTime, int count),参数 count 表示多击的次数。
  • 长按按键会调用 onLongPress()
  • 长时间地长按按键,会调用 onVeryLongPress()

实现按键手势

截断策略在处理按键事件时,会处理按键手势,如下

从这里可以看到,有三个类实现按键的手势,如下

  • KeyCombinationManager,它是用于实现组合按键的功能,例如,power 键 + 音量下键 实现的截屏功能。它的原理很简单,就是第一个按键按下后,在超时时间内等待第二个按键事件的到来。
  • GestureLauncherService,目前只实现了双击打开 Camera 功能,原理也很简单,当第一次按下 power 键,在规定时间内按下第二次 power 键,然后由 SystemUI 实现打开 Camera 功能。
  • SingleKeyGestureDetector,实现通用的按键的手势功能。

GestureLauncherService 在很多个 Android 版本中,都只实现了双击打开 Camera 的功能,它的功能明显与 SingleKeyGestureDetector 重合了。然而,更不幸的是,SingleKeyGestureDetector 实现的手势功能还有 bug,Google 的工程师是不是把这个按键手势功能给遗忘了?

KeyCombinationManager 会在下一篇文章中分析,GestureLauncherService 请大家自行分析,现在来看下 SingleKeyGestureDetector 是如何处理按键手势的

SingleKeyGestureDetector 分别处理了按键的 DOWN 事件和 UP 事件,这两者合起来才实现了整个手势功能。

首先看下如何处理 DOWN 事件

SingleKeyGestureDetector 对按键 DOWN 事件的处理过程如下

  1. 按键首次按下,为按键找到相应的规则,保存到 mActiveRule
  2. 按键首次按下,会发送一个延时的长按消息,实现长按功能。当超时时,也就是按键按下没有抬起,并且系统也没有发送按键的长按事件,那么会执行 SingleKeyRule#onLongPress() 或/和 SingleKeyRule#onVeryLongPress()
  3. 如果收到系统发送的按键的长按事件,那么移除长按消息,并立即SingleKeyRule#onLongPress()。为何要立即执行,而不是发送一个延时消息?因为系统已经表示这是一个长按事件,没有理由再使用一个延时来检测是否要触发长按。
  4. 如果多次(至少超过1次)点击按键,那么移除长按、单击/多击消息,并在点击次数达到最大时,立即执行SingleKeyRule#onMultiPress()

注意,第4点中,当达到最大点击次数时,立即执行SingleKeyRule#onMultiPress(),并重置按键点击次数 mKeyPressCounter 为 0,这是一个 Bug,这段代码应该去掉。在后面的分析中,我将证明这会造成 bug。

SingleKeyGestureDetector 对按键按下事件的处理,确切来说只实现了长按的功能,而按键的单击功能以及多击功能,是在处理 UP 事件中实现的,如下

SingleKeyGestureDetector 处理 UP 事件分两种情况

  1. 如果规则只支持单击,那么发送一个消息执行单击动作。这里我就有一个疑问了,为何不与前面一样,直接执行单击动作,反而还要多此一举地发送一个消息去执行单击动作?
  2. 如果规则支持多击,那么首先把点击次数 mKeyPressCounter 加1,然后发送一个延时消息执行单击或者多击动作。为何要设置一个延时?对于单击操作,需要使用一个延时来检测没有再次点击的操作,对于多击操作,需要检测后面是否还有点击操作。

注意,以上两种情况下发送的消息,Message#arg2 的值其实是按键的点击次数。

现在来看下 MSG_KEY_DELAYED_PRESS 消息的处理流程。

这个消息的处理过程,从表面上看,如果是单击就执行 SingleKeyRule#onPress(),如果是多击就执行 SingleKeyRule#onMultiPress()

然而实际情况,并非如此。由于没有使用 Message#arg2,而是直接使用 mKeyPressCounter 作为按键点击次数,这就导致两个问题

  1. 当规则只支持单击功能时,mKeyPressCounter 永远为0。于是单击按键时,调用 SingleKeyRule#onMultiPress(),而非 SingleKeyRule#onPress(),这岂不是笑话。
  2. 当规则支持多击功能时,如果单击按键,会调用 SingleKeyRule#onPress(),如果多击按键,会调用 SingleKeyRule#onMultiPress() 这一切看起来没有问题,实则不然。对于多击操作,前面分析过,在处理 DOWN 事件时,当达到最大点击次数时,会调用SingleKeyRule#onMultiPress(),并把 mKeyPressCounter 重置为0。之后再处理 UP 事件时,mKeyPressCounter 加1后变为了1,然后发送消息去执行,最后,奇迹般地执行了一次 SingleKeyRule#onPress()。对于一个多击操作,居然执行了一次单击动作,这简直是国际笑话。

以上两点问题,我用样机进行验证过,确实存在。但是,系统很好地支持了双击 power 打开 Camera,以及单击 power 亮/灭屏。这又是怎么回事呢?

  1. 双击 power 打开 Camera,是由 GestureLauncherService 实现的。
  2. 系统默认配置的 power 规则,只支持单击。

既然 power 规则只支持单击,理论上应该调用 SingleKeyRule#onMultiPress(long downTime, int count),并且参数 count 为0,这岂不还是错的。确实如此,不过源码又巧合地用另外一个Bug避开了这一个Bug。接着往下看

首先看下 power 键的规则

power 键规则,默认支持最大的点击数为1,这是在 config.xml 中进行配置的,这里不细讲。

power 键规则中,无论是单击还是多击,默认都调用同一个函数,如下

从整体看,如果参数 count 为2或者3,会执行多击的行为,否则,执行单击行为。

这个逻辑是不是有点奇怪呀,参数 count 不为2也不为3,难道就是一定为1吗?正是因为这个逻辑,才导致 count 为0时,也能执行单击动作,小朋友听了都直呼6。我想起了我一个前同事做的事,用一个 Bug 去解决另外一个 Bug。

power 键的亮屏与灭屏

好了,言归正传,power 键规则的单击行为,只包括了灭屏,并没有包含亮屏,这又是怎么回事呢?因为 power 键的亮屏,不是在规则中实现的,而是在截断策略中实现的,如下

截断策略处理 power 按键的 DOWN 事件过程如下

  1. 如果 power 按键事件没有被其它地方使用,那么,在非交互状态下,一般指灭屏状态,会执行亮屏。
  2. 如果 power 按键事件被其它地方使用,那么重置按键手势。

这里我又有一个疑问,为何要把 power 亮屏的代码的在这里单独处理?在创建规则时设置一个回调,是不是更好呢?

结束

我在支援公司的某个项目时,偶然发现有人在截断策略中,要实现一个新的双击power功能,以及添加三击power的功能,可谓是把源码修改得”鸡飞狗跳”,我当时还嗤之以鼻,现在发现我错怪他了。但是呢,由于他不知道如何修复这个源码的 bug,所以他实现的过程还是非常丑陋。

最后,给看我文章的人一个小福利,你可以参考 Input系统: InputReader 处理按键事件 ,学会从底层映射一个按键到上层,然后根据本文,实现按键的手势,这绝壁是一个非常叼的事情。当然,如果手势中要包括多击功能,还得解决本文提出的 Bug,这个不难,小小思考下就可以了。

等等,我话没有说完,看完记得点赞,关注,有事留言,溜了溜了 ~

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

发表回复

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