空间界面同时存在三种输入:视线追踪(凝视点选)、手势识别(指捏点选)、头部运动(转动视角)。三种输入各自产生一个焦点候选,大部分时候它们一致——你看着哪个、手指指哪个、头朝哪个,都是同一个对象。但有时候不一致:你凝视着左边的商品 A,手却指着右边的商品 B,这时焦点给谁?我们第一版用的是"最后操作优先",结果用户凝视 A 想看详情,手不小心动了一下焦点就跳到 B,投诉说"我明明在看 A 为什么弹出 B 的详情"。这篇把三种输入的焦点仲裁策略拆开讲,给一套实测过的优先级规则。

能力面:三种输入的焦点候选

每种输入产生焦点候选的方式不同:

  • 凝视:视线射线与场景求交,命中最近的 SpatialNode。延迟约 50~80ms(眼动追踪采样率 20~30Hz,加上滤波平滑)。
  • 手势:手指射线或捏合点与场景求交。延迟约 30~50ms(手势识别采样率 30~60Hz)。
  • 头动:头部朝向射线与场景求交,主要用于视角控制而非点选。延迟约 20~40ms(IMU 采样率高)。

三种延迟差异不大,但在快速操作时会错位。用户凝视 A 的同时手开始指向 B,手势延迟比凝视短,B 的焦点候选先到,如果仲裁策略是"最后到达优先",焦点会先跳到 B 再被凝视拉回 A,造成闪烁。

仲裁策略对比

我们试了四种仲裁策略:

策略规则优点缺点
最后操作优先谁最后产生焦点候选谁赢简单手误触抢焦点
固定优先级手势 > 凝视 > 头动明确凝视被手势长期压制
置信度加权按各输入置信度排序自适应置信度难标定
时间窗口融合200ms 内多输入取共识抗误触延迟感明显

最后选的是固定优先级 + 时间窗口融合的混合策略:手势有明确捏合动作时优先级最高(用户明确意图),纯悬停手势降级到凝视以下;凝视持续 150ms 以上才算有效焦点(过滤扫视);200ms 内多种输入冲突时取优先级最高的。这个策略是我们调了两周才定下来的,下面拆开讲。

约束面:误触率与延迟的权衡

仲裁策略的核心矛盾是误触率和延迟。时间窗口越长误触越少,但用户感觉"点了半天才响应"。窗口越短响应快,但手抖一下焦点就跳。

我们让 12 个同事做了 100 次点选测试,统计不同窗口的误触率和主观延迟感:

时间窗口误触率主观延迟感用户满意度
0ms(立即)18.3%“很跟手”6.2/10
100ms8.7%“略微迟钝”7.8/10
200ms3.2%“能接受”8.5/10
300ms1.8%“有点慢”7.1/10
500ms0.5%“明显卡”5.3/10

200ms 是甜点区,误触率 3.2% 可接受,延迟感不明显。但这个数字跟操作频率有关——快速连续点选时 200ms 窗口会让用户觉得每次都要等,慢速浏览时 200ms 完全无感。我们的做法是窗口自适应:检测到连续操作(两次点选间隔 <500ms)时缩窗到 100ms,否则用 200ms。

凝视的注视阈值

凝视焦点不能视线一落上去就触发,人眼会自然扫视,扫视过程中视线扫过的对象不该获得焦点。需要区分"扫视"和"注视"。

阈值定多少是个争论点。眼动研究里注视通常定义 >100ms,但空间界面里 100ms 太短,用户视线在一个商品上停留 100ms 可能只是在扫视路过。我们实测 150ms 注视阈值比较合适——扫视通常 80~120ms 完成,150ms 能过滤大部分扫视,又不至于让用户觉得"盯着半天才选中"。

这个阈值在密集列表场景要调高。商品列表 20 个项挤在一起,视线扫过时在每项上停留都可能超 150ms,导致焦点乱跳。密集场景我们调到 250ms,稀疏场景用 150ms。

场景落地:空间菜单的焦点仲裁

一个空间菜单有 6 个选项,排成两行三列。用户凝视选项 A(左上),同时手指指向选项 B(右下)。

仲裁过程:

  1. 凝视候选:A(注视 150ms 后生效)
  2. 手势候选:B(手指悬停,无捏合)
  3. 仲裁:手势无捏合动作,降级到凝视以下 → 焦点 = A
  4. 用户捏合 → 手势升级为最高优先级 → 焦点 = B

用一张判定链把上面四步串起来,顺序就是优先级顺序,从上往下匹配第一个满足条件的分支。

否

