基于 ThemeDemo 实战验证,覆盖 @State 条件函数、AppStorage 全局主题、资源限定词三种方案,附带完整可运行代码


写在前面

做深色模式这事儿,很多人觉得"不就是换个背景色嘛"。天真。你把背景色改成 #121212,结果文字还是 #333333——恭喜,深色背景配深色文字,用户什么都看不见。

深色模式的核心不是改颜色,是保证所有颜色都有对应的深色版本,并且切换时不能有遗漏。这篇文章把三种实现方案都走了一遍,坑多得你不敢信。


请添加图片描述

一、方案一:@State + 条件函数(Demo 采用)

最直觉的方案——用 @State isDark: boolean 驱动,所有颜色通过函数返回:

@State isDark: boolean = false

private bgColor(): string {
  return this.isDark ? '#121212' : '#ffffff'
}

private cardBg(): string {
  return this.isDark ? '#1e1e1e' : '#f8f8f8'
}

private textColor(): string {
  return this.isDark ? '#e0e0e0' : '#333333'
}

UI 里所有硬编码颜色替换成函数调用:

Column() {
  Text('标题')
    .fontColor(this.textColor())
    .backgroundColor(this.cardBg())
}
.backgroundColor(this.bgColor())

优点

  • 实现简单,不需要额外架构
  • 切换即时生效,@State 变了 UI 自动刷新
  • 方便调试,isDark 一眼就知道当前主题

缺点

  • 颜色散落在各组件的函数里,不好统一管理
  • 新增颜色需要在每个组件里加函数
  • 页面多了,函数重复率很高

适合场景

小项目、单页面 Demo、快速验证。颜色不多的时候这个方案够用。


请添加图片描述

二、方案二:AppStorage + @StorageLink 全局主题

项目稍微大一点,就需要全局管理主题色。把颜色存到 AppStorage,所有组件通过 @StorageLink 监听:

// 初始化(EntryAbility 或首组件 aboutToAppear)
AppStorage.setOrCreate('themeBgColor', '#ffffff')
AppStorage.setOrCreate('themeCardBg', '#f8f8f8')
AppStorage.setOrCreate('themeTextColor', '#333333')
AppStorage.setOrCreate('themePrimaryColor', '#1a73e8')
AppStorage.setOrCreate('isDarkMode', false)

// 各组件
@StorageLink('themeBgColor') bgColor: string = '#ffffff'
@StorageLink('themeTextColor') textColor: string = '#333333'

切换主题时,统一修改 AppStorage:

toggleDarkMode(): void {
  const isDark: boolean = !(AppStorage.get<boolean>('isDarkMode') ?? false)
  AppStorage.setOrCreate('isDarkMode', isDark)
  AppStorage.setOrCreate('themeBgColor', isDark ? '#121212' : '#ffffff')
  AppStorage.setOrCreate('themeCardBg', isDark ? '#1e1e1e' : '#f8f8f8')
  AppStorage.setOrCreate('themeTextColor', isDark ? '#e0e0e0' : '#333333')
}

优点

  • 全局统一,一处修改处处生效
  • 跨页面共享主题状态
  • 新页面只需声明 @StorageLink,不用重复定义函数

缺点

  • @StorageLink 的 key 必须先 setOrCreate,否则崩溃
  • 主题色多了,初始化代码很长
  • 需要维护一个主题色常量表

适合场景

中大型项目、多页面共享主题。主题色在 20 个以内的项目用这个方案最合适。


三、方案三:资源限定词(resources/dark/)

HarmonyOS 支持通过资源目录自动适配深色模式:

resources/
  base/
    element/
      color.json        // 浅色颜色
    media/
      icon.png          // 浅色图片
  dark/
    element/
      color.json        // 深色颜色
    media/
      icon.png          // 深色图片

UI 代码里用 $r() 引用资源,系统自动根据当前模式选择:

.backgroundColor($r('app.color.background'))
.fontColor($r('app.color.text_primary'))

优点

  • 系统原生支持,跟随系统深色模式自动切换
  • UI 代码不需要任何 isDark 判断
  • 图片资源也能自动适配

缺点

  • 只能跟随系统,不能应用内独立切换
  • 资源文件维护成本高,新增颜色要改两个 color.json
  • 调试不直观,看不出当前用的是哪个资源

