在这里插入图片描述

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

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

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

三行字,对应到 HarmonyOS 里就是沉浸光感加上悬浮页签这一组能力。

我原本以为这是个"查属性、抄代码"的活。真正动手之后才发现,这个能力根本不是"怎么写",而是一道连续决策题——你必须依次回答四个问题,任何一个答错,结果都一样:代码不报错,页面没变化。

这篇文章就是把这四道题拆开,加上一份我整理的上线自检清单。


一、第一层决策:接口分支,看前缀不看功能

沉浸光感的第一道坎,是它有两套接口

这件事官方文档里写得很清楚,但分散在不同章节里,第一遍看很容易滑过去。我把两套接口的关键差异整理成一张对照表:

判断维度分支 A分支 B
组件长什么样Hds 前缀(HdsNavigationHdsTabs…)普通 ArkUI 组件(ColumnRow、搜索框…)
引哪个模块hdsMaterial@kit.UIDesignKituiMaterial@kit.ArkUI
挂哪个属性systemMaterialEffectsystemMaterial
用什么枚举MaterialType + MaterialLevelImmersiveStyle + ImmersiveOptions
最低版本6.1.0(23)API 26.0.0

在这里插入图片描述

我的判断口诀只有六个字:看前缀,不看功能。

不要去问"我这个组件是不是玻璃相关的",要问"它是不是 Hds 开头的"。因为这两套接口的枚举不能互换——把 MaterialLevel 挂到普通组件上、把 ImmersiveStyle 挂到 HdsTabs 上,编译都能过,效果都没有。这是我第一遍动手时踩的坑。

至于为什么会分成两条路径,我自己的理解是这样:

HDS 是华为设计体系里的组件,材质能力先在自家组件上落地,所以 6.1.0(23) 就有了 hdsMaterial;而普通 ArkUI 组件要拿到材质,需要 SDK 提供一个可挂载的材质对象(ImmersiveMaterial),这个对象到 API 26 才随 ArkUI 一起提供。

"先 HDS、后通用"这个顺序,是理解后面所有差异的钥匙——包括为什么生效范围会不一样、为什么有些组件"自动就变好看了"。


二、第二层决策:档位,先查能力再谈效果

选完分支,下一个问题是"用什么档位"。

HDS 分支提供四个档位,枚举名和特性如下(档位定义与视觉特性来自官方文档):

档位枚举视觉效果性能开销我什么时候用它
EXQUISITE完整光效,通透感最强较高只在旗舰机 + 明确有高质感诉求时
均衡GENTLE适度,视觉与性能折中通用场景;亮色底 + 白色叠层时首选
SMOOTH只保留核心特性低端设备的降级目标
系统自适应ADAPTIVE由系统按设备能力决定由系统决定默认就给这个

在这里插入图片描述

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

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

  1. 先查支持:调 hdsMaterial.getSystemMaterialTypes(),返回的数组里包含 IMMERSIVE 才算这台设备支持沉浸材质。
  2. 再定默认:默认给 ADAPTIVE,让系统去挑。
  3. 把选择权交出去:把四档做成一个用户可切换的菜单(放进"我的 → 界面效果"这类位置),而不是替他决定。
  4. 只在确定时手动指定:只有当设备确认支持、且产品明确要求"要最强效果"时,才写死 EXQUISITE,并同时准备降级分支。

第 3 步是我强烈建议加的。原因很实在:系统档位是四变量之一(后面第五节展开),用户可以在"设置 → 桌面和个性化 → 沉浸光感"里选强/均衡/弱。你把代码写死最高档,用户的系统档位是"弱",最终呈现就是收敛的——这不是你的代码错,而是你少算了一个变量。与其较劲,不如把档位菜单开放给用户。

import { hdsMaterial } from '@kit.UIDesignKit';

// 能力查询:返回当前设备支持的 HDS 材质类型
function deviceSupportsImmersive(): boolean {
  try {
    const types = hdsMaterial.getSystemMaterialTypes();
    return types.indexOf(hdsMaterial.MaterialType.IMMERSIVE) >= 0;
  } catch (err) {
    // 查询本身可能因 SDK 版本、系统版本或运行环境而失败,失败即视为不支持
    return false;
  }
}

这里有个细节值得提醒:这个查询在模拟器上的返回值不一定代表真机能力。官方常见问题里提到过,调用失败可能与 SDK、系统版本或模拟器镜像有关(沉浸光感常见问题)。所以这类能力不要只在模拟器里下结论。


三、第三层决策:位置,决定属性有没有意义

这是最容易被忽略、也最伤人的一层。

沉浸光感不是"挂上属性就生效",它挑位置。 官方在 OS 平台行为变更说明里专门收紧了这一点,理由是"为确保性能和功耗体验最优,规范沉浸光感组件使用"。

我把生效规则整理成一张速查表,比读长文档快:

