空间界面把组件沿 Z 轴叠放,前面的挡住后面的,渲染上没问题——画家算法按 depth 排序自然处理遮挡。问题出在交互上:用户手势射线从手出发指向场景,射线可能穿过前面组件命中后面组件。API 26 默认行为是射线不穿透,只命中最近的。但有些场景你需要穿透——半透明信息卡后面的按钮,用户想直接点后面的按钮不想先把信息卡挪开。默认不穿透和需要穿透之间,遮挡处理成了空间交互的一个隐蔽坑。我们做空间设置面板时,前面有个半透明提示条挡住后面的开关按钮,用户点开关没反应,因为射线被提示条挡住了。这篇把遮挡渲染和交互穿透的规则拆清楚。

能力面:遮挡渲染与交互命中是两套逻辑

渲染遮挡和交互命中虽然都跟 depth 有关,但是两套独立逻辑:

  • 渲染遮挡:按 depth 从远到近排序绘制,近的覆盖远的。半透明组件(alpha < 1)可以让远的透出来,但排序逻辑不变。
  • 交互命中:射线从相机或手部出发,与场景中所有 SpatialNode 的碰撞体求交,取 depth 最近的命中。默认忽略被遮挡的组件。

两套逻辑的分歧点在半透明组件。渲染上半透明组件让后面透出来(用户能看到后面),但交互上默认还是挡住后面(用户点不到后面)。看得见但点不着,这就是我们踩的坑。

API 26 提供三种交互穿透模式:

模式行为适用场景
NONE(默认)射线命中最近组件,不穿透常规遮挡
OCCLUSION_AWARE被完全遮挡的组件不响应,部分可见的可交互半透明面板后的按钮
ALWAYS_PASS射线穿透所有组件,命中 depth 最远的调试/特殊场景

OCCLUSION_AWARE 是最常用的非默认模式。它判断组件是否被"完全遮挡"——如果前面有个不透明组件完全盖住它,它不响应交互;如果前面是半透明的或只挡住一部分,它正常响应。

约束面:遮挡判定的性能和精度

OCCLUSION_AWARE 听起来好,但有约束。

性能:遮挡判定要做组件间的 AABB 相交测试,组件数多时开销不小。我们测了 50 个组件全开 OCCLUSION_AWARE,交互响应延迟从 8ms 涨到 23ms,帧率影响不大但点击响应能感知到迟钝。50 个以内可接受,超过要谨慎。

精度:遮挡判定用的是 AABB(轴对齐包围盒),不是精确形状。一个圆形组件被方形组件挡住,AABB 判定可能说"完全遮挡"但实际圆形边缘还露着。用户看到露出来的边缘去点,判定说被遮挡不响应,点了没反应。

我们踩过这个坑:圆形按钮被方形面板的 AABB 判定为完全遮挡,但按钮右边缘露出来 5px,用户点边缘没反应。修复是把按钮的碰撞体从 AABB 改成精确圆形,但精确碰撞体性能开销更大,只在出问题的组件上用。

半透明的遮挡阈值

“半透明"到底 alpha 多少算"不完全遮挡”?API 26 的阈值是 alpha < 0.95 算部分可见。也就是说 alpha=0.95 的组件在 OCCLUSION_AWARE 模式下算"几乎不透明",后面的组件仍然能交互。这个阈值偏高——0.95 的面板视觉上几乎全不透明,但后面的组件还是可交互的,用户看不到后面却点得到后面,更困惑。

我们的做法是分两档:alpha < 0.5 算半透明,后面可交互(用户能看到后面);alpha >= 0.5 算不透明,后面不交互。这个分档要自己实现,API 的 0.95 阈值太高。

场景落地:半透明面板后的按钮

空间设置面板:前面是半透明提示条(alpha=0.7),后面是开关按钮。用户看到提示条透出来的开关,想直接点开关。

交互模式射线命中用户能否点开关体验
NONE(默认)提示条否“点不到开关”
OCCLUSION_AWARE开关按钮是“能直接点”
ALWAYS_PASS开关按钮是“能点但其他地方也穿透了”

OCCLUSION_AWARE 是正确选择。但要注意 alpha=0.7 按我们的分档算不透明(>=0.5),后面不交互。要让它可交互,alpha 要降到 0.5 以下,或者单独给提示条标 passThrough: true。

把默认、OCCLUSION_AWARE 和 alpha 分档三件事揉进一条命中判定链,就能解释上面表里每种模式的结果。

