Skip to content

识别的灵魂:深入手势识别状态机

在前面的章节里,我们已经完成了手势识别的各项准备工作:定义了常量,实现了事件系统,并创建了能够抹平设备差异的输入适配器。我们现在能源源不断地获取到标准化的输入数据流。现在,我们面临着最核心的问题:如何从这些连续的、看似杂乱的输入流中,识别出用户想要表达的特定手势?

答案,就蕴含在本章的标题中——状态机。

重新思考手势:手势是一种“模式”

让我们先抛开代码,重新审视一下我们熟悉的“手势”。你会发现,任何一个手势,其本质都不是一个孤立的、瞬时发生的事件,而是一个在时间序列上展开的、符合特定规则的“模式”。

  • Tap (点击):这个模式是“一次短暂的按下并迅速抬起,且整个过程中的位移变化非常小”。
  • Pan (拖拽):这个模式是“手指按下,然后移动超过一个微小的初始阈值,接着持续移动,最后手指抬起”。
  • Press (长按):这个模式是“手指按下,并保持位置基本不变,持续超过一段预设的时间”。

看到了吗?手势识别的本质,其实就是模式识别。我们的任务,就是在 InputAdapter 提供的连续输入流(INPUT_START, INPUT_MOVE, INPUT_END)中,判断用户的操作序列是否与我们预设的某种模式相匹配。

那么,有没有一种经典的、强大的、专门用来解决这类问题的工具呢?当然有,它就是计算机科学中的一个基础而重要的概念——有限状态机。

隆重介绍:有限状态机

有限状态机(Finite State Machine, FSM),听起来可能有些学术,但它的思想其实非常简单,并且早已融入我们生活的方方面面。它是一种数学模型,用来表示一个系统所能拥有的有限个状态,以及在这些状态之间如何根据输入进行转移和执行动作。

让我们用一个最经典的例子来理解它:十字路口的红绿灯

一个红绿灯系统,就是一个完美的状态机:

  • 状态 (States):它只有三个明确、有限的状态:红灯亮绿灯亮黄灯亮
  • 输入/条件 (Input/Condition):触发状态变化的条件非常简单,就是“预设的时间到了”。
  • 转移 (Transitions):状态之间的转移路径是固定的:绿灯亮 只能转移到 黄灯亮黄灯亮 只能转移到 红灯亮红灯亮 再转移回 绿灯亮
  • 动作 (Actions):每次状态转移时,都会执行一个动作——切换灯泡的颜色。

我们可以用一张图来清晰地描述这个过程:

      +------------------+
      |     绿灯亮       | <------+
      +------------------+        |
             | (时间到了)         |
             v                    |
      +------------------+        |
      |     黄灯亮       |        |
      +------------------+        |
             | (时间到了)         | (时间到了)
             v                    |
      +------------------+        |
      |     红灯亮       | -------+
      +------------------+

这个简单的模型,精确、无歧义地描述了红绿灯的全部工作逻辑。现在,让我们把这个强大的思想应用到手势识别中。

将状态机应用于手势:以 Pan 为例

现在,我们化身为手势库的设计者,尝试用状态机的思想来“设计”一个 Pan (拖拽) 手势的识别逻辑。我们将使用在 constants.js 中定义的状态常量。

