news 2026/7/28 3:10:05

HarmonyOS应用开发实战:猫猫大作战-资源目录分流(`base/` vs `dark/`)、colorMode 编程式切换、暗色配色原则三大要点讲

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
HarmonyOS应用开发实战:猫猫大作战-资源目录分流(`base/` vs `dark/`)、colorMode 编程式切换、暗色配色原则三大要点讲

前言

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_bgrgba(255,255,255,0.7)rgba(40,40,60,0.7)卡片背景
text_primary#2C3E50#ECF0F1主文字
text_secondary#7F8C8D#BDC3C7次要文字

3.2 暗色配色原则

  1. 背景深、文字浅——深色模式背景用#1A1A2E等深色,文字用#ECF0F1等浅色。
  2. 降低饱和度——亮色的#2ECC71(鲜绿)在深色模式下刺眼,改#27AE60(暗绿)。
  3. 对比度 ≥ 4.5:1——深底浅字也要保证可读性。
  4. 避免纯黑纯白——#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 })

七、调试技巧

  1. DevEco 预览器切换暗色:预览器右上角有「亮色/深色」切换按钮,实时看效果。
  2. 真机系统设置:设置 → 显示与亮度 → 深色模式 → 开启,验证应用适配。
  3. console.info打 colorModeonConfigurationUpdate里 log,追系统切换。
  4. 截图对比:亮色和深色各截一张,对比配色是否协调。

八、性能与最佳实践

  1. 暗色适配 = 资源目录分流 + $r 引用——业务代码零改动。
  2. dark/只放需要覆盖的资源——避免整个目录复制。
  3. 所有颜色统一用 $r——硬编码颜色不跟随暗色。
  4. 降低深色模式饱和度——鲜色在深底刺眼,改暗色版本。
  5. 避免纯黑纯白——用#1A1A2E#ECF0F1替代#000#FFF
  6. 提供三选项——跟随系统 / 强制亮色 / 强制深色。

总结

本篇我们从暗色模式适配切入,掌握了资源目录分流(base/vsdark/color.json 结构与同名覆盖colorMode 编程式切换与监听暗色配色原则四大要点,并给出了主菜单暗色适配完整代码。核心要点:暗色适配零代码改动,全靠 $r + 资源目录分流;深色降饱和度避免刺眼;提供跟随/强制三选项

下一篇我们将拆解多语言 i18n——string.json 多语言切换。

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!


相关资源:

  • 「猫猫大作战」项目源码:本仓库entry/src/main/ets/pages/Index.ets
  • HarmonyOS 资源管理与暗色模式官方指南
  • colorMode 编程式切换官方文档
  • i18n 国际化与暗色适配最佳实践
  • 开源鸿蒙跨平台社区
  • HarmonyOS 开发者官方文档首页
  • 系列索引:本仓库articles/INDEX.md
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/28 3:09:38

GitHub Actions 自动化运维实战:从入门到企业级落地

1. 引言:为什么要用 GitHub Actions 做自动化运维 在软件工程领域,自动化早已不是“锦上添花”,而是团队效率的生命线。从最早的 Cron 定时脚本,到 Jenkins 称霸 CI 服务器市场,再到如今云原生时代 CI/CD 与运维工具的…

作者头像 李华
网站建设 2026/7/28 3:09:21

ncmdump:网易云音乐NCM文件转换终极指南 - 轻松解锁你的加密音乐

ncmdump:网易云音乐NCM文件转换终极指南 - 轻松解锁你的加密音乐 【免费下载链接】ncmdump ncmdump - 网易云音乐NCM转换 项目地址: https://gitcode.com/gh_mirrors/ncmdu/ncmdump 你是否曾经从网易云音乐下载了喜欢的歌曲,却发现它们被加密为NC…

作者头像 李华
网站建设 2026/7/28 3:08:42

深入解析.NET MAUI:跨平台开发的架构设计与性能优化

深入解析.NET MAUI:跨平台开发的架构设计与性能优化 【免费下载链接】maui .NET MAUI is the .NET Multi-platform App UI, a framework for building native device applications spanning mobile, tablet, and desktop. 项目地址: https://gitcode.com/GitHub_T…

作者头像 李华
网站建设 2026/7/28 3:05:59

怎样高效使用ChatTTS文字转语音工具:新手快速入门手册

怎样高效使用ChatTTS文字转语音工具:新手快速入门手册 【免费下载链接】ChatTTS-ui 一个简单的本地网页界面,使用ChatTTS将文字合成为语音,同时支持对外提供API接口。A simple native web interface that uses ChatTTS to synthesize text in…

作者头像 李华
网站建设 2026/7/28 3:05:11

PID控制从原理到实战:系统化调参方法与工程避坑指南

1. 项目概述:从“玄学”到“科学”的PID控制之旅如果你在自动化、机器人、无人机或者任何需要精确调节的领域摸爬滚打过,那么“PID”这三个字母对你来说,可能既熟悉又陌生。熟悉是因为它无处不在,从你家热水器的恒温控制&#xff…

作者头像 李华