前言
HarmonyOS 设备支持系统级深色模式——用户在「设置 → 显示与亮度」开启后,所有适配深色的应用都会切换成暗色主题。如果你的应用不适配,用户开深色模式后看到的是「白底刺眼」,体验直接崩。
本篇以「猫猫大作战」暗色模式适配为锚点,把资源目录分流(base/vsdark/)、colorMode 编程式切换、暗色配色原则三大要点讲透。
提示:本系列不讲 ArkTS 基础语法与环境搭建,假设你已跟完第 1–27 篇。本篇是布局进阶的第八篇。
一、场景拆解:暗色模式适配
回顾「猫猫大作战」主菜单配色(第 6 篇):
// 来源:entry/src/main/ets/pages/Index.ets MainMenuView() Text('猫猫大作战') .fontSize(36) .fontWeight(FontWeight.Bold) .fontColor('#2C3E50') // ← 深色文字,亮色模式下 OK痛点:#2C3E50是深蓝灰,亮色模式下白底深字,视觉 OK。但用户切到系统深色模式后,这个颜色不变——深字配深底,看不见。
HarmonyOS 暗色适配方案:
resources/ ├─ base/element/color.json # 亮色模式颜色 │ { "color": [{ "name": "title_color", "value": "#2C3E50" }] } └─ dark/element/color.json # 深色模式颜色(同名覆盖) { "color": [{ "name": "title_color", "value": "#ECF0F1" }] }// 代码不变,$r 自动选对应模式的颜色 Text('猫猫大作战').fontColor($r('app.color.title_color')) // 亮色模式 → #2C3E50(深字) // 深色模式 → #ECF0F1(浅字)关键经验:暗色适配 = 资源目录分流 + 代码用 $r 引用——业务代码零改动。
二、资源目录分流机制
2.1 base/ 与 dark/ 的关系
resources/ ├─ base/ # 默认资源(所有设备/模式匹配) │ ├─ element/color.json │ ├─ element/string.json │ ├─ media/cat_logo.png │ └─ media/bg_main.jpg └─ dark/ # 深色模式覆盖(只放需要覆盖的) ├─ element/color.json # 覆盖颜色 ├─ media/cat_logo.png # 覆盖图标 └─ media/bg_main.jpg # 覆盖背景规则:
| 模式 | 加载顺序 |
|---|---|
| 亮色模式 | 直接用base/ |
| 深色模式 | 先找dark/,没有再回退base/ |
实战经验:dark/只放需要覆盖的资源——颜色必覆盖,图标按需覆盖,没必要整个目录复制一份。
2.2 color.json 结构
{ "color": [ { "name": "title_color", "value": "#2C3E50" }, { "name": "subtitle_color", "value": "#95A5A6" }, { "name": "bg_color", "value": "#E8F4F8" }, { "name": "primary_button", "value": "#2ECC71" } ] }字段说明:
| 字段 | 含义 |
|---|---|
name | 资源名(蛇形) |
value | 颜色值(#RGB / #RRGGBB / #AARRGGBB) |
2.3 dark/element/color.json 覆盖
{ "color": [ { "name": "title_color", "value": "#ECF0F1" }, { "name": "subtitle_color", "value": "#7F8C8D" }, { "name": "bg_color", "value": "#1A1A2E" }, { "name": "primary_button", "value": "#27AE60" } ] }关键经验:dark/的 color.json 只放需要改的色——名字必须与base/一致,否则编译报错。
三、猫猫大作战暗色配色方案
3.1 亮色 vs 深色对比表
| 资源名 | 亮色值 | 深色值 | 用途 |
|---|---|---|---|
title_color | #2C3E50 | #ECF0F1 | 主标题 |
subtitle_color | #95A5A6 | #95A5A6 | 副标题(不变) |
bg_color | #E8F4F8 | #1A1A2E | 背景 |
primary_button | #2ECC71 | #27AE60 | 主按钮 |
card_bg | rgba(255,255,255,0.7) | rgba(40,40,60,0.7) | 卡片背景 |
text_primary | #2C3E50 | #ECF0F1 | 主文字 |
text_secondary | #7F8C8D | #BDC3C7 | 次要文字 |
3.2 暗色配色原则
- 背景深、文字浅——深色模式背景用
#1A1A2E等深色,文字用#ECF0F1等浅色。 - 降低饱和度——亮色的
#2ECC71(鲜绿)在深色模式下刺眼,改#27AE60(暗绿)。 - 对比度 ≥ 4.5:1——深底浅字也要保证可读性。
- 避免纯黑纯白——
#000000和#FFFFFF过于刺眼,用#1A1A2E和#ECF0F1。
四、改造主菜单支持暗色
4.1 创建颜色资源
resources/base/element/color.json(亮色):
{ "color": [ { "name": "title_color", "value": "#2C3E50" }, { "name": "subtitle_color", "value": "#95A5A6" }, { "name": "text_primary", "value": "#2C3E50" }, { "name": "text_secondary", "value": "#7F8C8D" }, { "name": "bg_gradient_start", "value": "#E8F4F8" }, { "name": "bg_gradient_mid", "value": "#D6EEF5" }, { "name": "bg_gradient_end", "value": "#C9E8F2" }, { "name": "primary_button", "value": "#2ECC71" }, { "name": "card_bg", "value": "#FFFFFF" } ] }resources/dark/element/color.json(深色):
{ "color": [ { "name": "title_color", "value": "#ECF0F1" }, { "name": "subtitle_color", "value": "#95A5A6" }, { "name": "text_primary", "value": "#ECF0F1" }, { "name": "text_secondary", "value": "#BDC3C7" }, { "name": "bg_gradient_start", "value": "#1A1A2E" }, { "name": "bg_gradient_mid", "value": "#16213E" }, { "name": "bg_gradient_end", "value": "#0F3460" }, { "name": "primary_button", "value": "#27AE60" }, { "name": "card_bg", "value": "#2C2C3E" } ] }4.2 改造主菜单代码用 $r
@Builder MainMenuView() { Column() { Spacer().height('15%') // 标题区 Text('🐱').fontSize(72).margin({ bottom: 8 }) Text('猫猫大作战') .fontSize(36) .fontWeight(FontWeight.Bold) .fontColor($r('app.color.title_color')) // ← $r 自动适配 .margin({ bottom: 8 }) Text('合并进化 · 策略消除') .fontSize(16) .fontColor($r('app.color.subtitle_color')) // ← $r 自动适配 .margin({ bottom: 48 }) // 最高分 if (this.highScore > 0) { Row() { Text('🏆 最高分: ').fontSize(16).fontColor('#F1C40F') Text(this.highScore.toString()).fontSize(20).fontWeight(FontWeight.Bold).fontColor('#F1C40F') }.margin({ bottom: 32 }) } // 开始游戏按钮 Button('开始游戏') .width('70%').height(56) .fontSize(20).fontWeight(FontWeight.Bold) .fontColor('#FFFFFF') .backgroundColor($r('app.color.primary_button')) // ← $r 自动适配 .borderRadius(28) .shadow({ radius: 8, color: 'rgba(46, 204, 113, 0.4)', offsetY: 4 }) .onClick(() => { this.startGame(); }) Spacer().height(24) // 规则面板 Scroll() { Column() { Text('游戏规则') .fontSize(14).fontWeight(FontWeight.Bold) .fontColor($r('app.color.text_primary')) Text('• 点击列投放猫咪') .fontSize(13).fontColor($r('app.color.text_secondary')) /* ... 其他规则 */ } .alignItems(HorizontalAlign.Start) } .scrollable(ScrollDirection.Vertical) .scrollBar(BarState.Auto) .width('80%').height(180).padding(16) .backgroundColor($r('app.color.card_bg')) // ← 卡片背景自动适配 .borderRadius(12) Spacer() } .width('100%').height('100%') .linearGradient({ direction: GradientDirection.Bottom, colors: [ [$r('app.color.bg_gradient_start'), 0.0], // ← 渐变也用 $r [$r('app.color.bg_gradient_mid'), 0.5], [$r('app.color.bg_gradient_end'), 1.0] ] }) .alignItems(HorizontalAlign.Center) }改造要点:
| 原写法 | 改造后 | 效果 |
|---|---|---|
.fontColor('#2C3E50') | .fontColor($r('app.color.title_color')) | 亮深自动切 |
.backgroundColor('#2ECC71') | .backgroundColor($r('app.color.primary_button')) | 亮深自动切 |
.linearGradient({ colors: [['#E8F4F8', 0.0], ...] }) | .linearGradient({ colors: [[$r('app.color.bg_gradient_start'), 0.0], ...] }) | 渐变也适配 |
五、colorMode 编程式切换
5.1 跟随系统(默认)
// 不做任何设置,自动跟随系统暗色模式5.2 强制亮色 / 深色
import { ConfigurationConstant } from '@kit.AbilityKit'; // 强制深色模式 this.getUIContext().getHostContext()?.getApplicationContext() .setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_DARK); // 强制亮色模式 this.getUIContext().getHostContext()?.getApplicationContext() .setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT); // 恢复跟随系统 this.getUIContext().getHostContext()?.getApplicationContext() .setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET);实战经验:游戏类应用建议提供「跟随系统 / 强制亮色 / 强制深色」三选项——玩家在强光下可能想强制深色省电。
5.3 监听系统暗色模式变化
import { Configuration, ConfigurationConstant } from '@kit.AbilityKit'; export default class EntryAbility extends UIAbility { onCreate(): void { // 初始存储当前 colorMode AppStorage.setOrCreate('currentColorMode', this.context.config.colorMode); } onConfigurationUpdate(newConfig: Configuration): void { // 系统暗色模式切换时触发 AppStorage.setOrCreate('currentColorMode', newConfig.colorMode); } }组件内响应:
@StorageProp('currentColorMode') @Watch('onColorModeChange') currentColorMode: number = ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET; onColorModeChange(): void { const isDark = this.currentColorMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK; // 更新状态栏色、自定义逻辑等 }六、踩坑提示
6.1 dark/ 与 base/ 资源名不一致
// base/element/color.json { "name": "title_color", "value": "#2C3E50" } // dark/element/color.json(拼错) { "name": "title_Color", "value": "#ECF0F1" }后果:编译报错或 dark 覆盖失败。dark/ 的资源名必须与 base/ 完全一致。
6.2 硬编码颜色未替换
// ❌ 错误:部分颜色还是硬编码 Text('A').fontColor('#2C3E50') // 深色模式下看不见 Text('B').fontColor($r('app.color.title_color')) // 已适配 // ✅ 正确:所有颜色统一用 $r Text('A').fontColor($r('app.color.title_color')) Text('B').fontColor($r('app.color.title_color'))实战经验:全局搜索#开头的硬编码颜色,逐一替换为$r。
6.3 linearGradient 的 colors 用 $r
// ❌ 错误:渐变色硬编码 .linearGradient({ colors: [['#E8F4F8', 0.0], ['#D6EEF5', 0.5], ['#C9E8F2', 1.0]] }) // ✅ 正确:渐变色也用 $r .linearGradient({ colors: [ [$r('app.color.bg_gradient_start'), 0.0], [$r('app.color.bg_gradient_mid'), 0.5], [$r('app.color.bg_gradient_end'), 1.0] ] })6.4 shadow 颜色不适配
// shadow 颜色也要适配(深色模式阴影要更深) .shadow({ radius: 8, color: $r('app.color.shadow_color'), offsetY: 4 })七、调试技巧
- DevEco 预览器切换暗色:预览器右上角有「亮色/深色」切换按钮,实时看效果。
- 真机系统设置:设置 → 显示与亮度 → 深色模式 → 开启,验证应用适配。
console.info打 colorMode:onConfigurationUpdate里 log,追系统切换。- 截图对比:亮色和深色各截一张,对比配色是否协调。
八、性能与最佳实践
- 暗色适配 = 资源目录分流 + $r 引用——业务代码零改动。
dark/只放需要覆盖的资源——避免整个目录复制。- 所有颜色统一用 $r——硬编码颜色不跟随暗色。
- 降低深色模式饱和度——鲜色在深底刺眼,改暗色版本。
- 避免纯黑纯白——用
#1A1A2E和#ECF0F1替代#000和#FFF。 - 提供三选项——跟随系统 / 强制亮色 / 强制深色。
总结
本篇我们从暗色模式适配切入,掌握了资源目录分流(base/vsdark/)、color.json 结构与同名覆盖、colorMode 编程式切换与监听、暗色配色原则四大要点,并给出了主菜单暗色适配完整代码。核心要点:暗色适配零代码改动,全靠 $r + 资源目录分流;深色降饱和度避免刺眼;提供跟随/强制三选项。
下一篇我们将拆解多语言 i18n——string.json 多语言切换。
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
相关资源:
- 「猫猫大作战」项目源码:本仓库
entry/src/main/ets/pages/Index.ets - HarmonyOS 资源管理与暗色模式官方指南
- colorMode 编程式切换官方文档
- i18n 国际化与暗色适配最佳实践
- 开源鸿蒙跨平台社区
- HarmonyOS 开发者官方文档首页
- 系列索引:本仓库
articles/INDEX.md