前言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 / #AARRGGBB2.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_bgrgba(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 改造主菜单代码用 $rBuilder 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打 colorModeonConfigurationUpdate里 log追系统切换。截图对比亮色和深色各截一张对比配色是否协调。八、性能与最佳实践暗色适配 资源目录分流 $r 引用——业务代码零改动。dark/只放需要覆盖的资源——避免整个目录复制。所有颜色统一用 $r——硬编码颜色不跟随暗色。降低深色模式饱和度——鲜色在深底刺眼改暗色版本。避免纯黑纯白——用#1A1A2E和#ECF0F1替代#000和#FFF。提供三选项——跟随系统 / 强制亮色 / 强制深色。总结本篇我们从暗色模式适配切入掌握了资源目录分流base/vsdark/、color.json 结构与同名覆盖、colorMode 编程式切换与监听、暗色配色原则四大要点并给出了主菜单暗色适配完整代码。核心要点暗色适配零代码改动全靠 $r 资源目录分流深色降饱和度避免刺眼提供跟随/强制三选项。下一篇我们将拆解多语言 i18n——string.json 多语言切换。如果这篇文章对你有帮助欢迎点赞、收藏⭐、关注你的支持是我持续创作的动力相关资源「猫猫大作战」项目源码本仓库entry/src/main/ets/pages/Index.etsHarmonyOS 资源管理与暗色模式官方指南colorMode 编程式切换官方文档i18n 国际化与暗色适配最佳实践开源鸿蒙跨平台社区HarmonyOS 开发者官方文档首页系列索引本仓库articles/INDEX.md