HarmonyOS鸿蒙Next开发中响应式主题如何设计?

HarmonyOS鸿蒙Next开发中响应式主题如何设计?

如何在HarmonyOS应用中实现响应式多主题系统,包括手动切换主题和跟随系统主题变化的功能

3 回复

本文档详细记录了如何在HarmonyOS应用中实现响应式多主题系统,包括手动切换主题和跟随系统主题变化的功能。

目录

  1. 项目结构
  2. 核心概念
  3. 实施步骤
  4. 代码详解
  5. 运行项目
  6. 常见问题
  7. ArkTS 语法注意事项

项目结构

在开始之前,让我们了解一下项目的文件结构:

demo/
├── entry/
│   └── src/
│       └── main/
│           └── ets/
│               ├── entryability/
│               │   └── EntryAbility.ets      # Ability入口,监听系统配置变化
│               ├── pages/
│               │   └── Index.ets             # 演示页面
│               ├── models/
│               │   └── ThemeModel.ets        # 主题模型核心逻辑
│               ├── utils/
│               │   └── Logger.ets            # 日志工具类
│               └── components/
│                   └── icons/
│                       └── ThemeToggleIcon.ets # 主题切换图标组件

核心概念

1. 主题类型

系统支持三种主题模式:

  • light(亮色主题):固定使用亮色配色方案
  • dark(暗色主题):固定使用暗色配色方案
  • system(跟随系统):自动跟随系统主题变化

2. 状态管理

使用 AppStorage 进行全局状态管理:

  • app_theme_type:当前主题类型(light/dark/system)
  • app_is_dark_mode:是否为暗色模式(boolean)
  • app_theme_colors:当前主题颜色配置(JSON字符串)
  • currentColorMode:系统颜色模式(从系统配置同步)

3. 响应式更新

通过以下机制实现响应式更新:

  • EntryAbility.onConfigurationUpdate:监听系统配置变化
  • @StorageProp:在组件中自动响应 AppStorage 变化
  • ThemeModel:统一管理主题状态和切换逻辑

实施步骤

步骤1:创建工具类 Logger

文件路径entry/src/main/ets/utils/Logger.ets

作用:提供统一的日志输出接口,方便调试和问题排查。

操作

  1. entry/src/main/ets/ 目录下创建 utils 文件夹
  2. 创建 Logger.ets 文件
  3. 复制以下代码:
/*
 * 日志工具类
 */

enum LogLevel {
  DEBUG = 0,
  INFO = 1,
  WARN = 2,
  ERROR = 3
}

export class Logger {
  private static readonly LOG_LEVEL: LogLevel = LogLevel.INFO
  static info(tag: string, message: string): void {
    if (LogLevel.INFO >= Logger.LOG_LEVEL) {
      console.info(`[${tag}] ${message}`)
    }
  }
  static error(tag: string, message: string, error?: Error): void {
    if (LogLevel.ERROR >= Logger.LOG_LEVEL) {
      console.error(`[${tag}] ${message}`, error || '')
    }
  }
}

步骤2:创建主题模型 ThemeModel

文件路径entry/src/main/ets/models/ThemeModel.ets

作用:核心主题管理类,负责主题切换、状态同步和系统主题监听。

关键代码片段

import { Logger } from '../utils/Logger'
import { common, ConfigurationConstant } from '@kit.AbilityKit'

let uiContext: common.UIAbilityContext | null = null

export function setUIContext(context: common.UIAbilityContext): void {
  uiContext = context
}

export type ThemeType = 'light' | 'dark' | 'system'

export interface ThemeColors {
  primary: string
  background: string
  onBackground: string
  surface: string
  card: string
  // ... 更多颜色定义
}

// 亮色主题配色
export const LightTheme: ThemeColors = {
  primary: '#2E7D32',
  background: '#FFFFFF',
  onBackground: '#212121',
  surface: '#F5F5F5',
  card: '#FFFFFF',
  // ... 完整配色
}

// 暗色主题配色
export const DarkTheme: ThemeColors = {
  primary: '#4CAF50',
  background: '#121212',
  onBackground: '#FFFFFF',
  surface: '#1E1E1E',
  card: '#2C2C2C',
  // ... 完整配色
}

