一文中走进HarmonyOS APP开发中的系统主题

举报
Jack20 发表于 2026/06/20 16:11:52 2026/06/20
【摘要】 HarmonyOS APP开发中的系统主题:深色/浅色模式、系统主题监听、自定义主题、主题切换动画、无障碍主题适配📌 核心要点:通过监听系统主题变化、构建多主题资源体系和优雅的切换动画,实现深色/浅色模式的无缝适配与无障碍友好体验 一、背景与动机你有没有在深夜打开一个只有白色背景的应用?那感觉就像有人在你眼前打开了一盏探照灯——瞳孔瞬间收缩,眼泪差点流出来。然后你手忙脚乱地找"深色模式"...

HarmonyOS APP开发中的系统主题:深色/浅色模式、系统主题监听、自定义主题、主题切换动画、无障碍主题适配

📌 核心要点:通过监听系统主题变化、构建多主题资源体系和优雅的切换动画,实现深色/浅色模式的无缝适配与无障碍友好体验


一、背景与动机

你有没有在深夜打开一个只有白色背景的应用?那感觉就像有人在你眼前打开了一盏探照灯——瞳孔瞬间收缩,眼泪差点流出来。然后你手忙脚乱地找"深色模式"开关,翻遍设置页也没找到,最后只能默默把手机亮度调到最低。

这就是"没有主题适配"的代价。

深色模式早已不是"锦上添花"的功能,而是"必须拥有"的基础体验。HarmonyOS 从系统层面支持深色/浅色模式切换,用户可以在设置中一键切换,所有适配了的应用都会跟着变。但如果你不做适配,你的应用就会成为那个"深夜探照灯"。

主题适配不仅仅是换一套颜色那么简单。它涉及资源管理、状态监听、动画过渡、无障碍支持等多个维度。今天我们就来系统地搞懂它。


二、核心原理

2.1 主题系统架构

HarmonyOS 的主题系统基于资源限定词机制。系统会根据当前的主题模式(dark/light)自动选择对应的资源文件。

flowchart TD
    A[系统主题切换] --> B{当前模式}
    B --> C[浅色模式 light]
    B --> D[深色模式 dark]
    
    C --> E[加载 resources/base<br/>默认资源]
    D --> F[加载 resources/dark<br/>深色资源]
    
    E --> G[$r引用资源]
    F --> G
    
    G --> H{资源类型}
    H --> I[颜色 color.json]
    H --> J[图片 media]
    H --> K[字符串 string.json]
    H --> L[尺寸 float.json]
    
    I --> M[自动匹配当前主题]
    J --> M
    K --> M
    L --> M
    
    M --> N[UI自动刷新]
    
    style A fill:#4CAF50,stroke:#388E3C,color:#fff
    style B fill:#2196F3,stroke:#1976D2,color:#fff
    style C fill:#FF9800,stroke:#F57C00,color:#fff
    style D fill:#9C27B0,stroke:#7B1FA2,color:#fff
    style E fill:#FF9800,stroke:#F57C00,color:#fff
    style F fill:#9C27B0,stroke:#7B1FA2,color:#fff
    style G fill:#F44336,stroke:#D32F2F,color:#fff
    style H fill:#2196F3,stroke:#1976D2,color:#fff
    style I fill:#4CAF50,stroke:#388E3C,color:#fff
    style J fill:#4CAF50,stroke:#388E3C,color:#fff
    style K fill:#4CAF50,stroke:#388E3C,color:#fff
    style L fill:#4CAF50,stroke:#388E3C,color:#fff
    style M fill:#FF9800,stroke:#F57C00,color:#fff
    style N fill:#4CAF50,stroke:#388E3C,color:#fff

2.2 资源限定词目录结构

resources/
├── base/                    # 默认资源(浅色模式)
│   ├── element/
│   │   ├── color.json       # 默认颜色
│   │   ├── string.json      # 默认字符串
│   │   └── float.json       # 默认尺寸
│   └── media/               # 默认图片
│       └── icon.png
├── dark/                    # 深色模式资源
│   ├── element/
│   │   ├── color.json       # 深色颜色
│   │   └── float.json       # 深色尺寸(可选)
│   └── media/               # 深色图片
│       └── icon.png         # 深色版图标
└── rawfile/                 # 原始文件

2.3 颜色资源定义

base/element/color.json(浅色模式):

{
  "color": [
    { "name": "page_background", "value": "#F5F5F5" },
    { "name": "text_primary", "value": "#212121" },
    { "name": "text_secondary", "value": "#757575" },
    { "name": "card_background", "value": "#FFFFFF" },
    { "name": "divider_color", "value": "#E0E0E0" },
    { "name": "accent_color", "value": "#4CAF50" }
  ]
}

