为什么你的 App 到了夜里还会“闪瞎眼”?—鸿蒙深色模式与主题适配,一把梭!
本文介绍了鸿蒙(HarmonyOS)App开发中深色模式与主题适配的完整方案。主要内容包括:1)通过资源限定目录实现深浅色自动切换的基础配置;2)系统Token与自定义Theme的协作关系;3)使用WithTheme组件实现局部主题反转;4)动态颜色管理实现品牌主题切换。文章强调从主题定义到动态策略的全流程设计,既保证视觉一致性又提高代码可维护性,帮助开发者解决深色适配中常见的弹窗、局部浮层等UI
我是兰瓶Coding,一枚刚踏入鸿蒙领域的转型小白,原是移动开发中级,如下是我学习笔记《零基础学鸿蒙》,若对你所有帮助,还请不吝啬的给个大大的赞~
前言
先问个扎心的:你家 App 开了深色模式,结果弹窗还是白花花一片,按钮像夜空里的小太阳,用户一开就“哎哟我去,这谁顶得住”?是不是有点似曾相识🤦♂️。别急,今天咱就把鸿蒙(HarmonyOS)里深色模式与主题适配这一整套,从“主题定义 → 自动切换机制 → 动态颜色管理”三段论,给它彻底盘清楚。
我会一边讲思路一边上代码,围绕三个核心技术点——ThemeScope(主题作用域思维 / WithTheme 实现)、systemColorMode(系统深浅色)、@Style(准确说是 @Styles)——帮你把“视觉一致性 + 代码可维护性 + 可扩展性”同时拿下。准备好了么?咱们开整!
写在前面:这活为啥总被做“糙”了?
深色适配,很多团队要么“全局浅色→深色”一把梭,要么拿几种颜色资源糊一糊,就算完工。问题是——弹窗、局部浮层、混合容器(例如 Web、Unity 嵌套)经常要局部反转;品牌色还得在深色里“降饱和、提对比”;系统切换深/浅时,UI 要无感刷新;再加上组件库升级、主题 Token 变更,一多环境一多页面,分分钟炸裂。
所以我们不只讲“怎么改色”,更讲从主题定义、作用域管理到动态策略的完整打法。目标就一个:用户夜里看你 App,眼睛舒服,风格稳如老狗;你改色、换肤、上品牌活动,也能稳准狠。
主题定义:从系统 Token 到自定义 Theme
1)资源组织:先把地基打平
鸿蒙 ArkUI 原生支持深/浅两套资源限定目录。最少你要有:
resources/
base/element/color.json // 通用(浅色)默认
dark/element/color.json // 深色覆盖
dark/element/color.json 中放“同名”颜色键的深色取值,系统会在深色模式下自动命中。这是一切自动化的基础。官方“应用深浅色适配”文档明确说明了目录与命中规则;卡片/页面适配也一致。(华为开发者官网)
示例(dark/element/color.json):
{
"color": [
{ "name": "brand_primary", "value": "#66B2FF" },
{ "name": "surface_bg", "value": "#121212" },
{ "name": "text_primary", "value": "#FFFFFF" }
]
}
小贴士:尽量用系统 Token(如
$r('sys.color.background_primary'))兜底,再用自定义颜色扩展品牌感。系统 Token 天然适配深浅色,还能在主题升级时少背锅。(seaxiang.com)
2)系统 Token 与自定义 Token 的协作
系统给了一整套颜色 Token(背景、文本、图标、强调、分隔线等),你可以直接在组件里用:
Text('系统文本色')
.fontColor($r('sys.color.font_primary'))
.backgroundColor($r('sys.color.background_primary'))
当你需要品牌色、活动色时,再引入自定义 Token(brand_primary 等),在深色目录做差异化。这样既稳,又能玩出花来。(华为开发者官网)
3)应用级主题 vs. 页面/局部主题
- 应用级主题:用
ThemeControl.setDefaultTheme(customTheme)一次性设默认主题,所有 ArkUI 组件风格跟随。适合品牌色换肤、全局风格统一化。(华为开发者官网) - 页面/局部主题:当你需要“局部反转”(比如浅色页面里的一个深色弹窗),就该上主题作用域(Theme Scope)了。在 ArkUI 里,这个能力由
WithTheme组件提供(它就是你脑子里“ThemeScope”的实锤实现)。(Gitee)
自动切换机制:跟随系统与手动切换
1)systemColorMode:侦测系统深/浅
ArkUI 提供了内置环境变量 ColorMode(值为 LIGHT/DARK),用于表示系统当前深浅模式。你可以把它理解为“systemColorMode”。它是个环境变量,用于判定系统层面的深/浅色态。(华为开发者官网)
说明:官方称之为
ColorMode内置环境变量;在业务里大家常把它口头叫“systemColorMode”。读取它的常规实践是:跟随系统使用资源限定/系统 Token自动适配;若需显示切换或局部反转,再配合WithTheme的colorMode参数。
2)全局跟随:系统资源 + 默认 Theme
第一层:资源限定 + 系统 Token
第二层:全局 Theme
初始化阶段设置一个“默认主题”(可只改品牌色,其他跟系统 Token):
// app_theme.ets
import { CustomTheme, CustomColors, ThemeControl } from '@kit.ArkUI'
class BrandColors implements CustomColors {
// 只改品牌相关,其他全部继承系统 Token
brand = '#3D7DFF'
fontEmphasize = '#3D7DFF'
}
class AppTheme implements CustomTheme {
colors: CustomColors = new BrandColors()
}
// 入口处,设置默认主题
ThemeControl.setDefaultTheme(new AppTheme())
这样,你的 App 就在系统深/浅色切换时,由系统 Token 自动适配,而品牌色也能全局一致。更详细的
Theme/CustomTheme能力见官方文档。(华为开发者官网)
3)局部反转(ThemeScope 思维 → WithTheme 实现)
很多人嘴上说“ThemeScope”,实际 ArkUI 的落地就是**WithTheme:你把一段组件树包起来,参数里指定colorMode(LIGHT / DARK / SYSTEM)或者塞一个自定义 theme**,子树就按你说的来。(Gitee)
// 局部反转:浅色页面里的深色卡片
WithTheme({ colorMode: ThemeColorMode.DARK }) {
Column() {
Text('夜里我更能打')
.fontSize(18)
.fontColor($r('sys.color.font_primary'))
}
.padding(16)
.backgroundColor($r('sys.color.background_primary'))
}
关键点:要让局部深色真正生效,工程里必须提供
dark/element/color.json,否则没有对应深色资源可用。官方文档明确写了需要提供dark.json(即dark/element/color.json)方可正确渲染。(Gitee)
动态颜色管理:品牌色、可访问性与状态反馈
1)注入 CustomTheme / CustomColors,一键换肤
假设你要做“品牌主题三连”:系统默认 / 绿色主题 / 红色主题——WithTheme 接 theme,可热切换:
import { CustomTheme, CustomColors } from '@kit.ArkUI'
class GreenColors implements CustomColors {
fontPrimary = '#00A76F'
backgroundEmphasize = '#103B2F'
compEmphasizeSecondary = '#1B5E20AA' // 半透明
}
class RedColors implements CustomColors {
fontPrimary = '#D32F2F'
backgroundEmphasize = '#3F0B0B'
compEmphasizeSecondary = '#B71C1CAA'
}
class PageTheme implements CustomTheme {
constructor(public colors?: CustomColors) {}
}
@Entry
@Component
struct ThemePlayground {
private themes: (CustomTheme | undefined)[] = [
undefined, // System(跟随系统 Token)
new PageTheme(new GreenColors()), // 绿色
new PageTheme(new RedColors()) // 红色
]
@State idx: number = 0
build() {
Column({ space: 12 }) {
Button(`Switch Theme: ${this.idx}`)
.onClick(() => this.idx = (this.idx + 1) % this.themes.length)
// 未包裹:系统默认
Button('System Theme Button')
.buttonStyle(ButtonStyleMode.EMPHASIZED)
// 主题作用域:使用自定义配色
WithTheme({ theme: this.themes[this.idx] }) {
Column({ space: 12 }) {
Button('Scoped Button A').buttonStyle(ButtonStyleMode.NORMAL)
Button('Scoped Button B').buttonStyle(ButtonStyleMode.EMPHASIZED)
}
}
}
.padding(20)
}
}
以上模式与官方示例一致:
WithTheme({ theme })让作用域内组件使用你给的配色集,未包裹部分保持系统/全局默认。(Gitee)
2)用 @Styles(不是 @Style)把样式复用拉满
很多同学会写成 @Style,其实 ArkUI 正式文档叫 @Styles。它把一串通用样式封装成“样式函数”,在声明位置直接点出来用,支持组件内与全局两种声明方式。(华为开发者官网)
全局 + 组件内示例:
// 全局 styles(在文件顶层)
@Styles function btnBase() {
.padding(12)
.borderRadius(12)
.fontSize(16)
}
// 页面内 styles(优先级更高)
@Entry
@Component
struct StylesDemo {
@State danger: boolean = false
@Styles dangerTone() {
.backgroundColor(this.danger ? '#FF3B30' : $r('sys.color.background_emphasize'))
.fontColor($r('sys.color.font_on_primary'))
}
build() {
Column({ space: 8 }) {
Text('样式复用就这么简单~').fontSize(18)
Button('普通按钮').btnBase()
Button('危险操作').btnBase().dangerTone()
.onClick(() => this.danger = !this.danger)
}
.padding(20)
}
}
@Styles只支持通用属性/事件,组件内声明的优先级高于全局;导出复用推荐用AttributeModifier。官方指南写得很清楚,别再写成@Style啦。(华为开发者官网)
3)覆盖优先级与“可撤销”策略
- 优先级(从高到低):内联样式 > 组件内
@Styles> 全局@Styles> WithTheme 的 theme > 全局默认 Theme > 系统 Token。 - 可撤销:把品牌主题切换做成纯函数式(即不写死到资源文件,而是通过
CustomTheme注入),可在运行时恢复到undefined(回到系统 Token/全局 Theme)——这就是“可撤销”。
实战案例:一个页面,三种主题策略,一键切换
下面这段把三层能力打包:
(1)全局默认 Theme(只改品牌色)。
(2)自动深浅(靠系统 Token + 资源限定)。
(3)局部反转/自定义作用域主题(WithTheme)。
0)准备资源(务必有 dark/element/color.json)
// base/element/color.json
{
"color": [
{ "name": "brand_primary", "value": "#3D7DFF" },
{ "name": "surface_bg", "value": "#FFFFFF" },
{ "name": "text_primary", "value": "#000000" }
]
}
// dark/element/color.json
{
"color": [
{ "name": "brand_primary", "value": "#66B2FF" },
{ "name": "surface_bg", "value": "#121212" },
{ "name": "text_primary", "value": "#FFFFFF" }
]
}
这保证了在系统深/浅切换时,页面自动走正确的颜色。(华为开发者官网)
1)全局默认 Theme(品牌色)
// app_theme.ets
import { CustomTheme, CustomColors, ThemeControl } from '@kit.ArkUI'
class BrandOnly implements CustomColors {
brand = $r('app.color.brand_primary') // 用自定义 Token
}
class AppTheme implements CustomTheme {
colors: CustomColors = new BrandOnly()
}
export function applyAppTheme() {
ThemeControl.setDefaultTheme(new AppTheme())
}
在入口 Ability/页面 build 前调用:
// Entry
applyAppTheme()
官方
Theme/ThemeControl文档提供了onWillApplyTheme回调等能力,可在 Theme 切换时感知并更新状态。(华为开发者官网)
2)页面跟随系统(无需额外代码)
@Entry
@Component
struct HomePage {
build() {
Column() {
Text('跟随系统的标题')
.fontColor($r('sys.color.font_primary'))
// 背景跟随系统
Column() {
Text('这块就是跟随系统深浅色的区域')
}
.padding(16)
.backgroundColor($r('sys.color.background_primary'))
.borderRadius(12)
}
.padding(20)
.backgroundColor($r('app.color.surface_bg'))
}
}
核心点:你只要用系统 Token / 同名自定义 Token,系统切换深浅时自动命中。(seaxiang.com)
3)一个区域“反转深色”,另一个区域“挂自定义主题”
import { CustomTheme, CustomColors } from '@kit.ArkUI'
class CardGreen implements CustomColors {
compBackgroundTertiary = '#143F2E'
fontPrimary = '#A8E6CF'
}
class CardRed implements CustomColors {
compBackgroundTertiary = '#3F1616'
fontPrimary = '#FFCDD2'
}
class CardTheme implements CustomTheme {
constructor(public colors?: CustomColors) {}
}
@Entry
@Component
struct ScopeDemo {
@State themeIdx: number = 0
private scopeThemes: (CustomTheme | undefined)[] = [
undefined, // 跟随系统
new CardTheme(new CardGreen()),
new CardTheme(new CardRed())
]
@Styles cardBase() {
.padding(16)
.borderRadius(16)
.width('100%')
}
build() {
Column({ space: 16 }) {
Button(`切换局部主题:${this.themeIdx}`)
.onClick(() => this.themeIdx = (this.themeIdx + 1) % this.scopeThemes.length)
// 1) 局部“反转”为深色:即便外层是浅色
WithTheme({ colorMode: ThemeColorMode.DARK }) {
Column() {
Text('我是深色卡片(局部反转)')
.fontColor($r('sys.color.font_primary'))
}
.cardBase()
.backgroundColor($r('sys.color.background_primary'))
}
// 2) 局部绑定自定义主题(绿色 / 红色 / 系统)
WithTheme({ theme: this.scopeThemes[this.themeIdx] }) {
Column() {
Text('我是品牌卡片(自定义主题)')
.fontColor($r('sys.color.font_primary'))
}
.cardBase()
.backgroundColor($r('sys.color.comp_background_tertiary'))
}
}
.padding(20)
}
}
这段把 ThemeScope 思维完整呈现:
WithTheme作为主题作用域容器,既能强制深/浅(colorMode),也能注入自定义配色(theme)。官方WithTheme文档清晰列出了参数与示例。(Gitee)
工程化与踩坑锦囊
-
@Styles真名实姓
不要写成@Style,官方文档与编译器都认@Styles。它只支持通用属性/事件,组件内优先级更高,不支持export。想导出跨文件复用,请用AttributeModifier。(华为开发者官网) -
资源目录别乱
深色适配不是“我写了 if (dark) 就完事”,dark/element/color.json必须准备好,同名键才能命中深色值。局部深色(WithTheme)也要靠它吃饭。(Gitee) -
系统 Token 优先
能用系统 Token 就别自己造,减少维护成本;自定义 Token 主要为品牌差异化,其余靠系统兜底。(seaxiang.com) -
应用级主题与作用域主题的边界
- 全局一致性(品牌色、强调色统一)→
ThemeControl.setDefaultTheme。 - 局部反转 / 专题卡片 / 组合弹窗 →
WithTheme包围一小块组件树。(华为开发者官网)
- 全局一致性(品牌色、强调色统一)→
-
系统深浅“感知”→ 思维而非强依赖
你可以把ColorMode当做“systemColorMode”来理解:把它视作设计输入,不必每处都“显式读取并 if/else”。优先让资源限定 + Token自动工作,只有确实需要做逻辑分支或局部反转时,再动用WithTheme/自定义 Theme。官方环境变量文档明确存在ColorMode这一内置枚举。(华为开发者官网) -
可访问性(A11y)别忘
深色里要特别关注对比度:文字与背景至少 4.5:1,重要按钮 7:1 更稳;compEmphasizeSecondary这类“有透明度的强调色”在深色里不要太浅,否则漂。 -
跨容器(Web/Unity/Map)混合场景
外层 ArkUI 用WithTheme管边框/蒙层/标题,内层容器通过桥接同步“深/浅态 + 品牌色”。(如果你在 ArkUI-X + Unity 里玩过光照同步,那效果会非常丝滑。) -
性能
换肤如需全局刷新,尽量批量更新状态;作用域主题更建议局部包裹,减少重算重绘面。
参考要点(核心出处)
WithTheme(主题作用域实现):参数colorMode/theme、需要dark/element/color.json才能生效,官方示例与说明。(Gitee)Theme/ThemeControl/CustomTheme/CustomColors:全局默认主题设置与颜色 Token 列表。(华为开发者官网)ColorMode内置环境变量(systemColorMode 思维):系统当前深/浅色模式的官方定义。(华为开发者官网)- 应用深浅色适配指南(资源限定与自动切换):官方“应用深浅色适配”条目。(华为开发者官网)
@Styles装饰器:风格复用的官方名与使用方法。(华为开发者官网)
收尾:三句话复盘
- 默认靠系统,差异看品牌,局部用作用域:系统 Token + 资源限定确保“跟随系统”,
ThemeControl统一品牌,WithTheme做局部反转/专题卡片。 systemColorMode是输入,不是强依赖:优先让资源/Token自动跑,只在确有业务逻辑分支时再参与判断。- 样式复用用
@Styles:把通用样式抽出来,写得少、改得快,还更稳。
…
(未完待续)
更多推荐

所有评论(0)