一、前言:深色模式不是"事后补",是"地基"

先看一个真实的开发场景。

团队赶进度,所有页面都用硬编码颜色写了:

// 随处可见的硬编码
Text('标题').fontColor('#182431')
Column().backgroundColor('#FFFFFF')
Row().backgroundColor('#F6F8FA')

上线后产品说"我们要支持深色模式"。开发者打开真机切到 Dark Mode,发现:

  1. 白底白字#FFFFFF 背景在深色模式下还是白色,文字也是白色,完全看不见。

  2. 底部边缘发白:FloatingTabBar 的渐变蒙层 #66F1F3F5 在深色模式下把每个页面底部都"漂白"了。

  3. 渐变刺眼:浅色柔和渐变 #EEF4FF → #FFEDE2 在深色模式下像两个亮色斑块,非常刺眼。

  4. 状态栏文字看不见#182431 的状态栏内容颜色在深色背景下消失。

  5. 选中态文字低对比:选中项用了蓝色背景,文字也用蓝色,低到看不清。

于是团队花了两个星期,逐页排查、逐个替换颜色值。而且每加一个新页面,深色模式就要再补一次——永远在追赶。

这个故事的根因是:

深色模式不应该是"事后补",而应该是"地基"。如果主题基础设施在项目第一天就搭好,90% 的深色适配是自动的。

本文基于 ArkUILab E011 实验,讲清楚如何在鸿蒙里搭建 Theme Foundation,让深色模式成为框架默认能力。


二、鸿蒙主题能力地图:七层体系

E011 最核心的产出,是把鸿蒙的主题能力整理成一张七层地图。请先记住一句话:

鸿蒙的主题体系不是"一个深色开关",而是一个从系统令牌到动态主题的七层体系。

HarmonyOS Theme Capability Map

├── System Token(系统令牌)
│   └── $r('sys.color.*')
│       text / background / card / field / separator / state / primary
│       → 系统自动跟随深浅色,无需自己写 dark 资源
│
├── Resource Color(资源颜色)
│   └── resources/base/element/color.json  ← 浅色值
│   └── resources/dark/element/color.json  ← 深色值
│       → 自定义语义资源,系统按 colorMode 自动选择
│
├── Semantic Token(语义令牌)
│   └── ThemeTokens.ets
│       ThemePageBackground() / ThemeTextPrimary() / ...
│       → 项目级稳定命名,隔离"系统令牌选哪个"的决策
│
├── Component Token(组件令牌)
│   └── 未来:每个框架的专用令牌
│       → 同一语义出现至少两次后再提取
│
├── Dynamic Theme / WithTheme(动态主题)
│   └── WithTheme 支持应用内换肤
│       → v1 不用,先靠系统/资源令牌
│
├── Material(材质)
│   └── HDS blur/material APIs + ADAPTIVE 策略
│       → 未来 API 26 systemMaterial
│
└── Dark Mode(深色模式)
    └── 跟随系统 colorMode + 令牌/资源自动切换
        → 不需要自己监听模式切换

最关键的三层

  1. System Token —— $r('sys.color.*'),系统自动跟随深浅色,是你最该用的。

  2. Resource Color —— $r('app.color.*'),通过 base/dark 资源限定符自动切换。

  3. Semantic Token —— ThemeTokens.ets 里的 ThemePageBackground() 等函数,给你的项目一个稳定的命名层。


三、令牌分层策略:四层迁移链

E011 定义了一条从系统到业务的令牌迁移链:

System Token              系统令牌(首选)
  ↓
Project Semantic Token    项目语义令牌(ThemeTokens.ets)
  ↓
Component Token           框架/组件令牌(同一语义出现两次后提取)
  ↓
Business Color            业务/内容/演示颜色(保持本地,标注用途)

