我是兰瓶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'))

当你需要品牌色、活动色时,再引入自定义 Tokenbrand_primary 等),在深色目录做差异化。这样既稳,又能玩出花来。(华为开发者官网)

3)应用级主题 vs. 页面/局部主题

  • 应用级主题:用 ThemeControl.setDefaultTheme(customTheme) 一次性设默认主题,所有 ArkUI 组件风格跟随。适合品牌色换肤、全局风格统一化。(华为开发者官网)
  • 页面/局部主题:当你需要“局部反转”(比如浅色页面里的一个深色弹窗),就该上主题作用域(Theme Scope)了。在 ArkUI 里,这个能力由 WithTheme 组件提供(它就是你脑子里“ThemeScope”的实锤实现)。(Gitee)

自动切换机制:跟随系统与手动切换

1)systemColorMode:侦测系统深/浅

ArkUI 提供了内置环境变量 ColorMode(值为 LIGHT/DARK),用于表示系统当前深浅模式。你可以把它理解为“systemColorMode”。它是个环境变量,用于判定系统层面的深/浅色态。(华为开发者官网)

说明:官方称之为 ColorMode 内置环境变量;在业务里大家常把它口头叫“systemColorMode”。读取它的常规实践是:跟随系统使用资源限定/系统 Token自动适配;若需显示切换局部反转,再配合 WithThemecolorMode 参数。

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:你把一段组件树包起来,参数里指定colorModeLIGHT / 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,一键换肤

假设你要做“品牌主题三连”:系统默认 / 绿色主题 / 红色主题——WithThemetheme,可热切换:

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)

工程化与踩坑锦囊

  1. @Styles 真名实姓
    不要写成 @Style,官方文档与编译器都认 @Styles。它只支持通用属性/事件,组件内优先级更高,不支持 export。想导出跨文件复用,请用 AttributeModifier。(华为开发者官网)

  2. 资源目录别乱
    深色适配不是“我写了 if (dark) 就完事”,dark/element/color.json 必须准备好,同名键才能命中深色值。局部深色(WithTheme)也要靠它吃饭。(Gitee)

  3. 系统 Token 优先
    能用系统 Token 就别自己造,减少维护成本;自定义 Token 主要为品牌差异化,其余靠系统兜底。(seaxiang.com)

  4. 应用级主题与作用域主题的边界

    • 全局一致性(品牌色、强调色统一)→ ThemeControl.setDefaultTheme
    • 局部反转 / 专题卡片 / 组合弹窗 → WithTheme 包围一小块组件树。(华为开发者官网)
  5. 系统深浅“感知”→ 思维而非强依赖
    你可以把 ColorMode 当做“systemColorMode”来理解:把它视作设计输入,不必每处都“显式读取并 if/else”。优先让资源限定 + Token自动工作,只有确实需要做逻辑分支局部反转时,再动用 WithTheme/自定义 Theme。官方环境变量文档明确存在 ColorMode 这一内置枚举。(华为开发者官网)

  6. 可访问性(A11y)别忘
    深色里要特别关注对比度:文字与背景至少 4.5:1,重要按钮 7:1 更稳;compEmphasizeSecondary 这类“有透明度的强调色”在深色里不要太浅,否则漂。

  7. 跨容器(Web/Unity/Map)混合场景
    外层 ArkUI 用 WithTheme 管边框/蒙层/标题,内层容器通过桥接同步“深/浅态 + 品牌色”。(如果你在 ArkUI-X + Unity 里玩过光照同步,那效果会非常丝滑。)

  8. 性能
    换肤如需全局刷新,尽量批量更新状态;作用域主题更建议局部包裹,减少重算重绘面。

参考要点(核心出处)

  • WithTheme(主题作用域实现):参数 colorMode/theme、需要 dark/element/color.json 才能生效,官方示例与说明。(Gitee)
  • Theme / ThemeControl / CustomTheme / CustomColors:全局默认主题设置与颜色 Token 列表。(华为开发者官网)
  • ColorMode 内置环境变量(systemColorMode 思维):系统当前深/浅色模式的官方定义。(华为开发者官网)
  • 应用深浅色适配指南(资源限定与自动切换):官方“应用深浅色适配”条目。(华为开发者官网)
  • @Styles 装饰器:风格复用的官方名与使用方法。(华为开发者官网)

收尾:三句话复盘

  1. 默认靠系统,差异看品牌,局部用作用域:系统 Token + 资源限定确保“跟随系统”,ThemeControl 统一品牌,WithTheme 做局部反转/专题卡片。
  2. systemColorMode 是输入,不是强依赖:优先让资源/Token自动跑,只在确有业务逻辑分支时再参与判断。
  3. 样式复用用 @Styles:把通用样式抽出来,写得少、改得快,还更稳。

(未完待续)

Logo

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

更多推荐