@Observed
export class ThemeModel {
  themeType: ThemeType = 'system'
  isDarkMode: boolean = false
  currentThemeColors: ThemeColors = LightTheme
  constructor() {
    console.log('[ThemeModel] 📦 构造函数开始');
    
    // 确保立即设置默认主题颜色,避免初始化异步导致 undefined
    this.currentThemeColors = LightTheme
    this.themeType = 'system'
    this.isDarkMode = false
    
    console.log('[ThemeModel] 📦 构造函数完成,currentThemeColors:', this.currentThemeColors);
    // 异步初始化主题
    this.initTheme()
  }
  // 初始化主题
  async initTheme(): Promise<void> {
    const storedThemeType = AppStorage.get<ThemeType>('app_theme_type')
    if (storedThemeType) {
      this.themeType = storedThemeType
    } else {
      Logger.info('ThemeModel', '首次启动,使用system模式跟随系统')
    }
    
    await this.applyTheme(this.themeType)
    this.syncToAppStorage()
    
    Logger.info('ThemeModel', `主题初始化完成: type=${this.themeType}, isDark=${this.isDarkMode}`)
  }
  // 切换主题
  async toggleTheme(): Promise<void> {
    const nextTheme: ThemeType = this.isDarkMode ? 'light' : 'dark'
    await this.setTheme(nextTheme)
  }
  // 设置主题
  async setTheme(theme: ThemeType): Promise<void> {
    this.themeType = theme
    await this.applyTheme(theme)
    this.syncToAppStorage()
  }
  // 应用主题
  private async applyTheme(theme: ThemeType): Promise<void> {
    Logger.info('ThemeModel', `应用主题: ${theme}`)
    
    switch (theme) {
      case 'light':
        this.isDarkMode = false
        this.currentThemeColors = LightTheme
        break
      case 'dark':
        this.isDarkMode = true
        this.currentThemeColors = DarkTheme
        break
      case 'system':
        await this.followSystemTheme()
        break
    }
  }
  // 跟随系统主题
  private async followSystemTheme(): Promise<void> {
    Logger.info('ThemeModel', '=== 开始跟随系统主题 ===')
    const colorMode = AppStorage.get<number>('currentColorMode')
    
    Logger.info('ThemeModel', `从AppStorage读取系统颜色模式: ${colorMode}`)
    
    if (colorMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK) {
      this.isDarkMode = true
      this.currentThemeColors = DarkTheme
      Logger.info('ThemeModel', '✓ 系统处于暗色模式')
    } else {
      this.isDarkMode = false
      this.currentThemeColors = LightTheme
      Logger.info('ThemeModel', '✓ 系统处于亮色模式')
    }
    
    Logger.info('ThemeModel', `✓ 跟随系统主题完成: ${this.isDarkMode ? '暗色' : '亮色'} (colorMode: ${colorMode})`)
  }
  // 同步到 AppStorage
  private syncToAppStorage(): void {
    AppStorage.setOrCreate('app_theme_type', this.themeType)
    AppStorage.setOrCreate('app_is_dark_mode', this.isDarkMode)
    AppStorage.setOrCreate('app_theme_colors', JSON.stringify(this.currentThemeColors))
    
    Logger.info('ThemeModel', `主题状态已同步到 AppStorage: ${this.themeType}, isDark: ${this.isDarkMode}`)
  }
  // 系统配置变化回调
  async onConfigurationChanged(): Promise<void> {
    if (this.themeType === 'system') {
      await this.followSystemTheme()
      this.syncToAppStorage()
    }
  }
  
  // 其他辅助方法...
}

关键说明

  • 构造函数中立即设置默认值:确保对象创建后立即可用,避免异步初始化导致的 undefined 错误
  • @Observed 装饰器使类可被观察
  • followSystemTheme() 从 AppStorage 读取系统颜色模式
  • syncToAppStorage() 确保所有页面都能响应主题变化

步骤3:创建主题切换图标组件

文件路径entry/src/main/ets/components/icons/ThemeToggleIcon.ets

[@Component](/user/Component)
export struct ThemeToggleIcon {
  @Prop iconWidth: number = 24
  @Prop iconHeight: number = 24
  @Prop color: string = '#000000'
  @Prop isDarkMode: boolean = false
  build() {
    Stack() {
      if (this.isDarkMode) {
        // 太阳图标
        Path()
          .width(this.iconWidth)
          .height(this.iconHeight)
          .commands('M12 7c-2.76 0-5 2.24-5 5s2.24 5 5 5...')
          .fill(this.color)
      } else {
        // 月亮图标
        Path()
          .width(this.iconWidth)
          .height(this.iconHeight)
          .commands('M12 3c-4.97 0-9 4.03-9 9...')
          .fill(this.color)
      }
    }
  }
}

步骤4:更新 EntryAbility

文件路径entry/src/main/ets/entryability/EntryAbility.ets

关键代码