对应的七条规则

  1. 默认用系统令牌:文字、页面、卡片、组件、输入框、分割线、点击/悬停、主色都用 $r('sys.color...')

  2. 语义令牌用于框架稳定命名:当框架需要一个不依赖"具体选哪个系统令牌"的稳定名字时,用 ThemeTokens.ets

  3. 应用资源颜色用于需要 dark 分值的语义:只有当一个值需要不同的浅色/深色值,且没有系统令牌能表达这个语义时,才加 $r('app.color...')

  4. 组件令牌等重复出现后再提取:同一语义至少出现两次或成为框架 API 表面时,才提取组件令牌。

  5. 业务颜色保持本地:但要标注是"内容身份""数据可视化""渐变演示""蒙层实验"还是"官方 API 探针"。

  6. 不要因为效果在深色模式下失败就删除它:先问"它是否应该变成一个有 base/dark 分值的语义资源"。

  7. 填充主控件用专用按钮语义对:不要用通用 accent 令牌,用 ThemePrimaryButtonBackground() + ThemePrimaryButtonForeground()


四、系统令牌:深色模式的"免费午餐"

4.1 什么是系统令牌?

鸿蒙提供了一套系统级颜色令牌,通过 $r('sys.color.*') 引用。这些令牌自动跟随系统深浅色模式——你不需要写任何 dark 资源,系统会帮你切换。

E011 审计发现,ArkUILab 活跃代码中使用了 18 个系统颜色令牌

ohos_id_color_background          页面背景
ohos_id_color_sub_background      次级页面背景
ohos_id_color_list_card_bg        卡片背景
ohos_id_color_component_normal    组件常规填充
ohos_id_color_component_activated 组件激活态
ohos_id_color_text_primary        主要文字
ohos_id_color_text_secondary      次要文字
ohos_id_color_text_tertiary       第三级文字
ohos_id_color_text_field_bg       输入框背景
ohos_id_color_text_field_sub_bg   输入框次级背景
ohos_id_color_list_separator      列表分割线
ohos_id_color_subheading_separator 子标题分割线
ohos_id_color_click_effect        点击效果
ohos_id_color_hover               悬停效果
ohos_id_color_primary             主色
ohos_id_color_secondary           次色
ohos_id_color_tertiary            第三色
ohos_id_color_fourth              第四色

4.2 怎么用?

直接在代码里引用:

// 文字
Text('标题').fontColor($r('sys.color.ohos_id_color_text_primary'))
Text('说明').fontColor($r('sys.color.ohos_id_color_text_secondary'))

// 背景
Column().backgroundColor($r('sys.color.ohos_id_color_list_card_bg'))

// 分割线
Divider().color($r('sys.color.ohos_id_color_list_separator'))

这是深色模式最省心的方式——切到 Dark Mode 时,这些令牌自动变成深色版本的值,你什么都不用做。

4.3 审计数据

E011 迁移后扫描结果:

96 处 $r('sys.color...') 引用
18 个活跃系统令牌

最常用的类别:text_secondarylist_card_bglist_separatorprimarycomponent_normaltext_field_bg


五、语义令牌层:ThemeTokens.ets

5.1 为什么需要语义令牌?

系统令牌很好,但有一个问题:名字是系统级的,不是项目级的

如果你的代码里到处写 $r('sys.color.ohos_id_color_sub_background'),当某天你想把页面背景换一个系统令牌,要全局搜索替换。更糟的是,不同开发者可能对"页面背景"用了不同的系统令牌,导致不一致。

语义令牌层解决这个问题:给项目一个稳定的命名,隔离"选哪个系统令牌"的决策。

5.2 ThemeTokens.ets 的结构

// 页面背景
export function ThemePageBackground(): ResourceColor {
  return $r('sys.color.ohos_id_color_sub_background');
}

// 卡片/表面背景
export function ThemeSurfaceBackground(): ResourceColor {
  return $r('sys.color.ohos_id_color_list_card_bg');
}

// 组件常规填充
export function ThemeComponentBackground(): ResourceColor {
  return $r('sys.color.ohos_id_color_component_normal');
}

