问题描述
在 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: 为什么状态栏颜色没变?
检查两个地方:
- EntryAbility 中是否设置
- 主题切换时是否更新
// 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 体验标准
- 窗口管理开发指南