import { AbilityConstant, ConfigurationConstant, UIAbility, Want, Configuration } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';
import { Logger } from '../utils/Logger';
import { setUIContext } from '../models/ThemeModel';
import { ThemeModel } from '../models/ThemeModel';

export default class EntryAbility extends UIAbility {
  private themeModel: ThemeModel | null = null;
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate');
    setUIContext(this.context);
    const colorMode = this.context.config.colorMode;
    AppStorage.setOrCreate('currentColorMode', colorMode);
    Logger.info('EntryAbility', `当前系统颜色模式: ${colorMode}`);
    this.initializeTheme();
  }
  private async initializeTheme(): Promise<void> {
    try {
      this.themeModel = new ThemeModel();
      await this.themeModel.initTheme();
      Logger.info('EntryAbility', '主题模型初始化成功');
    } catch (error) {
      Logger.error('EntryAbility', '主题模型初始化失败:', error as Error);
    }
  }
  onConfigurationUpdate(newConfig: Configuration): void {
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onConfigurationUpdate');
    try {
      if (newConfig.colorMode !== undefined) {
        Logger.info('EntryAbility', `系统颜色模式变化: ${newConfig.colorMode}`);
        AppStorage.setOrCreate('currentColorMode', newConfig.colorMode);
        Logger.info('EntryAbility', `已同步颜色模式到 AppStorage: ${newConfig.colorMode}`);
        if (this.themeModel) {
          this.themeModel.onConfigurationChanged();
        }
      }
    } catch (error) {
      Logger.error('EntryAbility', '处理配置变化失败:', error as Error);
    }
  }
  // ... 其他生命周期方法
}

关键说明

  • setUIContext() 将 UI 上下文传递给 ThemeModel
  • onConfigurationUpdate 是系统回调,响应系统配置变化
  • 必须将系统颜色模式同步到 AppStorage

步骤5:创建演示页面

文件路径entry/src/main/ets/pages/Index.ets

⚠️ 重要:ArkTS 语法限制

ArkTS 不支持 ES6 的 get 关键字定义 getter!必须使用普通方法。

import { ThemeModel, ThemeColors, LightTheme } from '../models/ThemeModel';
import { ThemeToggleIcon } from '../components/icons/ThemeToggleIcon';