// 文字
export function ThemeTextPrimary(): ResourceColor {
  return $r('sys.color.ohos_id_color_text_primary');
}
export function ThemeTextSecondary(): ResourceColor {
  return $r('sys.color.ohos_id_color_text_secondary');
}
export function ThemeTextTertiary(): ResourceColor {
  return $r('sys.color.ohos_id_color_text_tertiary');
}

// 分割线
export function ThemeDividerColor(): ResourceColor {
  return $r('sys.color.ohos_id_color_list_separator');
}

// 输入框
export function ThemeTextFieldBackground(): ResourceColor {
  return $r('sys.color.ohos_id_color_text_field_bg');
}

5.3 完整语义令牌清单

E011 沉淀的语义令牌共 25+ 个,按用途分类:

类别

令牌

底层来源

页面

ThemePageBackground()

sys.color.sub_background

表面

ThemeSurfaceBackground()

sys.color.list_card_bg

组件

ThemeComponentBackground()

sys.color.component_normal

文字

ThemeTextPrimary/Secondary/Tertiary()

sys.color.text_*

强调

ThemeAccentColor()

sys.color.primary

OnAccent

ThemeOnAccentColor()

app.color.arkuilab_on_accent

主按钮

ThemePrimaryButtonBackground()

sys.color.component_activated

主按钮前景

ThemePrimaryButtonForeground()

app.color.arkuilab_on_accent

分割线

ThemeDividerColor()

sys.color.list_separator

输入框

ThemeTextFieldBackground()

sys.color.text_field_bg

透明

ThemeTransparentColor() / ThemeTransparentWhiteColor()

固定透明值

阴影

ThemeCardShadowColor()

app.color.arkuilab_shadow_card

蒙层

ThemeOverlayScrimColor()

app.color.arkuilab_overlay_scrim

搜索菜单

ThemeSearchMenuBackground/Border()

app.color.arkuilab_search_menu_*

渐变

ThemeGradientTonal/Cool/Warm/Violet/Accent Start/End()

app.color.arkuilab_gradient_*

底部蒙层

ThemeBottomGradientMask()

app.color.arkuilab_bottom_gradient_mask

浮层材质

ThemeFloatingMaterialColor()

app.color.arkuilab_floating_material_light

5.4 使用方式

在业务代码里,优先用语义令牌,不要直接用系统令牌

import { ThemeTextPrimary, ThemePageBackground, ThemeSurfaceBackground } from './theme/ThemeTokens';

build() {
  Column() {
    Text('标题').fontColor(ThemeTextPrimary())
  }
  .backgroundColor(ThemePageBackground())
}

例外:在一次性令牌实验或官方 API 探针代码里,直接用 $r('sys.color...') 是允许的。


六、base/dark 资源限定符:自定义语义的双值

6.1 什么时候需要自定义资源?

系统令牌覆盖了大部分场景,但有些语义系统令牌表达不了:

  • 卡片阴影的 alpha 值

  • Overlay 蒙层的 alpha 值

  • OnAccent 文字颜色(在激活蓝背景上的前景色)

  • 搜索菜单的半透明材质

  • 渐变色块

  • 底部渐变蒙层

  • 浮层材质颜色

这些需要 $r('app.color.*')——通过 resources/baseresources/dark 的资源限定符自动切换。

6.2 浅色值:base/element/color.json

{
  "color": [
    { "name": "arkuilab_shadow_card",        "value": "#18243114" },
    { "name": "arkuilab_overlay_scrim",      "value": "#1824311F" },
    { "name": "arkuilab_search_menu_background", "value": "#F2FFFFFF" },
    { "name": "arkuilab_search_menu_border", "value": "#FFFFFFFF" },
    { "name": "arkuilab_on_accent",          "value": "#FFFFFFFF" },
    { "name": "arkuilab_gradient_tonal_start", "value": "#EEF4FF" },
    { "name": "arkuilab_gradient_tonal_end",   "value": "#FFEDE2" },
    { "name": "arkuilab_bottom_gradient_mask", "value": "#66F1F3F5" },
    { "name": "arkuilab_floating_material_light", "value": "#FFFFFFFF" }
  ]
}

