Administrator
发布于 2026-09-03 / 8,009 阅读
0
0

鸿蒙主题与暗黑模式架构:颜色令牌、动态换肤与设计系统落地

系列文章《鸿蒙开发从入门到专家》第 58 篇 · 高级

第 44 篇讲 UX 规范时提到过设计系统,但没展开颜色系统怎么落地。这篇从代码层面讲:颜色令牌怎么设计、暗黑模式怎么切、运行时换肤怎么做。

大部分应用的颜色管理方式是在代码里到处写 #FF0000。改一次主色调要全局搜索替换,暗黑模式更是地狱——每个颜色都要写两份硬编码值。颜色令牌系统就是解决这个问题的。

一、颜色令牌体系

令牌(Token)是设计系统里的术语,指的是把颜色抽象成语义化变量。不是说"这个按钮用 #007AFF",而是说"这个按钮用 primaryColor"。primaryColor 在浅色模式下是 #007AFF,在暗色模式下是 #4A90D9。

1.1 三层令牌架构

┌────────────────────────────────────────────┐
│  组件层令牌(Component Token)              │
│  buttonBg → primaryColor                    │
│  cardBg → surfaceColor                      │
│  errorText → errorColor                     │
├────────────────────────────────────────────┤
│  语义层令牌(Semantic Token)               │
│  primaryColor → brand_blue                 │
│  surfaceColor → bg_primary                  │
│  errorColor → semantic_red                 │
├────────────────────────────────────────────┤
│  基础层令牌(Base Token)                   │
│  brand_blue → #007AFF(浅)/ #0A84FF(深)  │
│  bg_primary → #FFFFFF(浅)/ #1C1C1E(深)  │
│  semantic_red → #FF3B30(浅)/ #FF453A(深)│
└────────────────────────────────────────────┘

三层分离的好处:组件层只依赖语义,语义层映射到基础颜色。换皮肤时只改基础层的映射,组件层代码不动。

1.2 资源文件定义

鸿蒙用 resource 文件管理多模式资源。在 resources/ 下分别为浅色和深色定义颜色:

resources/base/element/color.json(浅色模式):

{
  "color": [
    { "name": "brand_blue", "value": "#007AFF" },
    { "name": "bg_primary", "value": "#FFFFFF" },
    { "name": "bg_secondary", "value": "#F2F2F7" },
    { "name": "semantic_red", "value": "#FF3B30" },
    { "name": "text_primary", "value": "#000000" },
    { "name": "text_secondary", "value": "#3C3C43" }
  ]
}

resources/dark/element/color.json(暗色模式):

{
  "color": [
    { "name": "brand_blue", "value": "#0A84FF" },
    { "name": "bg_primary", "value": "#1C1C1E" },
    { "name": "bg_secondary", "value": "#2C2C2E" },
    { "name": "semantic_red", "value": "#FF453A" },
    { "name": "text_primary", "value": "#FFFFFF" },
    { "name": "text_secondary", "value": "#EBEBF5" }
  ]
}

resources/dark/ 目录下的资源会在系统暗色模式启用时自动生效。

1.3 在组件中使用

@Entry
@Component
struct ThemedPage {
  build() {
    Column() {
      Text('Hello World')
        .fontColor($r('app.color.text_primary'))
        .fontSize(16)

      Button('Action')
        .backgroundColor($r('app.color.brand_blue'))
        .fontColor($r('app.color.bg_primary'))
    }
    .width('100%')
    .height('100%')
    .backgroundColor($r('app.color.bg_primary'))
  }
}

$r('app.color.xxx') 会根据当前系统模式自动取浅色或深色值。不需要在代码里判断暗黑模式。

二、暗黑模式适配

2.1 配置支持

在 module.json5 中声明应用支持的颜色模式:

{
  "module": {
    "name": "entry",
    "type": "entry",
    "colorMode": "auto"
  }
}

auto 表示跟随系统模式。如果设为 light 或 dark,应用锁定模式不随系统切换。

2.2 监听模式切换

有些逻辑需要在模式切换时做额外处理(比如更新状态栏颜色):

import { ConfigurationConstant, common } from '@kit.AbilityKit';

export default class EntryAbility extends UIAbility {
  onConfigurationUpdate(newConfig: ConfigurationConstant.Configuration): void {
    if (newConfig.colorMode !== undefined) {
      const isDark = newConfig.colorMode === ConfigurationConstant.ColorMode.COLOR_DARK;
      console.info(`Color mode changed: ${isDark ? 'dark' : 'light'}`);

      // 更新状态栏
      this.updateStatusBar(isDark);
    }
  }

  private updateStatusBar(isDark: boolean): void {
    const window = this.windowStage.getMainWindowSync();
    window.setWindowSystemBarProperties({
      statusBarContentColor: isDark ? '#FFFFFF' : '#000000'
    });
  }
}

2.3 图片资源适配

暗色模式下图片也需要适配。在 resources/dark/media/ 下放置暗色版本的图片,文件名和浅色版一致。系统会自动选择。

resources/
  base/
    media/
      logo.png        ← 浅色版
      empty_state.png ← 浅色版
  dark/
    media/
      logo.png        ← 暗色版
      empty_state.png ← 暗色版

SVG 图标不需要准备两份。用 fillColor 引用颜色令牌:

