鸿蒙主题切换:一个开关搞定白天/黑夜模式

我强烈推荐使用鸿蒙原生资源限定词方案,配合一个简单的开关控制。这是最优雅、最高效的实现方式。让我用一个完整可运行的示例告诉你为什么。

一、完整实现:一个开关控制白天/黑夜模式

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 老项目迁移:三步走

  1. 第一步 :创建dark/color.json,复制所有颜色
  2. 第二步:逐步替换硬编码为资源引用
  3. 第三步:添加主题切换开关

5.3 设计规范

  1. 使用语义化颜色名称:primary_textsecondary_bg
  2. 建立颜色设计系统文档
  3. 定期同步设计和开发的颜色值

总结

对于"一个开关控制白天/黑夜模式"的需求,鸿蒙的资源限定词方案是最佳选择。

它提供了:

  • 一键切换:一个开关控制全局
  • 自动适配:所有组件自动更新
  • 零代码侵入:业务组件无需修改
  • 高性能:系统级优化
  • 易维护:颜色集中管理
  • 可扩展:支持多主题

而工具类方案需要:

  • ❌ 每个组件监听主题变化
  • ❌ 手动更新所有颜色
  • ❌ 性能损耗
  • ❌ 维护困难

选择资源限定词方案,让你的主题切换像呼吸一样自然!

相关推荐
蜀道山QAQ5 小时前
HarmonyOS应用开发:弹窗封装
harmonyos·鸿蒙开发·harmonyos4.0
w139548564227 小时前
Flutter跨平台开发鸿蒙化JS-Dart通信桥接组件使用指南
javascript·flutter·harmonyos
御承扬8 小时前
鸿蒙原生系列之动画效果(关键帧动画)
华为·harmonyos·鸿蒙ndk ui·关键帧动画
Keya9 小时前
DevEco Studio 使用技巧全面解析
前端·前端框架·harmonyos
前端世界9 小时前
HarmonyOS 应用启动太慢?一套实战方案把首屏时间压下来
华为·harmonyos
灰灰勇闯IT11 小时前
鸿蒙 ArkUI 声明式 UI 核心:状态管理(@State/@Prop/@Link)实战解析
人工智能·计算机视觉·harmonyos
方白羽12 小时前
Android和HarmonyOS 设置透明度
android·app·harmonyos
特立独行的猫a14 小时前
HarmonyOS鸿蒙PC开源QT软件移植:基于 Qt Widgets 的网络调试助手工具
qt·开源·harmonyos·鸿蒙pc
不爱吃糖的程序媛14 小时前
仓颉Nightly Builds版本正式解锁鸿蒙PC开发
华为·harmonyos