6.3 深色值:dark/element/color.json

{
  "color": [
    { "name": "arkuilab_shadow_card",        "value": "#00000066" },
    { "name": "arkuilab_overlay_scrim",      "value": "#00000066" },
    { "name": "arkuilab_search_menu_background", "value": "#F21D2024" },
    { "name": "arkuilab_search_menu_border", "value": "#33FFFFFF" },
    { "name": "arkuilab_on_accent",          "value": "#FFFFFFFF" },
    { "name": "arkuilab_gradient_tonal_start", "value": "#1B2738" },
    { "name": "arkuilab_gradient_tonal_end",   "value": "#35271F" },
    { "name": "arkuilab_bottom_gradient_mask", "value": "#6613171D" },
    { "name": "arkuilab_floating_material_light", "value": "#FF1D2024" }
  ]
}

关键点

  • 两个 JSON 文件的 key 完全一致,只是值不同。

  • 系统根据当前 colorMode 自动选择读哪个文件——你不需要写 if (isDark)

  • 浅色柔和渐变 #EEF4FF → #FFEDE2 在深色模式下变成深色渐变 #1B2738 → #35271F,保留了"柔和渐变"的视觉语言,只是亮度适配了暗色环境。

6.4 ⚠️ 踩坑一:底部蒙层在深色模式发白

这是 E011 最经典的踩坑:

FloatingTabBar 底部有一个渐变蒙层,浅色值是 #66F1F3F5(半透明浅灰)。如果不提供 dark 值,深色模式下这个浅灰蒙层会让每个页面底部都发白

修复:提供 dark 值 #6613171D(半透明深色),让蒙层在深色模式下也是暗的。

规则:底部边缘是主题审计的一部分。TabBar 蒙层、模糊材质、渐变 overlay 必须和页面背景一起审计。


七、主题审计报告:真实数据

E011 迁移后的审计数据:

7.1 系统令牌使用

96 处 $r('sys.color...') 引用
18 个活跃系统令牌
最常用:text_secondary / list_card_bg / list_separator / primary / component_normal / text_field_bg

7.2 硬编码颜色残留

130 处 HEX 引用
75 个不同 HEX 值
Color.White:1 处(FloatingTabBarShell 的 HDS systemMaterialEffect.lightColor,保留为官方探针)
Color.Black / Color.Gray / RGB / ARGB:0 处

7.3 必须后续迁移的硬编码

残留位置

问题

对策

EntryAbility.ets 状态栏内容色 #182431

不自适应

需要 uiAppearance 或配置感知的窗口栏更新

Menu/ActionSheet/Feedback titleBar 透明背景 #00FFFFFF

应该用令牌

下次改动这些文件时迁移到 ThemeTransparentWhiteColor()

MenuLabDestination 自定义 Select 样本

有意保留的视觉探针

重复使用后应变成组件令牌

7.4 允许保留的硬编码

残留位置

为什么保留

转场/Action/Menu/Feedback Lab 的演示渐变

实验视觉语言的一部分

Quick Access 条目的业务身份色

内容身份,不是结构色

ActionSheet 蒙层值

等选择面框架有专用组件令牌后再迁移

HDS 官方模糊 TabBar 蒙层 #66F1F3F5

等设备对比确认令牌替代后迁移


八、迁移实战:怎么改才对

8.1 迁移前的错误写法

// ❌ 硬编码结构色
Text('标题').fontColor('#182431')
Text('说明').fontColor('#56606C')
Column().backgroundColor('#F6F8FA')
Column().backgroundColor('#FFFFFF')   // 卡片
Divider().color('#E5E8EE')
Column().backgroundColor('#F0F1F5')   // 输入框

