HarmonyOS NEXT 实战:主题切换与 UI 优化

前言

良好的主题系统是提升应用品质感的基础。HarmonyExplorer 实现了浅色、深色、自动三种主题模式,配合 ThemeUtil 工具类、AppStorage 全局状态与切换动画,打造一致的视觉体验。主题系统的核心难点在于全局状态同步与多模式下的 UI 适配,既要保证切换流畅,又要让每个组件都能正确响应主题变化。本文将完整拆解主题系统设计、ThemeUtil 封装、颜色定义、动画与 UI 一致性优化的落地实践。

提示:本文代码基于 HarmonyOS NEXT(API 12+)ArkTS 严格模式编写,禁用 any 类型与隐式断言,所有主题数据均使用命名接口显式声明。

一、主题系统设计

1.1 设计目标

HarmonyExplorer 的主题系统需要满足以下设计目标,保证用户体验与可维护性并重。

  1. 支持浅色、深色、跟随系统三种模式
  2. 主题切换实时生效,无需重启应用
  3. 主题配置持久化,下次启动自动恢复
  4. 颜色与字号统一管理,保证视觉一致性
  5. 组件自动响应主题变化,无需逐个手动处理

设计目标的核心诉求是"一处配置、全局生效",即用户在设置页切换主题后,所有页面与组件应立即同步更新,且配置在应用重启后依然保留。

1.2 主题架构

主题系统采用 ThemeUtil 管理 + AppStorage 全局状态 + 组件监听的架构,状态变更自动驱动 UI 刷新。

  • ThemeUtil:主题工具类,负责模式切换、持久化与颜色获取
  • AppStorage:全局主题状态存储,组件通过 @StorageLink 监听
  • constants:主题颜色与字号常量定义
  • 组件层:通过 @StorageLink / @StorageProp 自动响应主题

提示:AppStorage 是 ArkUI 提供的全局状态容器,配合 @StorageLink 可实现跨组件的自动状态同步,是主题系统的理想载体。

二、浅色/深色/自动模式

2.1 模式定义

主题模式通过枚举统一定义,自动模式根据系统配置动态决定实际生效的浅色或深色主题。自动模式在系统切换深浅色时,应用能够实时感知并跟随变化,无需用户手动干预,是最省心的默认选项。

// constants/ThemeConstants.ets
export enum ThemeMode {
  LIGHT = 'light',
  DARK = 'dark',
  AUTO = 'auto'
}

export interface ThemeColors {
  background: string
  surface: string
  primary: string
  textPrimary: string
  textSecondary: string
  divider: string
  cardBackground: string
  shadow: string
}

2.2 模式切换

模式切换时更新 AppStorage 状态并持久化到 Preferences,下次启动自动恢复。

// utils/ThemeUtil.ets
import { preferences } from '@kit.ArkData'
import { ThemeMode } from '../constants/ThemeConstants'

export class ThemeUtil {
  static readonly KEY_THEME_MODE: string = 'theme_mode'

  static async setMode(mode: ThemeMode): Promise<void> {
    AppStorage.setOrCreate<string>('themeMode', mode)
    const pref: preferences.Preferences = await preferences.getPreferences(getContext(), 'theme_pref')
    await pref.put(ThemeUtil.KEY_THEME_MODE, mode)
    await pref.flush()
  }

  static async getMode(): Promise<ThemeMode> {
    const pref: preferences.Preferences = await preferences.getPreferences(getContext(), 'theme_pref')
    const value: preferences.ValueType = await pref.get(ThemeUtil.KEY_THEME_MODE, ThemeMode.AUTO)
    return ThemeUtil.toThemeMode(value.toString())
  }

  static toThemeMode(value: string): ThemeMode {
    if (value === ThemeMode.LIGHT) {
      return ThemeMode.LIGHT
    }
    if (value === ThemeMode.DARK) {
      return ThemeMode.DARK
    }
    return ThemeMode.AUTO
  }
}

三种主题模式的特点对比如下表,用户可在设置页自由选择:

模式 行为 适用场景
浅色(LIGHT) 固定浅色主题 日间明亮环境
深色(DARK) 固定深色主题 夜间护眼
自动(AUTO) 跟随系统模式 省心默认

三、ThemeUtil 工具类封装

3.1 封装实现

ThemeUtil 整合模式管理、颜色获取与系统监听,对外提供统一的主题操作入口。ThemeUtil 是主题系统的中枢,所有主题相关逻辑都应在此收敛

// utils/ThemeUtil.ets
import { preferences } from '@kit.ArkData'
import { ThemeMode, ThemeColors } from '../constants/ThemeConstants'
import { LightColors, DarkColors } from '../constants/ColorPalette'

export class ThemeUtil {
  static async init(): Promise<void> {
    const mode: ThemeMode = await ThemeUtil.getMode()
    AppStorage.setOrCreate<string>('themeMode', mode)
    const colors: ThemeColors = ThemeUtil.resolveColors(mode)
    AppStorage.setOrCreate<ThemeColors>('themeColors', colors)
  }