Pan 手势的状态设计与转移路径

  1. STATE_POSSIBLE (初始/可能状态)

    • 这是所有识别器的起点。它表示:“我准备好了,正在等待输入。”
    • 转移条件:当接收到第一个输入事件 INPUT_START(用户手指按下)时,识别器并不会立即改变状态,它会保持在 POSSIBLE,因为它还不确定用户是想点击、长按还是拖拽。
  2. STATE_BEGAN (开始状态)

    • 转移条件:当识别器处于 POSSIBLE 状态,并接收到 INPUT_MOVE 事件,且移动的距离(distance)超过了我们设定的阈值(例如 10px)时,状态就从 POSSIBLE 转移到 BEGAN
    • 解读:这个转移的意义是:“用户的意图明确了,他不是想点击,而是要开始拖拽了!”
    • 动作:在状态转移的这一刻,立即触发 panstart 事件,通知外部“拖拽手势开始了”。
  3. STATE_CHANGED (变化状态)

    • 转移条件:当识别器已经处于 BEGANCHANGED 状态,并持续接收到 INPUT_MOVE 事件时,它会一直保持在 CHANGED 状态。
    • 解读:这表示“用户正在持续拖拽中”。
    • 动作:在每次进入此状态时,持续触发 panmove 事件,并不断更新 deltaX, deltaY 等手势数据,供外部使用。
  4. STATE_ENDED (结束状态)

    • 转移条件:当识别器处于 BEGANCHANGED 状态,突然接收到了 INPUT_END 事件(用户手指抬起)时,状态转移到 ENDED
    • 解读:这标志着“一次完整的拖拽手势结束了”。
    • 动作:触发 panend 事件。
  5. STATE_FAILED (失败状态)

    • 转移条件:如果识别器还处于 POSSIBLE 状态,但还没来得及移动足够距离,就接收到了 INPUT_END 事件(用户只是点了一下就抬手了),那么它就无法满足 Pan 的模式。此时,状态就转移到 FAILED
    • 解读:这意味着“本次输入序列不符合 Pan 手势的模式,识别失败”。

下面这张图,是 Pan 手势状态机的完整生命周期,它将是贯穿我们全书最重要的图之一:

                 INPUT_START
+--------------------------------------------------------------------+
|                                                                    |
|      +----------------+                                            |
|      | STATE_POSSIBLE |                                            |
|      +----------------+                                            |
|      |                |                                            |
|      | (初始状态)     |                                            |
|      +-------+--------+                                            |
|              |                                                     |
|              | (INPUT_MOVE && distance > threshold)                |
|              v                                                     |
|      +-------+--------+       INPUT_MOVE       +-----------------+ |
|      |  STATE_BEGAN   | ---------------------> |  STATE_CHANGED  | |
|      | (触发 panstart)|                        | (触发 panmove)  | |
|      +-------+--------+ <--------------------- +-------+---------+ |
|              |          (保持在 CHANGED 状态)          |           |
|              |                                         |           |
|              | (INPUT_END)                             | (INPUT_END)
|              v                                         v           |
|      +-------+--------+                        +-------+---------+ |
|      |  STATE_ENDED   |                        |  STATE_ENDED    | |
|      | (触发 panend)  |                        | (触发 panend)   | |
|      +----------------+                        +-----------------+ |
|                                                                    |
|                                                                    |
|      +----------------+                                            |
|      | STATE_FAILED   | <---- (INPUT_END, 未满足 BEGAN 条件) ------+ 
|      | (识别失败)     |                                            |
|      +----------------+                                            |
|                                                                    |
+--------------------------------------------------------------------+

状态机的价值:为什么它是完美的模型?

通过上面的设计,我们可以看到,状态机为我们解决手势识别问题带来了无与伦比的优势:

  1. 逻辑清晰:它将原本可能需要用大量嵌套的 if/else 语句才能实现的复杂逻辑,转化为了一系列清晰、离散的状态和明确的转移路径。代码的可读性和可维护性得到了质的飞跃。

  2. 易于扩展:如果未来我们想增加一个新的手势,比如 DoubleTap(双击),我们只需要设计一个新的、属于 DoubleTap 的状态机,而几乎不会影响到现有的 PanTap 的逻辑。

  3. 描述能力强:状态机提供了一种精确、无歧义的“语言”,可以用来描述任何一个手势从诞生到消亡的完整生命周期。

  4. 易于调试:当一个手势的行为不符合我们的预期时,我们可以非常方便地打印出它的状态转移日志,清晰地追溯到是在哪个状态、因为哪个输入,导致了非预期的转移。

本章小结

手势即模式,状态机是识别模式的利器。在本章,我们没有编写一行具体的实现代码,但我们掌握了手势识别的“内功心法”。通过理论学习和对 Pan 手势的设计演练,我们已经拥有了用“状态机思维”来分析和设计任何手-势识别逻辑的能力。

理论的基石已经奠定。从下一章开始,我们将把这套强大的理论付诸实践,亲手打造出手势库的中央协调者——Manager,它将负责统一管理和驱动我们未来创建的所有手势识别器(状态机)。

识别的灵魂:深入手势识别状态机 has loaded