鸿蒙系统深色模式功能详解与开发适配指南

深色模式概述

深色模式是一种满足用户个性化需求的界面显示模式,它基于人因研究设计了深色环境下舒适的颜色范围。当深色模式开启后,原本浅色主题的应用界面背景会变为深色,而文字、图标等前景元素则变为浅色。

深色模式具有以下优势:

  • 内容更加突出:深色背景使前景内容(文字、图标)更加醒目,提升信息识别效率。
  • OLED屏幕省电:在OLED屏幕设备上,深色模式下屏幕总发光量更少,从而降低功耗,延长续航。
  • 视觉舒适与沉浸式体验:尤其在暗光环境下,深色模式能减少刺眼感,提供更舒适的观看体验,同时营造沉浸感。

深色模式设计原则

由于部分用户倾向于在暗环境使用深色模式,也有相当比例的用户出于对深色风格的喜爱或节能省电需求(OLED屏幕功耗更低)而全天开启深色模式。因此,深色模式设计需要兼顾全天候清晰易读、暗环境使用舒适性,以及与浅色模式在层级感知、色彩语义方面的一致性。

易读性

深色模式下,文本、图标等前景与深色背景之间必须满足最小对比度要求。根据WCAG(Web Content Accessibility Guidelines)要求及华为人因团队研究结果,对比度分级标准如下:

场景 推荐(优秀) 一般 不建议
大字号(17fp或15fp以上粗体)、辅助文本(如列表二级文本,或其他对识别效率要求不高的场景)、功能性图标 大于3:1 1.9:1 -- 3:1 小于1.9:1
非大字号主要文本(如长文本正文、列表一级文本) 浅色模式大于4.5:1;深色模式大于5:1 浅色模式3:1-4.5:1;深色模式3:1-5:1 小于3:1
建议表示活动状态的可交互控件 控件背板与背景之间对比度不小于2.2:1 \ \

:装饰性文本、表示disable状态的文本、logo等特殊场景不需要满足以上要求。

此外,当同时使用多个多彩色表示不同状态或用于相邻位置时,除了满足对比度要求,还需满足色彩差异要求:

  • 一般要求:两个需要区分的颜色或相邻颜色,色彩差异△Euv >= 20。
  • 无障碍要求:考虑色盲群体,使用色盲模拟器后,两个要区分的颜色或相邻颜色,色彩差异△Euv >= 20。

舒适性

为避免暗环境下刺眼等舒适性问题,深色模式设计需满足以下要求:

要求 推荐(优秀) 一般 不合格
文字对比度上限 文字需要谨慎使用大于17.6:1的高对比度 15.7:1-17.6:1 大于17.6:1
小图片、图标背板 对比度建议不大于15.7:1 \ \
浅色控件使用与适配 同时满足:深色模式下应用切换为深色背景;应用内避免黑白跳转的页面;页面上不建议使用大面积白色图片或背景 深色模式下应用切换为深色背景,但存在黑白跳转的页面或大面积白色图片 深色模式下应用仍为浅色界面

一致性

多数应用设计及用户默认使用浅色模式,深色模式设计需保持与浅色模式的一致性,主要包括:

  • 层级的一致性:浅色模式下明度不是唯一表达层级的视觉线索,而在深色模式尤其是黑色背景上,投影感知降低,通常使用明度表达层级。不同层级之间需要有一定的明度区分,并与浅色模式感知一致。
  • 色彩语义的一致性:表示警示、通话等具有语义信息的颜色需要保持一致,可以在保持色相一致的前提下对明度进行调整。
  • 同类控件风格一致性:同一应用或不同页面中的同类控件,视觉风格(颜色、形状等)保持一致。

应用深浅色适配开发

当前系统存在深浅色两种显示模式,为了给用户更好的使用体验,应用应适配深浅色模式。从应用与系统配置关联的角度,适配可分为两种情况:

  1. 应用跟随系统的深浅色模式
  2. 应用主动设置深浅色模式

应用跟随系统的深浅色模式

颜色适配

(1)自定义资源实现

resources 目录下增加深色模式限定词目录(命名为 dark),并新建 color.json 文件,用于配置深色模式下的颜色资源。详细参考资源分类与访问。

目录结构示意:

复制代码
resources
├── base
│   └── element
│       └── color.json
└── dark
    └── element
        └── color.json

开发者可在两个 color.json 中定义同名配色并赋予不同的色值。例如:

base/element/color.json

json 复制代码
{
  "color": [
    {
      "name": "app_title_color",
      "value": "#000000"
    }
  ]
}

dark/element/color.json