8.2 迁移后的正确写法

// ✅ 语义令牌
Text('标题').fontColor(ThemeTextPrimary())
Text('说明').fontColor(ThemeTextSecondary())
Column().backgroundColor(ThemePageBackground())
Column().backgroundColor(ThemeSurfaceBackground())    // 卡片
Divider().color(ThemeDividerColor())
Column().backgroundColor(ThemeTextFieldBackground())  // 输入框

8.3 渐变的正确处理

不要因为渐变在深色模式下失败就删掉它。正确的做法是把渐变终点变成有 base/dark 分值的语义资源:

// ❌ 硬编码渐变终点
.linearGradient({
  angle: 135,
  colors: [['#EEF4FF', 0], ['#FFEDE2', 1]]
})

// ✅ 语义渐变令牌(base/dark 自动切换)
.linearGradient({
  angle: 135,
  colors: [
    [ThemeGradientTonalStart(), 0],
    [ThemeGradientTonalEnd(), 1]
  ]
})

规则:主题迁移不是视觉拍平。如果渐变、蒙层、材质是框架身份的一部分,保留效果,把终点/蒙层移到语义 base/dark 资源里。

8.4 主按钮的正确处理

每个填充背景令牌都需要一个前景搭档

// ❌ 选中态文字用了背景/激活色,低对比
MenuItem({ content: '选项' })
  .selectedOptionFontColor($r('sys.color.ohos_id_color_activated'))  // 蓝字蓝底
  .selectedOptionBgColor($r('sys.color.ohos_id_color_activated'))

// ✅ 填充背景用激活蓝,前景用 onAccent 白
MenuItem({ content: '选项' })
  .selectedOptionBgColor(ThemePrimaryButtonBackground())    // 蓝底
  .selectedOptionFontColor(ThemePrimaryButtonForeground()) // 白字

规则:蓝色填充控件应该用主按钮语义对(ThemePrimaryButtonBackground + ThemePrimaryButtonForeground),不要用通用 accent 令牌。


九、框架审计:哪些框架已就绪

E011 对 ArkUILab 的所有框架做了主题就绪审计:

框架

主题状态

说明

Navigation Framework

基本就绪

HDS Navigation + title bar 令牌用系统色;窗口状态栏有一个 gap

Navigation Header Framework

v1 就绪

Header 标题/菜单/搜索已迁移到系统/语义令牌

FloatingTabBar Framework

v1 就绪

浮层材质和底部渐变蒙层用 base/dark 资源

ApplicationShell

就绪

根背景用 ThemePageBackground()

Overlay Framework

v1 就绪

下拉浮层蒙层/卡片/输入/文字已迁移

Menu Framework

部分

大部分用系统令牌;自定义视觉探针保留;API 26 systemMaterial 待 SDK

Selection Framework

部分

核心 text/card 令牌已有;蒙层/透明值待迁移

Feedback Framework

基本就绪

用系统 card/text 令牌;hero 渐变保留为演示视觉

Search Framework

v1 就绪

会话页/搜索输入/菜单表面/边线/占位/结果行已迁移

结论:9 个框架中 5 个 v1 就绪,3 个部分就绪,1 个基本就绪。没有完全未就绪的框架。


十、最佳实践清单

10.1 禁止事项

  • ❌ 不要用 Color.WhiteColor.BlackColor.Gray 做结构 UI。

  • ❌ 不要用 #FFFFFF#000000#182431#56606C 做结构 UI。

  • ❌ 不要用 RGB / ARGB API 调用做结构 UI。

  • ❌ 不要在实验里覆盖 app colorMode,除非实验明确研究主题覆盖。

  • ❌ 不要创建 ThemeManager,除非有真实的用户可选应用内主题需求。