你把组件放在哪光感生效吗
Navigation / NavDestination 标题栏生效
横向 Tabs 中 barPositionBarPosition.End 的底部 TabBar生效
弹窗类组件(AlertDialog、CustomDialog、ActionSheet、各类 Picker、Menu 等)生效
弹窗类接口(PromptAction、Popup、Tips、菜单控制、半模态转场)生效
Slider / Toggle / Select生效
内容区的 Column / Row / 卡片 / 自绘工具栏不生效

在这里插入图片描述

官方给的反例非常直白:一个普通 Column 通过 systemMaterial 设置沉浸光感,这条约束收紧之后不再生效开启沉浸光感)。

这条约束只在 targetSdkVersion ≥ 26.0.0 时生效。 也就是说,同一个属性、同一份代码,在不同 targetSDK 下表现可能不一样——联调时遇到"昨天还好好的",先去看一眼 targetSDK。

对我那个资讯应用来说,这条规则直接排除了一个想法:我原本想给"文章卡片"也加上玻璃质感,但卡片在内容区,属性写得再对也不会生效。要做玻璃效果,只能挪进弹窗,或者换别的视觉方案。

所以第三层决策的正确问法是:我这个组件,在不在生效名单里? 不在,就别浪费时间去调参数。


四、落地:一份自己写的完整示例

把前两层决策落成代码,我写了一个最小可用的组合:标题栏渐变模糊 + 底部悬浮页签 + MiniBar。

先写能力判断和档位映射,收在一个独立文件里,避免每个页面各写一遍:

// common/MaterialGate.ets
import { uiMaterial } from '@kit.ArkUI';
import { hdsMaterial } from '@kit.UIDesignKit';
import { deviceInfo } from '@kit.BasicServicesKit';

export type LevelKey = 'adaptive' | 'exquisite' | 'gentle' | 'smooth';

export class MaterialGate {
  private static cached: boolean | undefined = undefined;

  // 结果缓存:能力在一台设备上不会变,没必要每次进页面都查
  static canUseImmersive(): boolean {
    if (MaterialGate.cached === undefined) {
      try {
        MaterialGate.cached =
          deviceInfo.apiAvailable('26.0.0') &&
          hdsMaterial.getSystemMaterialTypes().indexOf(hdsMaterial.MaterialType.IMMERSIVE) >= 0;
      } catch (err) {
        MaterialGate.cached = false;
      }
    }
    return MaterialGate.cached;
  }

  // 不支持时统一降级到 SMOOTH,调用方不需要自己写 if
  static toLevel(key: LevelKey): hdsMaterial.MaterialLevel {
    if (!MaterialGate.canUseImmersive()) {
      return hdsMaterial.MaterialLevel.SMOOTH;
    }
    switch (key) {
      case 'exquisite':
        return hdsMaterial.MaterialLevel.EXQUISITE;
      case 'gentle':
        return hdsMaterial.MaterialLevel.GENTLE;
      case 'smooth':
        return hdsMaterial.MaterialLevel.SMOOTH;
      default:
        return hdsMaterial.MaterialLevel.ADAPTIVE;
    }
  }

  // 普通 ArkUI 组件用的材质对象;不支持时返回 empty 表示"不加材质"
  static forComponent(key: LevelKey): uiMaterial.Material {
    if (!MaterialGate.canUseImmersive()) {
      return uiMaterial.Material.empty;
    }
    return new uiMaterial.ImmersiveMaterial({
      style: uiMaterial.ImmersiveStyle.REGULAR,
      interactive: true,
    });
  }
}

这个封装里有三个我自己加的东西,值得说一下:

  • 结果缓存:设备能力在一台机器上是常量,没必要每次进页面都调一次查询;
  • 降级收口:不支持就统一返回 SMOOTH,页面层不写 if,逻辑干净;
  • Material.empty 兜底:普通组件那条路径用 uiMaterial.Material.empty 表示"不加材质",这是官方给出的关闭方式。

然后是页面主体:

// pages/FeedPage.ets
import {
  hdsMaterial,
  HdsNavigation,
  HdsNavigationTitleMode,
  HdsTabs,
  HdsTabsController,
  HideMode,
  ScrollEffectType,
} from '@kit.UIDesignKit';
import { MaterialGate, LevelKey } from '../common/MaterialGate';

@Entry
@Component
struct FeedPage {
  private tabController: HdsTabsController = new HdsTabsController();
  private contentScroller: Scroller = new Scroller();
  // 档位做成状态,供设置页修改
  @State levelKey: LevelKey = 'adaptive';

  @Builder
  miniBar() {
    Row() {
      // 左侧:当前内容状态
      // 中间:一行摘要
      // 右侧:快捷操作按钮
    }
    .height('100%')
  }