json 复制代码
{
  "color": [
    {
      "name": "app_title_color",
      "value": "#FFFFFF"
    }
  ]
}

(2)通过系统资源实现

开发者可直接使用系统预置资源(分层参数),同一资源ID在设备类型、深浅色等不同配置下有不同的取值。通过使用系统资源,不同开发者可以开发出具有相同视觉风格的应用,无需自定义两份颜色资源,深浅色模式下会自动切换。例如,调用系统资源中的文本主要配色:

typescript 复制代码
Text('使用系统定义配色')
  .fontColor($r('sys.color.ohos_id_color_text_primary'))

图片资源适配

采用资源限定词目录方式,将深色模式下对应的同名图片放到 dark/media 目录下,通过 $r 方式加载图片资源的key值,系统深浅色切换时自动加载对应资源文件中的value值。

对于SVG格式的简单图标,可以使用 fillColor 属性配合系统资源改变图片绘制颜色,无需两套图片资源即可实现适配:

typescript 复制代码
Image($r('app.media.pic_svg'))
  .width(50)
  .fillColor($r('sys.color.ohos_id_color_text_primary'))

Web组件适配

Web组件支持对前端页面进行深色模式配置,可参考Web组件深色模式进行相关配置。

自定义节点适配

自定义节点 BuilderNodeComponentContent 需手动传递系统环境变化事件,触发节点的全量更新,详细参考 BuilderNode 系统环境变化更新 updateConfiguration

示例代码:

typescript 复制代码
// 记录创建的自定义节点对象
const builderNodeMap: BuilderNode<[Params]>[] = [];

class MyFrameCallback extends FrameCallback {
  onFrame() {
    updateColorMode();
  }
}

function updateColorMode() {
  builderNodeMap.forEach((value, index) => {
    // 通知BuilderNode环境变量改变,触发深浅色切换
    value.updateConfiguration();
  })
}
// ...
  aboutToAppear(): void {
    // ...
    this.getUIContext()?.postFrameCallback(new MyFrameCallback());
    // ...
  }

应用监听深浅色模式切换事件

应用可以主动监听系统深浅色模式变化,进行其他类型的资源初始化等自定义逻辑。注意 :应用使用 setColorMode 手动设置深浅色的情况下,将不会收到 onConfigurationUpdate 回调。除此之外,无论应用是否跟随系统深浅色模式变化,该监听方式均可生效。

步骤:

a.AbilityStageonCreate() 生命周期中获取APP当前的颜色模式并保存到 AppStorage

typescript 复制代码
onCreate(): void {
  // ...
  AppStorage.setOrCreate('currentColorMode', this.context.config.colorMode);
}

b.AbilityStageonConfigurationUpdate() 生命周期中获取最新更新的颜色模式并刷新到 AppStorage

typescript 复制代码
onConfigurationUpdate(newConfig: Configuration): void {
  AppStorage.setOrCreate('currentColorMode', newConfig.colorMode);
  hilog.info(0x0000, 'testTag', 'the newConfig.colorMode is %{public}s', JSON.stringify(AppStorage.get('currentColorMode')) ?? '');
}

c. 在Page中通过 @StorageProp + @Watch 方式获取当前最新颜色并监听设备深色模式变化:

typescript 复制代码
@StorageProp('currentColorMode') @Watch('onColorModeChange') currentMode: number =
  ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT;

d.aboutToAppear 初始化函数中根据当前最新颜色模式刷新状态变量:

typescript 复制代码
aboutToAppear(): void {
  // ...
  if (this.currentMode == ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT) {
    // 当前为浅色模式,资源初始化逻辑
    // ...
  } else {
    // 当前为深色模式,资源初始化逻辑
    // ...
  }
}

e.@Watch 回调函数中执行同样的适配逻辑:

typescript 复制代码
onColorModeChange(): void {
  if (this.currentMode == ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT) {
    // 当前为浅色模式,资源初始化逻辑
    // ...
  } else {
    // 当前为深色模式,资源初始化逻辑
    // ...
  }
}

局部深浅色适配

通过 WithTheme 可以设置三种颜色模式:跟随系统深浅色模式、固定使用浅色模式和固定使用深色模式。在 WithTheme 作用范围内,组件的样式资源值将依据指定模式,读取对应的深浅色模式系统和应用资源值,从而实现局部适配。

应用主动设置深浅色模式

应用默认配置为跟随系统切换深浅色模式。如不希望应用跟随系统深浅色模式变化,可主动设置应用的深浅色风格。设置后,应用的深浅色模式固定,不会随系统改变。

