深色模式概述
深色模式是一种满足用户个性化需求的界面显示模式,它基于人因研究设计了深色环境下舒适的颜色范围。当深色模式开启后,原本浅色主题的应用界面背景会变为深色,而文字、图标等前景元素则变为浅色。
深色模式具有以下优势:
- 内容更加突出:深色背景使前景内容(文字、图标)更加醒目,提升信息识别效率。
- 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)自定义资源实现
在 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组件深色模式进行相关配置。
自定义节点适配
自定义节点 BuilderNode 和 ComponentContent 需手动传递系统环境变化事件,触发节点的全量更新,详细参考 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. 在 AbilityStage 的 onCreate() 生命周期中获取APP当前的颜色模式并保存到 AppStorage:
typescript
onCreate(): void {
// ...
AppStorage.setOrCreate('currentColorMode', this.context.config.colorMode);
}
b. 在 AbilityStage 的 onConfigurationUpdate() 生命周期中获取最新更新的颜色模式并刷新到 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 的监听回调 onConfigurationUpdate 或 Ability 的监听回调 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开始,
HdsNavigation、HdsNavDestination、HdsTabs、HdsListItemCard四个高级组件支持高性能的深浅色切换。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);
不支持:
- 不支持全局禁用反色能力的同时仅对某类组件使用反色算法。
- 不支持全局使用反色能力的同时仅对某类组件禁用反色算法。
反色算法生效优先级:
- 使用开发者深色模式颜色资源的配置。
- 使用开发者为本进程中指定组件配置的反色算法。
- 使用开发者为本进程中所有组件配置的反色算法。
反色能力逃生通道 :
从API version 21开始,开发者可以通过主动设置 allowForceDark 属性,禁用指定组件的自动反色能力,维持深浅色切换时的原有逻辑,即使用主题或资源值切换。
总结
深色模式的设计与适配是一个系统工程。设计层面需要遵循易读性 、舒适性 和一致性 三大原则,确保深色界面在对比度、色彩语义和层级表达上满足用户需求。开发层面,应用适配深浅色模式可以通过资源限定符(dark目录) 、系统资源 、监听系统配置变化 、主动设置固定模式 、优化切换开销 以及反色能力等多种方式实现。开发者应根据应用实际情况选择合适的适配策略,兼顾开发效率与用户体验,同时注意避免不推荐的适配方式,以确保应用在深浅色切换时能够正确、高效地响应。