[鸿蒙2025领航者闯关] HarmonyOS深色模式实现

问题描述

在 HarmonyOS 应用开发中,如何实现符合官方 UX 规范的深色模式适配?开发者常遇到的问题:

  • 切换深色模式后状态栏颜色不变
  • 页面卡片在深色背景下显示异常
  • 颜色硬编码导致无法动态切换
  • 深色模式下文字对比度不足

华为应用市场审核要求: 应用需正确适配深色模式,状态栏、卡片、文字颜色需符合鸿蒙应用 UX 设计规范。

关键字:深色模式主题切换状态栏适配动态颜色

解决方案

1. 技术架构

复制代码
┌─────────────────────────────────────┐
│  AppColors (动态颜色管理类)         │
│  - 浅色配色类                        │
│  - 深色配色类                        │
│  - 动态getter属性                   │
└─────────────────────────────────────┘
              ↕
┌─────────────────────────────────────┐
│  AppSettings (主题设置服务)         │
│  - 保存主题模式                      │
│  - 判断深浅色                        │
└─────────────────────────────────────┘
              ↕
┌─────────────────────────────────────┐
│  UI组件                              │
│  - 使用AppColors动态颜色             │
│  - 监听主题变化                      │
│  - 更新状态栏                        │
└─────────────────────────────────────┘

2. 完整实现代码

步骤 1: 创建动态颜色管理类
复制代码
/**
 * 应用颜色配置类
 * 支持深浅色动态切换
 */
export class AppColors {
  private static isDarkMode: boolean = false;
  
  /**
   * 设置深色模式
   */
  static setDarkMode(isDark: boolean): void {
    AppColors.isDarkMode = isDark;
  }
  
  /**
   * 判断是否深色模式
   */
  static isDark(): boolean {
    return AppColors.isDarkMode;
  }
  
  /**
   * 获取当前颜色类
   */
  private static getCurrentColorClass() {
    return AppColors.isDarkMode ? DarkModeColors : LightModeColors;
  }
  
  // ========== 动态颜色属性 ==========
  
  /**
   * 主背景色
   */
  static get BG_PRIMARY(): string {
    return AppColors.getCurrentColorClass().BG_PRIMARY;
  }
  
  /**
   * 卡片背景色
   */
  static get BG_CARD(): string {
    return AppColors.getCurrentColorClass().BG_CARD;
  }
  
  /**
   * 主文字颜色
   */
  static get TEXT_PRIMARY(): string {
    return AppColors.getCurrentColorClass().TEXT_PRIMARY;
  }
  
  /**
   * 次要文字颜色
   */
  static get TEXT_SECONDARY(): string {
    return AppColors.getCurrentColorClass().TEXT_SECONDARY;
  }
  
  /**
   * 辅助文字颜色
   */
  static get TEXT_TERTIARY(): string {
    return AppColors.getCurrentColorClass().TEXT_TERTIARY;
  }
  
  /**
   * 分割线颜色
   */
  static get DIVIDER(): string {
    return AppColors.getCurrentColorClass().DIVIDER;
  }
  
  /**
   * 阴影颜色
   */
  static get SHADOW(): string {
    return AppColors.getCurrentColorClass().SHADOW;
  }
}
​
/**
 * 浅色模式配色
 */
class LightModeColors {
  static readonly BG_PRIMARY = '#FFFCF7';      // 米白色背景
  static readonly BG_CARD = '#FFFFFF';         // 纯白色卡片
  static readonly TEXT_PRIMARY = '#2D1F15';    // 深褐色文字
  static readonly TEXT_SECONDARY = '#6B5A48';  // 中褐色文字
  static readonly TEXT_TERTIARY = '#A89B8C';   // 浅褐色文字
  static readonly DIVIDER = '#F0E5D8';         // 淡边框
  static readonly SHADOW = 'rgba(0, 0, 0, 0.06)';  // 淡阴影
}
​
/**
 * 深色模式配色
 */