  static resolveColors(mode: ThemeMode): ThemeColors {
    if (mode === ThemeMode.DARK) {
      return DarkColors
    }
    return LightColors
  }

  static async applyMode(mode: ThemeMode): Promise<void> {
    await ThemeUtil.setMode(mode)
    const colors: ThemeColors = ThemeUtil.resolveColors(mode)
    AppStorage.setOrCreate<ThemeColors>('themeColors', colors)
  }
}

四、主题颜色定义

4.1 颜色常量

浅色与深色主题的颜色常量分别定义,保持语义化命名,便于组件统一引用。

// constants/ColorPalette.ets
import { ThemeColors } from './ThemeConstants'

export const LightColors: ThemeColors = {
  background: '#F5F5F5',
  surface: '#FFFFFF',
  primary: '#007DFF',
  textPrimary: '#333333',
  textSecondary: '#999999',
  divider: '#EEEEEE',
  cardBackground: '#FFFFFF',
  shadow: '#1A000000'
}

export const DarkColors: ThemeColors = {
  background: '#121212',
  surface: '#1E1E1E',
  primary: '#0A59F7',
  textPrimary: '#E6E6E6',
  textSecondary: '#999999',
  divider: '#333333',
  cardBackground: '#1E1E1E',
  shadow: '#33000000'
}

提示:颜色常量集中管理是保证主题一致性的基础,禁止在组件中硬编码颜色值,必须引用 ColorPalette 常量。

五、AppStorage 全局主题状态

5.1 状态管理

主题颜色存储在 AppStorage 中,组件通过 @StorageLink 监听变化,实现自动刷新。AppStorage 全局状态让主题切换一处修改、全局生效

@StorageLink 与 @StorageProp 的区别在于:前者双向同步,组件修改会回写 AppStorage;后者单向只读,适合仅展示主题色的场景。主题系统主要使用 @StorageLink,保证切换时全量组件同步更新。

// pages/HomePage.ets
import { ThemeColors } from '../constants/ThemeConstants'

@Entry
@Component
struct HomePage {
  @StorageLink('themeColors') themeColors: ThemeColors = {
    background: '#F5F5F5', surface: '#FFFFFF', primary: '#007DFF',
    textPrimary: '#333333', textSecondary: '#999999', divider: '#EEEEEE',
    cardBackground: '#FFFFFF', shadow: '#1A000000'
  }

  build() {
    Column() {
      Text('HarmonyExplorer')
        .fontSize(20)
        .fontColor(this.themeColors.textPrimary)
      Text('文件管理与效率工具')
        .fontSize(14)
        .fontColor(this.themeColors.textSecondary)
    }
    .width('100%')
    .height('100%')
    .backgroundColor(this.themeColors.background)
    .padding(16)
  }
}

六、主题切换动画

6.1 动画实现

主题切换时通过显式动画过渡颜色变化,避免生硬跳变,提升切换的流畅感与品质感。切换动画包含两个层面:一是按钮按压的缩放反馈,二是颜色过渡的渐变效果,两者配合让切换过程自然顺滑。

动画实现的关键点在于时序控制:先触发缩放动画给出操作反馈,再异步执行主题应用,最后恢复缩放状态,形成完整的交互闭环。

// components/ThemeToggle.ets
import { ThemeMode, ThemeColors } from '../constants/ThemeConstants'
import { ThemeUtil } from '../utils/ThemeUtil'

@Component
export struct ThemeToggle {
  @StorageLink('themeMode') currentMode: string = ThemeMode.AUTO
  @State animScale: number = 1.0

  build() {
    Row({ space: 8 }) {
      Button('浅色')
        .backgroundColor(this.currentMode === ThemeMode.LIGHT ? '#007DFF' : '#EEEEEE')
        .onClick(() => this.toggle(ThemeMode.LIGHT))
      Button('深色')
        .backgroundColor(this.currentMode === ThemeMode.DARK ? '#007DFF' : '#EEEEEE')
        .onClick(() => this.toggle(ThemeMode.DARK))
      Button('自动')
        .backgroundColor(this.currentMode === ThemeMode.AUTO ? '#007DFF' : '#EEEEEE')
        .onClick(() => this.toggle(ThemeMode.AUTO))
    }
    .scale({ x: this.animScale, y: this.animScale })
    .animation({ duration: 250, curve: Curve.EaseInOut })
  }

  toggle(mode: ThemeMode): void {
    this.animScale = 0.92
    ThemeUtil.applyMode(mode)
    setTimeout(() => { this.animScale = 1.0 }, 150)
  }
}

七、卡片布局优化

7.1 布局优化

FileCard、StorageCard 等卡片组件通过主题色绑定与统一间距规范,保证不同模式下布局一致。卡片是文件管理应用最核心的视觉单元,布局优化直接影响整体观感。卡片组件统一引用 themeColors 中的背景、文字与阴影色,主题切换时由 @StorageLink 自动驱动刷新,无需额外处理。