[@Entry](/user/Entry)
[@Component](/user/Component)
struct Index {
  [@State](/user/State) themeModel: ThemeModel = new ThemeModel();
  [@StorageProp](/user/StorageProp)('app_is_dark_mode') isDarkMode: boolean = false;
  [@StorageProp](/user/StorageProp)('app_theme_colors') themeColorsJson: string = '';
  // ✅ 正确:使用普通方法而不是 get getter(ArkTS 语法要求)
  getCurrentThemeColors(): ThemeColors {
    console.log('[Index] 🔍 getCurrentThemeColors - 开始');
    
    // 优先从 AppStorage 读取
    if (this.themeColorsJson && this.themeColorsJson.trim() !== '') {
      try {
        const colors = JSON.parse(this.themeColorsJson) as ThemeColors;
        if (colors && colors.background) {
          console.log('[Index] ✅ 从 AppStorage 返回颜色');
          return colors;
        }
      } catch (e) {
        console.log('[Index] ⚠️ JSON 解析失败:', e);
      }
    }
    
    // 如果 AppStorage 中没有,尝试使用 themeModel 的颜色
    if (this.themeModel && 
        this.themeModel.currentThemeColors && 
        this.themeModel.currentThemeColors.background) {
      console.log('[Index] ✅ 从 themeModel 返回颜色');
      return this.themeModel.currentThemeColors;
    }
    
    // 最后使用默认的亮色主题
    console.log('[Index] ✅ 返回默认 LightTheme');
    return LightTheme;
  }
  async aboutToAppear() {
    console.log('[Index] 📱 aboutToAppear 开始');
    try {
      await this.themeModel.loadThemeSettings();
      this.updateThemeFromStorage();
    } catch (error) {
      console.error('主题初始化失败,使用默认主题', error);
    }
  }
  updateThemeFromStorage(): void {
    const hasChanged = this.themeModel.updateFromAppStorage();
  }
  build() {
    Column() {
      // 顶部导航栏
      Row() {
        Text('主题演示')
          .fontSize(24)
          .fontWeight(FontWeight.Bold)
          // ✅ 调用方法获取颜色
          .fontColor(this.getCurrentThemeColors().onBackground)
          .layoutWeight(1)
          .textAlign(TextAlign.Center)
        // 主题切换按钮
        Button() {
          ThemeToggleIcon({
            iconWidth: 24,
            iconHeight: 24,
            color: this.getCurrentThemeColors().onBackground,
            isDarkMode: this.isDarkMode
          })
        }
        .type(ButtonType.Circle)
        .width(40)
        .height(40)
        .backgroundColor(Color.Transparent)
        .onClick(async () => {
          await this.themeModel.toggleTheme();
        })
      }
      .width('100%')
      .height(60)
      .padding({ left: 16, right: 16 })
      .backgroundColor(this.getCurrentThemeColors().surface)
      // 主内容区域
      Scroll() {
        Column({ space: 20 }) {
          this.buildThemeInfoCard()
          this.buildColorShowcaseCard()
          this.buildActionButtons()
        }
        .width('100%')
        .padding(16)
      }
      .layoutWeight(1)
      .width('100%')
    }
    .width('100%')
    .height('100%')
    .backgroundColor(this.getCurrentThemeColors().background)
  }
  [@Builder](/user/Builder)
  buildThemeInfoCard() {
    Column({ space: 12 }) {
      Text('当前主题信息')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor(this.getCurrentThemeColors().onSurface)
        .width('100%')
      Row() {
        Text('主题类型:')
          .fontSize(14)
          .fontColor(this.getCurrentThemeColors().onSurfaceVariant)
        Text(this.themeModel.getThemeDescription())
          .fontSize(14)
          .fontWeight(FontWeight.Medium)
          .fontColor(this.getCurrentThemeColors().primary)
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceBetween)
      Row() {
        Text('当前模式:')
          .fontSize(14)
          .fontColor(this.getCurrentThemeColors().onSurfaceVariant)
        Text(this.isDarkMode ? '暗色模式' : '亮色模式')
          .fontSize(14)
          .fontWeight(FontWeight.Medium)
          .fontColor(this.getCurrentThemeColors().secondary)
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceBetween)
    }
    .width('100%')
    .padding(16)
    .backgroundColor(this.getCurrentThemeColors().card)
    .borderRadius(12)
    .shadow({
      radius: 8,
      color: this.getCurrentThemeColors().shadow,
      offsetX: 0,
      offsetY: 2
    })
  }
  [@Builder](/user/Builder)
  buildColorShowcaseCard() {
    Column({ space: 12 }) {
      Text('主题颜色展示')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor(this.getCurrentThemeColors().onSurface)
        .width('100%')
      Row({ space: 12 }) {
        Column() {
          Text('主色')
            .fontSize(12)
            .fontColor('#FFFFFF')
        }
        .width('30%')
        .height(60)
        .backgroundColor(this.getCurrentThemeColors().primary)
        .borderRadius(8)
        .justifyContent(FlexAlign.Center)
        
        // 更多颜色展示...
      }
      .width('100%')
    }
    .width('100%')
    .padding(16)
    .backgroundColor(this.getCurrentThemeColors().card)
    .borderRadius

更多关于HarmonyOS鸿蒙Next开发中响应式主题如何设计?的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


在HarmonyOS Next中,响应式主题设计主要依赖ArkUI的UI状态管理机制。通过使用@State、@Prop、@Link等装饰器管理组件状态,结合媒体查询(如@ohos.mediaquery)监听设备屏幕变化,动态调整布局与主题变量。主题资源可预定义在resource目录下,利用系统提供的暗色/亮色模式适配能力,通过环境变量(如this.context.config.colorMode)自动切换。

在HarmonyOS Next中,设计响应式主题系统主要依赖其强大的UI框架和状态管理机制。以下是核心实现方案:

  1. 资源定义与组织resources目录下按主题分类(如basedarklight)定义颜色、样式等资源。使用ResourceManager进行动态加载。

  2. 主题状态管理 通过AppStorageLocalStorage全局管理当前主题状态(如'light''dark'、‘system’),并建立与系统主题(settings.getDisplayMode())的监听绑定。

  3. 组件响应式设计 在UI组件中使用资源引用($r('app.color.background'))或条件渲染,结合@State@Prop等装饰器监听主题状态变化,实现界面自动更新。

  4. 手动切换与系统跟随

    • 手动切换:通过用户操作更新AppStorage中的主题状态。
    • 系统跟随:监听系统主题变化事件(displayModeChange),自动同步到应用内部状态。
  5. 性能优化 建议使用增量更新机制,避免主题切换时全量重建UI。可对复杂组件做主题缓存或懒加载处理。

此方案能实现灵活、高效的主题切换,并保持与系统主题的实时同步。

回到顶部