否

是

是

否

交互射线出发求交

取 depth 最近的命中节点

前层是 OCCLUSION_AWARE?

命中前层,不穿透

前层 alpha 低于 0.5?

判定半透明,穿透到后层

判定不透明,前层挡住

返回后层可交互节点

返回前层节点

这条链里 alpha 阈值用的是我们自己定的 0.5,不是 API 默认的 0.95。

// entry/src/main/ets/pages/SpatialSettings.ets
import { SpatialNode, InteractionMode } from '@kit.ArkUI';

@Entry
@Component
struct SpatialSettings {
  build() {
    SpatialContainer() {
      // 后层:开关按钮,depth=-0.10
      SpatialNode({ depth: -0.10 }) {
        ToggleSwitch({ isOn: this.settingEnabled })
      }

      // 前层:半透明提示条,depth=0,alpha=0.4
      // 用 OCCLUSION_AWARE 让射线穿透到后面的开关
      SpatialNode({
        depth: 0,
        opacity: 0.4,
        interactive: InteractionMode.OCCLUSION_AWARE
      }) {
        HintBar({ text: '开启此功能后...' })
      }
    }
  }
}

alpha=0.4 让提示条足够透明能看到后面开关,同时 OCCLUSION_AWARE 让射线穿透。如果 alpha 调到 0.7,视觉上提示条更清楚但开关看不太清,且按我们的分档后面不交互——视觉和交互矛盾。

踩坑与取舍

坑一:默认不穿透导致点到前面

第一版没设 interactive,用默认 NONE。半透明提示条挡住开关,用户看到开关去点,命中的是提示条,提示条没有 onClick 回调,点了没反应。用户反复点以为卡了。

修复是给提示条设 OCCLUSION_AWARE。但设了之后发现提示条自己的 onClick 也不响应了——OCCLUSION_AWARE 模式下,如果后面有更近的可交互组件,前面组件让出交互权。提示条和开关都可交互时,开关更近(depth=-0.10 比提示条 depth=0 更近?不对,depth 正值朝向用户,提示条 depth=0 比开关 depth=-0.10 更近)。

这里我搞混了 depth 方向。正值朝用户,提示条 depth=0 比开关 depth=-0.10 更近,射线先命中提示条。OCCLUSION_AWARE 判定提示条 alpha=0.4 部分可见,不遮挡后面的开关,射线穿透命中开关。提示条自己的 onClick 在有更远组件可交互时不触发。如果用户想点提示条本身(比如点提示条关掉它),要在提示条上用捏合手势而不是普通点选——普通点选穿透给后面,捏合留在前面。这个区分要跟用户讲清楚,否则用户困惑"为什么有时候点提示条有用有时候没用"。

坑二:ALWAYS_PASS 全穿透导致误触

有一阵图省事把整个面板设成 ALWAYS_PASS,想着"让射线穿透到后面多方便"。结果面板上所有组件都穿透了,用户点前面的按钮命中后面,点哪都不对。ALWAYS_PASS 只适合调试或极特殊场景,生产环境别用。

被放弃的方案:手动射线求交

试过不用 API 的交互模式AABB穿透模式,自己拿射线和所有组件求交,按业务逻辑决定命中谁。灵活性最高但实现量大——每个组件的碰撞体要自己管,射线求交要自己写,性能还不如引擎内置的。调了三天跑通了但代码又长又脆,放弃了,回到 OCCLUSION_AWARE + alpha 分档。

总结一下下

  • 默认 NONE 不穿透,半透明面板后的组件点不到
  • OCCLUSION_AWARE 让部分可见的后面组件可交互,组件数 50 以内可接受
  • alpha 阈值按业务分档,API 的 0.95 太高,建议 0.5 分界
  • AABB 遮挡判定有精度问题,圆形/异形组件要用精确碰撞体
  • ALWAYS_PASS 只用于调试,生产环境别开
  • 前层组件想让射线穿透,alpha 要够低(<0.5)且设 OCCLUSION_AWARE

目前 alpha 分档阈值是写死的 0.5,换场景可能要调。下一步打算把阈值做成主题变量,让设计按场景的透明度风格调。另外 AABB 精度问题想找官方确认有没有计划支持精确形状遮挡判定,如果短期没有,我们给圆形组件统一加精确碰撞体的封装,省得每个都手设。

Logo

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

更多推荐