dark/element/color.json(深色模式):

{
  "color": [
    { "name": "page_background", "value": "#121212" },
    { "name": "text_primary", "value": "#E0E0E0" },
    { "name": "text_secondary", "value": "#9E9E9E" },
    { "name": "card_background", "value": "#1E1E2E" },
    { "name": "divider_color", "value": "#2A2A3E" },
    { "name": "accent_color", "value": "#66BB6A" }
  ]
}

2.4 主题切换的触发方式

  1. 系统设置切换:用户在"设置 > 显示与亮度"中切换深色模式
  2. 应用内切换:应用提供主题切换开关
  3. 自动切换:根据时间(日落后自动切换深色模式)

三、代码实战

3.1 基于资源限定词的自动主题适配

最简单也最推荐的方式——通过 $r() 引用资源,系统自动根据当前主题选择对应的值。

// AutoThemeDemo.ets
// 基于资源限定词的自动主题适配

@Entry
@Component
struct AutoThemeDemo {
  // 当前是否深色模式
  @State isDarkMode: boolean = false;

  aboutToAppear(): void {
    // 获取当前系统主题
    this.isDarkMode = this.getSystemTheme();
  }

  // 获取系统主题
  private getSystemTheme(): boolean {
    const config = getContext(this).resourceManager.getConfiguration();
    return config.colorMode === 1; // 0=浅色, 1=深色
  }

  build() {
    Column() {
      // 标题区域 - 使用$r()引用资源
      Column() {
        Text('自动主题适配')
          .fontSize(24)
          .fontWeight(FontWeight.Bold)
          // 通过$r()引用颜色,系统自动选择浅色/深色值
          .fontColor($r('app.color.text_primary'))
        Text('跟随系统主题自动切换')
          .fontSize(14)
          .fontColor($r('app.color.text_secondary'))
          .margin({ top: 4 })
      }
      .width('100%')
      .padding({ top: 56, left: 20, right: 20, bottom: 20 })
      .backgroundColor($r('app.color.card_background'))

      // 内容卡片
      Column({ space: 12 }) {
        ForEach([
          { icon: '🎨', title: '资源限定词机制', desc: '系统根据dark/light目录自动选择资源' },
          { icon: '🔄', title: '实时切换', desc: '用户切换系统主题后,应用自动刷新' },
          { icon: '📦', title: '零代码适配', desc: '只需定义资源文件,无需手动管理主题状态' },
          { icon: '♿', title: '无障碍友好', desc: '深色模式降低屏幕亮度,减少视觉疲劳' },
        ], (item: { icon: string; title: string; desc: string }) => {
          Column() {
            Row() {
              Text(item.icon)
                .fontSize(24)
              Column() {
                Text(item.title)
                  .fontSize(15)
                  .fontWeight(FontWeight.Medium)
                  .fontColor($r('app.color.text_primary'))
                Text(item.desc)
                  .fontSize(12)
                  .fontColor($r('app.color.text_secondary'))
                  .margin({ top: 2 })
              }
              .alignItems(HorizontalAlign.Start)
              .margin({ left: 12 })
            }
          }
          .width('100%')
          .padding(14)
          .borderRadius(10)
          .backgroundColor($r('app.color.card_background'))
        })
      }
      .padding({ left: 16, right: 16, top: 16 })

      // 当前主题状态
      Row() {
        Text('当前模式:')
          .fontSize(14)
          .fontColor($r('app.color.text_secondary'))
        Text(this.isDarkMode ? '深色 🌙' : '浅色 ☀️')
          .fontSize(14)
          .fontColor($r('app.color.accent_color'))
          .fontWeight(FontWeight.Medium)
          .margin({ left: 8 })
      }
      .width('100%')
      .justifyContent(FlexAlign.Center)
      .padding({ top: 24, bottom: 24 })

      // 提示
      Column() {
        Text('💡 如何测试')
          .fontSize(14)
          .fontWeight(FontWeight.Bold)
          .fontColor($r('app.color.accent_color'))
          .margin({ bottom: 8 })
        Text('1. 在系统设置中切换深色/浅色模式')
          .fontSize(12)
          .fontColor($r('app.color.text_secondary'))
        Text('2. 返回应用,界面自动切换')
          .fontSize(12)
          .fontColor($r('app.color.text_secondary'))
          .margin({ top: 4 })
        Text('3. 所有使用$r()的颜色都会自动更新')
          .fontSize(12)
          .fontColor($r('app.color.text_secondary'))
          .margin({ top: 4 })
      }
      .width('100%')
      .padding(14)
      .margin({ left: 16, right: 16, bottom: 16 })
      .borderRadius(10)
      .backgroundColor($r('app.color.card_background'))
      .border({ width: 1, color: $r('app.color.divider_color') })
    }
    .width('100%')
    .height('100%')
    .backgroundColor($r('app.color.page_background'))
  }
}

