一款设置页原本只有开关、滑杆和下拉选择。升级目标 SDK 后,控件的高光、通透材质与动效似乎发生了变化。第一反应往往是去逐个修改颜色和背景模糊,但这未必是根本原因:HarmonyOS 7(API 26)的 ArkUI 已提供沉浸光感能力,受支持的系统组件可能随配置策略呈现新的材质反馈。这个特性既不是所有组件无条件通用,也不是把一个“玻璃质感”图片贴到页面上。

最值得研究的问题,是它的启用范围、配置优先级和兼容性边界。把这三个问题梳理清楚,才能决定是让整个应用统一采用新系统材质,还是只针对特定交互区域控制效果,同时避免无效配置造成的误判。

HarmonyOS 7 沉浸光感技术封面

先看适用范围:不是任何按钮都能得到光感

华为开发者的《开启沉浸光感》文档明确指出:应用使用这项能力需要确保 targetSdkVersion 不低于 26.0.0。系统的支持范围也按组件和呈现位置区分。指定弹窗类组件与弹窗接口可以在全页面生效;Slider、Toggle、Select 等交互控件同样可在全页面生效。另一类组件则主要限于 Navigation 或 NavDestination 标题栏,以及符合位置条件的横向 Tabs 底部 TabBar 等指定区域。

这个限制很关键。开发者不该看到某个普通内容区容器没有光感,就去寻找不存在的通用“强制闪光 API”;更不能把系统产品宣传中的沉浸体验一律理解为应用可以调用的独立接口。具体能否在某个控件上工作,要回到官方公布的组件清单和运行环境核对,而不是用截图猜测。

控件/区域关注点验证方式
Toggle、Slider、Select全页面范围内的支持条件检查配置与实际组件能力
指定系统弹窗弹窗材质反馈是否生效检查弹窗类型与选项
Navigation 标题栏特定区域的系统材质核对布局位置和配置
普通自绘卡片不应默认按全局生效推断使用普通背景样式兜底

支持区域与普通内容区的组件边界图

与其一口气改整个页面,不如先挑三个容易复现的控件做“受控样本”:一个开关,一个滑杆,一个选择器。其他文字、背景、间距都保持不变,只比较沉浸光感配置状态。这样才能区别系统材质变化与原本的自定义视觉差异。

配置优先级:默认、全局启用、全局禁用不是一回事

应用级开关定义在 entry 类型模块的 module.json5 中,通过 module.metadata 指定系统识别的键。官方给出的名称必须完整写作 ohos.arkui.UIMaterial.state;取值 default、enable、disable 分别表示默认策略、开启和禁用。下面给出与实际配置结构一致的关键片段,合入项目时保留原有 module 其他字段,而不是用这一段覆盖整个配置文件。

{
  "module": {
    "name": "entry",
    "type": "entry",
    "metadata": [
      {
        "name": "ohos.arkui.UIMaterial.state",
        "value": "enable"
      }
    ]
  }
}

如果要比较三种策略,只改上面 value 一项,分别构建对应配置即可。不要把它写成普通业务开关,让用户点击一个 Toggle 就期望即时修改 module.json5;静态模块元数据属于构建与安装配置,和页面运行时 @State 的业务取值完全不是同一层。

还有一处容易被忽略:官方说明指出,项目从 API 26.0.0 之前升级到该版本及以上、且未主动配置元数据时,默认受支持组件会开启沉浸光感。因此“没有写 metadata”不能想当然等于“绝对关闭”。如果想进行清晰的 A/B 视觉检查,应该显式地选择 enable 与 disable,并记录各自构建配置。disable 是应用级全局禁用,不会被组件级配置反向强制开启。

从优先级上看,组件级接口可以覆盖应用级开启下的部分组件表现;但全局 disable 是更强的总开关。对于同时需要自定义控件效果与统一风格的应用,应由设计规范预先定好谁拥有最终视觉控制权,避免同一组件同时被多个层次配置成相互冲突的样式。

module.json5 中三种光感策略及优先级对比

用一份最小设置页把界面行为和系统材质分开

页面示例只使用 ArkUI 常规组件,不引入无来源的“光感效果函数”。创建 Stage 模型 ArkTS 工程,在 entry/src/main/ets/pages/Index.ets 放入以下页面。Switch、Slider 和 Select 的数值都由页面状态控制:Toggle 控制通知偏好,Slider 表示提醒强度,Select 负责优先级。它们的业务逻辑独立于 module.json5 的沉浸光感开关。

@Entry
@Component
struct Index {
  @State private notificationEnabled: boolean = true;
  @State private reminderLevel: number = 50;
  @State private priorityIndex: number = 0;