10.2 推荐事项

  • ✅ 新框架代码用 ThemeTokens.ets

  • ✅ 一次性令牌实验可直接用 $r('sys.color...')

  • ✅ 需要 dark 分值时用 resources/base + resources/dark

  • ✅ 透明色当工具令牌用(ThemeTransparentColor / ThemeTransparentWhiteColor),不当视觉设计令牌。

  • ✅ 渐变保留,但每个结构渐变终点用 ThemeGradient...() 令牌。

  • ✅ 填充主控件用 ThemePrimaryButtonBackground() + ThemePrimaryButtonForeground()

  • ✅ 底部边缘蒙层和材质颜色必须是主题资源。

  • ✅ 主题编辑后跑 HAP 构建验证。

10.3 审计命令

E011 沉淀了三条审计命令,建议加入 CI:

# 统计系统令牌使用
rg -o '\$r('\''sys\.color\.[^'\'']+'\''\)' entry/src/main/ets | sort | uniq -c | sort -nr

# 统计应用资源令牌使用
rg -o '\$r('\''app\.color\.[^'\'']+'\''\)' entry/src/main/ets | sort | uniq -c | sort -nr

# 扫描禁用的硬编码颜色
rg -n '(Color\.(White|Black|Gray|Grey)|#[0-9A-Fa-f]{3,8}\b|rgb\(|rgba\(|argb\(|ARGB|RGB)' entry/src/main/ets entry/src/main/resources

10.4 迁移检查清单

  • 页面背景用 ThemePageBackground()

  • 卡片/Sheet/Popover 用 ThemeSurfaceBackground()

  • 组件填充/输入芯片用 ThemeComponentBackground()ThemeTextFieldBackground()

  • 文字用 ThemeTextPrimary/Secondary/Tertiary()

  • 分割线/边框用 ThemeDividerColor()

  • 主操作用 ThemeAccentColor() + ThemeOnAccentColor()

  • 填充主按钮用 ThemePrimaryButtonBackground() + ThemePrimaryButtonForeground()

  • 结构渐变用 ThemeGradient...() 语义对

  • 底部蒙层/模糊材质用 ThemeBottomGradientMask() / ThemeFloatingMaterialColor()

  • 选中态前景与背景对比度足够

  • 蒙层/阴影用 ThemeTokens.ets 暴露的应用资源

  • HAP 构建通过


十一、常见错误对照表

错误做法

问题

正确做法

fontColor('#182431')

深色模式下文字消失

ThemeTextPrimary()

backgroundColor('#FFFFFF')

深色模式白底

ThemeSurfaceBackground()

backgroundColor('#F6F8FA')

深色模式浅灰底

ThemePageBackground()

渐变硬编码 #EEF4FF

深色模式刺眼

ThemeGradientTonalStart()

底部蒙层只有浅色值

每个页面底部发白

提供 dark 资源值

选中态蓝底蓝字

低对比看不清

ThemePrimaryButtonForeground() 白字

Color.White 做结构色

不自适应

语义令牌

深色失败就删渐变

丢失视觉语言

保留渐变,终点用 base/dark 资源

每个页面自己写 if (isDark)

不可维护

用令牌/资源自动切换

直接到处用 $r('sys.color...')

不一致、难维护

ThemeTokens.ets 语义层

accent 令牌做填充按钮

语义太宽泛

ThemePrimaryButton* 专用对

状态栏内容色硬编码

深色下不可见

需要 uiAppearance 或配置感知


十二、Review 教训:四条经验

E011 在真机 review 后总结了四条关键教训:

12.1 主题迁移不是视觉拍平

如果渐变、蒙层、材质是框架身份的一部分,保留效果,把终点/蒙层移到语义 base/dark 资源里。

不要为了"适配深色模式"把所有渐变都删掉变成纯色——那会让应用失去视觉个性。正确的做法是让渐变在深色模式下也有对应的深色版本。

12.2 令牌编码的是角色,不只是颜色

ThemeAccentColor() 太宽泛,不适合填充控件。ThemePrimaryButtonBackground() 更清晰,映射到系统激活蓝。