适合场景

跟随系统深色模式的项目,不需要应用内手动切换主题。


四、三种方案对比

方案 切换方式 作用域 跨页面 图片适配 维护成本 适合规模
@State+函数 应用内切换 组件级 需手动传递 不支持 小项目
AppStorage 应用内切换 全局 自动 不支持 中项目
资源限定词 跟随系统 全局 自动 支持 大项目

我的建议:中小项目用 AppStorage 方案,大项目用资源限定词 + AppStorage 组合。资源限定词处理跟随系统的自动适配,AppStorage 处理应用内手动切换。


五、主题色切换的实现

Demo 里还实现了主题色切换,用户可以选择不同的主色调:

@State primaryColor: string = '#1a73e8'

Row({ space: 8 }) {
  ForEach(['#1a73e8', '#e65100', '#34a853', '#9c27b0', '#d32f2f'], (color: string) => {
    Row()
      .width(36).height(36).borderRadius(18)
      .backgroundColor(color)
      .border({ width: this.primaryColor === color ? 3 : 0, color: '#333333' })
      .onClick(() => { this.primaryColor = color })
  }, (color: string, index: number) => index.toString())
}

深色模式下,主题色需要用更亮的变体,否则在深色背景上对比度不够:

private lightColors: string[] = ['#1a73e8', '#e65100', '#34a853', '#9c27b0', '#d32f2f']
private darkColors: string[] = ['#64b5f6', '#ff9800', '#81c784', '#ce93d8', '#ef5350']

请添加图片描述


六、踩坑合集

坑一:只改背景色不改文字色

这是最常见的低级错误。改了背景色忘了改文字色,深色背景上深色文字,用户什么都看不见。检查清单:背景色、文字色、副文字色、分割线色、卡片色,全都要改。

坑二:阴影在深色模式下不可见

浅色模式下 shadow({ radius: 8, color: '#1a000000' }) 看着挺好的,深色模式下透明度 10% 的黑色阴影在 #121212 背景上完全看不见。深色模式的阴影应该用更亮的颜色或者改用边框替代。

坑三:Web/Canvas 内容不跟随主题

WebView 加载的网页、Canvas 绘制的内容不会自动适配深色模式。网页需要自己实现深色主题(CSS prefers-color-scheme),Canvas 需要重新绘制。

坑四:系统弹窗不跟随应用主题

AlertDialog.show() 等系统弹窗默认跟随系统主题,不受应用内 isDark 控制。如果你的应用是浅色模式但系统是深色,弹窗会显示深色。目前没有好的解决方案,只能用 CustomDialog 替代。

坑五:PNG 图标不好适配

PNG 图标在深色模式下需要准备两套,维护成本翻倍。建议用 SVG 格式,通过 .fillColor() 动态改颜色,一套资源搞定两种模式。


七、透明度调整

深色模式下,相同的透明度值视觉效果不一样。浅色背景上 10% 透明度的叠加色很淡,深色背景上同样的值可能完全看不出。

通用规律:

  • 深色背景上,主色的透明度需要提高(10%→20%)
  • 浅色背景上,辅助色的透明度可以降低(20%→10%)

Demo 里的做法是浅色色板和深色色板分开定义,避免运行时算透明度。


八、状态栏颜色

深色模式切换后,状态栏颜色也要跟着改。通过窗口实例设置:

const mainWindow = AppStorage.get<window.Window>('mainWindow')
if (mainWindow) {
  mainWindow.setWindowSystemBarProperties({
    statusBarColor: this.isDark ? '#121212' : '#ffffff',
    statusBarContentColor: this.isDark ? '#e0e0e0' : '#333333'
  })
}

如果忘了改状态栏,深色页面顶着白色状态栏,割裂感极强。


写在最后

深色模式看着简单,做起来全是细节。颜色遗漏一个地方就穿帮,阴影不调就消失,图标不换就别扭。建议做深色模式的时候列一个颜色清单,改一个划一个,不然一定会漏。

另外,如果项目时间紧,优先保证核心页面的深色模式。设置页、关于页这些次要页面可以后做,但首页和主功能页面必须第一时间适配。

Logo

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

更多推荐