是

是

否

是

否

是

否

三路候选:凝视/手势/头动

按 200ms 时间窗口过滤

窗口内有候选?

焦点保持为空

手势带捏合?

手势胜出,聚焦该节点

凝视注视满 150ms?

凝视胜出,聚焦该节点

手势悬停满 500ms?

悬停升级为主动,手势胜出

头动不点选,不设焦点

注意凝视的注视阈值和悬停的长按阈值是两个独立的门槛,不能互相替代。

// entry/src/main/ets/focus/FocusArbiter.ets

interface FocusCandidate {
  nodeId: string;
  source: 'gaze' | 'gesture' | 'head';
  confidence: number;       // 0~1
  timestamp: number;        // ms
  hasPinch: boolean;        // 手势是否有捏合
}

export class FocusArbiter {
  private static WINDOW_MS = 200;
  private static GAZE_DWELL_MS = 150;

  // 三路候选输入,输出最终焦点
  static resolve(candidates: FocusCandidate[]): string | null {
    const now = Date.now();
    // 过滤时间窗口外的候选
    const valid = candidates.filter(c => now - c.timestamp < this.WINDOW_MS);
    if (valid.length === 0) return null;

    // 手势有捏合 → 最高优先级
    const pinchGesture = valid.find(c =>
      c.source === 'gesture' && c.hasPinch);
    if (pinchGesture) return pinchGesture.nodeId;

    // 凝视需满足注视阈值
    const gaze = valid.find(c =>
      c.source === 'gaze' &&
      now - c.timestamp >= this.GAZE_DWELL_MS);
    if (gaze) return gaze.nodeId;

    // 手势无捏合(纯悬停)→ 低于凝视
    const hoverGesture = valid.find(c =>
      c.source === 'gesture' && !c.hasPinch);
    if (hoverGesture && !gaze) return hoverGesture.nodeId;

    // 头动最低优先级,一般不产生点选焦点
    return null;
  }
}

关键逻辑:捏合手势 > 凝视(满足注视阈值)> 手势悬停 > 头动。纯悬停手势不抢凝视焦点,只有明确捏合才算"用户要选这个"。

踩坑与取舍

坑一:凝视和手势冲突时意图误判

用户凝视 A 想看详情,手自然垂下时手指恰好指向 B。第一版"最后操作优先"把手势候选判为最新,焦点跳到 B,弹出 B 详情。用户很困惑——“我在看 A 啊”。

修复是区分手势的"主动操作"和"无意悬停"。捏合是主动操作,纯悬停不是。只有主动操作才能抢凝视焦点。这个区分把误触率从 18% 降到 3%。

但有个边界 case:用户确实想用手指悬停选 B(不捏合,就指着等选中)。这种操作模式在远距离场景有用——对象太远捏合不好使。我们的处理是给手势悬停加一个 500ms 长按阈值,悬停超 500ms 升级为主动操作。短于 500ms 的悬停不抢焦点。

坑二:头动误触导致焦点乱跳

头动主要用于视角控制,但头部转动时射线会扫过场景中的对象,如果头动也产生焦点候选,转个头焦点就从头扫到尾。

解决方案是头动不产生点选焦点,只产生视角焦点。点选焦点只有凝视和手势能产生。头动影响的是"用户朝哪看"(视锥方向),不影响"选中了谁"。这个区分在 API 26 里要自己实现——headPose 回调只更新相机朝向,不调 setFocus。

被放弃的方案:置信度加权仲裁

试过给每种输入算置信度(凝视置信度按注视时长、手势置信度按捏合力度、头动置信度按角速度),取置信度最高的。问题是置信度标定太难——凝视注视 200ms 置信度多少?手势捏合力 0.3N 置信度多少?每个场景要重新标。调了一周发现置信度阈值换个场景就不对,放弃了,回到固定优先级。

总结一下下

  • 手势捏合 > 凝视(150ms 注视)> 手势悬停(500ms 长按升级)> 头动(不点选)
  • 时间窗口 200ms,连续操作时缩到 100ms
  • 凝视注视阈值 150ms,密集列表场景调到 250ms
  • 头动只控视角不产生点选焦点
  • 手势纯悬停不抢凝视焦点,需捏合或长按才算主动操作

下一步

目前注视阈值和窗口大小是写死的,换场景要改代码。下一步打算做成配置项,让业务方按自己场景的密度和操作频率调。另外想试一下用眼动追踪的扫视速度自适应注视阈值——检测到用户在快速扫视时自动拉高阈值,慢速浏览时降低,省去按场景手调。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