我强烈推荐使用鸿蒙原生资源限定词方案,配合一个简单的开关控制。这是最优雅、最高效的实现方式。让我用一个完整可运行的示例告诉你为什么。
一、完整实现:一个开关控制白天/黑夜模式
1.1 项目结构
bash
project/
├── AppScope/resources/
│ ├── base/element/color.json # 白天模式颜色
│ └── dark/element/color.json # 夜间模式颜色
├── entry/src/main/ets/
│ ├── pages/
│ │ └── Index.ets # 主页面
│ └── utils/
│ └── ThemeUtils.ets # 主题管理工具
└── entry/src/main/resources/
├── base/media/ # 白天模式图标
└── dark/media/ # 夜间模式图标
1.2 颜色资源定义
白天主题颜色 (base/element/color.json):
json
{
"color": [
{
"name": "app_bg_primary",
"value": "#F8F9FA"
},
{
"name": "app_text_primary",
"value": "#212529"
},
{
"name": "app_card_bg",
"value": "#FFFFFF"
},
{
"name": "app_accent_primary",
"value": "#0D6EFD"
},
{
"name": "app_switch_track_on",
"value": "#34C759"
},
{
"name": "app_switch_track_off",
"value": "#E9ECEF"
}
]
}
夜间主题颜色 (dark/element/color.json):
json
{
"color": [
{
"name": "app_bg_primary",
"value": "#121212"
},
{
"name": "app_text_primary",
"value": "#E9ECEF"
},
{
"name": "app_card_bg",
"value": "#1E1E1E"
},
{
"name": "app_accent_primary",
"value": "#6EA8FE"
},
{
"name": "app_switch_track_on",
"value": "#30D158"
},
{
"name": "app_switch_track_off",
"value": "#3A3A3C"
}
]
}
1.3 主题管理工具
typescript
// utils/ThemeUtils.ets
import common from '@ohos.app.ability.common';
import Configuration from '@ohos.app.ability.Configuration';
import Preferences from '@ohos.data.preferences';
export enum ThemeMode {
LIGHT = 'light',
DARK = 'dark',
SYSTEM = 'system'
}
export class ThemeUtils {
private static instance: ThemeUtils;
private appContext: common.ApplicationContext | null = null;
private preferences: Preferences | null = null;
private readonly PREF_NAME = 'app_theme_prefs';
private readonly THEME_KEY = 'current_theme';
// 主题变化监听器
private listeners: Array<(mode: ThemeMode) => void> = [];
// 单例模式
static getInstance(): ThemeUtils {
if (!ThemeUtils.instance) {
ThemeUtils.instance = new ThemeUtils();
}
return ThemeUtils.instance;
}
// 初始化
async initialize(context: common.Context): Promise<void> {
try {
// 获取应用上下文
const abilityContext = context as common.UIAbilityContext;
this.appContext = abilityContext.configuration.appContext;
// 初始化Preferences存储
this.preferences = await Preferences.getPreferences(context, this.PREF_NAME);
console.log('ThemeUtils 初始化完成');
} catch (error) {
console.error('ThemeUtils 初始化失败:', error);
}
}
// 获取当前主题
async getCurrentTheme(): Promise<ThemeMode> {
if (!this.preferences) {
return ThemeMode.SYSTEM;
}
try {
const theme = await this.preferences.get(this.THEME_KEY, ThemeMode.SYSTEM) as string;
return theme as ThemeMode;
} catch (error) {
console.error('获取主题失败:', error);
return ThemeMode.SYSTEM;
}
}
// 切换主题
async toggleTheme(): Promise<void> {
const current = await this.getCurrentTheme();
let newTheme: ThemeMode;
// 简单切换逻辑:白天 ↔ 夜间
if (current === ThemeMode.LIGHT) {
newTheme = ThemeMode.DARK;
} else if (current === ThemeMode.DARK) {
newTheme = ThemeMode.LIGHT;
} else {
// 如果是跟随系统,默认切换到夜间
newTheme = ThemeMode.DARK;
}
await this.setTheme(newTheme);
}
// 设置主题
async setTheme(mode: ThemeMode): Promise<void> {
if (!this.appContext || !this.preferences) {
console.error('ThemeUtils 未初始化');
return;
}
try {
// 保存到Preferences
await this.preferences.put(this.THEME_KEY, mode);
await this.preferences.flush();
// 应用到系统
let colorMode: Configuration.ColorMode;
switch (mode) {
case ThemeMode.LIGHT:
colorMode = Configuration.ColorMode.COLOR_MODE_LIGHT;
break;
case ThemeMode.DARK:
colorMode = Configuration.ColorMode.COLOR_MODE_DARK;
break;
case ThemeMode.SYSTEM:
colorMode = Configuration.ColorMode.COLOR_MODE_SYSTEM;
break;
default:
colorMode = Configuration.ColorMode.COLOR_MODE_LIGHT;
}
this.appContext.setColorMode(colorMode);
console.log(`主题已切换为: ${mode}`);
// 通知所有监听器
this.notifyListeners(mode);
} catch (error) {
console.error('设置主题失败:', error);
}
}
// 监听主题变化
addListener(callback: (mode: ThemeMode) => void): void {
this.listeners.push(callback);
}
// 移除监听器
removeListener(callback: (mode: ThemeMode) => void): void {
const index = this.listeners.indexOf(callback);
if (index > -1) {
this.listeners.splice(index, 1);
}
}
// 通知所有监听器
private notifyListeners(mode: ThemeMode): void {
for (const listener of this.listeners) {
try {
listener(mode);
} catch (error) {
console.error('监听器执行失败:', error);
}
}
}
}
1.4 主页面实现(包含切换开关)
typescript
// pages/Index.ets
import { ThemeUtils, ThemeMode } from '../utils/ThemeUtils';
@Entry
@Component
struct Index {
@State currentTheme: ThemeMode = ThemeMode.LIGHT;
@State isDarkMode: boolean = false;
// 在页面显示时初始化主题
aboutToAppear(): void {
this.initTheme();
}
// 初始化主题
async initTheme(): Promise<void> {
const themeUtils = ThemeUtils.getInstance();
this.currentTheme = await themeUtils.getCurrentTheme();
this.isDarkMode = this.currentTheme === ThemeMode.DARK;
// 监听主题变化
themeUtils.addListener((mode: ThemeMode) => {
this.currentTheme = mode;
this.isDarkMode = mode === ThemeMode.DARK;
});
}
// 切换主题
async toggleTheme(): Promise<void> {
const themeUtils = ThemeUtils.getInstance();
await themeUtils.toggleTheme();
}
// 获取开关状态文字
getSwitchText(): string {
return this.isDarkMode ? '夜间模式' : '白天模式';
}
// 获取模式图标
getModeIcon(): Resource {
return this.isDarkMode
? $r('app.media.icon_moon') // 月亮图标(夜间)
: $r('app.media.icon_sun'); // 太阳图标(白天)
}
// 构建界面
build() {
Column() {
// 顶部标题栏
Row() {
Text('主题设置')
.fontSize(24)
.fontColor($r('app.color.app_text_primary'))
.fontWeight(FontWeight.Medium)
Blank()
Image(this.getModeIcon())
.width(24)
.height(24)
}
.width('100%')
.padding({ left: 20, right: 20, top: 12, bottom: 12 })
.backgroundColor($r('app.color.app_card_bg'))
// 主要内容区域
Scroll() {
Column() {
// 主题切换卡片
Column() {
Row() {
Column() {
Text('外观模式')
.fontSize(18)
.fontColor($r('app.color.app_text_primary'))
.fontWeight(FontWeight.Medium)
Text(this.isDarkMode ? '深色主题,保护眼睛' : '浅色主题,清晰明亮')
.fontSize(14)
.fontColor($r('app.color.app_text_primary'))
.opacity(0.6)
.margin({ top: 4 })
}
.flexGrow(1)
// 主题切换开关
Toggle({ type: ToggleType.Switch, isOn: this.isDarkMode })
.selectedColor($r('app.color.app_switch_track_on'))
.switchPointColor($r('app.color.app_card_bg'))
.onChange((isOn: boolean) => {
this.toggleTheme();
})
}
.padding(20)
}
.backgroundColor($r('app.color.app_card_bg'))
.borderRadius(12)
.margin({ top: 20, left: 20, right: 20 })
// 示例内容区域
Column() {
Text('示例内容')
.fontSize(20)
.fontColor($r('app.color.app_text_primary'))
.fontWeight(FontWeight.Bold)
.margin({ bottom: 16 })
Text('这是一个文本示例,用于展示不同主题下的显示效果。在白天模式下,文字为深色;在夜间模式下,文字为浅色。')
.fontSize(16)
.fontColor($r('app.color.app_text_primary'))
.opacity(0.8)
.lineHeight(24)
Divider()
.strokeWidth(1)
.color($r('app.color.app_text_primary'))
.opacity(0.1)
.margin({ top: 20, bottom: 20 })
Row() {
Button('主要按钮')
.backgroundColor($r('app.color.app_accent_primary'))
.fontColor('#FFFFFF')
.borderRadius(8)
.padding({ left: 20, right: 20 })
Button('次要按钮')
.backgroundColor($r('app.color.app_card_bg'))
.fontColor($r('app.color.app_text_primary'))
.border({
color: $r('app.color.app_text_primary'),
width: 1,
style: BorderStyle.Solid
})
.borderRadius(8)
.margin({ left: 12 })
.padding({ left: 20, right: 20 })
}
.margin({ top: 16 })
}
.padding(24)
.backgroundColor($r('app.color.app_card_bg'))
.borderRadius(12)
.margin({ top: 20, left: 20, right: 20, bottom: 40 })
}
.width('100%')
}
.flexGrow(1)
}
.width('100%')
.height('100%')
.backgroundColor($r('app.color.app_bg_primary'))
}
}
1.5 在EntryAbility中初始化
typescript
// entry/src/main/ets/entryability/EntryAbility.ets
import { ThemeUtils } from '../utils/ThemeUtils';
import UIAbility from '@ohos.app.ability.UIAbility';
export default class EntryAbility extends UIAbility {
onCreate(want, launchParam) {
console.log('EntryAbility onCreate');
// 初始化主题工具
ThemeUtils.getInstance().initialize(this.context);
}
}
二、为什么这是最佳方案?
2.1 简单直观,一行代码切换
typescript
// 核心切换逻辑,只需一行代码!
this.appContext.setColorMode(colorMode);
2.2 自动适配所有组件
一旦切换系统颜色模式,所有使用资源引用的组件都会自动更新:
- 文本颜色
- 背景颜色
- 边框颜色
- 图标资源
- 甚至图片资源(如果有dark目录版本)
2.3 零耦合,高内聚
- 业务组件 :只关心
$r('app.color.xxx'),不关心当前主题 - 主题管理 :集中在
ThemeUtils,统一管理状态 - 资源定义:设计师可以直接修改JSON文件
2.4 持久化存储
使用Preferences自动保存用户选择,下次启动自动恢复:
typescript
// 保存主题选择
await preferences.put('current_theme', mode);
await preferences.flush();
// 读取主题选择
const theme = await preferences.get('current_theme', 'light');
三、对比工具类方案的劣势
如果用工具类方案,你需要:
3.1 繁琐的状态管理
typescript
// 每个组件都需要监听主题变化
@Component
struct MyComponent {
@State textColor: string = '#000000';
aboutToAppear() {
// 监听主题变化
ThemeManager.addListener(() => {
this.textColor = ThemeManager.getTextColor();
});
}
build() {
Text('示例')
.fontColor(this.textColor) // 需要状态变量
}
}
3.2 手动更新所有组件
typescript
// 切换主题时需要手动更新所有组件
class ThemeManager {
static toggleTheme() {
this.isDark = !this.isDark;
// 需要手动触发所有组件更新
for (const component of this.registeredComponents) {
component.updateTheme();
}
}
}
3.3 性能问题
- 每次切换都要重新计算所有颜色
- 组件需要频繁重绘
- 无法利用系统的优化机制
四、高级功能扩展
4.1 添加跟随系统选项
typescript
// 在ThemeUtils中添加
async setFollowSystem(enable: boolean): Promise<void> {
if (enable) {
await this.setTheme(ThemeMode.SYSTEM);
// 监听系统主题变化
this.appContext.on('colorModeChange', (newMode: Configuration.ColorMode) => {
console.log('系统主题已改变:', newMode);
this.notifyListeners(this.mapSystemMode(newMode));
});
}
}
private mapSystemMode(mode: Configuration.ColorMode): ThemeMode {
switch (mode) {
case Configuration.ColorMode.COLOR_MODE_DARK:
return ThemeMode.DARK;
case Configuration.ColorMode.COLOR_MODE_LIGHT:
return ThemeMode.LIGHT;
default:
return ThemeMode.LIGHT;
}
}
4.2 添加动画效果
typescript
// 在切换主题时添加过渡动画
async toggleThemeWithAnimation(): Promise<void> {
// 先设置半透明
this.applyOpacityAnimation();
// 延迟切换主题
setTimeout(async () => {
await this.toggleTheme();
// 恢复不透明
this.removeOpacityAnimation();
}, 300);
}
4.3 多主题支持(节日主题等)
typescript
// 扩展支持更多主题
enum ExtendedThemeMode {
LIGHT = 'light',
DARK = 'dark',
SYSTEM = 'system',
FESTIVAL = 'festival', // 节日主题
HIGH_CONTRAST = 'high_contrast' // 高对比度
}
// 创建对应的资源目录
// resources/festival/element/color.json
// resources/high_contrast/element/color.json
五、实践建议
5.1 新项目:直接使用资源限定词方案
- 从第一天就建立
base/和dark/目录 - 所有颜色使用
$r('app.color.xxx') - 一个开关控制全部
5.2 老项目迁移:三步走
- 第一步 :创建
dark/color.json,复制所有颜色 - 第二步:逐步替换硬编码为资源引用
- 第三步:添加主题切换开关
5.3 设计规范
- 使用语义化颜色名称:
primary_text、secondary_bg - 建立颜色设计系统文档
- 定期同步设计和开发的颜色值
总结
对于"一个开关控制白天/黑夜模式"的需求,鸿蒙的资源限定词方案是最佳选择。
它提供了:
- ✅ 一键切换:一个开关控制全局
- ✅ 自动适配:所有组件自动更新
- ✅ 零代码侵入:业务组件无需修改
- ✅ 高性能:系统级优化
- ✅ 易维护:颜色集中管理
- ✅ 可扩展:支持多主题
而工具类方案需要:
- ❌ 每个组件监听主题变化
- ❌ 手动更新所有颜色
- ❌ 性能损耗
- ❌ 维护困难
选择资源限定词方案,让你的主题切换像呼吸一样自然!