  build() {
    Scroll() {
      Column({ space: 20 }) {
        Text('提醒偏好').fontSize(29).fontWeight(FontWeight.Bold)
        Text('调整通知、提醒强度和处理优先级')
          .fontSize(14).fontColor('#64748B')

        Row() {
          Column({ space: 6 }) {
            Text('接收通知').fontSize(19)
            Text('允许显示任务状态变化提醒')
              .fontSize(12).fontColor('#64748B')
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)
          Toggle({
            type: ToggleType.Switch,
            isOn: this.notificationEnabled
          })
            .onChange((value: boolean) => {
              this.notificationEnabled = value;
            })
        }
        .width('100%')
        .padding(16).borderRadius(14)
        .backgroundColor('#EFF6FF')

        Column({ space: 10 }) {
          Text('提醒强度:' + this.reminderLevel.toString())
            .fontSize(19)
          Slider({
            value: this.reminderLevel,
            min: 0,
            max: 100,
            step: 1,
            style: SliderStyle.OutSet
          })
            .onChange((value: number) => {
              this.reminderLevel = value;
            })
        }
        .width('100%')
        .alignItems(HorizontalAlign.Start)
        .padding(16).borderRadius(14)
        .backgroundColor('#EFF6FF')

        Row({ space: 14 }) {
          Text('处理优先级').fontSize(19).layoutWeight(1)
          Select([
            { value: '普通' },
            { value: '紧急' },
            { value: '稍后' }
          ])
            .selected(this.priorityIndex)
            .onSelect((index: number) => {
              this.priorityIndex = index;
            })
        }
        .width('100%')
        .padding(16).borderRadius(14)
        .backgroundColor('#EFF6FF')

        Text('通知:' + (this.notificationEnabled ? '开启' : '关闭'))
          .fontSize(15).fontColor('#475569')
        Text('优先级序号:' + this.priorityIndex.toString())
          .fontSize(15).fontColor('#475569')
      }
      .padding(20).width('100%')
      .alignItems(HorizontalAlign.Start)
    }
    .width('100%').height('100%')
    .backgroundColor('#F8FAFC')
  }
}

代码刻意使用纯粹的业务交互。原因是沉浸光感属于系统支持组件的视觉材质能力,不应该用业务状态变量去伪造一个“实际光感已开启”的运行结果。切换 Toggle 只改变通知偏好;它不会实时切换应用级 metadata。Select 的下拉按钮和菜单在系统材质配置上还可能由不同接口控制,评审时需要将两个区域分别观察。

本文示例也没有额外申请相机、麦克风、定位或跨设备权限。这些控件不需要借助无关敏感权限来表现自身的交互。真正需要系统特性兼容分支的代码,应以实际引用的 API 起始版本、Kit 类型和使用场景为准,再决定是否做 canIUse 或版本条件判断;只因为标题写了 API 26,不能把每个组件都当成只在该版本首次出现。

材质不生效,先排四个点,而不是先调颜色

第一步看构建目标:是否使用与官方新能力匹配的 SDK,targetSdkVersion 是否满足 26.0.0 条件。第二步看入口模块:配置是否写在 entry 类型模块、键名是否完全一致、值是否为允许的三种字符串之一。第三步看控件与位置:某些组件只在指定标题栏或 TabBar 位置支持,普通自绘容器不属于同一覆盖范围。第四步看全局禁用:如果应用级配置为 disable,别误以为某个组件级选项还能绕过全局关闭。

此外,观察中应固定其他条件,例如同一套背景资源、相同亮暗模式、相同系统设置和相同设备支持能力。如果在多台设备上比较,要分开记录设备与系统版本,不能把硬件差异和代码改动混在一起。沉浸光感涉及图形资源和交互反馈,官方文档特别提示其 GPU 资源开销,复杂页面还应进行功耗与流畅度检查;但没有采样数据时,不应该写出看似精确的“省电百分比”或帧率结论。

如果目标是兼容更早的系统版本,务必把“目标 SDK 支持该配置”与“某个老版本系统具备该视觉效果”区别开。设计上应准备没有沉浸光感时也清晰可读的默认背景、对比度与控件反馈,必要时采用官方兼容性保护机制。对业务而言,提醒是否开启、优先级是否改变,永远不能依赖系统材质动画是否渲染出来。

这个案例最有价值的经验,是将应用级策略、组件支持矩阵和业务交互独立管理。配置统一,才能让设计评审有可靠的对照;控件范围明确,才能减少“写了属性却不生效”的无效排障;业务状态与视觉材质分离,才能在多版本、多形态设备上保持功能一致。

参考资料:

Logo

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

更多推荐