3.2 系统主题监听与应用内切换

有时候我们需要在代码中感知主题变化(比如切换状态栏颜色),或者提供应用内的主题切换开关。

// ThemeSwitchDemo.ets
// 系统主题监听与应用内切换

import { window } from '@kit.ArkUI';
import { common, Configuration } from '@kit.AbilityKit';
import { AbilityConstant } from '@kit.AbilityKit';

@Entry
@Component
struct ThemeSwitchDemo {
  // 当前主题模式
  @StorageProp('isDarkMode') @Watch('onThemeChanged')
  isDarkMode: boolean = false;
  // 主题跟随系统
  @State followSystem: boolean = true;
  // 主题切换动画进度
  @State themeTransitionProgress: number = 0;

  aboutToAppear(): void {
    // 初始化主题状态
    const config = getContext(this).resourceManager.getConfiguration();
    AppStorage.setOrCreate('isDarkMode', config.colorMode === 1);
  }

  // 主题变化回调
  private onThemeChanged(): void {
    console.info(`主题已切换为: ${this.isDarkMode ? '深色' : '浅色'}`);
    this.updateSystemBarColor();
    this.playThemeTransition();
  }

  // 更新系统栏颜色
  private async updateSystemBarColor(): Promise<void> {
    try {
      const context = getContext(this) as common.UIAbilityContext;
      const mainWindow = await window.getLastWindow(context);

      if (this.isDarkMode) {
        await mainWindow.setWindowSystemBarProperties({
          statusBarColor: '#FF121212',
          statusBarContentColor: '#FFFFFF',
          navigationBarColor: '#FF121212',
          navigationBarContentColor: '#FFFFFF',
        });
      } else {
        await mainWindow.setWindowSystemBarProperties({
          statusBarColor: '#FFF5F5F5',
          statusBarContentColor: '#212121',
          navigationBarColor: '#FFF5F5F5',
          navigationBarContentColor: '#212121',
        });
      }
    } catch (error) {
      console.error(`更新系统栏颜色失败: ${JSON.stringify(error)}`);
    }
  }

  // 播放主题切换动画
  private playThemeTransition(): void {
    this.themeTransitionProgress = 0;
    // 简单的进度动画模拟
    const interval = setInterval(() => {
      this.themeTransitionProgress += 0.05;
      if (this.themeTransitionProgress >= 1) {
        this.themeTransitionProgress = 1;
        clearInterval(interval);
      }
    }, 16);
  }

  // 手动切换主题
  private toggleTheme(): void {
    this.followSystem = false;
    AppStorage.setOrCreate('isDarkMode', !this.isDarkMode);
  }

  // 恢复跟随系统
  private restoreFollowSystem(): void {
    this.followSystem = true;
    const config = getContext(this).resourceManager.getConfiguration();
    AppStorage.setOrCreate('isDarkMode', config.colorMode === 1);
  }

