【共创稿事节】HarmonyOS 7Z 轴叠放时的遮挡处理与交互穿透
空间界面把组件沿 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 分档三件事揉进一条命中判定链,就能解释上面表里每种模式的结果。
这条链里 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 精度问题想找官方确认有没有计划支持精确形状遮挡判定,如果短期没有,我们给圆形组件统一加精确碰撞体的封装,省得每个都手设。
更多推荐


所有评论(0)