为文章《HarmonyOS7 创意实战:深色/浅色主题切换的丝滑实现》绘制一张手绘笔记风信息图,主题

前言

深色模式已经从"可选"变成了"标配"。用户在晚上刷手机的时候,一个白得刺眼的页面能直接把好感度打到零。iOS 和 Android 都原生支持深色模式,HarmonyOS 也不例外。但很多场景下,我们需要在应用内提供手动切换的能力,让用户自己选。

这篇文章实现一个深色/浅色主题的实时切换效果,带过渡动画,所有元素的颜色同步变化。

灵感来源

很多优秀应用都有主题切换功能——微信的"深色模式"、知乎的"夜间模式"、GitHub 的 Dark Theme。它们的共同特点是:一键切换、全局生效、过渡平滑。这个案例就模仿这种体验。

设计思路

为文章中“设计思路”部分绘制一张手绘框架图,标题围绕“主题切换的三种方案”。画面分成三列对比:系统跟

主题切换的核心方案有两种:

方案 原理 优缺点
系统跟随 使用 $r() 资源引用,跟随系统配置 简单但不能手动切换
状态驱动 @State isDarkMode 控制所有颜色值 灵活,可手动切换
配置驱动 定义主题对象,整体切换 扩展性好但复杂度高

这个案例用状态驱动方案——一个 @State isDarkMode: boolean 控制所有 UI 元素的颜色。简单直接,适合中小规模项目。

实现过程

主题色彩定义

为文章中“实现过程”和“核心切换逻辑”绘制一张手绘流程图,表现 HarmonyOS7 深色/浅色主题

先定义深色和浅色两套颜色方案:

浅色模式:
- 背景色: #F5F6FA
- 卡片色: #FFFFFF
- 主文本: #1A1A1A / #333333
- 次文本: #666666
- 分隔线: #F0F0F0

深色模式:
- 背景色: #121212
- 卡片色: #1E1E1E
- 主文本: #FFFFFF / #CCCCCC
- 次文本: #999999
- 分隔线: #333333

核心切换逻辑

用一个 Toggle 开关控制 isDarkMode 状态:

Toggle({ type: ToggleType.Switch, isOn: $$this.isDarkMode })
  .selectedColor('#4D96FF')

然后在每个 UI 元素上,根据 this.isDarkMode 选择对应颜色:

Text('设置项')
  .fontColor(this.isDarkMode ? '#CCCCCC' : '#1A1A1A')

Column()
  .backgroundColor(this.isDarkMode ? '#1E1E1E' : '#FFFFFF')

添加过渡动画

关键来了——加上 .animation() 让颜色变化有平滑过渡:

Column()
  .backgroundColor(this.isDarkMode ? '#1E1E1E' : '#FFFFFF')
  .animation({ duration: 300, curve: Curve.EaseInOut })

这样切换时颜色不会"啪"一下变掉,而是用 300ms 平滑过渡。

完整页面代码

@Entry
@Component
struct ThemeSwitchPage {
  @State isDarkMode: boolean = false