  build() {
    Column() {
      // 标题区域
      Column() {
        Row() {
          Column() {
            Text('主题切换')
              .fontSize(24)
              .fontWeight(FontWeight.Bold)
              .fontColor(this.isDarkMode ? '#E0E0E0' : '#212121')
            Text(this.isDarkMode ? '深色模式 🌙' : '浅色模式 ☀️')
              .fontSize(14)
              .fontColor(this.isDarkMode ? '#9E9E9E' : '#757575')
              .margin({ top: 4 })
          }
          .alignItems(HorizontalAlign.Start)
          Blank()
          // 主题切换开关
          Row() {
            Text('☀️')
              .fontSize(18)
              .opacity(this.isDarkMode ? 0.4 : 1)
            Toggle({ type: ToggleType.Switch, isOn: this.isDarkMode })
              .onChange((isOn: boolean) => {
                this.toggleTheme();
              })
              .selectedColor('#9C27B0')
              .margin({ left: 8, right: 8 })
            Text('🌙')
              .fontSize(18)
              .opacity(this.isDarkMode ? 1 : 0.4)
          }
        }
        .width('100%')
      }
      .width('100%')
      .padding({ top: 56, left: 20, right: 20, bottom: 20 })
      .backgroundColor(this.isDarkMode ? '#1A1A2E' : '#FFFFFF')

      // 跟随系统开关
      Row() {
        Text('跟随系统主题')
          .fontSize(15)
          .fontColor(this.isDarkMode ? '#E0E0E0' : '#212121')
        Blank()
        Toggle({ type: ToggleType.Switch, isOn: this.followSystem })
          .onChange((isOn: boolean) => {
            if (isOn) {
              this.restoreFollowSystem();
            }
          })
          .selectedColor('#4CAF50')
      }
      .width('100%')
      .padding({ left: 20, right: 20, top: 12, bottom: 12 })
      .margin({ top: 8 })
      .backgroundColor(this.isDarkMode ? '#1E1E2E' : '#F5F5F5')
      .borderRadius(10)
      .margin({ left: 16, right: 16, top: 8 })

      // 主题预览卡片
      Text('主题预览')
        .fontSize(16)
        .fontWeight(FontWeight.Medium)
        .fontColor(this.isDarkMode ? '#E0E0E0' : '#212121')
        .margin({ left: 20, top: 20, bottom: 8 })

      Scroll() {
        Column({ space: 10 }) {
          // 颜色板预览
          Column() {
            Text('调色板')
              .fontSize(14)
              .fontColor(this.isDarkMode ? '#9E9E9E' : '#757575')
              .margin({ bottom: 10 })

            // 主色调
            Row({ space: 8 }) {
              ForEach([
                { name: '背景', color: this.isDarkMode ? '#121212' : '#F5F5F5' },
                { name: '卡片', color: this.isDarkMode ? '#1E1E2E' : '#FFFFFF' },
                { name: '强调', color: this.isDarkMode ? '#66BB6A' : '#4CAF50' },
                { name: '警告', color: '#FF9800' },
                { name: '错误', color: '#F44336' },
              ], (item: { name: string; color: string }) => {
                Column() {
                  Row()
                    .width(36)
                    .height(36)
                    .borderRadius(8)
                    .backgroundColor(item.color)
                    .border({ width: 1, color: this.isDarkMode ? '#FFFFFF22' : '#00000011' })
                  Text(item.name)
                    .fontSize(10)
                    .fontColor(this.isDarkMode ? '#9E9E9E' : '#757575')
                    .margin({ top: 4 })
                }
              })
            }
          }
          .width('100%')
          .padding(14)
          .borderRadius(10)
          .backgroundColor(this.isDarkMode ? '#1E1E2E' : '#FFFFFF')

          // 文字样式预览
          Column() {
            Text('文字层级')
              .fontSize(14)
              .fontColor(this.isDarkMode ? '#9E9E9E' : '#757575')
              .margin({ bottom: 10 })

            Column({ space: 6 }) {
              Text('标题文字 - 24px Bold')
                .fontSize(24)
                .fontWeight(FontWeight.Bold)
                .fontColor(this.isDarkMode ? '#E0E0E0' : '#212121')
              Text('正文文字 - 15px Regular')
                .fontSize(15)
                .fontColor(this.isDarkMode ? '#BDBDBD' : '#424242')
              Text('辅助文字 - 13px Light')
                .fontSize(13)
                .fontColor(this.isDarkMode ? '#9E9E9E' : '#757575')
              Text('禁用文字 - 12px')
                .fontSize(12)
                .fontColor(this.isDarkMode ? '#616161' : '#BDBDBD')
            }
            .alignItems(HorizontalAlign.Start)
          }
          .width('100%')
          .padding(14)
          .borderRadius(10)
          .backgroundColor(this.isDarkMode ? '#1E1E2E' : '#FFFFFF')

          // 按钮样式预览
          Column() {
            Text('按钮样式')
              .fontSize(14)
              .fontColor(this.isDarkMode ? '#9E9E9E' : '#757575')
              .margin({ bottom: 10 })

            Row({ space: 8 }) {
              Text('主要按钮')
                .fontSize(14)
                .fontColor('#FFFFFF')
                .padding({ left: 20, right: 20, top: 10, bottom: 10 })
                .borderRadius(20)
                .backgroundColor(this.isDarkMode ? '#66BB6A' : '#4CAF50')

              Text('次要按钮')
                .fontSize(14)
                .fontColor(this.isDarkMode ? '#66BB6A' : '#4CAF50')
                .padding({ left: 20, right: 20, top: 10, bottom: 10 })
                .borderRadius(20)
                .backgroundColor(this.isDarkMode ? '#66BB6A22' : '#4CAF5022')
            }
          }
          .width('100%')
          .padding(14)
          .borderRadius(10)
          .backgroundColor(this.isDarkMode ? '#1E1E2E' : '#FFFFFF')
        }
        .padding({ left: 16, right: 16 })
      }
      .scrollable(ScrollDirection.Vertical)
      .layoutWeight(1)
    }
    .width('100%')
    .height('100%')
    .backgroundColor(this.isDarkMode ? '#121212' : '#F5F5F5')
    .animation({ duration: 300, curve: Curve.EaseInOut })
  }
}

