一文中走进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 主题切换的触发方式
- 系统设置切换:用户在"设置 > 显示与亮度"中切换深色模式
- 应用内切换:应用提供主题切换开关
- 自动切换:根据时间(日落后自动切换深色模式)
三、代码实战
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级标准 |
主题适配的本质是"以用户为中心"。深夜用手机的用户需要深色模式保护眼睛,视力不佳的用户需要高对比度看清内容,对动画敏感的用户需要减弱动画。这些都不是"锦上添花",而是"必须拥有"。做好了主题适配,你的应用才能真正称得上"用户友好"。
- 点赞
- 收藏
- 关注作者
评论(0)