HarmonyOS 深色模式最佳实践与案例详解
HarmonyOS 深色模式最佳实践与案例详解
本文基于 HarmonyOS 的深色模式适配体系,系统介绍颜色适配、图片适配、Web 组件适配、模式切换机制、反色能力及最佳实践。
一、引言
深夜,你窝在沙发上刷手机,屏幕亮白刺眼;清晨,你打开应用,界面却暗沉难辨——深色模式的缺失,让无数用户在这些场景中被迫忍受不佳的视觉体验。正因如此,深色模式(Dark Mode)早已从“加分项”演变为现代操作系统的“必选项”。从 iOS 到 Android ,从微信到钉钉,主流平台与应用纷纷将其纳入基础体验。对于 HarmonyOS 而言,深色模式不仅顺应了这一趋势,更借助 OLED 屏幕的像素级发光特性,在暗光环境下显著降低视觉疲劳,同时节省 30%~60% 的屏幕功耗——这对移动设备的续航尤为关键。
然而,为应用适配深色模式远非“把背景变黑、文字变白”那般简单。真实项目中,开发者通常面临三重挑战:第一,工作量巨大——动辄数十上百个页面,每个页面的背景、文字、分割线、图标、卡片都需要逐一调整,若全靠硬编码条件判断,代码将变得臃肿且难以维护;第二,品牌一致性难保——品牌色、Logo、核心视觉元素需要在深浅模式下都保持辨识度,简单的反色算法往往导致色彩失真;第三,混合内容复杂——应用内嵌入的 Web 页面、服务卡片、第三方组件,各有各的渲染机制,深色适配需要跨技术栈协同。
面对这些痛点,HarmonyOS 从 API 9 开始便提供了一套系统级、声明式的深色模式适配方案。其核心是 资源限定词(Resource Qualifiers) 机制:开发者只需在 base/ 和 dark/ 两套资源目录中分别定义浅色与深色下的颜色、图片等资源,系统便会根据当前模式自动选择对应资源,业务代码中几乎无需任何 if-else 判断。这种“数据与逻辑分离”的设计,既降低了适配成本,又避免了运行时条件分支带来的性能损耗。更进一步,HarmonyOS 还提供了 setColorMode() 接口,允许应用内独立控制深浅模式,以及 allowForceDark() 反色能力(API 21+),为快速适配或敏感区域保护提供了灵活补充。
本文将从资源适配、系统跟随、主动控制、反色机制到真实案例(喵屿 App),系统化地讲解 HarmonyOS 深色模式适配的完整技术路径。无论你是准备启动新项目,还是正在改造存量代码,都能从中找到一套可落地、可扩展的实践方案。接下来,我们从最基础的资源适配开始,逐步深入。
二、资源适配:深浅色模式的基础设施
HarmonyOS 的资源管理框架内置了对深色模式的支持。其核心设计理念是“资源覆盖”——开发者只需在 resources/ 目录下创建 dark/ 子目录,放置深色模式专属的资源文件,系统就会根据当前颜色模式自动选择对应资源。这种设计的好处在于:
- 分离关注点:颜色、图片等视觉资源与业务逻辑解耦,UI 代码只关心“用什么资源名”,不关心“当前是什么模式”。
- 可维护性强:新增或修改某个颜色值,只需修改资源文件,无需遍历所有页面。
- 性能优异:资源选择在系统底层完成,没有运行时字符串比较或反射开销。
接下来,我们分别从颜色、图片和 Web 组件三个维度,详细讲解资源适配的具体做法。
2.1 颜色资源适配
颜色是深色模式适配中最基础、最频繁的工作。HarmonyOS 推荐的目录结构如下:
entry/src/main/resources/
├── base/ # 默认资源(浅色模式兜底)
│ └── element/
│ └── color.json # 浅色颜色定义
├── dark/ # 深色模式限定词目录
│ └── element/
│ └── color.json # 深色颜色覆盖(仅需定义与浅色不同的颜色)
└── rawfile/
需要注意的是,dark/ 目录并不是要求你把所有颜色都重新定义一遍,而是仅覆盖那些在深色模式下需要变化的颜色。未在 dark/ 中定义的颜色,系统会自动回退到 base/ 中的值。这既减少了冗余配置,也降低了维护成本。
下面给出一个典型的浅色配色方案(base/element/color.json):
// entry/src/main/resources/base/element/color.json
// 浅色模式下的默认配色方案
{
"color": [
{
"name": "start_window_background", // 启动页背景色
"value": "#FFFFFF"
},
{
"name": "page_background", // 页面主背景色
"value": "#F1F3F5"
},
{
"name": "text_primary", // 一级文字颜色
"value": "#182431"
},
{
"name": "text_secondary", // 二级文字颜色
"value": "#99182431"
},
{
"name": "card_background", // 卡片背景色
"value": "#FFFFFF"
},
{
"name": "divider_color", // 分割线颜色
"value": "#E5E5E5"
},
{
"name": "brand_primary", // 品牌主色(深浅模式一般不同)
"value": "#007DFF"
}
]
}
对应的深色覆盖配置(dark/element/color.json):
// entry/src/main/resources/dark/element/color.json
// 深色模式下的配色方案(仅覆盖需要变化的颜色,其余沿用 base/ 的值)
{
"color": [
{
"name": "start_window_background",
"value": "#000000" // 启动页:白色 → 黑色
},
{
"name": "page_background",
"value": "#1A1A2E" // 页面背景:淡灰 → 深蓝黑
},
{
"name": "text_primary",
"value": "#EEEEEE" // 主文字:深色 → 亮色
},
{
"name": "text_secondary",
"value": "#AAAAAA" // 次文字:半透明深色 → 半透明亮色
},
{
"name": "card_background",
"value": "#2A2A3C" // 卡片背景:白色 → 深灰蓝
},
{
"name": "divider_color",
"value": "#333344" // 分割线:浅灰 → 深灰
},
{
"name": "brand_primary",
"value": "#0A84FF" // 品牌主色:略微调亮以适应深色背景
}
]
}
在组件中引用这些颜色资源时,只需使用 $r('app.color.xxx') 语法,系统会自动根据当前模式选择 base/ 或 dark/ 中的值:
// 正确用法:通过 $r() 引用颜色资源,系统自动根据当前模式选择 base/ 或 dark/ 的值
// 无需任何条件判断代码
@Entry
@Component
struct Index {
build() {
Column() {
Text('鸿蒙深色模式适配')
.fontSize(24)
.fontWeight(FontWeight.Bold)
// $r('app.color.text_primary') 在浅色模式返回 #182431,深色模式返回 #EEEEEE
.fontColor($r('app.color.text_primary'))
Text('这是一段说明文字')
.fontSize(16)
// 次级文字同样自动适配
.fontColor($r('app.color.text_secondary'))
}
.width('100%')
.height('100%')
// 页面背景色自动适配
.backgroundColor($r('app.color.page_background'))
}
}
关键约束与最佳实践:
dark/目录下的资源文件name必须与base/中完全一致,系统按名称匹配而非按位置匹配。- 只有应用存在
dark/资源目录,系统才将应用识别为支持深色模式,并在系统切换深色模式时主动通知应用。 - 所有 UI 组件的颜色必须使用
$r('app.color.xxx')引用,禁止硬编码色值(如'#FFFFFF'或Color.White)。硬编码会破坏自动适配机制,导致深色模式下部分元素“漏网”。 - 建议采用语义化命名(如
text_primary、card_background),而非色值命名(如color_white),这样在深色模式下语义仍然成立。
2.2 图片资源适配
与颜色类似,图片资源也可以为深色模式提供专属版本。例如,Logo 图在浅色背景下通常是深色版本,而在深色背景下则需要换成浅色版本,否则会“隐身”或对比度不足。
目录结构如下:
entry/src/main/resources/
├── base/media/
│ └── logo.png # 浅色模式使用的 Logo
└── dark/media/
└── logo.png # 深色模式使用的 Logo(颜色适配暗色背景)
在代码中引用时,依然使用 $r('app.media.logo'),系统会根据当前模式自动选择对应文件:
// 图片引用方式与颜色相同,系统自动选择对应资源文件
Image($r('app.media.logo'))
.width(120)
.height(60)
.objectFit(ImageFit.Contain)
对于 SVG 图标,更推荐使用“单套资源 + 动态着色”的方案。这样无需为每个图标准备两套图片文件,减少了包体积和维护成本:
// SVG 图标通过 fillColor 动态着色,无需两套图片文件
// fillColor 同样可以使用 $r() 引用颜色资源实现自动适配
Image($r('app.media.icon_settings'))
.width(24)
.height(24)
.fillColor($r('app.color.text_primary')) // 深色模式自动变亮色
选型建议:对于纯色图标,优先使用 SVG + fillColor;对于复杂的位图(如照片、纹理),则应准备两套图片资源,或者使用 allowForceDark(false) 保护(详见第五章)。
2.3 Web 组件适配
在混合开发场景中,应用内嵌入的 Web 页面同样需要适配深色模式。HarmonyOS 的 Web 组件提供了 darkMode 属性和 forceDarkAccess 能力,让开发者可以灵活控制 Web 内容的深色表现。
ArkTS 侧配置:
// entry/src/main/ets/pages/WebPage.ets
import { webview } from '@kit.ArkWeb';
import { ConfigurationConstant } from '@kit.AbilityKit';
@Entry
@Component
struct WebPage {
// Web 控制器
controller: webview.WebviewController = new webview.WebviewController();
// 存储当前颜色模式
@StorageProp('currentColorMode') currentMode: number =
ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET;
build() {
Column() {
Web({ src: $rawfile('index.html'), controller: this.controller })
// darkMode 设置:Auto = 跟随系统深色模式
.darkMode(this.currentMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK
? WebDarkMode.On
: WebDarkMode.Off)
// 允许 Web 页面使用强制深色算法(对未适配深色的传统网页有效)
.forceDarkAccess(true)
// 深色模式下 Web 组件背景色适配
.backgroundColor(this.currentMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK
? '#1A1A2E'
: '#FFFFFF')
}
}
}
前端 HTML 侧适配:
现代 Web 标准已经支持 prefers-color-scheme 媒体查询,前端开发者可以据此实现 CSS 变量切换:
<!-- rawfile/index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<!-- 告知浏览器此页面支持深色模式 -->
<meta name="color-scheme" content="light dark">
<style>
/* 浅色模式的 CSS 变量(默认) */
:root {
--bg-color: #FFFFFF;
--text-color: #182431;
--link-color: #007DFF;
}
/* 深色模式下覆盖 CSS 变量 */
@media (prefers-color-scheme: dark) {
:root {
--bg-color: #1A1A2E;
--text-color: #EEEEEE;
--link-color: #0A84FF;
}
}
body {
background-color: var(--bg-color);
color: var(--text-color);
}
a { color: var(--link-color); }
</style>
</head>
<body>
<h1>Web 页面深色模式适配示例</h1>
</body>
</html>
这里的关键是 color-scheme meta 标签,它告诉浏览器该页面已经考虑了深色模式,避免浏览器额外应用默认的反色策略,从而保证前端样式与 ArkTS 侧协调一致。
三、应用跟随系统的深色模式
完成了资源适配之后,下一步是让应用能够感知系统级深色模式的变化,并自动切换。HarmonyOS 提供了非常简洁的 API 来完成这一任务。
3.1 基础配置:跟随系统
在 EntryAbility.onCreate() 中设置颜色模式为 COLOR_MODE_NOT_SET,即可让应用跟随系统设置:
// entry/src/main/ets/entryability/EntryAbility.ets
import { AbilityConstant, ConfigurationConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
const TAG = 'EntryAbility';
export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
hilog.info(0x0000, TAG, 'EntryAbility onCreate');
/**
* 设置应用颜色模式为跟随系统
* COLOR_MODE_NOT_SET(值:-1):跟随系统自动切换
* COLOR_MODE_LIGHT(值:1):强制浅色模式
* COLOR_MODE_DARK(值:0):强制深色模式
*
* 注意:此设置应在 Ability 启动最早阶段执行,确保所有页面创建前生效
*/
this.context.getApplicationContext()
.setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET);
}
}
setColorMode() 是一个全局设置,作用于整个应用的所有 UI 组件。一旦设置,所有通过 $r() 引用的资源都会自动切换到对应的颜色模式。
为什么要在 onCreate 中尽早设置? 因为如果设置太晚,某些页面可能已经在浅色模式下完成了布局和绘制,切换时会出现短暂的闪烁或颜色错乱。最佳实践是在 Ability 生命周期的最早阶段(onCreate)完成模式设置。
3.2 监听系统模式切换
当用户在系统设置中切换深色/浅色模式时,如果应用正处于前台,我们需要能够感知这一变化并做出响应。HarmonyOS 提供了两种方式:
方式一:通过 onConfigurationUpdate 监听
UIAbility 的 onConfigurationUpdate 回调会在系统配置(包括颜色模式、语言、屏幕方向等)发生变化时被调用。我们可以在其中将当前颜色模式存入 AppStorage,供全局组件使用:
// EntryAbility.onConfigurationUpdate — 系统配置变更回调
onConfigurationUpdate(newConfig: Configuration): void {
// 将当前颜色模式存入 AppStorage,供全局组件响应
AppStorage.setOrCreate('currentColorMode', newConfig.colorMode);
hilog.info(0x0000, TAG, `Color mode changed to: ${newConfig.colorMode}`);
}
方式二:页面中使用 @StorageProp + @Watch 响应
在任意页面组件中,我们可以通过 @StorageProp 订阅 AppStorage 中的颜色模式值,并用 @Watch 监听其变化:
// 任意页面组件
import { ConfigurationConstant } from '@kit.AbilityKit';
@Entry
@Component
struct SettingsPage {
// 从 AppStorage 读取颜色模式,并监听变化
@StorageProp('currentColorMode') @Watch('onColorModeChanged')
currentMode: number = ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET;
/**
* 颜色模式切换时的回调
* 适用于需要在代码层面做额外处理的场景(如动态加载不同图表配色)
*/
onColorModeChanged(): void {
if (this.currentMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK) {
console.info('切换到深色模式,执行额外逻辑');
// 例如:更换 ECharts 图表的暗色主题
} else {
console.info('切换到浅色模式');
}
}
build() {
Column() {
Text(`当前模式:${this.currentMode}`)
}
}
}
这种方式的优势在于,你可以在任意组件中响应模式变化,而不仅仅是 Ability 层面。例如,某些自定义绘制组件可能需要根据模式重新计算颜色值,@Watch 回调就提供了这样的入口。
四、应用主动设置深浅色模式
跟随系统固然方便,但很多用户仍希望应用内能提供独立的“深色/浅色/跟随系统”开关,以便在夜间阅读时单独将应用调暗,而不影响系统其他界面。HarmonyOS 的 setColorMode() 接口同样支持这种主动控制。
下面是一个完整的主题设置页面示例,包含了三种选项的 UI 和持久化逻辑:
// entry/src/main/ets/pages/ThemeSettings.ets
import { ConfigurationConstant, common } from '@kit.AbilityKit';
import PreferenceUtil from 'resource/src/main/ets/utils/PreferenceUtil';
// 存储在 Preferences 中的主题 key
const KEY_THEME_MODE = 'userThemeMode';
@Entry
@Component
struct ThemeSettings {
// 当前选中模式:-1=跟随系统, 0=深色, 1=浅色
@State selectedMode: number = ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET;
// 提示弹窗控制器
promptAction = this.getUIContext().getPromptAction();
aboutToAppear(): void {
// 从本地持久化读取用户上次选择的主题模式
const savedMode = PreferenceUtil.getPreferenceByNameSync('app_data', KEY_THEME_MODE, -1) as number;
this.selectedMode = savedMode;
}
/**
* 应用用户选择的颜色模式
* @param mode - ConfigurationConstant.ColorMode 枚举值
*/
applyColorMode(mode: number): void {
// 通过 ApplicationContext 设置全局颜色模式
const ctx = this.getUIContext().getHostContext() as common.UIAbilityContext;
ctx.getApplicationContext().setColorMode(mode);
// 持久化用户选择
PreferenceUtil.putPreferenceByNameSync('app_data', KEY_THEME_MODE, mode);
this.selectedMode = mode;
this.promptAction.showToast({
message: mode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK
? '已切换为深色模式'
: mode === ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT
? '已切换为浅色模式'
: '已切换为跟随系统'
});
}
build() {
Column({ space: 12 }) {
Text('主题模式').fontSize(18).fontWeight(FontWeight.Bold)
// 跟随系统
Row() {
Text('跟随系统')
Radio({
value: 'system',
group: 'themeGroup'
})
.checked(this.selectedMode === ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET)
.onChange((isChecked: boolean) => {
if (isChecked) {
this.applyColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET);
}
})
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.padding({ left: 16, right: 16 })
// 浅色模式
Row() {
Text('浅色模式')
Radio({
value: 'light',
group: 'themeGroup'
})
.checked(this.selectedMode === ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT)
.onChange((isChecked: boolean) => {
if (isChecked) {
this.applyColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT);
}
})
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.padding({ left: 16, right: 16 })
// 深色模式
Row() {
Text('深色模式')
Radio({
value: 'dark',
group: 'themeGroup'
})
.checked(this.selectedMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK)
.onChange((isChecked: boolean) => {
if (isChecked) {
this.applyColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_DARK);
}
})
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.padding({ left: 16, right: 16 })
}
.width('100%')
.height('100%')
.backgroundColor($r('app.color.page_background'))
}
}
设计要点:
- 持久化:用户的选择需要保存在本地(如 Preferences),这样下次启动应用时才能恢复正确的模式。在
EntryAbility.onCreate()中读取该值并调用setColorMode()(参考第七章的喵屿案例)。 - 即时生效:
setColorMode()是全局且实时的,调用后所有正在显示的页面都会立即刷新颜色,无需重启应用。 - 与系统解耦:当用户选择“浅色”或“深色”时,应用不再跟随系统变化;选择“跟随系统”则重新绑定系统事件。这一逻辑完全由应用层控制,系统 API 只提供设置能力,不干预应用的选择策略。
五、利用反色能力快速适配深色模式
对于存量项目或快速原型阶段,开发者可能希望以最小的改动实现深色模式的基本可用性。HarmonyOS 从 API 21 开始提供的 allowForceDark() 反色能力,正是为此而生。
5.1 allowForceDark 的基本原理
当组件启用反色时,系统会在深色模式下自动对其颜色值进行“智能反色”或“亮度变换”——通常是将浅色背景变为深色,深色文字变为浅色,同时保持一定色相偏移,避免颜色失真。这一过程由系统底层算法完成,开发者无需手动计算。
// allowForceDark API 来自 @kit.ArkUI,API 21+ 可用
@Entry
@Component
struct ForceDarkDemo {
build() {
Column() {
// 区域一:启用反色 — 深色模式下文字自动变亮、背景自动变暗
Column() {
Text('这段文字在深色模式下会自动反色')
.fontSize(20)
.fontColor(Color.Blue) // 深色模式自动变浅蓝色
}
.backgroundColor('#FFE0E0') // 深色模式自动变暗红色
.allowForceDark(true) // 启用反色能力
.padding(16)
.width('100%')
.margin({ bottom: 16 })
// 区域二:禁用反色 — 保持原有颜色不变
Column() {
Text('这段文字在任何模式下颜色不变')
.fontSize(20)
.fontColor(Color.Blue) // 始终为 Blue
}
.backgroundColor('#FFE0E0') // 始终为浅红色
.allowForceDark(false) // 禁用反色能力
.padding(16)
.width('100%')
}
.padding(20)
}
}
5.2 反色能力的继承与隔离
反色能力遵循以下继承规则,这使得开发者可以在大范围启用反色的同时,精准地“隔离”某些不需要反色的区域:
- 父组件设置
allowForceDark(true):所有子组件默认继承反色能力。 - 子组件设置
allowForceDark(false):该子组件及其后代不受反色影响,即便祖先设置了allowForceDark(true)。
这种“就近覆盖”的规则,与 CSS 的 color-scheme 继承机制类似,非常直观:
@Entry
@Component
struct ForceDarkInheritance {
build() {
Column() {
// 父级启用反色
Column() {
Text('父组件启用反色,此文字会被反色')
.fontColor(Color.Black)
// 子组件禁用反色 — 隔离父级影响
Row() {
Button('我不受反色影响')
.backgroundColor(Color.Grey)
}
.allowForceDark(false) // 按钮及其内部元素不会被反色算法处理
}
.allowForceDark(true)
}
.width('100%').height('100%')
}
}
5.3 典型使用场景与建议
| 场景 | 建议 | 原因 |
|---|---|---|
| 品牌 Logo、品牌色区域 | allowForceDark(false) |
品牌视觉需要保持一致性,反色可能改变品牌识别色 |
| 用户上传的图片 | allowForceDark(false) |
反色会破坏图片原始色彩,导致照片出现“负片”效果 |
| 普通文本内容 | allowForceDark(true) |
自动适配,减少适配工作,尤其适合快速迭代阶段 |
| 已做双套资源适配的区域 | allowForceDark(false) |
避免反色与手动适配冲突,造成双重变换 |
重要提示:allowForceDark 要求 API 21+,低版本设备(如 compatibleSdkVersion 低于 21)无法使用。对于需要兼容 API 17~20 的项目,只能完全依赖资源限定词方式适配深色模式。此外,反色算法并非万能,对于渐变、阴影、透明度等复杂样式可能效果不佳,因此仍建议优先采用资源适配方案。
六、深浅色模式的使用建议与注意事项
6.1 设计层面的最佳实践
- 语义化命名:使用
text_primary、page_background等语义名称,而非color_white、color_black_1等色值命名。语义命名在深色模式下更有意义,也方便未来拓展更多主题(如高对比度模式)。 - 品牌色微调:深色模式下的品牌色应略微调亮(如
#007DFF→#0A84FF),保持视觉辨识度,避免在暗背景下显得“发灰”。 - 对比度合规:主文字对比度 ≥ 4.5:1,大号文字(≥18sp)≥ 3:1,这是 WCAG 无障碍标准的基本要求。可以使用在线工具检测对比度。
- 纯黑避免使用:深色背景推荐
#1A1A1A或#121212(Material Design 推荐),而非#000000。纯黑背景在 OLED 屏幕上虽然省电,但容易产生“黑洞”效应,削弱层次感。 - 分割线减淡:深色模式下分割线颜色应更暗(例如从
#E5E5E5变为#333344),避免亮线在暗背景上过于突兀。 - 阴影调整:深色模式下阴影应减弱或使用亮色边框替代,因为深色背景上投射深色阴影几乎不可见。
6.2 常见问题排查
| 问题 | 可能原因 | 解决方案 |
|---|---|---|
| 深色模式切换无效 | 未创建 dark/ 资源目录 |
创建并填充 dark/element/color.json |
| 部分文字未适配 | 代码中硬编码了色值 | 改用 $r('app.color.xxx') |
| Web 组件全黑 | 未配置 Web 深色模式 | 添加 .darkMode(WebDarkMode.Auto) |
| 品牌 Logo 变色 | 反色算法影响了图片 | 添加 .allowForceDark(false)(API 21+) |
| SVG 图标颜色异常 | 未设置 fillColor |
使用 Image.fillColor($r(...)) 动态着色 |
| 切换模式后部分页面未刷新 | 页面未使用 @StorageProp 监听 |
在 Ability 的 onConfigurationUpdate 中更新 AppStorage |
七、喵屿应用中的深色模式实践
理论终须落地。喵屿App 作为一款已在鸿蒙应用市场上架的产品,其深色模式适配经历了从“能用”到“好用”的迭代过程。本节我们将以喵屿的真实代码为例,展示深色模式在完整应用中的集成方式。

7.1 双套颜色 Token 体系
喵屿将应用中所有颜色收敛为语义化 Token,集中存放于 base/element/color.json(浅色)和 dark/element/color.json(深色)中。所有卡片、页面、按钮均通过 $r('app.color.xxx') 引用这些 Token,系统根据当前模式自动选择对应值。
浅色 Token 示例(摘录):
// entry/src/main/resources/base/element/color.json(浅色 — 摘录)
{
"color": [
{ "name": "card_bg", "value": "#FFFBF5" },
{ "name": "card_text_primary", "value": "#1A1A1A" },
{ "name": "card_text_secondary", "value": "#334155" },
{ "name": "card_text_hint", "value": "#999999" },
{ "name": "card_accent", "value": "#FF6B35" },
{ "name": "card_badge_error_bg", "value": "#FFF0F0" },
{ "name": "card_badge_error_text","value": "#D81E06" },
{ "name": "card_badge_warn_bg", "value": "#FFF8E1" },
{ "name": "card_badge_warn_text", "value": "#EA9518" },
{ "name": "card_divider", "value": "#EEEEEE" },
{ "name": "card_brand", "value": "#BBBBBB" }
]
}
深色 Token 覆盖(仅列出需要变化的项):
// entry/src/main/resources/dark/element/color.json(深色 — 摘录)
// 仅覆盖需要变化的颜色,其余沿用 base/ 的值
{
"color": [
{ "name": "card_bg", "value": "#1E1E1E" },
{ "name": "card_text_primary", "value": "#E5E5E5" },
{ "name": "card_text_secondary", "value": "#CCCCCC" },
{ "name": "card_text_hint", "value": "#888888" },
{ "name": "card_accent", "value": "#FF8C55" },
{ "name": "card_badge_error_bg", "value": "#3D1818" },
{ "name": "card_badge_error_text","value": "#FF6B6B" },
{ "name": "card_badge_warn_bg", "value": "#3D3018" },
{ "name": "card_badge_warn_text", "value": "#FFC107" },
{ "name": "card_divider", "value": "#333333" },
{ "name": "card_brand", "value": "#5A5A5A" }
]
}
这种 Token 化设计的优势在于:当需要进行配色调整时,我们只需修改 color.json 文件,所有引用该 Token 的组件自动生效,无需逐个页面查找修改。
7.2 启动时读取用户偏好
喵屿在 EntryAbility.onCreate() 中,从 Preferences 读取用户上次选择的模式(KEY_SETTING_LIGHT_STATUS),并在页面加载前应用该模式,避免启动后二次切换造成的闪烁:
// entry/src/main/ets/entryability/EntryAbility.ets(摘录)
// 系统 API:setColorMode() — API 9+,ConfigurationConstant.ColorMode
import { ConfigurationConstant } from '@kit.AbilityKit';
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
// 初始化 Preferences 上下文
PreferenceUtil.initContext(this.context.getApplicationContext());
try {
// 从本地持久化读取用户偏好模式:-1=自动, 0=深色, 1=浅色
const lightStatus = PreferenceUtil.getPreferenceByNameSync(
BaseConstants.DEFAULT_NAME, // "catIsland"
BaseConstants.KEY_SETTING_LIGHT_STATUS, // "lightStatus"
1 // 默认浅色
) as number;
// 在页面加载前设置颜色模式,避免启动后二次切换
this.context.getApplicationContext().setColorMode(lightStatus);
} catch (err) {
hilog.error(DOMAIN, 'testTag', 'Failed to set colorMode. Cause: %{public}s',
JSON.stringify(err));
}
}
7.3 设置页模式切换
喵屿的设置页右上角工具栏提供了模式切换入口,使用 bindContextMenu 弹出三选一菜单。菜单项分别对应“开灯”(浅色)、“关灯”(深色)和“自动”(跟随系统)。
// entry/src/main/ets/pages/Settings.ets(摘录)
// 系统 API:ConfigurationConstant.ColorMode — API 9+
import { ConfigurationConstant } from '@kit.AbilityKit';
@Component
export struct Settings {
/*
* lightStatus 取值:
* -1 = 自动(跟随系统)
* 0 = 关灯(强制深色)
* 1 = 开灯(强制浅色)
*/
@State lightStatus: number = 1;
aboutToAppear(): void {
// 页面出现时从 Preferences 读取当前模式
this.lightStatus = PreferenceUtil.getPreferenceByNameSync(
BaseConstants.DEFAULT_NAME, BaseConstants.KEY_SETTING_LIGHT_STATUS, 1
) as number;
}
build() {
// ...标题栏中显示当前模式图标...
Row({ space: 12 }) {
// 根据当前模式显示不同图标:自动 / 关灯 / 开灯
Image(this.lightStatus == -1 ? $r('[resource].media.auto')
: (this.lightStatus == 0 ? $r('[resource].media.no_light')
: $r('[resource].media.light')))
.size({ width: 30, height: 30 })
.clickEffect({ level: ClickEffectLevel.HEAVY, scale: 0.9 })
.bindContextMenu(this.isShowLight, this.lightMenu, {
enableArrow: true,
arrowOffset: 15,
placement: Placement.LeftTop,
backgroundBlurStyle: BlurStyle.Thick,
onDisappear: () => { this.isShowLight = false }
})
.onClick(() => {
VibrateUtil.buttonVibrate(VibrateEffect.soft)
this.isShowLight = true
})
}
}
/**
* 模式切换菜单:开灯(浅色)/ 关灯(深色)/ 自动(跟随系统)
* 每次选择后持久化到 Preferences,下次启动时自动应用
*/
@Builder lightMenu() {
Menu() {
// 选项一:开灯 — 强制浅色模式
MenuItem({
startIcon: $r("[resource].media.light"),
content: "开灯"
})
.clickEffect({ level: ClickEffectLevel.HEAVY, scale: 0.9 })
.onClick(() => {
VibrateUtil.buttonVibrate(VibrateEffect.soft)
// 通过 ApplicationContext 设置全局颜色模式
this.getUIContext()
.getHostContext()?.getApplicationContext()
.setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT);
// 持久化用户选择
this.lightStatus = 1
PreferenceUtil.putPreferenceByName(
BaseConstants.DEFAULT_NAME, BaseConstants.KEY_SETTING_LIGHT_STATUS,
this.lightStatus
)
})
// 选项二:关灯 — 强制深色模式
MenuItem({
startIcon: $r("[resource].media.no_light"),
content: "关灯"
})
.clickEffect({ level: ClickEffectLevel.HEAVY, scale: 0.9 })
.onClick(() => {
VibrateUtil.buttonVibrate(VibrateEffect.soft)
this.lightStatus = 0
this.getUIContext()
.getHostContext()?.getApplicationContext()
.setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_DARK);
PreferenceUtil.putPreferenceByName(
BaseConstants.DEFAULT_NAME, BaseConstants.KEY_SETTING_LIGHT_STATUS,
this.lightStatus
)
})
// 选项三:自动 — 跟随系统
MenuItem({
startIcon: $r("[resource].media.auto"),
content: "自动"
})
.clickEffect({ level: ClickEffectLevel.HEAVY, scale: 0.9 })
.onClick(() => {
VibrateUtil.buttonVibrate(VibrateEffect.soft)
this.lightStatus = -1
this.getUIContext()
.getHostContext()?.getApplicationContext()
.setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET);
PreferenceUtil.putPreferenceByName(
BaseConstants.DEFAULT_NAME, BaseConstants.KEY_SETTING_LIGHT_STATUS,
this.lightStatus
)
})
}
}
}
交互细节:喵屿通过 onClick 短按弹出弹窗,提升了易用性。用户选择后,setColorMode() 立即生效,同时将选择持久化,确保下次启动应用时恢复相同的模式。
7.4 服务卡片深色适配
喵屿包含三张服务卡片(catInfo、petInfo、petTodo),卡片进程独立于主应用,因此其深色模式需要单独配置。我们在 form_config.json 中为每张卡片设置 "colorMode": "auto",使得卡片能够自动跟随系统深色模式,而不受主应用内部切换的影响。
卡片页面中的所有颜色同样通过 $r('app.color.card_xxx') 引用,系统在卡片渲染时根据当前系统模式自动选择深浅色值。由于卡片资源与主应用共享同一套 dark/ 目录,因此颜色 Token 的定义保持一致,无需为卡片单独维护一套配色。
这种设计保证了主应用和卡片在深色模式下的视觉一致性,同时卡片代码中没有任何模式判断逻辑,保持了代码的简洁性。
八、总结
HarmonyOS 深色模式适配的核心思路是关注点分离:将颜色定义从业务逻辑中抽离到资源文件,借助系统资源限定词机制自动切换。这种设计不仅降低了适配成本,还提升了代码的可维护性和可测试性。
适配路径推荐(按优先级排序):
- 基础适配 — 创建
dark/element/color.json,定义语义化 Token,替换所有硬编码色值为$r()引用。 - 跟随系统 —
EntryAbility.onCreate()中调用setColorMode(COLOR_MODE_NOT_SET),让应用自动响应系统变化。 - 用户控制 — 可选提供应用内深色/浅色/跟随系统三选一开关,并持久化用户选择。
- 图片与 Web 适配 — 深色图片放
dark/media/,Web 组件配置darkMode属性并配合前端prefers-color-scheme。 - 反色增强(API 21+) — 对品牌色、图片等敏感区域使用
allowForceDark(false)保护,其他区域可开启反色作为快速兜底。
核心原则:
- 所有颜色使用
$r('app.color.xxx')引用,禁止硬编码。 dark/目录只需覆盖需要变化的颜色,未覆盖的自动回退到base/。- 语义化命名优于色值描述,便于长期维护。
- 启动时尽早设置颜色模式,避免闪烁。
- 注意 API 版本兼容(
allowForceDark需 API 21+),低版本设备需完全依赖资源限定词。
通过上述方法和实践,你的 HarmonyOS 应用将能够以较低的成本、较高的质量完成深色模式适配,为用户带来一致且舒适的视觉体验。
参考来源:
更多推荐




所有评论(0)