class DarkModeColors {
  static readonly BG_PRIMARY = '#1A1A1A';      // 深灰背景
  static readonly BG_CARD = '#2C2C2C';         // 卡片背景
  static readonly TEXT_PRIMARY = '#F0F0F0';    // 浅灰文字
  static readonly TEXT_SECONDARY = '#C8C8C8';  // 中灰文字
  static readonly TEXT_TERTIARY = '#999999';   // 深灰文字
  static readonly DIVIDER = '#3A3A3A';         // 深色边框
  static readonly SHADOW = 'rgba(0, 0, 0, 0.3)';  // 深色阴影
}
步骤 2: 创建主题设置服务
复制代码
import { preferences } from '@kit.ArkData';
​
/**
 * 主题模式枚举
 */
export enum ThemeMode {
  AUTO = 'auto',    // 跟随系统
  LIGHT = 'light',  // 浅色
  DARK = 'dark'     // 深色
}
​
/**
 * 应用设置服务
 */
export class AppSettings {
  private static instance: AppSettings;
  private dataPreferences: preferences.Preferences | null = null;
  private readonly THEME_MODE_KEY = 'theme_mode';
  
  private constructor() {}
  
  static getInstance(): AppSettings {
    if (!AppSettings.instance) {
      AppSettings.instance = new AppSettings();
    }
    return AppSettings.instance;
  }
  
  /**
   * 初始化
   */
  async init(context: Context): Promise<void> {
    this.dataPreferences = await preferences.getPreferences(context, 'app_settings');
  }
  
  /**
   * 获取主题模式
   */
  async getThemeMode(): Promise<ThemeMode> {
    if (!this.dataPreferences) {
      return ThemeMode.LIGHT;
    }
    
    const mode = await this.dataPreferences.get(this.THEME_MODE_KEY, ThemeMode.LIGHT);
    return mode as ThemeMode;
  }
  
  /**
   * 设置主题模式
   */
  async setThemeMode(mode: ThemeMode): Promise<void> {
    if (!this.dataPreferences) {
      return;
    }
    
    await this.dataPreferences.put(this.THEME_MODE_KEY, mode);
    await this.dataPreferences.flush();
  }
  
  /**
   * 判断是否使用深色模式
   */
  shouldUseDarkMode(mode: ThemeMode): boolean {
    if (mode === ThemeMode.LIGHT) {
      return false;
    } else if (mode === ThemeMode.DARK) {
      return true;
    } else {
      // AUTO: 可以获取系统设置
      // 这里简化为返回false,实际可以检测系统设置
      return false;
    }
  }
}
步骤 3: 在 EntryAbility 中初始化并设置状态栏
复制代码
import { UIAbility } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
import { AppSettings } from '../services/AppSettings';
import { AppColors } from '../common/constants/AppColors';
​
export default class EntryAbility extends UIAbility {
  async onWindowStageCreate(windowStage: window.WindowStage): Promise<void> {
    // 初始化设置
    await AppSettings.getInstance().init(this.context);
    
    // 获取主题模式
    const themeMode = await AppSettings.getInstance().getThemeMode();
    const isDark = AppSettings.getInstance().shouldUseDarkMode(themeMode);
    
    // 设置AppColors
    AppColors.setDarkMode(isDark);
    
    // ✅ 关键: 设置状态栏颜色
    try {
      const mainWindow = windowStage.getMainWindowSync();
      
      await mainWindow.setWindowSystemBarProperties({
        statusBarColor: isDark ? '#000000' : '#FFFFFF',
        statusBarContentColor: isDark ? '#FFFFFF' : '#000000',
        navigationBarColor: isDark ? '#000000' : '#FFFFFF',
        navigationBarContentColor: isDark ? '#FFFFFF' : '#000000'
      });
      
      console.info('状态栏颜色设置成功, 深色模式:', isDark);
    } catch (err) {
      console.error('设置状态栏失败:', JSON.stringify(err));
    }
    
    windowStage.loadContent('pages/Index');
  }
}
步骤 4: 主页面监听主题变化
复制代码
import { window } from '@kit.ArkUI';
import { AppColors } from '../common/constants/AppColors';
​
@Entry
@Component
struct Index {
  @State isDarkMode: boolean = false;
  