说明:应用未专门适配深色模式,直接跟随系统切换可能遇到深色模式下的显示异常,也可考虑使用该方法将本应用固定为浅色模式。

示例代码(在 onCreate 中设置固定为浅色模式):

typescript 复制代码
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
  try {
    hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onCreate');
    this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT);
  } catch (err) {
    hilog.error(DOMAIN, 'testTag', 'Failed to set colorMode. Cause: %{public}s', JSON.stringify(err));
  }
  hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onCreate');
}

系统判定应用深浅色模式的规则

  • 如果应用调用 setColorMode 接口主动设置了深浅色,则以接口效果优先。
  • 应用没有调用 setColorMode 接口时:
    • 如果应用工程 dark 目录下有深色资源,则系统组件在深色模式下会自动切换成为深色。
    • 如果应用工程 dark 目录下没有任何深色资源,则系统组件在深色模式下仍会保持浅色体验。

如果应用全部由系统组件/系统颜色开发,且想要跟随系统切换深浅色模式,应显式设置 COLOR_MODE_NOT_SET

typescript 复制代码
onCreate(): void {
  try {
    this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET);
  } catch (e) {
    hilog.error(DOMAIN, 'EntryAbility', `setColorMode failed, error: ${JSON.stringify(e)}`);
  }
  // ...
}

深浅色模式的使用建议与注意事项

建议方法 :当应用跟随系统深色或浅色模式时,建议采用 AbilityStage 的监听回调 onConfigurationUpdateAbility 的监听回调 onConfigurationUpdate 方式,主动监听系统深浅色模式变化。一旦颜色模式发生变化,应通过绑定状态变量等方法,执行特定的业务逻辑。

不推荐方法:开发者在使用资源时,未采用监听系统深浅色模式变化的方式,而是在属性设置中通过函数返回值实现深浅色切换。例如:

typescript 复制代码
getResource() : string {
  // 获取系统颜色模式
  if (colorMode == "dark") {
    return "#FF000000"
  } else {
    return "#FFFFFFFF"
  }
}
// ... other code ...
build() {
  // ... other code ...
  Button.backgroundColor(this.getResource())
  // ... other code ...
}

这种方式依赖于切换流程中重新执行属性设置代码,随着系统的发展和性能优化,并不能确保所有属性代码均被重新执行。因为在大部分热更新场景中,重新执行全部页面构建和属性设置代码显然是冗余的。

优化深浅色模式切换开销

默认情况下,深浅色模式的切换需要执行全量重绘,包括重新设置所有组件的属性,性能开销会随着应用UI的复杂度线性增加。从API version 20开始,系统提供了一种高性能的深浅色切换流程,开发者可通过新增 metadata 配置项开启该能力,从而实现深浅色切换时开销更小。

配置方法 :在 module.json5 文件中新增 metadata 字段:

json 复制代码
"metadata": [
  {
    "name": "configColorModeChangePerformanceInArkUI",
    "value": "true"
  }
]

注意事项

  • 配置此 metadata 时,必须确保在属性设置中没有通过函数返回值实现深浅色切换。
  • 从API版本26.0.0开始,HdsNavigationHdsNavDestinationHdsTabsHdsListItemCard 四个高级组件支持高性能的深浅色切换。API版本26.0.0之前,这些高级组件暂未适配,其颜色相关属性均需使用 AbilityStage 的监听回调或 Ability 的监听回调方式来处理。

开启优化后,深浅色切换不会全量重新执行前端代码和属性设置,仅会更新、重绘必要的属性。如果开发者之前在属性设置中通过函数适配深浅色更新将不会生效,需要正确适配。以下为三个典型的适配场景及示例:

场景一:根据实时读取的深浅色模式返回不同资源值

开启优化后,应采用监听回调更新状态变量,示例代码:

typescript 复制代码
// EntryAbility.ets
import { Configuration, UIAbility } from '@kit.AbilityKit';

export default class EntryAbility extends UIAbility {
  onConfigurationUpdate(newConfig: Configuration): void {
    AppStorage.setOrCreate('colorMode', newConfig.colorMode);
  }
}

// Index.ets
import { ConfigurationConstant } from '@kit.AbilityKit';

@Entry
@Component
struct MainPage {
  @StorageLink('colorMode') @Watch('colorModeChange') colorMode: ConfigurationConstant.ColorMode = ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET;
  @State textColor: Resource = $r("app.color.color_light");

  colorModeChange() {
    if (this.colorMode === ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT) {
      this.textColor = $r("app.color.color_light")
    } else {
      this.textColor = $r("app.color.color_night")
    }
  }

