
在手机上用户只有一个明确的指向器官——手指点哪里就是哪里。到了空间里设备同时拥有了三双眼睛看哪儿视线、朝哪儿头部姿态、做什么动作手势。能力变多麻烦也跟着来了三个通道各说各话时系统到底听谁的这篇先把三条输入通道的能力边界摊开再谈怎么让它们协同工作而不至于互相打架。三个输入通道各自能干什么空间交互不是把触摸事件搬到空中那么简单。每个通道采集的物理量不同能表达的用户意图层级也不同。视线追踪Eye Tracking提供的是注视点Gaze Point与注视时长。它的优势是快——人眼扫过一个目标只需要 100~200ms比手指移动快一个数量级。视线天然表达我想看什么也就是指向意图Pointing Intent。但它有个硬伤眼动包含大量无意识的扫视Saccade和微颤Micro-tremor设备没法区分看了一眼和盯着看。所以视线擅长选目标不擅长下命令。头部运动Head Motion通过陀螺仪与加速度计采集姿态角Pitch/Yaw/Roll表达的是朝向意图Orientation Intent——用户把注意力转向哪个方向。它比视线稳定适合做视角控制、区域切换、粗粒度导航。缺点是慢转头是个大动作且连续转动容易引发不适。手势识别Gesture Recognition从相机或手部追踪传感器拿到手部关键点识别捏合Pinch、抓取Grab、滑移Swipe/Slide等动作表达操作意图Manipulation Intent——用户想对目标做什么。它最接近传统触摸的操作心智精确度也最高代价是需要抬手、有学习成本长时间悬空操作还会累Gorilla Arm 问题。把这三者放在一张表里选型时的取舍会清楚很多。通道采集量表达意图响应速度精度主要短板视线追踪注视点、注视时长指向极快100~200ms中易抖动无法区分看与盯不能独立下命令头部运动姿态角、角速度朝向中300~500ms中动作幅度大易疲劳易晕手势识别手部关键点、动作类型操作中含识别延迟高需抬手长时悬空累误识别2D 触摸基线触点、压力操作快极高局限于平面无深度信息一句话概括这张表的用法视线负责选谁头部负责面向哪手势负责对它做什么。多通道融合的交互模型单个通道都不足以独立支撑一次完整操作融合是必然的。工程上常用的融合方式有三类复杂度递增。串行确认注视选择 手势确认这是最容易落地、也最不容易出错的模型。视线先把光标投到某个目标上完成预选择Pre-selection再用一个明确的手势完成提交Commit。经典的Gaze Pinch就属于这种盯住卡片捏合确认。这样做把选择和确认两个动作拆给了两个通道各取所长。视线快负责找手势稳负责定。它同时消解了视线的迈达斯之触问题——因为光看不做任何事必须配合手势才生效。并行融合加权投票系统对同一时刻的三个通道信号做加权用一个统一的置信度分数决定最终意图。比如视线落在 A、手势指向 B、头部朝向 C 时根据场景动态调整权重。这在需要高吞吐的专业工具里有价值但调试成本高误判时用户很难理解系统为什么这么判断。状态机编排分阶段接管把一次交互拆成若干阶段每个阶段由一个通道主导阶段之间用状态迁移衔接。例如进入空间 → 头部定位区域 → 视线选中对象 → 手势操作 → 视线确认离开。状态机的好处是任何时刻只有一个通道在说话冲突面最小。下面这张图画的是一次典型的注视选择 手势确认流程。否是否 且 注视移开否 且 注视保持是是否用户看向空间中的目标注视点是否稳定停留超过阈值目标进入预选择状态 高亮描边是否检测到捏合手势取消预选择 恢复原状态提交操作 触发选中回调状态切换 进入对象操作态头部朝向是否偏离超过容忍角退出对象操作态 释放焦点冲突消解的基本原则多通道同时活跃时冲突不可避免靠三条规则压住显式动作优先于隐式意图。手势是用户主动做出的动作视线和头部常有无意识成分冲突时选手势。时间上后发者优先空间上前景者优先。最近的动作、离相机最近的目标拿优先权。保守拒绝而非冒险执行。置信度不够时不做任何事比做错事代价小。宁可让用户再来一次。代码把三路信号接进统一的交互处理器下面这段代码演示在 ArkUI 里同时接入手势、传感器姿态和悬停作为视线/指针的简化代理三类事件并用一个统一的InteractionResolver做冲突消解。传感器姿态用kit.SensorServiceKit的陀螺仪数据手势用PanGesture与PanGesture的捏合替代示例PinchGesture。import{sensor}fromkit.SensorServiceKit;import{BusinessError}fromkit.BasicServicesKit;// 三个通道的原始信号统一收敛成一个意图对象interfaceInteractionSignal{focusTarget:string|null;// 当前注视/悬停目标gazeDwellMs:number;// 注视停留时长headYaw:number;// 头部偏航角单位度gestureActive:boolean;// 是否有主动手势进行中}Componentexportstruct SpatialInteractionHost{StatefocusTarget:string|nullnull;StatedwellMs:number0;StateheadYaw:number0;StategestureActive:booleanfalse;// 注视停留计时器目标变化时清零避免“路过”被算作注视privatedwellTimer:number-1;privatereadonlyDWELL_THRESHOLD_MS:number600;aboutToAppear():void{this.startHeadTracking();}aboutToDisappear():void{// 订阅与取消订阅必须成对避免资源泄漏sensor.off(sensor.SensorId.GYROSCOPE);if(this.dwellTimer!-1){clearInterval(this.dwellTimer);}}// 头部姿态用陀螺仪 z 轴角速度近似积分出偏航角// 真实项目建议用 ROTATION_VECTOR 或姿态融合算法替代privatestartHeadTracking():void{try{sensor.on(sensor.SensorId.GYROSCOPE,(data:sensor.GyroscopeResponse){this.headYawdata.z*0.01;// 简易积分仅作演示if(Math.abs(this.headYaw)60){// 头部偏离过大主动释放焦点避免用户“看着别处还在操作”this.cancelFocus();}},{interval:20000000});// 50Hz姿态控制需要较高频率}catch(error){consteerrorasBusinessError;console.error(head tracking failed:${e.code}${e.message});}}privateupdateFocus(target:string|null):void{if(targetthis.focusTarget){return;}this.focusTargettarget;this.dwellMs0;if(this.dwellTimer!-1){clearInterval(this.dwellTimer);this.dwellTimer-1;}if(target!null){conststartDate.now();this.dwellTimersetInterval((){this.dwellMsDate.now()-start;},50);}}privatecancelFocus():void{this.updateFocus(null);this.gestureActivefalse;}// 冲突消解手势显式动作优先之后才看注视停留privateresolveIntent():string{if(this.gestureActive){returncommit:${this.focusTarget??none};}if(this.focusTarget!nullthis.dwellMsthis.DWELL_THRESHOLD_MS){returnpreselect:${this.focusTarget};}returnidle;}build(){Column({space:16}){Text(intent ${this.resolveIntent()}).fontSize(18)// 三个可聚焦目标用 onHover 模拟视线/指针指到目标ForEach([card-A,card-B,card-C],(id:string){Text(id).width(200).height(80).textAlign(TextAlign.Center).borderRadius(12).backgroundColor(this.focusTargetid?#4A90D9:#E8E8E8).onHover((isHover:boolean){// 指针进入即视为注视点落在该目标上this.updateFocus(isHover?id:(this.focusTargetid?null:this.focusTarget));})})// 手势通道捏合作为提交动作Column().width(200).height(60).backgroundColor(#2C2C2C).gesture(PinchGesture({fingers:2}).onActionStart((){this.gestureActivetrue;}).onActionEnd((){this.gestureActivefalse;}))}.width(100%).height(100%).justifyContent(FlexAlign.Center)}}PinchGesture、GestureEvent等属于 ArkUI 声明式范式内置能力在.ets文件中可直接使用无需额外 import。代码里有几个关键点。updateFocus在目标变化时刻意把dwellMs清零——注视计时必须绑定到具体目标否则用户一路扫过会被误判为注视。resolveIntent里手势判断放在最前面对应的是显式动作优先这条消解规则。头部偏航超过阈值直接cancelFocus是在防止用户转开头之后系统还在对旧目标执行操作。案例商品详情页的注视放大 手势查看一个很实际的场景用户在一个空间化的商品陈列里浏览面前有一排悬浮商品卡。纯手势操作的问题是——用户得先用手去够卡片卡片一多手臂来回划很累。纯视线的问题是——用户扫视浏览时卡片不断被误触发。融合方案是这样的头部用户转动头部在不同商品分组之间切换。转头到某一分区时该分区的卡片整体提亮其余变暗。这是一个粗粒度的频道切换用头部最合适。视线悬停在某张卡片上超过 500ms卡片轻微放大并显示价格与评分但不进入详情。这一步只是信息预览不产生副作用。手势确实想进入详情时做一个抓取并拉近的动作卡片飞入视野中心并展开详情。这就是提交动作。三个通道各管一段头部分组、视线预览、手势提交。用户从头到尾只做一次明确的抓取手势其余都在自然行为中完成。实测里这种设计把误触率压得很低因为唯一能产生状态跳转的通道是手势。小小总结把视线当注意力而非控制信号。它天生有噪声且无意识任何看一眼就触发的设计几乎必然会遭遇迈达斯之触。正确的姿势是让视线做选择和预览把副作用留给手势。给每个通道划定职责。视线负责选谁头部负责面向哪手势负责对它做什么。职责交叉越多冲突消解越难做也越难向用户解释系统为什么这么响应。阈值按场景可配置。注视时长、头部转角容忍度、手势置信度都不该是一组固定值。熟练用户和初次使用者、浏览模式和操作模式合适的阈值都不一样至少要留出按场景切换的入口。始终保留安全出口。空间交互一旦误触发用户往往不知道该怎么退回。任何状态下都要有一个明确、低成本的取消路径并且撤销操作要及时可见。注意一下下哦忽略通道之间的时序耦合。注视 手势不是两个独立事件碰巧同时发生而是有先后顺序的因果链。如果手势发生在注视之前用户先抬手再找目标系统应该容忍这个顺序而不是要求严格同时。传感器订阅忘记配对取消。sensor.on和sensor.off必须成对调用页面销毁时要释放。陀螺仪数据频率高忘记取消会让后台持续耗电长时间运行的页面还会累积积分误差。把三个通道当三个独立功能做。如果视线、头部、手势各写一套互不相干的状态和判定逻辑界面很快会出现视线高亮着 A、手势却操作了 B这类矛盾。融合的前提是有一个统一的状态和意图模型三个通道都往同一个模型里写。