Skip to content

一张图手势交互体系 ​

这篇不是「手势列表」,而是一套车载地图交互系统的工程案例:输入识别、策略分发、地图状态机、相机锚点和渲染时机必须一起设计,缺任何一环都会变成用户手上的「黏手」或「跳变」。

手势策略8 类
关键收益0 帧拖拽
地图中心三态状态机

背景 ​

一张图的核心体验由手势驱动:拖拽移动地图、双指缩放、单/双指点击缩放、惯性甩图、无操作回正、俯仰角控制、LD/SD 切换。难点不在于识别某个手势,而在于 Unity、Android 原生层、XMapEngine、相机状态和地图锚点同时参与一次交互。

问题 ​

问题现象工程风险
输入源不统一Android 与 Unity 各有识别链路同一手势在不同端行为不一致
状态依赖隐式手势直接影响相机、高度、比例尺、锚点修一个交互容易牵动全局
跟车逻辑散「地图什么时候跟车」没有单一判定拖完不回正、车不在屏幕中心
渲染时机错位Update 处理输入,渲染晚一帧拖拽跟手感差,错 1 帧

方案 ​

手势系统四层拆分 ​

层级责任
输入层FingerGestures / Unity Legacy Input / Android 原生手势识别
策略层单指拖拽、双指缩放、点击缩放、惯性、回正、俯仰角等策略
状态层XGestureCtrl、LDGestureRoot、手势状态机、比例尺、相机锚点
渲染层XMap、MasterDisplay、相机节点、地图节点和自车节点

关键不是把所有逻辑塞进一个手势类,而是把「输入是什么」「状态怎么变」「画面何时更新」分开。业务层只依赖统一手势接口,底层识别实现(FingerGestures 或 Unity Legacy Input)可以替换。

地图中心三态状态机 ​

「地图什么时候跟车」收敛成一个三态状态机,核心规则只有一句话:只有 Idle 状态时,MapCenter 才等于车辆位置,地图才跟车。

mermaid
flowchart TD
    IDLE["Idle 跟车<br>MapCenter = 车辆位置"] -->|"手指按下拖动<br>停止跟车"| HAND["Handling 操作中<br>MapCenter = 屏幕中心"]
    HAND -->|"手指松开"| COOL["Cooldown 冷却期<br>等待反悔"]
    COOL -->|"再次触摸<br>打断回正动画"| HAND
    COOL -->|"约 3 秒无操作<br>播放回正动画"| IDLE
  • Idle(跟车):MapCenter = 车辆位置,自车始终在屏幕中心,是默认状态。每帧在 MapStatusService.OnNormalFresh() 里检查:若处于 Idle 就把 MapCenter 刷成车辆位置。
  • Handling(操作中):用户正在拖拽/捏合,MapCenter = 屏幕中心,不再跟车,车可以偏出屏幕中心。
  • Cooldown(冷却期):手指松开但不立刻回正,给用户「反悔」的机会——继续拖就打断回正动画回到 Handling;约 3 秒无操作才由 AnimationOrchestrator 启动 CorrectAnimation,把 MapCenter 插值回车辆位置,进入 Idle。进入 Idle 必须走动画,不允许瞬切。

这个状态机直接回答了两个高频问题:「拖完为什么不回正」(还在 Cooldown 期间又碰了屏幕)、「车为什么不在屏幕中心」(当前不在 Idle)。

相机手势框架:移除 Cinemachine ​

Napa XOS6.0 镜头架构的一个重要决策是移除 Cinemachine 插件,替换为自研相机手势框架。Cinemachine 的虚拟相机机制强大但黑盒,手势要改相机行为必须绕过它的混合逻辑,改一处动全身。自研框架把相机和手势的关系显式化:

组件职责
ICameraParamsOutput相机参数输出:fov、正交尺寸、裁剪面、位置/旋转、isChanged 等
CameraGestureContext手势上下文:手指类型、触点位置、相机实例、上次手势时间
ICameraGesture手势相机接口:Update / Break / Reset,按手指类型索引 Action
CameraGestureAction抽象手势动作:Execute / Stop / 阈值起手势 / 正交尺寸与裁剪面计算
IMCameraInput触点输入更新,维护当前/上次手型
IMCameraCtrl每帧驱动:参数 isUpdating 时更新手势相机,isChanged 时回写相机参数

一个细节:CameraGestureContext 的 lastGestureTime 在运行超过 48 小时精度下降时要转 double——车机是长开机的设备,float 时间戳的精度问题在普通 App 里永远不会暴露。框架按 FloorGestures / MapGestures 分组管理手势动作,LD 与非 LD 场景分别走 XMapGestureHelper 和 CameraRootNode 两条入口。

关键实现 ​

8 类手势的处理要点 ​

手势关键处理
单指拖拽屏幕坐标转墨卡托,再转 Unity 世界坐标,更新地图锚点
双指缩放缩放中心对齐地图中心,避免缩放偏位
单指双击缩放点击点作为缩放中心,进入动画过渡
双指单击缩放与普通缩放区分优先级,避免误触
惯性甩图根据离手速度做衰减,手感参数可调
无操作回正交给状态机 Cooldown 分支,动画插值回车辆位置
俯仰角控制受场景和 pitch 区间约束,避免与缩放冲突
回自车start / ing / end 显式状态化,降低抖动

0 帧拖拽 ​

旧问题的本质是时序错位:手势输入在当前帧改变状态,但地图移动和相机渲染没有在同一时机对齐,视觉上慢一拍。按帧拆开看:输入事件进入 → 策略计算位移 → 锚点状态更新 → 相机和地图节点更新 → 渲染提交,任何一步晚一帧,用户感知都是「拖不动」。解决方式是把 LD 地图移动统一放到 LateUpdate 对齐手势结果,让输入、状态和渲染同帧收敛(详见 拖拽 0 帧延迟优化)。

指标 ​

指标结果
手势覆盖8 类手势策略化 + 全量支持表
拖拽延迟错 1 帧(~16ms @60fps)→ 0 帧
跟车判定收敛为 Idle / Handling / Cooldown 三态
相机框架Cinemachine 移除,自研框架接管虚拟相机效果

复盘 ​

  1. 交互系统要按「输入、策略、状态、渲染」拆,不要按类名堆。
  2. 「什么时候跟车」这种产品语义问题,最好的解法是收敛成一个状态机里的一条规则,而不是散落在十几个回调里的 if。
  3. 第三方插件(Cinemachine)在 demo 阶段是加速器,在精细交互阶段是黑盒——当调试成本超过学习自研的成本时就该换。
  4. 车机场景有自己的长尾问题:长开机精度、台架复现、双端输入一致性,都不能照搬移动 App 的经验。

罗子初 · AI 座舱专家 · 8年端侧研发

Lv.1