news 2026/1/20 8:42:46

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
[鸿蒙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

文字对比度:

  • 主文字:#F0F0F0on#1A1A1A≈ 13.9:1 ✅
  • 次要文字:#C8C8C8on#1A1A1A≈ 9.4:1 ✅
  • 辅助文字:#999999on#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 ✓

整体协调性:

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

参考资料

  • 鸿蒙深色模式适配指南
  • 通用应用 UX 体验标准
  • 窗口管理开发指南
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!