  /**
   * 主题变化回调
   */
  private async onThemeChanged(isDark: boolean): Promise<void> {
    this.isDarkMode = isDark;
    console.info('主题已切换:', isDark ? '深色' : '浅色');
    
    // ✅ 更新状态栏颜色
    try {
      const mainWindow = await window.getLastWindow(getContext(this));
      
      await mainWindow.setWindowSystemBarProperties({
        statusBarColor: isDark ? '#000000' : '#FFFFFF',
        statusBarContentColor: isDark ? '#FFFFFF' : '#000000',
        navigationBarColor: isDark ? '#000000' : '#FFFFFF',
        navigationBarContentColor: isDark ? '#FFFFFF' : '#000000'
      });
      
      console.info('状态栏颜色已更新');
    } catch (err) {
      console.error('更新状态栏失败:', JSON.stringify(err));
    }
  }
  
  build() {
    Column() {
      // 页面内容
      // ...
    }
    .width('100%')
    .height('100%')
    .backgroundColor(AppColors.BG_PRIMARY)  // ✅ 使用动态颜色
  }
}
步骤 5: 设置页面实现主题切换
复制代码
import { AppSettings, ThemeMode } from '../services/AppSettings';
import { AppColors } from '../common/constants/AppColors';
​
@Component
export struct SettingsPage {
  @State currentThemeMode: ThemeMode = ThemeMode.LIGHT;
  private appSettings: AppSettings = AppSettings.getInstance();
  
  // 主题变化回调函数
  onThemeChange?: (isDark: boolean) => void;
  
  async aboutToAppear(): Promise<void> {
    this.currentThemeMode = await this.appSettings.getThemeMode();
  }
  
  /**
   * 切换主题模式
   */
  private async changeThemeMode(mode: ThemeMode): Promise<void> {
    this.currentThemeMode = mode;
    
    // 保存设置
    await this.appSettings.setThemeMode(mode);
    
    // 判断是否深色模式
    const isDark = this.appSettings.shouldUseDarkMode(mode);
    
    // 更新AppColors
    AppColors.setDarkMode(isDark);
    
    // 通知父组件更新状态栏
    if (this.onThemeChange) {
      this.onThemeChange(isDark);
    }
  }
  
  build() {
    Column({ space: 16 }) {
      Text('主题设置')
        .fontSize(16)
        .fontWeight(FontWeight.Medium)
        .fontColor(AppColors.TEXT_PRIMARY);  // ✅ 动态颜色
      
      // 浅色模式
      Row() {
        Text('☀️ 浅色模式')
          .fontSize(15)
          .fontColor(AppColors.TEXT_PRIMARY);
        
        Blank();
        
        if (this.currentThemeMode === ThemeMode.LIGHT) {
          Text('✓').fontSize(20).fontColor('#FF6B3D');
        }
      }
      .width('100%')
      .padding(14)
      .backgroundColor(AppColors.BG_CARD)  // ✅ 动态颜色
      .borderRadius(12)
      .onClick(() => this.changeThemeMode(ThemeMode.LIGHT))
      
      // 深色模式
      Row() {
        Text('🌙 深色模式')
          .fontSize(15)
          .fontColor(AppColors.TEXT_PRIMARY);
        
        Blank();
        
        if (this.currentThemeMode === ThemeMode.DARK) {
          Text('✓').fontSize(20).fontColor('#FF6B3D');
        }
      }
      .width('100%')
      .padding(14)
      .backgroundColor(AppColors.BG_CARD)
      .borderRadius(12)
      .onClick(() => this.changeThemeMode(ThemeMode.DARK))
      
      // 跟随系统
      Row() {
        Text('⚙️ 跟随系统')
          .fontSize(15)
          .fontColor(AppColors.TEXT_PRIMARY);
        
        Blank();
        
        if (this.currentThemeMode === ThemeMode.AUTO) {
          Text('✓').fontSize(20).fontColor('#FF6B3D');
        }
      }
      .width('100%')
      .padding(14)
      .backgroundColor(AppColors.BG_CARD)
      .borderRadius(12)
      .onClick(() => this.changeThemeMode(ThemeMode.AUTO))
    }
    .width('100%')
    .padding(16)
  }
}
步骤 6: UI 组件使用动态颜色
复制代码
@Component
struct ItemCard {
  @Prop item: Item;
  
