引子:需求单上只有三行字
给一个资讯类应用做界面升级,产品给的需求单很短:
一、列表往下滑的时候,顶部标题栏要"慢慢糊起来",不要一条硬邦邦的白条。 二、底部导航别再贴着屏幕底边,要浮起来,内容能从它下面透过去。 三、整屏质感要统一——不能标题栏像玻璃、页签像塑料。
三行字,对应到 HarmonyOS 里就是沉浸光感加上悬浮页签这一组能力。
V哥原本以为这是个"查属性、抄代码"的活。真正动手之后才发现,这个能力根本不是"怎么写",而是一道连续决策题——你必须依次回答四个问题,任何一个答错,结果都一样:代码不报错,页面没变化。
这篇文章就是把这四道题拆开,加上一份V哥整理的上线自检清单。
一、第一层决策:接口分支,看前缀不看功能
沉浸光感的第一道坎,是它有两套接口。
这件事官方文档里写得很清楚,但分散在不同章节里,第一遍看很容易滑过去。V哥把两套接口的关键差异整理成一张对照表:
| 判断维度 | 分支 A | 分支 B |
|---|---|---|
| 组件长什么样 | `Hds` 前缀(`HdsNavigation`、`HdsTabs`…) | 普通 ArkUI 组件(`Column`、`Row`、搜索框…) |
| 引哪个模块 | `hdsMaterial`(`@kit.UIDesignKit`) | `uiMaterial`(`@kit.ArkUI`) |
| 挂哪个属性 | `systemMaterialEffect` | `systemMaterial` |
| 用什么枚举 | `MaterialType` + `MaterialLevel` | `ImmersiveStyle` + `ImmersiveOptions` |
| 最低版本 | 6.1.0(23) | API 26.0.0 |
V哥的判断口诀只有六个字:看前缀,不看功能。
不要去问"V哥这个组件是不是玻璃相关的",要问"它是不是 Hds 开头的"。因为这两套接口的枚举不能互换——把 MaterialLevel 挂到普通组件上、把 ImmersiveStyle 挂到 HdsTabs 上,编译都能过,效果都没有。这是V哥第一遍动手时踩的坑。
至于为什么会分成两条路径,V哥自己的理解是这样:
HDS 是华为设计体系里的组件,材质能力先在自家组件上落地,所以 6.1.0(23) 就有了 hdsMaterial;而普通 ArkUI 组件要拿到材质,需要 SDK 提供一个可挂载的材质对象(ImmersiveMaterial),这个对象到 API 26 才随 ArkUI 一起提供。
"先 HDS、后通用"这个顺序,是理解后面所有差异的钥匙——包括为什么生效范围会不一样、为什么有些组件"自动就变好看了"。
二、第二层决策:档位,先查能力再谈效果
选完分支,下一个问题是"用什么档位"。
HDS 分支提供四个档位,枚举名和特性如下(档位定义与视觉特性来自官方文档):
| 档位 | 枚举 | 视觉效果 | 性能开销 | V哥什么时候用它 |
|---|---|---|---|---|
| 强 | `EXQUISITE` | 完整光效,通透感最强 | 较高 | 只在旗舰机 + 明确有高质感诉求时 |
| 均衡 | `GENTLE` | 适度,视觉与性能折中 | 中 | 通用场景;**亮色底 + 白色叠层时首选** |
| 弱 | `SMOOTH` | 只保留核心特性 | 低 | 低端设备的降级目标 |
| 系统自适应 | `ADAPTIVE` | 由系统按设备能力决定 | 由系统决定 | **默认就给这个** |
官方对档位选择的建议是优先用 ADAPTIVE,并在需要手动指定时先查询设备支持情况——原因是并非所有设备都支持高级沉浸光感,强行开启可能导致卡顿和发热(沉浸光感 · 最佳实践)。
V哥把这段建议落成了一个四步决策流程,比记结论更实用:
- 先查支持:调
hdsMaterial.getSystemMaterialTypes(),返回的数组里包含IMMERSIVE才算这台设备支持沉浸材质。 - 再定默认:默认给
ADAPTIVE,让系统去挑。 - 把选择权交出去:把四档做成一个用户可切换的菜单(放进"V哥的 → 界面效果"这类位置),而不是替他决定。
- 只在确定时手动指定:只有当设备确认支持、且产品明确要求"要最强效果"时,才写死
EXQUISITE,并同时准备降级分支。
第 3 步是V哥强烈建议加的。原因很实在:系统档位是四变量之一(后面第五节展开),用户可以在"设置 → 桌面和个性化 → 沉浸光感"里选强/均衡/弱。你把代码写死最高档,用户的系统档位是"弱",最终呈现就是收敛的——这不是你的代码错,而是你少算了一个变量。与其较劲,不如把档位菜单开放给用户。
<span>import</span> { hdsMaterial } <span>from</span> <span>'@kit.UIDesignKit'</span>;
<span>// 能力查询:返回当前设备支持的 HDS 材质类型</span>
<span>function</span> <span>deviceSupportsImmersive</span>(<span></span>): <span>boolean</span> {
<span>try</span> {
<span>const</span> types = hdsMaterial.<span>getSystemMaterialTypes</span>();
<span>return</span> types.<span>indexOf</span>(hdsMaterial.<span>MaterialType</span>.<span>IMMERSIVE</span>) >= <span>0</span>;
} <span>catch</span> (err) {
<span>// 查询本身可能因 SDK 版本、系统版本或运行环境而失败,失败即视为不支持</span>
<span>return</span> <span>false</span>;
}
}
这里有个细节值得提醒:这个查询在模拟器上的返回值不一定代表真机能力。官方常见问题里提到过,调用失败可能与 SDK、系统版本或模拟器镜像有关(沉浸光感常见问题)。所以这类能力不要只在模拟器里下结论。
三、第三层决策:位置,决定属性有没有意义
这是最容易被忽略、也最伤人的一层。
沉浸光感不是"挂上属性就生效",它挑位置。 官方在 OS 平台行为变更说明里专门收紧了这一点,理由是"为确保性能和功耗体验最优,规范沉浸光感组件使用"。
V哥把生效规则整理成一张速查表,比读长文档快:
| 你把组件放在哪 | 光感生效吗 |
|---|---|
| `Navigation` / `NavDestination` 标题栏 | 生效 |
| 横向 Tabs 中 `barPosition` 为 `BarPosition.End` 的底部 TabBar | 生效 |
| 弹窗类组件(AlertDialog、CustomDialog、ActionSheet、各类 Picker、Menu 等) | 生效 |
| 弹窗类接口(PromptAction、Popup、Tips、菜单控制、半模态转场) | 生效 |
| `Slider` / `Toggle` / `Select` | 生效 |
| 内容区的 `Column` / `Row` / 卡片 / 自绘工具栏 | **不生效** |
官方给的反例非常直白:一个普通 Column 通过 systemMaterial 设置沉浸光感,这条约束收紧之后不再生效(开启沉浸光感)。
这条约束只在 targetSdkVersion ≥ 26.0.0 时生效。 也就是说,同一个属性、同一份代码,在不同 targetSDK 下表现可能不一样——联调时遇到"昨天还好好的",先去看一眼 targetSDK。
对V哥那个资讯应用来说,这条规则直接排除了一个想法:V哥原本想给"文章卡片"也加上玻璃质感,但卡片在内容区,属性写得再对也不会生效。要做玻璃效果,只能挪进弹窗,或者换别的视觉方案。
所以第三层决策的正确问法是:V哥这个组件,在不在生效名单里? 不在,就别浪费时间去调参数。
四、落地:一份自己写的完整示例
把前两层决策落成代码,V哥写了一个最小可用的组合:标题栏渐变模糊 + 底部悬浮页签 + MiniBar。
先写能力判断和档位映射,收在一个独立文件里,避免每个页面各写一遍:
<span>// common/MaterialGate.ets</span>
<span>import</span> { uiMaterial } <span>from</span> <span>'@kit.ArkUI'</span>;
<span>import</span> { hdsMaterial } <span>from</span> <span>'@kit.UIDesignKit'</span>;
<span>import</span> { deviceInfo } <span>from</span> <span>'@kit.BasicServicesKit'</span>;
<span>export</span> <span>type</span> <span>LevelKey</span> = <span>'adaptive'</span> | <span>'exquisite'</span> | <span>'gentle'</span> | <span>'smooth'</span>;
<span>export</span> <span>class</span> <span>MaterialGate</span> {
<span>private</span> <span>static</span> <span>cached</span>: <span>boolean</span> | <span>undefined</span> = <span>undefined</span>;
<span>// 结果缓存:能力在一台设备上不会变,没必要每次进页面都查</span>
<span>static</span> <span>canUseImmersive</span>(): <span>boolean</span> {
<span>if</span> (<span>MaterialGate</span>.<span>cached</span> === <span>undefined</span>) {
<span>try</span> {
<span>MaterialGate</span>.<span>cached</span> =
deviceInfo.<span>apiAvailable</span>(<span>'26.0.0'</span>) &&
hdsMaterial.<span>getSystemMaterialTypes</span>().<span>indexOf</span>(hdsMaterial.<span>MaterialType</span>.<span>IMMERSIVE</span>) >= <span>0</span>;
} <span>catch</span> (err) {
<span>MaterialGate</span>.<span>cached</span> = <span>false</span>;
}
}
<span>return</span> <span>MaterialGate</span>.<span>cached</span>;
}
<span>// 不支持时统一降级到 SMOOTH,调用方不需要自己写 if</span>
<span>static</span> <span>toLevel</span>(<span>key</span>: <span>LevelKey</span>): hdsMaterial.<span>MaterialLevel</span> {
<span>if</span> (!<span>MaterialGate</span>.<span>canUseImmersive</span>()) {
<span>return</span> hdsMaterial.<span>MaterialLevel</span>.<span>SMOOTH</span>;
}
<span>switch</span> (key) {
<span>case</span> <span>'exquisite'</span>:
<span>return</span> hdsMaterial.<span>MaterialLevel</span>.<span>EXQUISITE</span>;
<span>case</span> <span>'gentle'</span>:
<span>return</span> hdsMaterial.<span>MaterialLevel</span>.<span>GENTLE</span>;
<span>case</span> <span>'smooth'</span>:
<span>return</span> hdsMaterial.<span>MaterialLevel</span>.<span>SMOOTH</span>;
<span>default</span>:
<span>return</span> hdsMaterial.<span>MaterialLevel</span>.<span>ADAPTIVE</span>;
}
}
<span>// 普通 ArkUI 组件用的材质对象;不支持时返回 empty 表示"不加材质"</span>
<span>static</span> <span>forComponent</span>(<span>key</span>: <span>LevelKey</span>): uiMaterial.<span>Material</span> {
<span>if</span> (!<span>MaterialGate</span>.<span>canUseImmersive</span>()) {
<span>return</span> uiMaterial.<span>Material</span>.<span>empty</span>;
}
<span>return</span> <span>new</span> uiMaterial.<span>ImmersiveMaterial</span>({
<span>style</span>: uiMaterial.<span>ImmersiveStyle</span>.<span>REGULAR</span>,
<span>interactive</span>: <span>true</span>,
});
}
}
这个封装里有三个V哥自己加的东西,值得说一下:
- 结果缓存:设备能力在一台机器上是常量,没必要每次进页面都调一次查询;
- 降级收口:不支持就统一返回
SMOOTH,页面层不写if,逻辑干净; Material.empty兜底:普通组件那条路径用uiMaterial.Material.empty表示"不加材质",这是官方给出的关闭方式。
然后是页面主体:
<span>// pages/FeedPage.ets</span>
<span>import</span> {
hdsMaterial,
<span>HdsNavigation</span>,
<span>HdsNavigationTitleMode</span>,
<span>HdsTabs</span>,
<span>HdsTabsController</span>,
<span>HideMode</span>,
<span>ScrollEffectType</span>,
} <span>from</span> <span>'@kit.UIDesignKit'</span>;
<span>import</span> { <span>MaterialGate</span>, <span>LevelKey</span> } <span>from</span> <span>'../common/MaterialGate'</span>;
<span>@Entry</span>
<span>@Component</span>
struct <span>FeedPage</span> {
<span>private</span> <span>tabController</span>: <span>HdsTabsController</span> = <span>new</span> <span>HdsTabsController</span>();
<span>private</span> <span>contentScroller</span>: <span>Scroller</span> = <span>new</span> <span>Scroller</span>();
<span>// 档位做成状态,供设置页修改</span>
<span>@State</span> <span>levelKey</span>: <span>LevelKey</span> = <span>'adaptive'</span>;
<span>@Builder</span>
<span>miniBar</span>(<span></span>) {
<span>Row</span>() {
<span>// 左侧:当前内容状态</span>
<span>// 中间:一行摘要</span>
<span>// 右侧:快捷操作按钮</span>
}
.<span>height</span>(<span>'100%'</span>)
}
<span>build</span>(<span></span>) {
<span>HdsNavigation</span>() {
<span>HdsTabs</span>({ <span>controller</span>: <span>this</span>.<span>tabController</span> }) {
<span>// TabContent 列表内容,此处省略</span>
}
.<span>scrollable</span>(<span>false</span>)
.<span>barOverlap</span>(<span>true</span>) <span>// 页签栏与内容重叠,这是"浮"起来的前提</span>
.<span>vertical</span>(<span>false</span>) <span>// 横向排列</span>
.<span>barPosition</span>(<span>BarPosition</span>.<span>End</span>) <span>// 放在底部</span>
.<span>barFloatingStyle</span>({
<span>barBottomMargin</span>: <span>28</span>,
<span>adaptToHandedness</span>: <span>true</span>, <span>// 左右跟手</span>
<span>gradientMask</span>: {
<span>maskColor</span>: <span>'#66F1F3F5'</span>,
<span>maskHeight</span>: <span>92</span>,
},
<span>systemMaterialEffect</span>: {
<span>materialType</span>: hdsMaterial.<span>MaterialType</span>.<span>ADAPTIVE</span>,
<span>materialLevel</span>: <span>MaterialGate</span>.<span>toLevel</span>(<span>this</span>.<span>levelKey</span>),
},
<span>miniBar</span>: {
<span>miniBarBuilder</span>: <span>() =></span> <span>this</span>.<span>miniBar</span>(),
},
})
}
.<span>titleMode</span>(<span>HdsNavigationTitleMode</span>.<span>MINI</span>)
.<span>titleBar</span>({
<span>style</span>: {
<span>scrollEffectOpts</span>: {
<span>enableScrollEffect</span>: <span>true</span>,
<span>// 从完全透明渐变到模糊,比 GRADIENT_BLUR 的过渡更自然</span>
<span>scrollEffectType</span>: <span>ScrollEffectType</span>.<span>IMMERSIVE_GRADIENT_BLUR</span>,
},
<span>systemMaterialEffect</span>: {
<span>materialType</span>: hdsMaterial.<span>MaterialType</span>.<span>ADAPTIVE</span>,
<span>materialLevel</span>: <span>MaterialGate</span>.<span>toLevel</span>(<span>this</span>.<span>levelKey</span>),
},
},
})
.<span>dynamicHideTitleBar</span>({
<span>hideTitleArea</span>: <span>true</span>,
<span>hideStatusBar</span>: <span>true</span>,
<span>mode</span>: <span>HideMode</span>.<span>SCROLL_UP_TO</span>,
})
.<span>bindToScrollable</span>([<span>this</span>.<span>contentScroller</span>]) <span>// 不绑,标题栏就不会跟着滚动</span>
}
}
这段代码里有三处必须同时成立才会出现"浮"的效果:barOverlap(true)、barPosition(BarPosition.End)、vertical(false)。少任何一个,页签栏都会老老实实占住底部空间。
还有一处特别隐蔽:.bindToScrollable() 忘了绑,标题栏不会报错,只是"不跟手"。滚动效果和材质是两件事——只写 scrollEffectOpts 不写 systemMaterialEffect,你得到的是普通模糊标题栏,不是沉浸光感。
五、第四层决策:性能,四个变量一起看
前三个决策答对,效果就有了。但"有"不等于"稳"。
沉浸光感的本质是 GPU 实时渲染,官方专门出了功耗优化文档,这说明它不是能无脑铺满全屏的能力。
V哥把它总结成一个四变量模型,用来解释所有"V哥明明配了但效果不对"的情况:
系统档位 × 设备算力 × 应用开关 × 组件参数 = 你看到的最终效果
| 变量 | 由谁决定 | 你需要做什么 |
|---|---|---|
| 系统档位 | 用户在系统设置里选强/均衡/弱 | 把档位菜单做进应用,别替他决定 |
| 设备算力 | 厂商分档,你控制不了 | 先查能力,不支持就降级 |
| 应用开关 | `module.json5` 中 `ohos.arkui.UIMaterial.state`(default / enable / disable) | 确认只在 entry 模块生效;升级到 26 且未配置时,组件默认开启 |
| 组件参数 | 你的代码 | 走对分支、选对档位、放对位置 |
排查"没效果"时,按这个顺序问自己:组件位置对吗 → targetSDK 够吗 → 应用开关是 disable 吗 → 设备支持吗 → 档位写对了吗。按这个顺序查,比乱试属性快得多。
应用开关还可以读出来核对:
<span>import</span> { uiMaterial } <span>from</span> <span>'@kit.ArkUI'</span>;
<span>const</span> <span>info</span>: uiMaterial.<span>MaterialInfo</span> = uiMaterial.<span>getMaterialInfo</span>();
<span>// info.state:DEFAULT / ENABLE / DISABLE</span>
<span>// info.type :应用配置的材质类型</span>
要区分清楚:应用配置决定材质"是否允许生效",组件参数决定"生效成什么样"。两个都对了才有结果。
性能验证V哥认同官方给的方法:在目标设备上分别用 ADAPTIVE 与 EXQUISITE 跑同一个页面,用 hiperf 或 DevEco Profiler 观察帧率和功耗;低端机出现掉帧或发热,先退回 GENTLE / SMOOTH。
这里有个好消息可以减轻焦虑:SDK 26 支持 LTPO 可变帧率,界面静止时刷新率可以降下来。所以**"开了光感就一定一直高刷耗电"这个担心是不成立的**。
但也不代表可以随便铺。如果页面本身已经很重(长列表 + 动效 + 网络请求),再叠加光感仍可能掉帧。折中方案是选择性开启——只给当前焦点组件上光感。
最后是一条设计上的铁律,V哥建议直接写进评审清单:
焦点组件用光感,背景组件不要用——满屏都是光感,就等于没有光感。
落地成三条可执行的规则:
- 优先给交互频繁的组件:底部导航、筛选标签、主操作按钮;
- 慎给纯展示组件:文本阅读区加光感会分散注意力;
- 暗色背景下效果更明显:同一档位,深色底上的视觉冲击强于浅色底。
六、版本取舍:6.1 够用,还是必须上 7.0
两个版本的能力边界差异很大,V哥做了一张取舍表:
| 你的需求 | 6.1.0(23) 够吗 | 说明 |
|---|---|---|
| 只做标题栏 + 底部页签的材质 | 够 | HDS 分支在 6.1 就已提供 |
| 想让普通组件(搜索框、自绘工具栏)也有材质 | 不够 | 需要 `uiMaterial`,API 26 起 |
| 想吃"Toast / Tips 自动变好看" | 不够 | API 26 起,升上去即可自动生效 |
| 需要光随指动等新动效 | 不够 | 7.0 新增 |
V哥的建议是分两步走:如果当前只做导航区域,先用 6.1 的 HDS 分支上线,成本最低;等产品明确要求"全组件统一质感"时,再整体升到 API 26。
升 targetSDK 前必须做两件事:
- 回归生效范围:第三节那条约束只在 26 生效,升级后原本"生效"的组件可能变成"不生效",必须逐个核对;
- 回归亮色主题:亮色底 + 白色叠层场景下,
EXQUISITE可能盖住叠层,宜换GENTLE(官方在最佳实践中提示过这一点)。
另外提醒一个历史限制:在 API 23 及以前版本的 SDK 中,同层渲染场景下控件使能沉浸光感会变透明(例如 Web 组件内嵌 ArkUI 控件)。遇到这类场景,二选一:在对应控件上关闭光感,或者关闭同层渲染。
七、上线自检清单
V哥把上面的决策点整理成了一份可以贴进 PR 描述的清单:
- 组件的接口分支走对了吗?(看
Hds前缀,不看功能) targetSdkVersion≥ 26.0.0 了吗?否则"开启沉浸光感"这件事不成立- 组件在生效名单里吗?(标题栏 / 底部 TabBar / 弹窗 / Slider·Toggle·Select)
- 调用
getSystemMaterialTypes()做过能力查询了吗?有不支持时的降级分支吗? - 老版本兼容做了吗?(版本判断 + 能力判断,两个条件缺一不可)
barOverlap(true)加了吗?页签不"浮",先查这里- 标题栏
.bindToScrollable()绑了吗?不绑就没有联动 - 档位菜单开放给用户了吗?还是替他写死了?
- 亮色主题 + 白色叠层场景,
EXQUISITE换GENTLE试过了吗? - 用 Profiler 在真机上对比过
ADAPTIVE与EXQUISITE的帧率与功耗吗? - 同层渲染场景(Web 内嵌 ArkUI)关掉光感了吗?
- 光感只用在焦点组件上,而不是铺满全屏吗?
参考与出处
本文涉及的事实性信息(API 名称、枚举取值、版本号、官方约束)来自以下官方文档,文中的结构、代码示例、决策流程与自检清单为本人整理编写:
最后一句:这个能力真正难的不是 API——API 就那么几行。难的是在动手前把四层决策依次答对:走哪个分支、选哪个档位、放哪个位置、怎么兜性能。前四层想清楚了,一行属性就出效果;没想清楚,写十行也是白板。
这篇实战把不报错但没效果拆成可执行决策链,适合 HarmonyOS 开发者做沉浸光感升级、联调排错与上线自检,能少走弯路。