Appearance
识别的灵魂:深入手势识别状态机
在前面的章节里,我们已经完成了手势识别的各项准备工作:定义了常量,实现了事件系统,并创建了能够抹平设备差异的输入适配器。我们现在能源源不断地获取到标准化的输入数据流。现在,我们面临着最核心的问题:如何从这些连续的、看似杂乱的输入流中,识别出用户想要表达的特定手势?
答案,就蕴含在本章的标题中——状态机。
重新思考手势:手势是一种“模式”
让我们先抛开代码,重新审视一下我们熟悉的“手势”。你会发现,任何一个手势,其本质都不是一个孤立的、瞬时发生的事件,而是一个在时间序列上展开的、符合特定规则的“模式”。
- 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 手势的状态设计与转移路径:
STATE_POSSIBLE(初始/可能状态)- 这是所有识别器的起点。它表示:“我准备好了,正在等待输入。”
- 转移条件:当接收到第一个输入事件
INPUT_START(用户手指按下)时,识别器并不会立即改变状态,它会保持在POSSIBLE,因为它还不确定用户是想点击、长按还是拖拽。
STATE_BEGAN(开始状态)- 转移条件:当识别器处于
POSSIBLE状态,并接收到INPUT_MOVE事件,且移动的距离(distance)超过了我们设定的阈值(例如 10px)时,状态就从POSSIBLE转移到BEGAN。 - 解读:这个转移的意义是:“用户的意图明确了,他不是想点击,而是要开始拖拽了!”
- 动作:在状态转移的这一刻,立即触发
panstart事件,通知外部“拖拽手势开始了”。
- 转移条件:当识别器处于
STATE_CHANGED(变化状态)- 转移条件:当识别器已经处于
BEGAN或CHANGED状态,并持续接收到INPUT_MOVE事件时,它会一直保持在CHANGED状态。 - 解读:这表示“用户正在持续拖拽中”。
- 动作:在每次进入此状态时,持续触发
panmove事件,并不断更新deltaX,deltaY等手势数据,供外部使用。
- 转移条件:当识别器已经处于
STATE_ENDED(结束状态)- 转移条件:当识别器处于
BEGAN或CHANGED状态,突然接收到了INPUT_END事件(用户手指抬起)时,状态转移到ENDED。 - 解读:这标志着“一次完整的拖拽手势结束了”。
- 动作:触发
panend事件。
- 转移条件:当识别器处于
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 条件) ------+
| | (识别失败) | |
| +----------------+ |
| |
+--------------------------------------------------------------------+状态机的价值:为什么它是完美的模型?
通过上面的设计,我们可以看到,状态机为我们解决手势识别问题带来了无与伦比的优势:
逻辑清晰:它将原本可能需要用大量嵌套的
if/else语句才能实现的复杂逻辑,转化为了一系列清晰、离散的状态和明确的转移路径。代码的可读性和可维护性得到了质的飞跃。易于扩展:如果未来我们想增加一个新的手势,比如
DoubleTap(双击),我们只需要设计一个新的、属于DoubleTap的状态机,而几乎不会影响到现有的Pan或Tap的逻辑。描述能力强:状态机提供了一种精确、无歧义的“语言”,可以用来描述任何一个手势从诞生到消亡的完整生命周期。
易于调试:当一个手势的行为不符合我们的预期时,我们可以非常方便地打印出它的状态转移日志,清晰地追溯到是在哪个状态、因为哪个输入,导致了非预期的转移。
本章小结
手势即模式,状态机是识别模式的利器。在本章,我们没有编写一行具体的实现代码,但我们掌握了手势识别的“内功心法”。通过理论学习和对 Pan 手势的设计演练,我们已经拥有了用“状态机思维”来分析和设计任何手-势识别逻辑的能力。
理论的基石已经奠定。从下一章开始,我们将把这套强大的理论付诸实践,亲手打造出手势库的中央协调者——Manager,它将负责统一管理和驱动我们未来创建的所有手势识别器(状态机)。