布局优化遵循"间距统一、圆角统一、阴影自适应"三原则,所有卡片采用 12 圆角与 12 间距基准,深色模式下阴影自动切换为柔和参数,保证层次感的同时避免过强对比。

// components/FileCard.ets
import { ThemeColors } from '../constants/ThemeConstants'
import { LightColors } from '../constants/ColorPalette'

@Component
export struct FileCard {
  @StorageLink('themeColors') themeColors: ThemeColors = LightColors
  @Prop fileName: string
  @Prop fileSize: string

  build() {
    Row({ space: 12 }) {
      Image($r('app.media.ic_file'))
        .width(40).height(40)
      Column({ space: 4 }) {
        Text(this.fileName)
          .fontSize(14)
          .fontColor(this.themeColors.textPrimary)
          .maxLines(1)
        Text(this.fileSize)
          .fontSize(12)
          .fontColor(this.themeColors.textSecondary)
      }.layoutWeight(1).alignItems(HorizontalAlign.Start)
    }
    .width('100%')
    .padding(12)
    .backgroundColor(this.themeColors.cardBackground)
    .borderRadius(12)
    .shadow({ radius: 8, color: this.themeColors.shadow, offsetX: 0, offsetY: 2 })
  }
}

八、阴影效果调整

8.1 阴影适配

浅色与深色主题对阴影的感知不同,深色主题下需要更柔和的阴影,避免过强对比显得突兀。阴影参数对照如下表:

主题 阴影颜色 阴影半径 视觉效果
浅色 #1A000000 8 清晰层次
深色 #33000000 12 柔和过渡
// constants/ColorPalette.ets(续)
export interface ShadowConfig {
  radius: number
  color: string
  offsetX: number
  offsetY: number
}

export const LightShadow: ShadowConfig = {
  radius: 8, color: '#1A000000', offsetX: 0, offsetY: 2
}

export const DarkShadow: ShadowConfig = {
  radius: 12, color: '#33000000', offsetX: 0, offsetY: 2
}

阴影配置通过 ThemeUtil 在主题切换时同步更新到 AppStorage,卡片组件读取当前主题对应的 ShadowConfig 应用阴影,实现深浅模式下阴影的自动适配。

九、字体大小适配

9.1 字体适配

HarmonyExplorer 定义统一的字号常量,配合系统字体缩放设置,保证不同用户群体的可读性。字号体系按语义分为五级,从标题到微标逐级递减,覆盖页面中所有文本场景。

字号适配还兼顾无障碍需求,通过 FontScale 系数支持小、标准、大三档缩放,老年用户或视力不佳用户可切换到大字号模式,提升信息获取效率。

// constants/FontSize.ets
export class FontSize {
  static readonly TITLE: number = 20
  static readonly HEADING: number = 16
  static readonly BODY: number = 14
  static readonly CAPTION: number = 12
  static readonly MICRO: number = 10
}

export interface FontScale {
  small: number
  standard: number
  large: number
}

export const FontScaleConfig: FontScale = {
  small: 0.9,
  standard: 1.0,
  large: 1.15
}

字号常量与缩放系数配合使用,组件中通过 FontSize.BODY 乘以当前 FontScale 系数得到最终渲染字号。字号使用规范如下表,组件按语义引用对应常量,禁止硬编码字号:

语义 常量 字号 使用场景
标题 TITLE 20 页面主标题
标题段 HEADING 16 章节标题
正文 BODY 14 主要内容
说明 CAPTION 12 辅助说明
微标 MICRO 10 角标标签

提示:字号常量集中定义便于全局调整,若需支持无障碍大字号,可在 ThemeUtil 中乘以 FontScale 系数动态计算。

十、UI 一致性检查

10.1 一致性规范

为保证全应用视觉统一,HarmonyExplorer 制定 UI 一致性检查清单,开发与评审时逐项核对。一致性检查应贯穿开发全流程,从设计稿评审到代码审查再到真机验收,每个环节都需对照清单执行,避免主题适配遗漏导致深色模式下的显示异常。

  1. 所有颜色引用 ColorPalette 常量,禁止硬编码
  2. 所有字号引用 FontSize 常量,禁止硬编码
  3. 卡片圆角统一为 12,间距统一为 12 或 16
  4. 主题切换通过 @StorageLink 自动响应,无残留硬编码色
  5. 深色模式下阴影与对比度符合规范

在这里插入图片描述

总结

本文完整实现了 HarmonyExplorer 的主题系统,涵盖主题模式设计、ThemeUtil 封装、颜色常量定义、AppStorage 全局状态、切换动画与 UI 一致性优化。AppStorage 全局状态配合 @StorageLink 实现了一处修改、全局生效的主题切换体验,统一的颜色与字号常量也保证了视觉一致性。希望这套方案能帮助你在鸿蒙项目中构建可维护的主题系统。

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!

相关资源

Logo

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

更多推荐