// 在 UIAbility 中监听系统主题变化
// EntryAbility.ets 中添加:
// onConfigurationUpdate(newConfig: Configuration): void {
//   if (newConfig.colorMode !== undefined) {
//     AppStorage.setOrCreate('isDarkMode', newConfig.colorMode === 1);
//   }
// }

3.3 自定义主题体系与无障碍适配

对于更复杂的主题需求(比如多种主题色、高对比度模式),我们需要构建一个完整的主题管理体系。

// CustomThemeSystem.ets
// 自定义主题体系与无障碍适配

// 主题配置接口
interface ThemeConfig {
  // 页面级颜色
  pageBackground: string;
  cardBackground: string;
  cardBackgroundBlur: string;
  // 文字颜色
  textPrimary: string;
  textSecondary: string;
  textDisabled: string;
  // 功能色
  accentColor: string;
  accentColorLight: string;
  warningColor: string;
  errorColor: string;
  successColor: string;
  // 边框和分割线
  dividerColor: string;
  borderColor: string;
  // 系统栏
  statusBarColor: string;
  statusBarContentColor: string;
  navBarColor: string;
  navBarContentColor: string;
}

// 预定义主题
class ThemePresets {
  // 浅色主题
  static readonly LIGHT: ThemeConfig = {
    pageBackground: '#F5F5F5',
    cardBackground: '#FFFFFF',
    cardBackgroundBlur: '#FFFFFFEE',
    textPrimary: '#212121',
    textSecondary: '#757575',
    textDisabled: '#BDBDBD',
    accentColor: '#4CAF50',
    accentColorLight: '#4CAF5022',
    warningColor: '#FF9800',
    errorColor: '#F44336',
    successColor: '#4CAF50',
    dividerColor: '#E0E0E0',
    borderColor: '#E0E0E0',
    statusBarColor: '#FFF5F5F5',
    statusBarContentColor: '#212121',
    navBarColor: '#FFF5F5F5',
    navBarContentColor: '#212121',
  };

  // 深色主题
  static readonly DARK: ThemeConfig = {
    pageBackground: '#121212',
    cardBackground: '#1E1E2E',
    cardBackgroundBlur: '#1E1E2EEE',
    textPrimary: '#E0E0E0',
    textSecondary: '#9E9E9E',
    textDisabled: '#616161',
    accentColor: '#66BB6A',
    accentColorLight: '#66BB6A22',
    warningColor: '#FFB74D',
    errorColor: '#EF5350',
    successColor: '#66BB6A',
    dividerColor: '#2A2A3E',
    borderColor: '#2A2A3E',
    statusBarColor: '#FF121212',
    statusBarContentColor: '#FFFFFF',
    navBarColor: '#FF121212',
    navBarContentColor: '#FFFFFF',
  };

  // 高对比度深色主题(无障碍)
  static readonly HIGH_CONTRAST_DARK: ThemeConfig = {
    pageBackground: '#000000',
    cardBackground: '#1A1A1A',
    cardBackgroundBlur: '#1A1A1AEE',
    textPrimary: '#FFFFFF',
    textSecondary: '#CCCCCC',
    textDisabled: '#888888',
    accentColor: '#76FF03',
    accentColorLight: '#76FF0344',
    warningColor: '#FFD600',
    errorColor: '#FF1744',
    successColor: '#76FF03',
    dividerColor: '#444444',
    borderColor: '#444444',
    statusBarColor: '#FF000000',
    statusBarContentColor: '#FFFFFF',
    navBarColor: '#FF000000',
    navBarContentColor: '#FFFFFF',
  };

  // 薰衣草主题(自定义)
  static readonly LAVENDER: ThemeConfig = {
    pageBackground: '#1A1625',
    cardBackground: '#2A2438',
    cardBackgroundBlur: '#2A2438EE',
    textPrimary: '#E8E0F0',
    textSecondary: '#A898B8',
    textDisabled: '#6A5A7A',
    accentColor: '#B388FF',
    accentColorLight: '#B388FF22',
    warningColor: '#FFB74D',
    errorColor: '#EF5350',
    successColor: '#69F0AE',
    dividerColor: '#3A3450',
    borderColor: '#3A3450',
    statusBarColor: '#FF1A1625',
    statusBarContentColor: '#FFFFFF',
    navBarColor: '#FF1A1625',
    navBarContentColor: '#FFFFFF',
  };
}

