手势

一切触摸交互,都始于手势 — 系统从手指的运动轨迹中识别特定模式。 Tap、Swipe、Drag、Fling、Pinch:每种手势各有判定条件, 由时间、位移、速度和指针数量共同界定。

·····

Touch Down(按下)

在系统识别出具体手势之前,有一件更基础的事先要发生: 手指接触屏幕。这是一切交互的起点。界面应当即刻回应 — 缩放、高亮、颜色变化皆可 — 以此确认触摸已被接收。

这和 Tap 不同。Tap 需要手指快速抬起;而 Touch Down 只有 press 阶段, 它应当立即且无条件地触发,无需等待系统判断后续手势类型。 若后续发展为拖拽,按下态应平滑过渡;若是 Tap,则在抬手时完成闭环。

fig-099 · touch down feedback
任意位置点击
·····

Tap(单击)

最基础的手势。手指按下后几乎不移动,随即快速抬起。 它相当于触摸世界里的鼠标点击,而且常由"没有发生的事"来定义: 没有明显位移(小于约 10px),没有长时间停留(小于约 300ms)。

Tap:快速触摸,位移极小
fig-100 · tap detector
任意位置点击
·····

Double Tap(双击)

在接近同一位置连续点击两次。难点在于:系统必须判断 — 第一次点击究竟是"单击本身",还是"双击的前奏"。经典做法是第一次点击后 等待 300ms:窗口内出现第二次点击,则判定双击; 否则触发单击。

正因为这个等待窗口,同时支持单击与双击的界面常让人觉得"慢半拍"。 更好的策略是:先立即执行单击,若随后检测到双击再做回滚; 或者把单击与双击分配到互不冲突的目标上,从根源消除歧义。

Double tap:在 300ms 与 25px 范围内完成两次点击
fig-101 · single vs double tap
单击或双击
·····

Long Press(长按)

手指按下后保持不动。当按住时间达到阈值(通常约 400ms),系统触发长按。 这是少数由时间本身驱动识别、且手指仍停留在屏幕上的手势。

设计关键在于提供进度反馈 — 例如触点周围逐步填满的圆环, 让用户明确知道"继续按住就会触发动作"。若没有可见反馈,用户只能盯着屏幕 猜测系统是否收到了指令。这种"死寂感"是手势设计中最糟糕的体验之一。

Long press:手指未抬起时,计时器到点触发
fig-102 · long press with progress
按住不放
·····

Swipe(滑动)

一次快速、方向明确的手指运动 — 短促、迅捷、果断。 Swipe 同时依赖速度(抬手瞬间有多快)和 方向(左、右、上、下)。它不同于慢速拖拽: 系统会判断释放速度是否超过阈值(通常 600–1000 px/s), 同时检查主方向上是否有足够位移。

这个 demo 故意把两条规则同时展示:当释放速度超过 650px/s,或水平位移超过 60px 时就会识别为 Swipe。 你可以分别测试“速度”与“位移”对识别结果的影响。

Swipe 驱动了大量经典移动交互 — 通知划走、页面切换、操作露出。 识别本身并不复杂:它就是"以清晰方向结束、且速度够快的拖拽"。 真正复杂的,是识别之后如何处理(见 Chapter 3)。

fig-103 · swipe card
拖拽后快速松手
·····

Drag / Pan(拖拽 / 平移)

最基础的连续手势:手指按下、移动,元素跟着走。 1:1 跟手意味着元素的位移与手指完全一致 — 没有延迟,没有漂移。正是这种直接耦合,让拖拽有种"真被抓住了"的实感。

但拖拽不会在按下瞬间就启动。系统会设置一段死区,称为 touch slop(通常为 10px), 手指越过它才正式进入拖拽状态。这能避免本想点按或长按时被误判为拖拽。

手指释放后,元素会经由弹簧回到静止位。这一步提供了"收口感" — 用户能清晰感知:手势已结束,系统已回到稳定状态。

Drag:越过 touch slop 后进入 1:1 连续跟手
fig-104a · 1:1 tracking + velocity transfer
拖动后快速松手

让 1:1 跟手自然的关键,在于把手指释放时的速度传递给元素。 常见做法是采样最后约 100ms 的移动数据来计算速度,再将其注入弹簧作为初速度。 这样元素便会沿拖动方向继续运动,然后平滑收敛到目标位。