  build() {
    Column() {
      Text('fontColor')
        .fontColor(this.textColor)
    }
  }
}

场景二:根据判断自定义主题模式返回不同资源值

需要将文本内容和颜色与状态变量绑定,通过回调更新,示例代码:

typescript 复制代码
// ResourceTheme.ets
export enum ThemeMode {
  mode1 = 0,
  mode2
}

export class ResourceTheme {
  fontColor: ResourceColor = this.getColor();
  themeMode: ThemeMode = ThemeMode.mode1;

  setThemeMode(mode: ThemeMode) {
    this.themeMode = mode
  }
  getThemeMode(): ThemeMode {
    return this.themeMode
  }
  getColor(): ResourceColor {
    if (this.themeMode === ThemeMode.mode1) {
      return $r("app.color.color_light")
    } else {
      return $r("app.color.color_night")
    }
  }
}

// Index.ets
import { ConfigurationConstant } from '@kit.AbilityKit';
import { ResourceTheme, ThemeMode } from './ResourceTheme';

@Entry
@Component
struct MainPage {
  @StorageLink('colorMode') @Watch('colorModeChange') colorMode: ConfigurationConstant.ColorMode = ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET;
  resourceTheme = new ResourceTheme();
  @State textColor: ResourceColor = this.resourceTheme.getColor();
  @State textContent: string = this.resourceTheme.getThemeMode().toString();

  colorModeChange() {
    if (this.colorMode === ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT) {
      this.resourceTheme.setThemeMode(ThemeMode.mode1)
    } else {
      this.resourceTheme.setThemeMode(ThemeMode.mode2)
    }
    this.textContent = this.resourceTheme.getThemeMode().toString()
    this.textColor = this.resourceTheme.getColor()
  }

  build() {
    Column() {
      Text('ThemeMode is ' + this.textContent)
        .fontColor(this.textColor)
    }
  }
}

场景三:根据读取的成员变量值返回不同资源值

需要将颜色属性与状态变量绑定,通过回调更新状态变量,示例代码:

typescript 复制代码
// Index.ets
import { ConfigurationConstant } from '@kit.AbilityKit';

@Entry
@Component
struct MainPage {
  mode: number = 0;
  @StorageLink('colorMode') @Watch('colorModeChange') colorMode: ConfigurationConstant.ColorMode = ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET;
  @State textColor: Resource = $r("app.color.color_light");

  colorModeChange() {
    if (this.mode % 2 === 0) {
      this.textColor = $r("app.color.color_light")
    } else {
      this.textColor = $r("app.color.color_night")
    }
  }

  build() {
    Column() {
      Button('change mode')
        .onClick((event: ClickEvent) => {
          this.mode++
        })
      Text('fontColor')
        .fontColor(this.textColor)
    }
  }
}

利用反色能力快速适配深色模式

从API version 20开始,对于有大量存量代码,之前已经通过资源配置模式或主题 WithTheme 方式实现部分深色模式适配的应用,可使用系统提供的反色能力,快速实现全量深色模式适配。这种方式虽然管理上不如资源配置和主题方式精细可控,但适配工作量更低,应用包也不会因为大量的资源配置而膨胀,同时能带来一定程度上可以接受的视觉效果。

使用前提

  • 反色能力需在优化深浅色模式切换开销使用的前提下使用。
  • 跨进程场景如果想使用反色能力,需要 UIExtensionAbility 和对应 UIExtensionAbility 的宿主同时适配。宿主相关接口请参考 @ohos.arkui.uiExtension

接口说明

从API version 20开始,ArkUI开发框架新增 OH_ArkUI_SetForceDarkConfig 接口,提供反色能力。该功能可根据开发者自定义的反色算法,在深浅色切换时自动对颜色属性进行反色。反色能力只有在颜色属性设置为非资源值时生效,若通过 $r 设置颜色属性,则优先生效资源文件中配置的颜色值。

注意事项

  • 调用 OH_ArkUI_SetForceDarkConfig 前,需确保已加载过 OH_ArkUI_QueryModuleInterfaceByName(ARKUI_NATIVE_NODE, "ArkUI_NativeNodeAPI_1")
  • 接口一定要在节点创建前的UI线程中调用。页面创建完成后,不支持通过该接口动态修改应用的反色能力生效状态。
  • 接口仅支持进程级生效,暂不支持不同实例使用不同的反色算法。
  • 接口仅支持CAPI接口,避免跨语言调用开销。
  • 如果组件设置了异常值颜色或者 undefined,反色能力不生效。