@Entry
@Component
struct CustomThemeSystemDemo {
  // 当前主题
  @State currentTheme: ThemeConfig = ThemePresets.DARK;
  // 当前主题名称
  @State currentThemeName: string = '深色';

  // 可选主题列表
  private themeOptions: Array<{ name: string; theme: ThemeConfig; icon: string; desc: string }> = [
    { name: '浅色', theme: ThemePresets.LIGHT, icon: '☀️', desc: '经典浅色模式' },
    { name: '深色', theme: ThemePresets.DARK, icon: '🌙', desc: '护眼深色模式' },
    { name: '高对比度', theme: ThemePresets.HIGH_CONTRAST_DARK, icon: '♿', desc: '无障碍高对比度' },
    { name: '薰衣草', theme: ThemePresets.LAVENDER, icon: '💜', desc: '薰衣草紫主题' },
  ];

  // 切换主题
  private switchTheme(option: { name: string; theme: ThemeConfig }): void {
    this.currentTheme = option.theme;
    this.currentThemeName = option.name;
    this.updateSystemBarForTheme(option.theme);
  }

  // 更新系统栏颜色
  private async updateSystemBarForTheme(theme: ThemeConfig): Promise<void> {
    try {
      const context = getContext(this) as common.UIAbilityContext;
      const mainWindow = await window.getLastWindow(context);
      await mainWindow.setWindowSystemBarProperties({
        statusBarColor: theme.statusBarColor,
        statusBarContentColor: theme.statusBarContentColor,
        navigationBarColor: theme.navBarColor,
        navigationBarContentColor: theme.navBarContentColor,
      });
    } catch (error) {
      console.error(`更新系统栏颜色失败: ${JSON.stringify(error)}`);
    }
  }