Image($r('app.media.ic_search'))
  .fillColor($r('app.color.text_secondary'))

三、运行时动态换肤

暗黑模式是跟随系统的,但有些应用需要提供"主题选择"功能:用户在应用内选主题色(蓝色、绿色、紫色…),实时生效。

3.1 方案:AppStorage + ResourceStr

// theme/ThemeManager.ts
export enum ThemeType {
  BLUE = 'blue',
  GREEN = 'green',
  PURPLE = 'purple'
}

interface ThemeColors {
  primary: string;
  primaryLight: string;
  primaryDark: string;
}

const themes: Record<ThemeType, ThemeColors> = {
  [ThemeType.BLUE]: {
    primary: '#007AFF',
    primaryLight: '#5AC8FA',
    primaryDark: '#004E92'
  },
  [ThemeType.GREEN]: {
    primary: '#34C759',
    primaryLight: '#7ED957',
    primaryDark: '#248A3D'
  },
  [ThemeType.PURPLE]: {
    primary: '#AF52DE',
    primaryLight: '#D0B3FF',
    primaryDark: '#6B2CA0'
  }
};

export class ThemeManager {
  private static currentTheme: ThemeType = ThemeType.BLUE;

  static getTheme(): ThemeType {
    return this.currentTheme;
  }

  static setTheme(theme: ThemeType): void {
    this.currentTheme = theme;
    const colors = themes[theme];

    // 写入 AppStorage,触发 UI 刷新
    AppStorage.set('primaryColor', colors.primary);
    AppStorage.set('primaryLightColor', colors.primaryLight);
    AppStorage.set('primaryDarkColor', colors.primaryDark);
  }

  static init(): void {
    // 初始化默认主题
    this.setTheme(this.currentTheme);
  }
}

3.2 在组件中使用

@Entry
@Component
struct ThemedPage {
  @StorageLink('primaryColor') primaryColor: string = '#007AFF';

  build() {
    Column() {
      Text('Dynamic Theme')
        .fontColor(this.primaryColor)

      Button('Blue')
        .backgroundColor(this.primaryColor)
        .onClick(() => ThemeManager.setTheme(ThemeType.BLUE))

      Button('Green')
        .backgroundColor(this.primaryColor)
        .onClick(() => ThemeManager.setTheme(ThemeType.GREEN))
    }
  }
}

@StorageLink 会自动监听 AppStorage 中对应 key 的变化,主题切换后所有引用了该 key 的组件自动刷新。

3.3 持久化用户选择

import { preferences } from '@kit.ArkData';

export class ThemeManager {
  private static readonly PREF_KEY = 'theme_type';

  static async loadSavedTheme(): Promise<void> {
    const pref = await preferences.getPreferences(getContext(), 'app_prefs');
    const saved = await pref.get(ThemeManager.PREF_KEY, ThemeType.BLUE) as ThemeType;
    this.setTheme(saved);
  }

  static async saveTheme(theme: ThemeType): Promise<void> {
    const pref = await preferences.getPreferences(getContext(), 'app_prefs');
    await pref.put(ThemeManager.PREF_KEY, theme);
    await pref.flush();
    this.setTheme(theme);
  }
}

应用启动时调 loadSavedTheme() 恢复上次选择。

四、暗黑模式适配清单

适配项

方式

说明

背景色

$r('app.color.bg_primary')

用令牌不用硬编码

文字色

$r('app.color.text_primary')

主文字用 text_primary

边框/分隔线

$r('app.color.bg_secondary')

用背景色变体

图标填充色

.fillColor($r('app.color.text_secondary'))

SVG 图标适配

图片资源

resources/dark/media/

位图准备暗色版

状态栏

setWindowSystemBarProperties

模式切换时更新

阴影/蒙层

半透明黑色调

暗色下用更低的 alpha

4.1 半透明处理

暗色模式下,浅色的半透明遮罩会很突兀。用令牌管理:

// base/element/color.json
{ "name": "overlay_bg", "value": "#80000000" }

// dark/element/color.json
{ "name": "overlay_bg", "value": "#CC000000" }

五、常见问题

问题 1:颜色不切换。 确认 module.json5 的 colorMode 设为 auto,且 resources/dark/ 目录结构和 resources/base/ 完全一致(element/color.json 和 media/ 下的文件名要对应)。

问题 2:图片闪烁。 模式切换时图片资源重新加载可能闪烁。对纯色图标用 fillColor 而非多套位图,避免加载开销。

问题 3:第三方 SDK 颜色不适配。 第三方组件可能用硬编码颜色,无法通过令牌切换。需要在 onConfigurationUpdate 中主动通知第三方 SDK 刷新主题。

小结

颜色令牌三层架构(基础→语义→组件)是设计系统落地的标准模式。暗黑模式用鸿蒙的 resources/dark/ 目录自动切换,代码里用 $r() 引用。运行时动态换肤用 AppStorage + @StorageLink 做响应式更新。核心原则:代码里不写硬编码颜色值,一切走令牌。

下篇讲无障碍开发。Accessibility 属性怎么用、语义树怎么管理。

参考阅读:

  • 第 44 篇:鸿蒙设计规范(UX 适配实战)

  • 第 50 篇:设计模式在鸿蒙里的应用

  • 官方文档:color 资源、ConfigurationConstant


评论