  build() {
    Row() {
      Column({ space: 4 }) {
        // ✅ 所有颜色都使用AppColors动态属性
        Text(this.item.name)
          .fontSize(16)
          .fontWeight(FontWeight.Medium)
          .fontColor(AppColors.TEXT_PRIMARY);  // 主文字
        
        Text(`数量: ${this.item.quantity}`)
          .fontSize(14)
          .fontColor(AppColors.TEXT_SECONDARY);  // 次要文字
        
        Text('备注信息')
          .fontSize(12)
          .fontColor(AppColors.TEXT_TERTIARY);  // 辅助文字
      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)
    }
    .width('100%')
    .padding(16)
    .backgroundColor(AppColors.BG_CARD)  // 卡片背景
    .borderRadius(12)
    .border({ width: 1, color: AppColors.DIVIDER })  // 边框
    .shadow({
      radius: 8,
      color: AppColors.SHADOW,  // 阴影
      offsetY: 2
    })
  }
}

3. 运行效果

浅色模式:

复制代码
┌────────────────────────────────┐
│ ●●●●●●●●●●●●  10:30      ← 白色状态栏,黑色图标
├────────────────────────────────┤
│  主页                          │ ← 米白背景
│                                │
│  ┌──────────────────────┐     │
│  │ 物品名称              │     │ ← 白色卡片
│  │ 数量: 10个            │     │
│  └──────────────────────┘     │
└────────────────────────────────┘

深色模式:

复制代码
┌────────────────────────────────┐
│ ●●●●●●●●●●●●  10:30      ← 黑色状态栏,白色图标
├────────────────────────────────┤
│  主页                          │ ← 深灰背景
│                                │
│  ┌──────────────────────┐     │
│  │ 物品名称              │     │ ← 深色卡片
│  │ 数量: 10个            │     │
│  └──────────────────────┘     │
└────────────────────────────────┘

关键要点

1. 状态栏适配是关键

必须设置的地方:

  • EntryAbility 初始化时

  • 主题切换时

    await mainWindow.setWindowSystemBarProperties({
    statusBarColor: isDark ? '#000000' : '#FFFFFF',
    statusBarContentColor: isDark ? '#FFFFFF' : '#000000'
    });

2. 颜色管理集中化

使用 AppColors 统一管理:

复制代码
// ✅ 推荐
.fontColor(AppColors.TEXT_PRIMARY)

// ❌ 不推荐
.fontColor('#2D1F15')  // 硬编码

3. 深色模式配色原则

背景色:

  • ❌ 不使用纯黑 #000000 作为主背景
  • ✅ 使用深灰 #1A1A1A
  • ✅ 卡片用更浅的灰 #2C2C2C

文字对比度:

  • 主文字: #F0F0F0 on #1A1A1A ≈ 13.9:1 ✅
  • 次要文字: #C8C8C8 on #1A1A1A ≈ 9.4:1 ✅
  • 辅助文字: #999999 on #1A1A1A ≈ 5.1:1 ✅

4. 动态颜色实现原理

复制代码
export class AppColors {
  private static isDarkMode: boolean = false;

  // 通过getter实现动态切换
  static get BG_PRIMARY(): string {
    return this.isDarkMode ? '#1A1A1A' : '#FFFCF7';
  }
}

优势:

  • UI 组件无需修改代码
  • 切换主题时自动更新
  • 类型安全

最佳实践

1. 避免硬编码颜色

错误示例:

复制代码
Text('标题')
  .fontColor('#333333')  // 深色模式下看不清
  .backgroundColor('#FFFFFF')  // 深色模式下刺眼

正确示例:

复制代码
Text('标题')
  .fontColor(AppColors.TEXT_PRIMARY)
  .backgroundColor(AppColors.BG_CARD)

2. 卡片背景统一

浅色模式 : 所有卡片用白色 深色模式: 所有卡片用深灰

复制代码
Column() {
  // 统计卡片
  this.buildCard();

  // 列表卡片
  this.buildCard();

  // 详情卡片
  this.buildCard();
}

@Builder
buildCard() {
  Column() {
    // ...
  }
  .backgroundColor(AppColors.BG_CARD)  // ✅ 统一使用
  .borderRadius(12)
}