基础使用示例

cpp 复制代码
// 对所有组件使用系统默认反色算法,即三原色取反。
OH_ArkUI_SetForceDarkConfig(nullptr, true, ArkUI_NodeType::ARKUI_NODE_UNDEFINED, nullptr);
typescript 复制代码
// ArkTS侧创建组件使用反色能力。
// 前置已默认对所有组件使用默认反色算法,深浅色切换时会对文本的文字颜色进行反色,
// 浅色模式下展示为黑色字体,深色模式下展示为白色字体。
build() {
  // ... other code ...
  Text("测试反色算法")
    .fontColor(Color.Black)
  // ... other code ...
}

不同入参效果

cpp 复制代码
// 开发者自定义的反色算法函数。
uint32_t colorInvertFunc(uint32_t color) {
  return ~color;
}
// 对所有组件使用自定义反色算法。
OH_ArkUI_SetForceDarkConfig(nullptr, true, ArkUI_NodeType::ARKUI_NODE_UNDEFINED, colorInvertFunc);
// 对所有组件停用反色能力,深浅色切换使用系统原始逻辑。
OH_ArkUI_SetForceDarkConfig(nullptr, false, ArkUI_NodeType::ARKUI_NODE_UNDEFINED, nullptr);
// 仅对文本组件使用默认反色算法。
OH_ArkUI_SetForceDarkConfig(nullptr, true, ArkUI_NodeType::ARKUI_NODE_TEXT, nullptr);
// 仅对文本组件使用自定义反色算法。
OH_ArkUI_SetForceDarkConfig(nullptr, true, ArkUI_NodeType::ARKUI_NODE_TEXT, colorInvertFunc);

不支持

  • 不支持全局禁用反色能力的同时仅对某类组件使用反色算法。
  • 不支持全局使用反色能力的同时仅对某类组件禁用反色算法。

反色算法生效优先级

  1. 使用开发者深色模式颜色资源的配置。
  2. 使用开发者为本进程中指定组件配置的反色算法。
  3. 使用开发者为本进程中所有组件配置的反色算法。

反色能力逃生通道

从API version 21开始,开发者可以通过主动设置 allowForceDark 属性,禁用指定组件的自动反色能力,维持深浅色切换时的原有逻辑,即使用主题或资源值切换。

总结

深色模式的设计与适配是一个系统工程。设计层面需要遵循易读性舒适性一致性 三大原则,确保深色界面在对比度、色彩语义和层级表达上满足用户需求。开发层面,应用适配深浅色模式可以通过资源限定符(dark目录)系统资源监听系统配置变化主动设置固定模式优化切换开销 以及反色能力等多种方式实现。开发者应根据应用实际情况选择合适的适配策略,兼顾开发效率与用户体验,同时注意避免不推荐的适配方式,以确保应用在深浅色切换时能够正确、高效地响应。

相关推荐
贾伟康43 分钟前
【时光清单|16】HarmonyOS ArkTS 多设备布局实战:适配手机、平板和 PC/2in1 的窗口变化
harmonyos·arkts·arkui·响应式布局·多设备适配
lilian2339 小时前
HarmonyOS 7 新特性(二十五)|智慧手势:意图识别与误触治理
华为·harmonyos
贾伟康11 小时前
【时光清单|14】HarmonyOS ArkTS 主导航实战:统一页面入口、返回路径和参数校验
harmonyos·arkts·arkui·navigation·路由管理
贾伟康16 小时前
【时光清单|05】HarmonyOS ArkTS 倒计时卡片实战:适配 2x2、2x4 与 4x4 多尺寸
harmonyos·arkts·arkui·服务卡片·formextensionability
小雨青年16 小时前
【HarmonyOS 7 悬浮页签深度实战】07 折叠屏、平板与宽窗口的布局适配
华为·harmonyos
熊猫钓鱼>_>18 小时前
鸿蒙AI Agent新范式:从“对话式辅助”到“工程化代理”的Harness架构实战解析
人工智能·笔记·学习·华为·架构·harmonyos
大锅盖118 小时前
HarmonyOS ArkUI 非遗纹样素材平台设计复盘:朱砂宣纸与鎏金黛蓝的文化守护
华为·harmonyos
2501_9197490319 小时前
华为鸿蒙管理学习生活与工作APP—小羊管理
学习·华为·生活·harmonyos·鸿蒙
贾伟康19 小时前
【时光清单|20】HarmonyOS ArkTS AppGallery 发布复查实战:核对包名、版本、设备、素材和离线声明
harmonyos·arkts·agc·appgallery·发布复查