HarmonyOS APP<<古今职鉴定>>开源教程第7篇:主题适配:深色模式与浅色模式
本篇学习系统主题监听、全局状态管理,实现完整的主题切换功能
·
本篇学习系统主题监听、全局状态管理,实现完整的主题切换功能
图:古今职鉴开源教程封面。本篇围绕「主题适配:深色模式与浅色模式」展开。
学习目标
完成本篇后,你将能够:
- ✅ 监听系统主题变化
- ✅ 使用 AppStorage 管理全局状态
- ✅ 设计符合 WCAG 标准的颜色体系
- ✅ 实现深色/浅色模式自动切换
预计学习时间
约 90 分钟
---
实战一:获取当前系统主题
第一步:创建 lesson07 目录和文件
在 products/jiaocheng/src/main/ets/ 下创建 lesson07 文件夹,新建 Lesson07Page.ets:
// 文件路径:products/jiaocheng/src/main/ets/lesson07/Lesson07Page.ets
import { ConfigurationConstant } from '@kit.AbilityKit';
@Entry
@Component
struct Lesson07Page {
@State isDarkMode: boolean = false;
aboutToAppear(): void {
// 获取当前系统主题
const context = getContext(this);
const colorMode = context.config.colorMode;
this.isDarkMode = colorMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK;
}
build() {
Column() {
Text(this.isDarkMode ? '当前是深色模式 🌙' : '当前是浅色模式 ☀️')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor(this.isDarkMode ? Color.White : '#1e293b')
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.backgroundColor(this.isDarkMode ? '#221210' : '#f8f6f5')
}
}
第二步:理解 ConfigurationConstant.ColorMode
| 值 | 含义 |
|---|---|
COLOR_MODE_DARK |
深色模式 |
COLOR_MODE_LIGHT |
浅色模式 |
COLOR_MODE_NOT_SET |
未设置(跟随系统) |
第三步:运行查看效果
hvigorw assembleHap --no-daemon
预期效果:
- 页面显示当前系统主题状态
- 背景色和文字颜色根据主题变化
问题:切换系统主题后,应用不会自动更新。下一步解决这个问题。
---
实战二:使用 AppStorage 全局状态
第一步:理解 AppStorage
AppStorage 是应用级别的状态存储:
- 所有页面都可以访问
- 支持响应式更新
- 应用关闭后数据丢失(不持久化)
第二步:在 EntryAbility 中初始化主题状态
修改 products/jiaocheng/src/main/ets/jiaochengability/JiaochengAbility.ets:
import { AbilityConstant, ConfigurationConstant, UIAbility, Want, Configuration } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
export default class JiaochengAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
// 初始化主题状态到 AppStorage
const isDark = this.context.config.colorMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK;
AppStorage.setOrCreate('isDarkMode', isDark);
console.info(`初始化主题: ${isDark ? '深色' : '浅色'}`);
}
// 监听系统配置变化(包括主题切换)
onConfigurationUpdate(newConfig: Configuration): void {
const isDark = newConfig.colorMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK;
AppStorage.set('isDarkMode', isDark);
console.info(`主题切换: ${isDark ? '深色' : '浅色'}`);
}
onWindowStageCreate(windowStage: window.WindowStage): void {
windowStage.loadContent('pages/Index', (err) => {
if (err.code) {
console.error('Failed to load content');
}
});
}
}
第三步:在页面中使用 @StorageProp
修改 Lesson07Page.ets:
@Entry
@Component
struct Lesson07Page {
// 从 AppStorage 读取主题状态(单向绑定)
@StorageProp('isDarkMode') isDarkMode: boolean = false;
build() {
Column() {
Text(this.isDarkMode ? '当前是深色模式 🌙' : '当前是浅色模式 ☀️')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor(this.isDarkMode ? Color.White : '#1e293b')
Text('切换系统主题试试看')
.fontSize(14)
.fontColor(this.isDarkMode ? '#9ca3af' : '#64748b')
.margin({ top: 16 })
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.backgroundColor(this.isDarkMode ? '#221210' : '#f8f6f5')
}
}
第四步:理解 @StorageProp 和 @StorageLink
| 装饰器 | 数据流向 | 适用场景 |
|---|---|---|
@StorageProp |
单向(只读) | 只需要显示,不修改 |
@StorageLink |
双向 | 需要修改全局状态 |
// 单向绑定:只读取,不写回
@StorageProp('isDarkMode') isDarkMode: boolean = false;
// 双向绑定:修改会同步到 AppStorage
@StorageLink('isDarkMode') isDarkMode: boolean = false;
第五步:运行验证
hvigorw assembleHap --no-daemon
预期效果:
- 切换系统主题后,应用界面自动更新
- 无需重启应用
---
实战三:WCAG 色彩对比度规范
第一步:理解对比度要求
根据华为应用市场上架要求和 WCAG 标准:
| 场景 | 最低对比度 |
|---|---|
| 图标或标题文字与背景色 | > 3:1 |
| 正文文字与背景色 | > 4.5:1 |
第二步:深色模式推荐颜色
背景色 #221210 下的推荐颜色:
| 用途 | 颜色值 | 对比度 | 是否合规 |
|---|---|---|---|
| 主要文字 | Color.White |
15.8:1 | ✅ |
| 次要文字 | #9ca3af |
5.2:1 | ✅ |
| 提示文字 | #6b7280 |
3.1:1 | ⚠️ 仅标题可用 |
重要:深色模式下避免使用 #6b7280 作为正文颜色!
第三步:浅色模式推荐颜色
背景色 #f8f6f5 下的推荐颜色:
| 用途 | 颜色值 | 对比度 | 是否合规 |
|---|---|---|---|
| 主要文字 | #1e293b |
12.5:1 | ✅ |
| 次要文字 | #64748b |
4.8:1 | ✅ |
| 提示文字 | #94a3b8 |
3.2:1 | ⚠️ 仅标题可用 |
第四步:创建颜色速查表
在代码中使用这些颜色:
// 主要文字
.fontColor(this.isDarkMode ? Color.White : '#1e293b')
// 次要文字(满足 4.5:1)
.fontColor(this.isDarkMode ? '#9ca3af' : '#64748b')
// 页面背景
.backgroundColor(this.isDarkMode ? '#221210' : '#f8f6f5')
// 卡片背景
.backgroundColor(this.isDarkMode ? '#2d1f1d' : Color.White)
---
实战四:完整主题适配页面
第一步:定义数据接口
interface CardItem {
id: number;
title: string;
subtitle: string;
}
第二步:创建完整页面
// 文件路径:products/jiaocheng/src/main/ets/lesson07/Lesson07Page.ets
interface CardItem {
id: number;
title: string;
subtitle: string;
}
@Entry
@Component
struct Lesson07Page {
@StorageProp('isDarkMode') isDarkMode: boolean = false;
private items: CardItem[] = [
{ id: 1, title: '丞相', subtitle: '百官之长,辅佐皇帝处理政务' },
{ id: 2, title: '太尉', subtitle: '掌管全国军事' },
{ id: 3, title: '御史大夫', subtitle: '监察百官,掌管图籍' }
];
build() {
Column() {
this.PageHeader()
this.ThemeStatus()
List() {
ForEach(this.items, (item: CardItem) => {
ListItem() {
this.ItemCard(item)
}
}, (item: CardItem) => item.id.toString())
}
.width('100%')
.layoutWeight(1)
.padding({ left: 16, right: 16 })
}
.width('100%')
.height('100%')
.backgroundColor(this.isDarkMode ? '#221210' : '#f8f6f5')
}
@Builder
PageHeader() {
Row() {
Image($r('app.media.ic_back'))
.width(24)
.height(24)
.fillColor(this.isDarkMode ? '#9ca3af' : '#1e293b')
Text('主题适配演示')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(this.isDarkMode ? Color.White : '#1e293b')
.margin({ left: 12 })
Blank()
}
.width('100%')
.height(56)
.padding({ left: 16, right: 16 })
.backgroundColor(this.isDarkMode ? '#2d1f1d' : Color.White)
}
@Builder
ThemeStatus() {
Row() {
Text('当前主题:')
.fontSize(14)
.fontColor(this.isDarkMode ? '#9ca3af' : '#64748b')
Text(this.isDarkMode ? '深色模式 🌙' : '浅色模式 ☀️')
.fontSize(14)
.fontWeight(FontWeight.Medium)
.fontColor(this.isDarkMode ? Color.White : '#1e293b')
}
.width('100%')
.padding(16)
.justifyContent(FlexAlign.Center)
}
@Builder
ItemCard(item: CardItem) {
Column() {
Text(item.title)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.fontColor(this.isDarkMode ? Color.White : '#1e293b')
Text(item.subtitle)
.fontSize(13)
.fontColor(this.isDarkMode ? '#9ca3af' : '#64748b')
.margin({ top: 8 })
}
.width('100%')
.padding(16)
.margin({ bottom: 12 })
.alignItems(HorizontalAlign.Start)
.backgroundColor(this.isDarkMode ? '#2d1f1d' : Color.White)
.borderRadius(12)
.shadow({
radius: 4,
color: this.isDarkMode ? 'rgba(0, 0, 0, 0.3)' : 'rgba(0, 0, 0, 0.05)',
offsetX: 0,
offsetY: 2
})
}
}
@Builder
export function Lesson07PageBuilder() {
Lesson07Page()
}
第三步:运行验证
hvigorw assembleHap --no-daemon
预期效果:
- 页面头部、卡片、文字颜色都随主题变化
- 切换系统主题后自动更新
- 所有文字对比度满足 WCAG 标准
---
实战五:创建主题工具类
第一步:创建 ThemeUtil.ets
// 文件路径:products/jiaocheng/src/main/ets/lesson07/ThemeUtil.ets
export class ThemeUtil {
// ===== 背景色 =====
static getBgPage(isDark: boolean): string {
return isDark ? '#221210' : '#f8f6f5';
}
static getBgCard(isDark: boolean): string {
return isDark ? '#2d1f1d' : '#ffffff';
}
static getBgInput(isDark: boolean): string {
return isDark ? '#3d2f2d' : '#f0f0f0';
}
// ===== 文字颜色 =====
static getTextPrimary(isDark: boolean): ResourceColor {
return isDark ? Color.White : '#1e293b';
}
static getTextSecondary(isDark: boolean): string {
return isDark ? '#9ca3af' : '#64748b';
}
// ===== 图标颜色 =====
static getIconColor(isDark: boolean): string {
return isDark ? '#9ca3af' : '#64748b';
}
// ===== 强调色(不随主题变化)=====
static readonly primaryColor = '#c41e3a';
}
第二步:使用工具类简化代码
import { ThemeUtil } from './ThemeUtil';
// 使用前
.fontColor(this.isDarkMode ? Color.White : '#1e293b')
// 使用后
.fontColor(ThemeUtil.getTextPrimary(this.isDarkMode))
第三步:工具类的优势
- 颜色值集中管理,修改方便
- 代码更简洁,可读性更好
- 避免颜色值写错
---
完整代码
// 文件路径:products/jiaocheng/src/main/ets/lesson07/Lesson07Page.ets
interface CardItem {
id: number;
title: string;
subtitle: string;
}
@Entry
@Component
struct Lesson07Page {
@StorageProp('isDarkMode') isDarkMode: boolean = false;
private items: CardItem[] = [
{ id: 1, title: '丞相', subtitle: '百官之长,辅佐皇帝处理政务' },
{ id: 2, title: '太尉', subtitle: '掌管全国军事' },
{ id: 3, title: '御史大夫', subtitle: '监察百官,掌管图籍' }
];
build() {
Column() {
this.PageHeader()
this.ThemeStatus()
List() {
ForEach(this.items, (item: CardItem) => {
ListItem() {
this.ItemCard(item)
}
}, (item: CardItem) => item.id.toString())
}
.width('100%')
.layoutWeight(1)
.padding({ left: 16, right: 16 })
}
.width('100%')
.height('100%')
.backgroundColor(this.isDarkMode ? '#221210' : '#f8f6f5')
}
@Builder
PageHeader() {
Row() {
Image($r('app.media.ic_back'))
.width(24)
.height(24)
.fillColor(this.isDarkMode ? '#9ca3af' : '#1e293b')
Text('主题适配演示')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(this.isDarkMode ? Color.White : '#1e293b')
.margin({ left: 12 })
Blank()
}
.width('100%')
.height(56)
.padding({ left: 16, right: 16 })
.backgroundColor(this.isDarkMode ? '#2d1f1d' : Color.White)
}
@Builder
ThemeStatus() {
Row() {
Text('当前主题:')
.fontSize(14)
.fontColor(this.isDarkMode ? '#9ca3af' : '#64748b')
Text(this.isDarkMode ? '深色模式 🌙' : '浅色模式 ☀️')
.fontSize(14)
.fontWeight(FontWeight.Medium)
.fontColor(this.isDarkMode ? Color.White : '#1e293b')
}
.width('100%')
.padding(16)
.justifyContent(FlexAlign.Center)
}
@Builder
ItemCard(item: CardItem) {
Column() {
Text(item.title)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.fontColor(this.isDarkMode ? Color.White : '#1e293b')
Text(item.subtitle)
.fontSize(13)
.fontColor(this.isDarkMode ? '#9ca3af' : '#64748b')
.margin({ top: 8 })
}
.width('100%')
.padding(16)
.margin({ bottom: 12 })
.alignItems(HorizontalAlign.Start)
.backgroundColor(this.isDarkMode ? '#2d1f1d' : Color.White)
.borderRadius(12)
.shadow({
radius: 4,
color: this.isDarkMode ? 'rgba(0, 0, 0, 0.3)' : 'rgba(0, 0, 0, 0.05)',
offsetX: 0,
offsetY: 2
})
}
}
@Builder
export function Lesson07PageBuilder() {
Lesson07Page()
}
---
本课小结
核心知识点
| 知识点 | 说明 |
|---|---|
| ConfigurationConstant.ColorMode | 获取系统主题 |
| onConfigurationUpdate | 监听主题变化 |
| AppStorage | 应用级全局状态 |
| @StorageProp | 单向绑定(只读) |
| @StorageLink | 双向绑定 |
| WCAG 对比度 | 正文 > 4.5:1,标题 > 3:1 |
颜色速查表
| 模式 | 用途 | 颜色值 |
|---|---|---|
| 深色 | 页面背景 | #221210 |
| 深色 | 卡片背景 | #2d1f1d |
| 深色 | 主要文字 | Color.White |
| 深色 | 次要文字 | #9ca3af |
| 浅色 | 页面背景 | #f8f6f5 |
| 浅色 | 卡片背景 | #ffffff |
| 浅色 | 主要文字 | #1e293b |
| 浅色 | 次要文字 | #64748b |
---
课后练习
练习1:添加手动切换开关
使用 @StorageLink 实现手动切换主题:
@StorageLink('isDarkMode') isDarkMode: boolean = false;
Toggle({ type: ToggleType.Switch, isOn: this.isDarkMode })
.onChange((isOn: boolean) => {
this.isDarkMode = isOn;
})
练习2:为输入框添加主题适配
TextInput({ placeholder: '请输入' })
.backgroundColor(this.isDarkMode ? '#3d2f2d' : '#f0f0f0')
.fontColor(this.isDarkMode ? Color.White : '#1e293b')
.placeholderColor(this.isDarkMode ? '#6b7280' : '#94a3b8')
---
下一课预告
第8课我们将学习动画与交互,包括:
- 属性动画 animateTo
- 转场动画 transition
- 手势识别系统
- 实现卡片弹出动画
项目开源地址
更多推荐


所有评论(0)