3. 边框和阴影

深色模式下边框更重要:

复制代码
Column() {
  // ...
}
.border({ 
  width: 1, 
  color: AppColors.DIVIDER  // 深色模式: #3A3A3A
})
.shadow({
  radius: 8,
  color: AppColors.SHADOW,  // 深色模式: rgba(0,0,0,0.3)
  offsetY: 2
})

4. 图标颜色适配

复制代码
Image($r('app.media.icon'))
  .fillColor(AppColors.TEXT_PRIMARY)  // 图标也要动态颜色

常见问题

Q1: 为什么状态栏颜色没变?

检查两个地方:

  1. EntryAbility 中是否设置

  2. 主题切换时是否更新

    // EntryAbility.ets
    async onWindowStageCreate(windowStage: window.WindowStage) {
    // ✅ 第一处: 应用启动时设置
    const mainWindow = windowStage.getMainWindowSync();
    await mainWindow.setWindowSystemBarProperties({...});
    }

    // Index.ets
    private async onThemeChanged(isDark: boolean) {
    // ✅ 第二处: 主题切换时更新
    const mainWindow = await window.getLastWindow(getContext(this));
    await mainWindow.setWindowSystemBarProperties({...});
    }

Q2: 切换主题后部分颜色没变?

检查是否有硬编码颜色:

复制代码
// ❌ 硬编码,不会动态切换
.fontColor('#333333')

// ✅ 动态颜色,会自动切换
.fontColor(AppColors.TEXT_PRIMARY)

Q3: 深色模式下文字看不清?

检查对比度是否足够:

复制代码
// ❌ 对比度不足
static readonly TEXT_PRIMARY = '#666666';  // 中灰

// ✅ 对比度充足
static readonly TEXT_PRIMARY = '#F0F0F0';  // 浅灰

Q4: 如何跟随系统深色模式?

可以通过监听系统配置变化:

复制代码
import { ConfigurationConstant } from '@kit.AbilityKit';

onConfigurationUpdate(newConfig: Configuration): void {
  if (newConfig.colorMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK) {
    AppColors.setDarkMode(true);
  } else {
    AppColors.setDarkMode(false);
  }
}

审核要点对照

根据华为应用市场 UX 规范:

状态栏适配:

  • 浅色模式: 白色背景 + 黑色内容 ✓
  • 深色模式: 黑色背景 + 白色内容 ✓

页面背景:

  • 浅色模式: 浅色背景 ✓
  • 深色模式: 深色背景 ✓

卡片背景:

  • 浅色模式: 白色卡片 ✓
  • 深色模式: 深灰卡片 ✓

文字对比度:

  • 主要文字对比度 > 7:1 ✓
  • 次要文字对比度 > 4.5:1 ✓

整体协调性:

  • 无刺眼的强对比 ✓
  • 颜色层次清晰 ✓

参考资料

相关推荐
灯前目力虽非昔,犹课蝇头二万言。8 小时前
HarmonyOS笔记9:UIAbility之间的切换和数据的传递
笔记·harmonyos
花启莫你是不是傻9 小时前
在鸿蒙中调用 FFmpeg 命令行工具
华为·harmonyos
L、2189 小时前
性能调优实战:Flutter 在 OpenHarmony 上的内存、渲染与启动速度优化指南
javascript·华为·智能手机·electron·harmonyos
0x0414 小时前
鸿蒙应用开发笔记:签名文件
harmonyos
马剑威(威哥爱编程)15 小时前
【鸿蒙开发案例篇】鸿蒙6.0计算器实现详解
华为·harmonyos
用户7649328076815 小时前
HarmonyOS6.0开发之Select组件,就像一个“会收缩的魔法抽屉”
harmonyos
用户7649328076815 小时前
一文彻底搞明白HarmonyOS6.0基础之ArkTS中的所有循环语句
harmonyos
用户7649328076815 小时前
HarmonyOS6.0开发之记忆翻牌游戏,轻松拿捏!
harmonyos
晚霞的不甘15 小时前
[鸿蒙2025领航者闯关]鸿蒙实战终极篇:构建全场景智能应用的工程化体系与生态融合
华为·harmonyos