一个令牌的名字应该表达用途("主按钮背景"),而不是色相("蓝色")。这样当你换品牌色时,只需要改一个地方。

12.3 每个填充背景令牌都需要前景搭档

Icon Select 的回归就是因为选中态文字用了背景/激活色,而不是 ThemePrimaryButtonForeground()

定义 ThemePrimaryButtonBackground() 的同时必须定义 ThemePrimaryButtonForeground()。否则一定会有人把低对比度的文字放到同色背景上。

12.4 底部边缘是主题审计的一部分

TabBar 蒙层、模糊材质、渐变 overlay 必须和页面背景一起审计。

底部边缘很容易被忽略——它不在页面正中间,但在每个页面都出现。一个 #66F1F3F5 的浅色蒙层会让整个应用在深色模式下底部发白。

12.5 构建成功 ≠ 主题正确

Build success 证明了类型安全,但只有模式切换才能发现对比度、漂白和过度拍平的问题。

HAP 构建通过只能证明代码能编译,不能证明深色模式看起来对。真机切到 Dark Mode 走一遍是主题验证的必要步骤。


十三、当前限制与后续方向

E011 的 Theme Foundation v1 已经成立,但仍有后续工作:

待办项

说明

状态栏/导航栏内容色

需要 uiAppearance 或配置感知的窗口栏更新

透明 titleBar 值

迁移到 ThemeTransparentWhiteColor()

选择面/菜单组件令牌

等设备 review 后、重复使用稳定后提取

API 26 systemMaterial

等本地 SDK 暴露后验证

API 26 menuSystemMaterial

同上

API 26 uiAppearance

同上

CI 审计脚本

自动扫描禁用的结构硬编码颜色

暂不创建

  • ThemeManager —— 没有真实用户可选主题需求时不需要。

  • ThemeConfiguration —— 没有本地/用户主题覆盖实验时不需要。

  • 组件令牌注册表 —— 等重复的组件级语义出现后再提取。


十四、构建验证

DEVECO_SDK_HOME=/Applications/DevEco-Studio.app/Contents/sdk \
  /Applications/DevEco-Studio.app/Contents/tools/hvigor/bin/hvigorw assembleHap --no-daemon

E011 构建结果:

CompileArkTS passed
PackageHap passed
BUILD SUCCESSFUL

十五、写在最后

深色模式不是"事后补的体力活",而是"第一天就该搭好的地基"。如果你的主题基础设施在项目开始时就搭对,90% 的深色适配是自动的——系统令牌帮你切,base/dark 资源帮你切,你只需要管那 10% 的自定义语义。

鸿蒙的主题体系给了你足够的工具:

  • sys.color.* 系统令牌——免费自动切换

  • base/dark 资源限定符——自定义语义的双值

  • ThemeTokens.ets 语义层——项目级稳定命名

  • HDS 材质 ADAPTIVE 策略——模糊/材质自动适配

你需要做的是选对令牌、分层管理、审计到底

记住这几条口诀:

系统令牌优先用,语义令牌稳命名。 base dark 双值备,自动切换不用写 if。 渐变别拍平,终点存资源。 填充按钮有搭档,背景前景配对走。 底部边缘要审计,蒙层发白坑全站。 Color.White 别用,#182431 别留。 构建过了不算对,真机切暗才算行。

本文的实现基于 ArkUILab 项目 E011 实验。它不是最终产品,但已经形成了一个可以让每个新框架自动继承的主题基础设施。如果你正在从零开始做一个鸿蒙应用,或者在给现有应用补深色模式,可以直接复用这套令牌分层策略和审计方法作为起点。

主题适配看似只是"换个深色背景",背后却涉及令牌分层、资源限定符、语义命名、渐变保留、前景搭档、底部边缘审计和真机验证等多个维度。把这些维度拆清楚,你的深色模式就不会是"逐页改造的体力活"了。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