  build() {
    Column() {
      // 标题区域
      Column() {
        Text('自定义主题体系')
          .fontSize(24)
          .fontWeight(FontWeight.Bold)
          .fontColor(this.currentTheme.textPrimary)
        Text(`当前主题: ${this.currentThemeName}`)
          .fontSize(14)
          .fontColor(this.currentTheme.textSecondary)
          .margin({ top: 4 })
      }
      .width('100%')
      .padding({ top: 56, left: 20, right: 20, bottom: 20 })
      .backgroundColor(this.currentTheme.cardBackground)

      // 主题选择
      Text('选择主题')
        .fontSize(16)
        .fontWeight(FontWeight.Medium)
        .fontColor(this.currentTheme.textPrimary)
        .margin({ left: 20, top: 16, bottom: 8 })

      Scroll() {
        Row({ space: 10 }) {
          ForEach(this.themeOptions, (option: { name: string; theme: ThemeConfig; icon: string; desc: string }) => {
            Column() {
              Text(option.icon)
                .fontSize(28)
              Text(option.name)
                .fontSize(13)
                .fontWeight(FontWeight.Medium)
                .fontColor(this.currentThemeName === option.name ?
                  this.currentTheme.accentColor : this.currentTheme.textSecondary)
                .margin({ top: 6 })
              Text(option.desc)
                .fontSize(10)
                .fontColor(this.currentTheme.textDisabled)
                .margin({ top: 2 })
            }
            .padding(12)
            .borderRadius(12)
            .backgroundColor(this.currentThemeName === option.name ?
              this.currentTheme.accentColorLight : this.currentTheme.cardBackground)
            .border({
              width: this.currentThemeName === option.name ? 2 : 1,
              color: this.currentThemeName === option.name ?
                this.currentTheme.accentColor : this.currentTheme.borderColor
            })
            .onClick(() => this.switchTheme(option))
          })
        }
        .padding({ left: 16, right: 16 })
      }
      .scrollable(ScrollDirection.Horizontal)
      .scrollBar(BarState.Off)

      // 主题效果预览
      Text('效果预览')
        .fontSize(16)
        .fontWeight(FontWeight.Medium)
        .fontColor(this.currentTheme.textPrimary)
        .margin({ left: 20, top: 20, bottom: 8 })

      Scroll() {
        Column({ space: 10 }) {
          // 数据卡片
          Column() {
            Row() {
              Column() {
                Text('今日步数')
                  .fontSize(13)
                  .fontColor(this.currentTheme.textSecondary)
                Text('8,642')
                  .fontSize(28)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(this.currentTheme.accentColor)
                  .margin({ top: 4 })
              }
              .alignItems(HorizontalAlign.Start)
              Blank()
              Text('🎯 目标: 10,000')
                .fontSize(12)
                .fontColor(this.currentTheme.textSecondary)
            }
            // 进度条
            Progress({ value: 8642, total: 10000, type: ProgressType.Linear })
              .width('100%')
              .color(this.currentTheme.accentColor)
              .backgroundColor(this.currentTheme.borderColor)
              .margin({ top: 12 })
          }
          .width('100%')
          .padding(16)
          .borderRadius(12)
          .backgroundColor(this.currentTheme.cardBackground)

          // 功能按钮组
          Row({ space: 8 }) {
            Text('主要操作')
              .fontSize(14)
              .fontColor('#FFFFFF')
              .layoutWeight(1)
              .textAlign(TextAlign.Center)
              .padding({ top: 12, bottom: 12 })
              .borderRadius(10)
              .backgroundColor(this.currentTheme.accentColor)

            Text('次要操作')
              .fontSize(14)
              .fontColor(this.currentTheme.accentColor)
              .layoutWeight(1)
              .textAlign(TextAlign.Center)
              .padding({ top: 12, bottom: 12 })
              .borderRadius(10)
              .backgroundColor(this.currentTheme.accentColorLight)

            Text('警告')
              .fontSize(14)
              .fontColor('#FFFFFF')
              .layoutWeight(1)
              .textAlign(TextAlign.Center)
              .padding({ top: 12, bottom: 12 })
              .borderRadius(10)
              .backgroundColor(this.currentTheme.warningColor)
          }

          // 无障碍信息
          Column() {
            Text('♿ 无障碍适配说明')
              .fontSize(14)
              .fontWeight(FontWeight.Bold)
              .fontColor(this.currentTheme.accentColor)
              .margin({ bottom: 8 })
            Text('• 高对比度模式:文字与背景对比度 ≥ 7:1')
              .fontSize(12)
              .fontColor(this.currentTheme.textSecondary)
            Text('• 大字体支持:关键信息使用较大字号')
              .fontSize(12)
              .fontColor(this.currentTheme.textSecondary)
              .margin({ top: 4 })
            Text('• 色盲友好:不仅依赖颜色传达信息,同时使用图标/文字')
              .fontSize(12)
              .fontColor(this.currentTheme.textSecondary)
              .margin({ top: 4 })
            Text('• 触摸目标:按钮区域 ≥ 44×44vp')
              .fontSize(12)
              .fontColor(this.currentTheme.textSecondary)
              .margin({ top: 4 })
            Text('• 动画减弱:尊重系统"减弱动画"设置')
              .fontSize(12)
              .fontColor(this.currentTheme.textSecondary)
              .margin({ top: 4 })
          }
          .width('100%')
          .padding(14)
          .borderRadius(10)
          .backgroundColor(this.currentTheme.cardBackground)
          .border({ width: 1, color: this.currentTheme.accentColor + '33' })
        }
        .padding({ left: 16, right: 16 })
      }
      .scrollable(ScrollDirection.Vertical)
      .layoutWeight(1)
    }
    .width('100%')
    .height('100%')
    .backgroundColor(this.currentTheme.pageBackground)
    .animation({ duration: 350, curve: Curve.EaseInOut })
  }
}

import { window } from '@kit.ArkUI';
import { common } from '@kit.AbilityKit';

四、踩坑与注意事项

4.1 深色模式不是简单的颜色反转

很多开发者以为深色模式就是把白色变黑色、黑色变白色。这是大错特错的。深色模式的设计有自己的规则:

  • 背景不是纯黑:使用 #121212 而不是 #000000,纯黑会让眼睛更累
  • 文字不是纯白:使用 #E0E0E0 而不是 #FFFFFF,纯白在深色背景上太刺眼
  • 强调色需要提亮:浅色模式的 #4CAF50 在深色背景上不够醒目,应该用 #66BB6A
  • 阴影效果减弱:深色模式下阴影几乎看不见,改用边框或微妙的高亮

4.2 图片资源的深色适配

有些图片在浅色背景下很好看,但在深色背景下就"消失"了(比如白色图标的PNG)。需要为深色模式准备对应的图片资源,放在 resources/dark/media/ 目录下。

4.3 WebP和SVG的深色适配

SVG 图片可以通过 $r() 引用后在代码中动态修改颜色,但 WebP 和 PNG 不行。对于需要变色的图标,优先使用 SVG 格式。

4.4 主题切换时的状态保存

用户手动切换主题后,应该将选择保存到本地存储(Preferences),下次启动时恢复。否则每次启动都会重置为系统默认主题。

