A hand-drawn doodle illustration on pure white pap

前言

我之前上架一个 App,审核直接打回来了——原因就是没适配暗黑模式。说实话当时挺不爽的,但用了几天暗黑模式后真香了。HarmonyOS7 的暗黑模式适配其实没那么难,3 步就能搞定,今天手把手教你。
暗黑模式不是"换个黑色背景"那么简单。文字颜色、图片资源、分割线、阴影——所有视觉元素都得跟着调。如果硬编码颜色值,适配起来就是灾难。核心思路是:所有颜色和图片都走资源引用,系统自动帮你切换。

为什么要适配暗黑模式

除了审核要求,还有几个实际好处:

  • 省电——OLED 屏幕上黑色像素不发光,暗黑模式真的能省电
  • 护眼——夜间使用时白色背景太刺眼,暗黑模式舒服多了
  • 高级感——说实话,很多 App 暗黑模式就是比亮色好看

如果实在不想适配,可以强制锁定亮色模式。但这招不推荐,迟早得改。

强制亮色的兜底代码(不推荐,但你知道有这招):

A hand-drawn doodle illustration on pure white pap

let applicationContext = this.context.getApplicationContext()
applicationContext.setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT)

颜色资源定义

适配暗黑模式的第一步:把所有硬编码的颜色值抽到资源文件里

亮色模式颜色(base/element/color.json)
{
  "color": [
    { "name": "app_background", "value": "#FFFFFF" },
    { "name": "app_title_color", "value": "#000000" },
    { "name": "app_subtitle_color", "value": "#666666" },
    { "name": "app_divider", "value": "#E5E5E5" },
    { "name": "app_primary", "value": "#007DFF" }
  ]
}
暗黑模式颜色(dark/element/color.json)
{
  "color": [
    { "name": "app_background", "value": "#1A1A1A" },
    { "name": "app_title_color", "value": "#E6E6E6" },
    { "name": "app_subtitle_color", "value": "#999999" },
    { "name": "app_divider", "value": "#333333" },
    { "name": "app_primary", "value": "#317AF7" }
  ]
}

A hand-drawn doodle illustration on pure white pap

关键点:两个文件的 name 必须完全一致!系统根据 name 匹配,当前模式是暗黑就用 dark 目录下的值,否则用 base 目录的。你代码里只需要写 $r('app.color.app_background'),剩下的系统全包了。

3 步适配

第一步:配置 module.json5

确认你的应用没有强制锁定颜色模式。默认情况下应用会跟随系统,不需要额外配置。如果你之前加了 colorMode 配置,删掉就行。

第二步:资源文件按目录组织

目录结构长这样:

resources/
├── base/
│   └── element/
│       └── color.json       ← 亮色模式颜色
├── dark/
│   └── element/
│       └── color.json       ← 暗黑模式颜色
└── rawfile/

在代码中引用颜色资源:

@Entry
@Component
struct DarkModeDemo {
  build() {
    Column() {
      Text('标题文字')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .fontColor($r('app.color.app_title_color'))

      Text('副标题')
        .fontSize(14)
        .fontColor($r('app.color.app_subtitle_color'))

      Divider()
        .color($r('app.color.app_divider'))
    }
    .width('100%')
    .height('100%')
    .backgroundColor($r('app.color.app_background'))
    .padding(16)
  }
}

注意:背景色、文字色、分割线色——全部用 $r() 引用,一个硬编码的 #xxx 都不要留。

第三步:监听模式切换

有些场景需要在代码中感知暗黑模式变化,比如需要更新 Web 组件的背景色。

import { Configuration, UIAbility, ConfigurationConstant } from '@kit.AbilityKit'

export default class EntryAbility extends UIAbility {
  onConfigurationUpdate(newConfig: Configuration): void {
    AppStorage.setOrCreate('colorMode', newConfig.colorMode)
  }
}

解释onConfigurationUpdate 在系统模式变化时触发,把当前 colorMode 存到 AppStorage,全局可用。

在页面中监听变化:

@Entry
@Component
struct ThemeAwarePage {
  @StorageLink('colorMode')
  @Watch('onColorModeChange')
  colorMode: ConfigurationConstant.ColorMode =
    ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET

  onColorModeChange() {
    if (this.colorMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK) {
      console.info('切换到暗黑模式')
    } else {
      console.info('切换到亮色模式')
    }
  }

  build() {
    Column() {
      Text('主题感知页面')
        .fontColor($r('app.color.app_title_color'))
    }
    .width('100%')
    .height('100%')
    .backgroundColor($r('app.color.app_background'))
  }
}

@StorageLink 双向绑定 AppStorage 中的值,@Watch 在值变化时触发回调。这两个装饰器配合,实现模式切换的即时响应。

动态切换

有时候用户想在 App 内手动切换主题,不走系统设置。可以这样实现:

@Entry
@Component
struct ManualThemeSwitch {
  @StorageLink('colorMode') colorMode: ConfigurationConstant.ColorMode =
    ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET
  @State isDark: boolean = false

  toggleTheme() {
    this.isDark = !this.isDark
    let applicationContext = getContext(this).getApplicationContext()
    if (this.isDark) {
      applicationContext.setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_DARK)
    } else {
      applicationContext.setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT)
    }
  }

  build() {
    Column() {
      Toggle({ type: ToggleType.Switch, isOn: this.isDark })
        .onChange((isOn: boolean) => {
          this.toggleTheme()
        })

      Text('手动切换主题')
        .fontColor($r('app.color.app_title_color'))
        .margin({ top: 20 })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .backgroundColor($r('app.color.app_background'))
  }
}

setColorMode 可以主动设置应用的颜色模式,不跟随系统。但用了这个方法后,应用就不再自动跟随系统了,除非你再次调用时传入 COLOR_MODE_NOT_SET

图片资源适配

颜色有 dark 目录,图片也一样。把暗黑模式下的同名图片放到 dark/media/ 目录:

resources/
├── base/
│   ├── element/color.json
│   └── media/
│       └── logo.png          ← 亮色模式图片
└── dark/
    ├── element/color.json
    └── media/
        └── logo.png          ← 暗黑模式图片

代码中正常引用,系统自动切换:

Image($r('app.media.logo'))
  .width(100)
  .height(100)

对于 SVG 图标,还有更省事的办法——用 fillColor 动态改颜色,不用准备两套图:

Image($r('app.media.icon_svg'))
  .width(24)
  .height(24)
  .fillColor($r('app.color.app_title_color'))

这招真香:一个 SVG 文件配合颜色资源就能适配两种模式,省了双倍图片资源。

系统监听

还有一个更快的方式:直接用系统预置资源。HarmonyOS 提供了 sys.color 系列资源,本身就适配了暗黑模式:

Text('使用系统配色')
  .fontColor($r('sys.color.ohos_id_color_text_primary'))

用系统资源的好处是零配置,不用建 dark 目录。缺点是你的 App 会跟着系统风格走,没法做个性化定制。

写在最后

暗黑模式适配的核心就是一句话:别硬编码颜色和图片,全走资源引用。只要做到这一点,3 步就能搞定:

  1. 建立 dark 资源目录
  2. 所有视觉元素用 $r() 引用
  3. 需要时监听模式变化

提前做好适配,比后面补不知道省多少事。等项目大了再回来改硬编码的颜色值,那个痛苦你不想体验的。

Logo

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

更多推荐