HarmonyOS 6.1 沉浸光感实战:接口路径选错,代码不报错、页面没效果

文章来源声明: 原文作者:威哥爱编程; 来源站点:掘金; 原文链接:https://juejin.cn/post/7685370109297705011; 本文基于上述来源整理/加工,觅优补充点评,仅供技术学习交流。版权归原作者所有。
觅优短评

这篇实战把不报错但没效果拆成可执行决策链,适合 HarmonyOS 开发者做沉浸光感升级、联调排错与上线自检,能少走弯路。

> 本文涉及 HarmonyOS 6.1.0(23) 与 7.0(26) 两个版本分支。文中代码是为说明问题编写的完整示例,不是官方示例的搬运;API 名称与版本号等事实性信息均标注官方出处;涉及真机表现的部分已明确标注,未做任何实测数据编造。

HarmonyOS 6.1 沉浸光感实战

引子:需求单上只有三行字

给一个资讯类应用做界面升级,产品给的需求单很短:

一、列表往下滑的时候,顶部标题栏要"慢慢糊起来",不要一条硬邦邦的白条。 二、底部导航别再贴着屏幕底边,要浮起来,内容能从它下面透过去。 三、整屏质感要统一——不能标题栏像玻璃、页签像塑料。

三行字,对应到 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`由系统按设备能力决定由系统决定**默认就给这个**

MaterialLevel 四档位

官方对档位选择的建议是优先用 ADAPTIVE,并在需要手动指定时先查询设备支持情况——原因是并非所有设备都支持高级沉浸光感,强行开启可能导致卡顿和发热(沉浸光感 · 最佳实践)。

V哥把这段建议落成了一个四步决策流程,比记结论更实用:

  1. 先查支持:调 hdsMaterial.getSystemMaterialTypes(),返回的数组里包含 IMMERSIVE 才算这台设备支持沉浸材质。
  2. 再定默认:默认给 ADAPTIVE,让系统去挑。
  3. 把选择权交出去:把四档做成一个用户可切换的菜单(放进"V哥的 → 界面效果"这类位置),而不是替他决定。
  4. 只在确定时手动指定:只有当设备确认支持、且产品明确要求"要最强效果"时,才写死 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哥认同官方给的方法:在目标设备上分别用 ADAPTIVEEXQUISITE 跑同一个页面,用 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 前必须做两件事:

  1. 回归生效范围:第三节那条约束只在 26 生效,升级后原本"生效"的组件可能变成"不生效",必须逐个核对;
  2. 回归亮色主题:亮色底 + 白色叠层场景下,EXQUISITE 可能盖住叠层,宜换 GENTLE(官方在最佳实践中提示过这一点)。

另外提醒一个历史限制:在 API 23 及以前版本的 SDK 中,同层渲染场景下控件使能沉浸光感会变透明(例如 Web 组件内嵌 ArkUI 控件)。遇到这类场景,二选一:在对应控件上关闭光感,或者关闭同层渲染。


七、上线自检清单

V哥把上面的决策点整理成了一份可以贴进 PR 描述的清单:

  • 组件的接口分支走对了吗?(看 Hds 前缀,不看功能)
  • targetSdkVersion ≥ 26.0.0 了吗?否则"开启沉浸光感"这件事不成立
  • 组件在生效名单里吗?(标题栏 / 底部 TabBar / 弹窗 / Slider·Toggle·Select)
  • 调用 getSystemMaterialTypes() 做过能力查询了吗?有不支持时的降级分支吗?
  • 老版本兼容做了吗?(版本判断 + 能力判断,两个条件缺一不可)
  • barOverlap(true) 加了吗?页签不"浮",先查这里
  • 标题栏 .bindToScrollable() 绑了吗?不绑就没有联动
  • 档位菜单开放给用户了吗?还是替他写死了?
  • 亮色主题 + 白色叠层场景,EXQUISITEGENTLE 试过了吗?
  • 用 Profiler 在真机上对比过 ADAPTIVEEXQUISITE 的帧率与功耗吗?
  • 同层渲染场景(Web 内嵌 ArkUI)关掉光感了吗?
  • 光感只用在焦点组件上,而不是铺满全屏吗?

参考与出处

本文涉及的事实性信息(API 名称、枚举取值、版本号、官方约束)来自以下官方文档,文中的结构、代码示例、决策流程与自检清单为本人整理编写:


最后一句:这个能力真正难的不是 API——API 就那么几行。难的是在动手前把四层决策依次答对:走哪个分支、选哪个档位、放哪个位置、怎么兜性能。前四层想清楚了,一行属性就出效果;没想清楚,写十行也是白板。