  build() {
    Column() {
      Column() {
        Row() {
          Text('主题切换')
            .fontSize(18).fontWeight(FontWeight.Bold)
            .fontColor(this.isDarkMode ? '#FFFFFF' : '#1A1A1A')
            .layoutWeight(1)
          Toggle({ type: ToggleType.Switch, isOn: $$this.isDarkMode })
            .selectedColor('#4D96FF')
        }
        .width('100%')

        Scroll() {
          Column() {
            // 设置列表卡片
            Column() {
              Text('设置项')
                .fontSize(14).fontWeight(FontWeight.Medium)
                .fontColor(this.isDarkMode ? '#CCCCCC' : '#1A1A1A')
                .margin({ bottom: 12 })

              ForEach([
                '个人资料', '账号安全', '通知设置',
                '隐私权限', '通用设置', '关于应用'
              ], (item: string) => {
                Row() {
                  Text(item)
                    .fontSize(14)
                    .fontColor(this.isDarkMode ? '#CCCCCC' : '#333333')
                    .layoutWeight(1)
                  Text('>')
                    .fontSize(14)
                    .fontColor(this.isDarkMode ? '#666666' : '#CCCCCC')
                }
                .width('100%').padding(14)
                .border({
                  width: { bottom: 0.5 },
                  color: this.isDarkMode ? '#333333' : '#F0F0F0'
                })
              })
            }
            .width('100%')
            .backgroundColor(this.isDarkMode ? '#1E1E1E' : '#FFFFFF')
            .borderRadius(12).padding(16)
            .animation({ duration: 300, curve: Curve.EaseInOut })

            // 预览卡片
            Column() {
              Text('主题预览卡片')
                .fontSize(14).fontWeight(FontWeight.Medium)
                .fontColor(this.isDarkMode ? '#CCCCCC' : '#1A1A1A')
                .margin({ bottom: 12 })

              Column() {
                Text('卡片标题')
                  .fontSize(16).fontWeight(FontWeight.Bold)
                  .fontColor(this.isDarkMode ? '#FFFFFF' : '#1A1A1A')
                Text('这是卡片的描述内容,展示主题切换时文字颜色的变化')
                  .fontSize(13)
                  .fontColor(this.isDarkMode ? '#999999' : '#666666')
                  .margin({ top: 6 })
                Button('操作按钮')
                  .width('100%').margin({ top: 12 })
              }
              .width('100%')
              .backgroundColor(this.isDarkMode ? '#2A2A2A' : '#F8F9FA')
              .borderRadius(12).padding(16)
            }
            .width('100%')
            .backgroundColor(this.isDarkMode ? '#1E1E1E' : '#FFFFFF')
            .borderRadius(12).padding(16).margin({ top: 10 })
            .animation({ duration: 300, curve: Curve.EaseInOut })
          }
          .width('100%')
        }
        .layoutWeight(1)
      }
      .width('100%').height('100%')
      .backgroundColor(this.isDarkMode ? '#121212' : '#F5F6FA')
      .padding(16)
      .animation({ duration: 300, curve: Curve.EaseInOut })
    }
    .width('100%').height('100%')
    .backgroundColor(this.isDarkMode ? '#121212' : '#F5F6FA')
  }
}

效果调优

三元表达式太多怎么办

页面里到处都是 this.isDarkMode ? 'darkColor' : 'lightColor' 这种三元表达式,看着很乱。可以封装一个主题管理器:

class ThemeManager {
  static colors(isDark: boolean) {
    return {
      bg: isDark ? '#121212' : '#F5F6FA',
      card: isDark ? '#1E1E1E' : '#FFFFFF',
      primaryText: isDark ? '#FFFFFF' : '#1A1A1A',
      secondaryText: isDark ? '#999999' : '#666666',
      divider: isDark ? '#333333' : '#F0F0F0',
      subCard: isDark ? '#2A2A2A' : '#F8F9FA',
    }
  }
}

// 使用
const theme = ThemeManager.colors(this.isDarkMode)
Text('标题').fontColor(theme.primaryText)
Column().backgroundColor(theme.card)

这样代码清爽很多,颜色值也集中管理了。

配合 @Provide 全局共享主题

isDarkMode@Provide 共享,所有后代组件都能消费:

@Entry
@Component
struct AppRoot {
  @Provide isDarkMode: boolean = false
  // ...
}

@Component
struct AnyChildComponent {
  @Consume isDarkMode: boolean
  // 直接使用 this.isDarkMode
}

脑洞扩展

多主题支持

不只是深色/浅色,还可以做"护眼模式"、"高对比度"等:

enum ThemeType { Light, Dark, EyeProtection, HighContrast }
@State currentTheme: ThemeType = ThemeType.Light

记住用户选择

@StorageLink 持久化主题选择:

@StorageLink('app_theme') isDarkMode: boolean = false

下次打开应用自动应用用户上次的选择。

跟随系统

监听系统深色模式变化:

aboutToAppear(): void {
  const config = getContext(this).config
  this.isDarkMode = config.colorMode === ColorMode.COLOR_MODE_DARK
}

写在最后

主题切换是提升应用品质的一个低成本高回报功能。用一个 @State 变量驱动所有颜色值,加上 animation 做平滑过渡,就能实现丝滑的深浅色切换。如果项目规模较大,建议封装一个主题管理器,把颜色值集中定义和管理。

Logo

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

更多推荐