你也可以对比下方的弹簧延迟跟手方案: 拖动过程中元素会有一点软性滞后,重量感更强,但精确性随之下降。

fig-104b · spring delay tracking
拖动圆点
·····

Axis Lock(轴向锁定)

列表滚动时,你要的通常是纯垂直或纯水平,而非两轴同时漂移。 轴向锁定会在初始移动阶段判断主方向,随后将后续移动约束到单轴上。 斜向抖动因此减少,滚动更精准、更有意图感。

一般在手指越过 touch slop 的那一刻做出决定: 哪个方向位移更大,就锁定哪个方向;另一方向在本次手势中被抑制。 好用的列表滚动之所以像"沿轨道前进",原因正在于此。

Axis lock:初始位移决定 X 或 Y 约束
fig-105 · axis lock demo
拖动圆点
·····

Pinch / Zoom(捏合缩放)

常见手势里唯一稳定依赖双指的类型。两指触屏,分开即放大, 靠拢即缩小。缩放比例取自当前两指距离与起始距离的比值 — 比如两指间距翻倍,缩放通常也随之翻倍。

系统通过 pointer ID 分别追踪两根手指。双指同时激活时, 需实时计算两指间距(pinch span)和中点(zoom center)。 中点至关重要 — 它决定了缩放围绕哪里发生。 你两指之间的点应当尽量"钉"在屏幕上,其余内容围绕它缩放。

手指释放时,若当前缩放超出允许区间(过小或过大), 弹簧会将其拉回最近合法值 — 这相当于把 rubber band 思路从"位置"搬到了"缩放值"上。 桌面端可用滚轮、右键拖动和键盘快捷键(+/-/0)作为替代输入。

fig-106 · pinch to zoom
双指捏合缩小 · 双指外扩放大滚轮 · 右键拖动
·····

Fling(甩动)

当一次拖拽在手指仍高速运动时结束,这次释放就是 fling。 抬手瞬间的速度直接决定后续行为:速度超过阈值,系统便将其判定为 fling, 而非普通释放。

Fling 是"手势识别"与"物理系统"之间的桥梁。 识别器负责判定是否发生了 fling;物理系统负责处理后续 — 动量滚动、分页吸附,或 dismiss 动画。 你甩手那一刻的速度,就是后续动画的初始速度。

Fling:以高速度结束的拖拽会触发 fling 事件
fig-107 · fling detector
快速拖动后松手
·····

手势竞争(Gesture arbitration)

这是手势系统最根本的挑战:tap、long press 和 drag 都从同一个动作开始 — finger down。系统无法未卜先知,只能等待、观察、再决策。

这就是 gesture arbitration(手势仲裁)。 手指落下的瞬间,多个识别器同时进入竞争。谁先满足触发条件,谁胜出:

  • 手指移动超过约 10px → 判定为拖拽(速度足够高时可进入 swipe)。同时取消 long-press 计时器。
  • 手指快速抬起 → 判定为 tap。取消 long-press 计时器。
  • 400ms 内几乎无位移 → 判定为 long press。

在下方试试:快速点按、按住不动、或按下后拖动。观察状态徽章的变化, 看最终是哪个识别器赢得仲裁。

Gesture arbitration:从 PRESSED 状态分流的三条出口
fig-109 · recognizer arena
idle pressed tap long press drag
点击、按住或拖动
·····

综合起来看

所有手势都源自同一组原始输入:pointer down、move、up。 区分它们的,是时间、位移、速度和指针数量的不同组合:

  • Tap — 在 300ms 内快速按下抬起,位移很小
  • Double tap — 两次点击间隔小于约 300ms
  • Long press — 基本不移动地按住约 400ms
  • Drag — 位移超过 touch slop(约 10px),进入 1:1 跟手
  • Swipe — 方向明确且速度快(本 demo 中 >650px/s 或位移 >60px)
  • Fling — 以高速度释放拖拽(>950 px/s)
  • Pinch / Zoom — 双指距离变化映射缩放比例

这些原语,组合起来便是你每天都在使用的触摸交互 — 滚动、关闭、缩放、选择。 下一章我们将进入 物理手感:动量、弹簧、回弹, 以及把原始输入变成"有温度的运动"的数学机制。