  build() {
    HdsNavigation() {
      HdsTabs({ controller: this.tabController }) {
        // TabContent 列表内容,此处省略
      }
      .scrollable(false)
      .barOverlap(true)                 // 页签栏与内容重叠,这是"浮"起来的前提
      .vertical(false)                  // 横向排列
      .barPosition(BarPosition.End)     // 放在底部
      .barFloatingStyle({
        barBottomMargin: 28,
        adaptToHandedness: true,        // 左右跟手
        gradientMask: {
          maskColor: '#66F1F3F5',
          maskHeight: 92,
        },
        systemMaterialEffect: {
          materialType: hdsMaterial.MaterialType.ADAPTIVE,
          materialLevel: MaterialGate.toLevel(this.levelKey),
        },
        miniBar: {
          miniBarBuilder: () => this.miniBar(),
        },
      })
    }
    .titleMode(HdsNavigationTitleMode.MINI)
    .titleBar({
      style: {
        scrollEffectOpts: {
          enableScrollEffect: true,
          // 从完全透明渐变到模糊,比 GRADIENT_BLUR 的过渡更自然
          scrollEffectType: ScrollEffectType.IMMERSIVE_GRADIENT_BLUR,
        },
        systemMaterialEffect: {
          materialType: hdsMaterial.MaterialType.ADAPTIVE,
          materialLevel: MaterialGate.toLevel(this.levelKey),
        },
      },
    })
    .dynamicHideTitleBar({
      hideTitleArea: true,
      hideStatusBar: true,
      mode: HideMode.SCROLL_UP_TO,
    })
    .bindToScrollable([this.contentScroller])  // 不绑,标题栏就不会跟着滚动
  }
}

这段代码里有三处必须同时成立才会出现"浮"的效果:barOverlap(true)barPosition(BarPosition.End)vertical(false)。少任何一个,页签栏都会老老实实占住底部空间。

还有一处特别隐蔽:.bindToScrollable() 忘了绑,标题栏不会报错,只是"不跟手"。滚动效果和材质是两件事——只写 scrollEffectOpts 不写 systemMaterialEffect,你得到的是普通模糊标题栏,不是沉浸光感。


五、第四层决策:性能,四个变量一起看

前三个决策答对,效果就有了。但"有"不等于"稳"。

沉浸光感的本质是 GPU 实时渲染,官方专门出了功耗优化文档,这说明它不是能无脑铺满全屏的能力。

我把它总结成一个四变量模型,用来解释所有"我明明配了但效果不对"的情况:

系统档位 × 设备算力 × 应用开关 × 组件参数 = 你看到的最终效果
变量由谁决定你需要做什么
系统档位用户在系统设置里选强/均衡/弱把档位菜单做进应用,别替他决定
设备算力厂商分档,你控制不了先查能力,不支持就降级
应用开关module.json5ohos.arkui.UIMaterial.state(default / enable / disable)确认只在 entry 模块生效;升级到 26 且未配置时,组件默认开启
组件参数你的代码走对分支、选对档位、放对位置

排查"没效果"时,按这个顺序问自己:组件位置对吗 → targetSDK 够吗 → 应用开关是 disable 吗 → 设备支持吗 → 档位写对了吗。按这个顺序查,比乱试属性快得多。

应用开关还可以读出来核对:

import { uiMaterial } from '@kit.ArkUI';

const info: uiMaterial.MaterialInfo = uiMaterial.getMaterialInfo();
// info.state:DEFAULT / ENABLE / DISABLE
// info.type :应用配置的材质类型

要区分清楚:应用配置决定材质"是否允许生效",组件参数决定"生效成什么样"。两个都对了才有结果。

性能验证我认同官方给的方法:在目标设备上分别用 ADAPTIVEEXQUISITE 跑同一个页面,用 hiperf 或 DevEco Profiler 观察帧率和功耗;低端机出现掉帧或发热,先退回 GENTLE / SMOOTH

这里有个好消息可以减轻焦虑:SDK 26 支持 LTPO 可变帧率,界面静止时刷新率可以降下来。所以**"开了光感就一定一直高刷耗电"这个担心是不成立的**。

但也不代表可以随便铺。如果页面本身已经很重(长列表 + 动效 + 网络请求),再叠加光感仍可能掉帧。折中方案是选择性开启——只给当前焦点组件上光感。

最后是一条设计上的铁律,我建议直接写进评审清单:

焦点组件用光感,背景组件不要用——满屏都是光感,就等于没有光感。

落地成三条可执行的规则:

  • 优先给交互频繁的组件:底部导航、筛选标签、主操作按钮;
  • 慎给纯展示组件:文本阅读区加光感会分散注意力;
  • 暗色背景下效果更明显:同一档位,深色底上的视觉冲击强于浅色底。

六、版本取舍:6.1 够用,还是必须上 7.0

两个版本的能力边界差异很大,我做了一张取舍表:

你的需求6.1.0(23) 够吗说明
只做标题栏 + 底部页签的材质HDS 分支在 6.1 就已提供
想让普通组件(搜索框、自绘工具栏)也有材质不够需要 uiMaterial,API 26 起
想吃"Toast / Tips 自动变好看"不够API 26 起,升上去即可自动生效
需要光随指动等新动效不够7.0 新增

我的建议是分两步走:如果当前只做导航区域,先用 6.1 的 HDS 分支上线,成本最低;等产品明确要求"全组件统一质感"时,再整体升到 API 26。

升 targetSDK 前必须做两件事:

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

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


七、上线自检清单

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

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

参考与出处

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


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

Logo

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

更多推荐