// 保存主题选择
import { preferences } from '@kit.ArkData';

async saveThemePreference(isDark: boolean): Promise<void> {
  const context = getContext(this);
  const pref = await preferences.getPreferences(context, 'theme_prefs');
  await pref.put('is_dark_mode', isDark);
  await pref.flush();
}

// 读取主题选择
async loadThemePreference(): Promise<boolean> {
  const context = getContext(this);
  const pref = await preferences.getPreferences(context, 'theme_prefs');
  return await pref.get('is_dark_mode', false) as boolean;
}

4.5 无障碍对比度要求

WCAG 2.1 标准对文字与背景的对比度有明确要求:

  • AA级(最低要求):普通文字对比度 ≥ 4.5:1,大文字 ≥ 3:1
  • AAA级(推荐):普通文字对比度 ≥ 7:1,大文字 ≥ 4.5:1

可以使用在线工具(如 WebAIM Contrast Checker)验证你的颜色方案是否达标。

4.6 动画减弱适配

部分用户对动画敏感,系统提供了"减弱动画"的辅助功能设置。你的主题切换动画应该尊重这个设置:

// 检查是否启用了减弱动画
const config = getContext(this).resourceManager.getConfiguration();
const shouldReduceAnimation = config.accessibility?.reduceMotion ?? false;

// 根据设置决定动画时长
.animation({ duration: shouldReduceAnimation ? 0 : 300 })

五、HarmonyOS 6适配

5.1 API变更

变更项 HarmonyOS 5 HarmonyOS 6
主题监听 onConfigurationUpdate 新增 @Watch(ColorMode) 自动监听
资源限定词 base/ + dark/ 新增 high-contrast/ 高对比度目录
动态主题 新增 DynamicColor 系统动态取色
无障碍 手动适配 新增 AccessibilityManager 统一管理

5.2 迁移指南

// HarmonyOS 5 写法:手动监听
onConfigurationUpdate(newConfig: Configuration): void {
  if (newConfig.colorMode !== undefined) {
    AppStorage.setOrCreate('isDarkMode', newConfig.colorMode === 1);
  }
}

// HarmonyOS 6 写法:自动监听
@StorageProp('isDarkMode') @Watch('onThemeChanged')
isDarkMode: boolean = false;
// 系统自动更新 AppStorage 中的值

5.3 动态取色

HarmonyOS 6 引入了 DynamicColor,可以从壁纸中提取主色调,自动生成一套协调的主题色。这让应用的主题能够与系统壁纸"融为一体"。


六、总结

mindmap
  root((系统主题))
    资源限定词
      base/ 默认浅色
      dark/ 深色资源
      $r()自动匹配
      零代码适配
    主题监听
      onConfigurationUpdate
      AppStorage + @Watch
      系统栏颜色同步
      状态保存Preferences
    自定义主题
      ThemeConfig接口
      多套预设主题
      运行时切换
      系统栏联动更新
    切换动画
      animation属性
      300ms EaseInOut
      减弱动画适配
      全局颜色过渡
    无障碍适配
      对比度 ≥ 4.5:1
      高对比度主题
      色盲友好设计
      触摸目标 ≥ 44vp
      动画减弱
    深色模式设计
      背景用#121212
      文字用#E0E0E0
      强调色提亮
      阴影改边框
      图片深色版本
    HarmonyOS 6
      @Watch(ColorMode)
      high-contrast目录
      DynamicColor动态取色
      AccessibilityManager
    
    classDef primary fill:#4CAF50,stroke:#388E3C,color:#fff
    classDef warning fill:#FF9800,stroke:#F57C00,color:#fff
    classDef error fill:#F44336,stroke:#D32F2F,color:#fff
    classDef info fill:#2196F3,stroke:#1976D2,color:#fff
    classDef purple fill:#9C27B0,stroke:#7B1FA2,color:#fff
知识点 实现方式 核心要点
自动适配 $r() + 资源限定词 零代码,系统自动切换
主题监听 onConfigurationUpdate 感知系统主题变化
应用内切换 AppStorage + @Watch 手动切换 + 状态持久化
自定义主题 ThemeConfig 接口 多主题预设 + 运行时切换
切换动画 .animation() 300ms过渡,尊重减弱动画
无障碍 高对比度 + 对比度检测 WCAG 2.1 AA级标准

主题适配的本质是"以用户为中心"。深夜用手机的用户需要深色模式保护眼睛,视力不佳的用户需要高对比度看清内容,对动画敏感的用户需要减弱动画。这些都不是"锦上添花",而是"必须拥有"。做好了主题适配,你的应用才能真正称得上"用户友好"。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。