资源管理与多主题适配

应用实拍

鸿蒙原生开发手记:徒步迹 - 资源管理与多主题适配

学习鸿蒙资源系统,实现深色模式和主题切换


前言

资源管理是 App 国际化、多主题适配的基础。HarmonyOS 提供了完善的资源管理框架,支持多语言、多屏幕密度、深色/浅色模式等能力。在"徒步迹"项目中,我们需要管理超过 50 个字符串资源12 组颜色主题值多语言适配 以及 深色模式 的完整切换。

本文将深入讲解 HarmonyOS 资源系统的架构设计,并以"徒步迹"的实际项目为例,展示从资源目录规划、颜色体系定义到运行时动态切换的完整实现方案。


一、资源目录结构设计

1.1 标准资源目录

HarmonyOS 的资源目录按照 限定词目录 + 默认目录 的层级组织,系统会根据当前设备的语言、屏幕密度、颜色模式等配置自动匹配对应的资源文件:

resources/
├── base/                              # 基础资源(默认)
│   ├── element/
│   │   ├── string.json
│   │   ├── color.json
│   │   ├── float.json
│   │   └── boolean.json
│   ├── media/                         # 图片资源
│   │   ├── app_icon.png
│   │   ├── marker_start.png
│   │   ├── marker_end.png
│   │   └── ...
│   ├── profile/                       # 配置文件
│   │   └── main_pages.json
│   └── rawfile/                       # 原始文件
│       └── hiking_routes.json
├── en_US/                             # 英语(美国)
│   └── element/
│       └── string.json
├── zh_CN/                             # 简体中文
│   └── element/
│       └── string.json
└── dark/                              # 深色模式资源
    └── element/
        └── color.json

关键原则:在 base 目录与 dark 目录中定义 同名资源,系统会在深浅色模式切换时自动加载对应目录下的值。例如 base/element/color.json 中定义 text_color#212121dark/element/color.json 中定义 text_color#E0E0E0,切换时无需额外代码。

1.2 限定词目录匹配规则

限定词目录的命名格式为 限定词_取值,例如 zh_CNen_USdark。系统支持的限定词类型包括:

  1. 语言与地区zh_CNen_USja_JP
  2. 屏幕密度mdpihdpixhdpixxhdpi
  3. 颜色模式dark(深色模式专属)
  4. 屏幕尺寸smallnormallargexlarge

当存在多个限定词目录时,系统按照 精确匹配 > 默认匹配 的优先级查找资源。例如,在 zh_CN-darkdark 两个目录都存在时,zh_CN-dark 的优先级更高。


二、徒步迹的字符串资源设计

2.1 基础字符串定义

resources/base/element/string.json 中定义应用的核心字符串:

{
  "string": [
    {
      "name": "app_name",
      "value": "徒步迹"
    },
    {
      "name": "login_title",
      "value": "登录"
    },
    {
      "name": "home_title",
      "value": "首页"
    },
    {
      "name": "tracking_title",
      "value": "轨迹记录"
    },
    {
      "name": "health_title",
      "value": "健康数据"
    },
    {
      "name": "profile_title",
      "value": "个人中心"
    },
    {
      "name": "reason_location",
      "value": "用于记录徒步轨迹和显示当前位置"
    },
    {
      "name": "reason_camera",
      "value": "用于拍摄沿途风景照片"
    }
  ]
}

2.2 多语言资源配

英文资源文件 resources/en_US/element/string.json

{
  "string": [
    { "name": "app_name", "value": "Tuji" },
    { "name": "home_title", "value": "Home" },
    { "name": "tracking_title", "value": "Tracking" },
    { "name": "health_title", "value": "Health" },
    { "name": "profile_title", "value": "Profile" }
  ]
}

2.3 字符串的引用方式

在代码中使用 $r() 语法引用资源:

// 引用字符串资源
Text($r('app.string.home_title'))
  .fontSize(20)
  .fontWeight(FontWeight.Bold);

// 获取字符串(支持参数替换)
let context: Context = getContext(this);
let resMgr = context.resourceManager;
let appName: string = resMgr.getStringSync($r('app.string.app_name'));

提示$r() 语法支持三级引用路径:app.资源类型.资源名称。资源类型包括 stringcolorfloatbooleanintegermedia 等。


三、颜色体系与主题系统

3.1 浅色模式颜色定义

resources/base/element/color.json 中定义应用在浅色模式下的完整颜色体系:

{
  "color": [
    { "name": "primary_color", "value": "#4CAF50" },
    { "name": "primary_dark", "value": "#388E3C" },
    { "name": "primary_light", "value": "#81C784" },
    { "name": "accent_color", "value": "#FF6F00" },
    { "name": "background_color", "value": "#F5F5F5" },
    { "name": "card_background", "value": "#FFFFFF" },
    { "name": "text_primary", "value": "#212121" },
    { "name": "text_secondary", "value": "#757575" },
    { "name": "divider_color", "value": "#E0E0E0" },
    { "name": "error_color", "value": "#F44336" },
    { "name": "success_color", "value": "#4CAF50" },
    { "name": "warning_color", "value": "#FF9800" }
  ]
}

3.2 深色模式颜色定义

resources/dark/element/color.json 中定义深色模式下的颜色覆盖:

{
  "color": [
    { "name": "background_color", "value": "#121212" },
    { "name": "card_background", "value": "#1E1E1E" },
    { "name": "text_primary", "value": "#E0E0E0" },
    { "name": "text_secondary", "value": "#9E9E9E" },
    { "name": "divider_color", "value": "#333333" }
  ]
}

深色模式下无需重写所有颜色,只需覆盖与浅色模式不同的部分。未覆盖的颜色(如 primary_color)会沿用 base 目录中的定义。

3.3 在代码中使用颜色资源

// 使用 $r 引用颜色资源,系统自动适配深浅色模式
Text('徒步迹')
  .fontColor($r('app.color.primary_color'));

Column()
  .backgroundColor($r('app.color.background_color'));

Text('详细描述')
  .fontColor($r('app.color.text_secondary'));

// 卡片背景在深浅色模式下自动切换
Card()
  .backgroundColor($r('app.color.card_background'));

3.4 颜色适配的常见问题

问题场景 错误表现 解决方案
自定义弹窗背景色 深色模式下弹窗内容看不清 使用 $r() 引用颜色资源,而非硬编码色值
图标颜色不适配 深色模式下图标不可见 使用 SVG 图标 + fillColor() 属性
列表分割线 深色模式下对比度不足 定义 divider_color 资源并引用
输入框占位文字 深色模式下占位文字过暗 使用 text_secondary 语义颜色

表 1:颜色资源适配常见问题对照表


四、深色模式适配方案

4.1 跟随系统切换

当应用需要跟随系统深浅色模式自动切换时,调用 setColorMode() 方法将 ColorMode 设置为 COLOR_MODE_NOT_SET

import { ApplicationContext } from '@kit.AbilityKit';
import { ConfigurationConstant } from '@kit.AbilityKit';

function followSystemTheme(context: ApplicationContext): void {
  context.setColorMode(
    ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET
  );
  // 应用运行过程中自动感知系统颜色模式切换
}

4.2 应用内手动切换

在"徒步迹"的设置页面中,用户可以通过开关手动控制主题:

@Entry
@Component
struct SettingsPage {
  @State isDarkMode: boolean = false;
  @State currentTheme: string = '浅色模式';

  aboutToAppear(): void {
    // 从 Preferences 读取已保存的主题设置
    let data = AppStorage.get<string>('theme_mode');
    this.isDarkMode = data === 'dark';
    this.currentTheme = this.isDarkMode ? '深色模式' : '浅色模式';
  }

  build() {
    Column() {
      // 主题切换区域
      Column() {
        Row() {
          Text('深色模式')
            .fontSize(16)
            .fontWeight(FontWeight.Medium)
            .layoutWeight(1);

          Text(this.currentTheme)
            .fontSize(14)
            .fontColor($r('app.color.text_secondary'))
            .margin({ right: 12 });

          Toggle({ type: ToggleType.Switch, isOn: this.isDarkMode })
            .selectedColor($r('app.color.primary_color'))
            .onChange((isOn: boolean) => {
              this.isDarkMode = isOn;
              this.switchTheme(isOn);
            });
        }
        .width('100%')
        .padding(16);
      }
      .width('100%')
      .backgroundColor($r('app.color.card_background'))
      .borderRadius(12)
      .margin({ left: 16, right: 16, top: 16 });

      // 主题说明
      Text('开启后,应用将使用深色背景,减少屏幕亮度对眼睛的刺激')
        .fontSize(13)
        .fontColor($r('app.color.text_secondary'))
        .width('100%')
        .padding({ left: 16, right: 16, top: 8 });
    }
    .width('100%')
    .height('100%')
    .backgroundColor($r('app.color.background_color'));
  }

  switchTheme(dark: boolean): void {
    let context = getContext(this) as ApplicationContext;
    if (dark) {
      context.setColorMode(
        ConfigurationConstant.ColorMode.COLOR_MODE_DARK
      );
      this.currentTheme = '深色模式';
      AppStorage.setOrCreate('theme_mode', 'dark');
    } else {
      context.setColorMode(
        ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT
      );
      this.currentTheme = '浅色模式';
      AppStorage.setOrCreate('theme_mode', 'light');
    }
  }
}

4.3 媒体资源适配

深色模式下,部分图片和图标需要适配。SVG 类型的图标可以使用 fillColor() 属性动态修改颜色:

// SVG 图标自动适配深色模式
Image($r('app.media.ic_hiking'))
  .width(24)
  .height(24)
  .fillColor($r('app.color.text_primary'));
// 系统会根据当前深浅色模式自动切换 fillColor 的值
媒体类型 适配策略 实现方式
SVG 图标 使用 fillColor() 属性 Image().fillColor($r('app.color.text_primary'))
PNG 图片 深色模式下使用不同图片 dark/media/ 目录放置替换图片
启动页 配置深色模式启动页 resources/dark/media/ 放置 start_up.png
背景图 降低透明度或替换 使用 dark/media/ 目录下的替换资源

表 2:媒体资源深色模式适配策略

4.4 状态栏适配

深色模式下状态栏的文字颜色需要跟随调整:

function setStatusBarStyle(dark: boolean): void {
  let windowClass = getContext(this).window;
  windowClass?.setWindowSystemBarProperties({
    isDarkMode: dark,
    // 深色模式使用浅色状态栏文字,浅色模式使用深色状态栏文字
    statusBarContentColor: dark ? '#FFFFFF' : '#000000',
  });
}

五、国际化完整实践

5.1 浮点数与布尔资源

除了字符串和颜色,资源管理器还支持浮点数、布尔值和整型资源:

// resources/base/element/float.json
{
  "float": [
    { "name": "card_radius", "value": "16vp" },
    { "name": "avatar_size", "value": "40vp" },
    { "name": "icon_size_small", "value": "16vp" },
    { "name": "icon_size_large", "value": "32vp" }
  ]
}
// resources/base/element/boolean.json
{
  "boolean": [
    { "name": "feature_enabled", "value": "true" },
    { "name": "show_ads", "value": "false" }
  ]
}

5.2 运行时获取多种类型资源

let context: Context = getContext(this);
let resMgr = context.resourceManager;

// 获取浮点数(卡片圆角半径)
let cardRadius = resMgr.getNumberSync($r('app.float.card_radius'));

// 获取布尔值(功能开关)
let isFeatureEnabled = resMgr.getBooleanSync($r('app.boolean.feature_enabled'));

// 获取整型(列表最大条目数)
let maxItems = resMgr.getNumberSync($r('app.integer.max_list_items'));

// 获取当前系统语言
let language = resMgr.getSystemLanguage();  // "zh-CN" / "en-US"

// 获取当前系统地区
let region = resMgr.getSystemRegion();  // "CN" / "US"

5.3 资源热更新

在应用运行时,可以通过 resourceManagerupdateResource 接口实现资源热更新,无需重启应用:

// 动态更新资源(适用于在线主题切换)
async function updateThemeResources(context: Context): Promise<void> {
  let resMgr = context.resourceManager;
  try {
    await resMgr.updateResource({
      // 在线下载的主题资源包路径
      path: '/data/app/el1/100/base/com.hiking.tuji/haps/entry/files/theme_autumn.zip',
    });
    console.info('主题资源更新成功');
  } catch (err) {
    console.error('主题资源更新失败:', JSON.stringify(err));
  }
}

六、动态主题切换与扩展

6.1 多主题扩展思路

除了深浅色模式,HarmonyOS 的资源限定机制还可以用于实现多套主题。例如,为"徒步迹"设计"春、夏、秋、冬"四季主题:

主题名称 主色调 背景色 卡片色 适用场景
春季主题 #4CAF50 绿 #F1F8E9 #FFFFFF 默认主题
夏季主题 #FF9800 #FFF3E0 #FFFFFF 夏季活动
秋季主题 #795548 #EFEBE9 #FFFFFF 秋季徒步
冬季主题 #2196F3 #E3F2FD #FFFFFF 冬季活动

表 4:徒步迹四季主题配色方案

实现时,可以动态替换 color.json 中的颜色值,或者通过自定义 @State 状态变量控制组件颜色,实现运行时主题切换。

6.2 组件级主题注入

对于需要精细控制的场景,可以将主题数据通过 @Provide 注入到子组件中:

@Entry
@Component
struct ThemeProvider {
  @Provide('theme') theme: string = 'spring';

  build() {
    Column() {
      // 主题选择器
      Row() {
        this.ThemeButton('春季', 'spring');
        this.ThemeButton('夏季', 'summer');
        this.ThemeButton('秋季', 'autumn');
        this.ThemeButton('冬季', 'winter');
      }
      .width('100%')
      .padding(16)
      .justifyContent(FlexAlign.SpaceAround);

      // 子组件自动获取主题
      ThemeConsumer()
    }
    .width('100%')
    .height('100%')
    .backgroundColor($r('app.color.background_color'));
  }

  @Builder
  ThemeButton(label: string, theme: string) {
    Button(label)
      .fontSize(14)
      .backgroundColor(this.theme === theme ? '#4CAF50' : '#E0E0E0')
      .fontColor(this.theme === theme ? Color.White : '#666')
      .borderRadius(20)
      .onClick(() => {
        this.theme = theme;
      });
  }
}

七、资源访问性能优化

7.1 资源缓存策略

频繁调用 getStringSync() 等资源获取接口会有性能开销,建议在组件初始化时缓存常用资源:

@Component
struct HomePage {
  @State private cachedStrings: Map<string, string> = new Map();
  @State private cachedColors: Map<string, Resource> = new Map();

  aboutToAppear(): void {
    // 预缓存常用资源
    this.cachedStrings.set('app_name', $r('app.string.app_name'));
    this.cachedStrings.set('home_title', $r('app.string.home_title'));
    this.cachedColors.set('primary', $r('app.color.primary_color'));
    this.cachedColors.set('background', $r('app.color.background_color'));
  }

  build() {
    Column() {
      // 使用缓存资源
      Text(this.cachedStrings.get('app_name'))
        .fontColor(this.cachedColors.get('primary'));
    }
  }
}

6.2 资源引用与硬编码的对比

实现方式 多语言支持 多主题支持 维护成本
$r('app.string.xxx')
硬编码字符串
全局常量 手动切换 手动切换
resMgr.getStringSync()

表 3:资源引用方式对比


七、常见问题与调试技巧

7.1 资源加载失败排查

资源引用不生效的常见原因排查清单:

  1. 资源名称拼写错误$r('app.color.primary')color.json 中定义的 primary_color 不匹配
  2. 资源文件未打包:检查 resources/ 目录是否在 module.json5srcPath 配置中
  3. 限定词目录未匹配:确保 dark/ 目录下的资源名称与 base/ 目录一致
  4. 缓存未刷新:修改资源文件后,需要重新编译运行

7.2 调试工具

// 使用 hilog 输出当前资源加载状态
import { hilog } from '@kit.PerformanceAnalysisKit';

function debugResourceLoading(context: Context): void {
  let resMgr = context.resourceManager;
  const TAG = 'ResourceDebug';

  // 输出当前系统配置
  hilog.info(0x0001, TAG, `语言: ${resMgr.getSystemLanguage()}`);
  hilog.info(0x0001, TAG, `地区: ${resMgr.getSystemRegion()}`);
  hilog.info(0x0001, TAG, `设备类型: ${resMgr.getDeviceType()}`);
}

调试建议:在 DevEco Studio 中启用 “Show Resource Loading Log” 选项,可以实时查看资源加载路径和匹配结果。

八、总结

资源管理是应用质量的重要体,合理的资源结构让多语言、多主题适配变得简单。本文从"徒步迹"项目出发,覆盖了从资源目录设计、颜色体系定义、深色模式适配到国际化实践的完整链路。

关键要点回顾:

  1. 资源目录规范:严格遵循 base/ + 限定词目录的组织结构
  2. 颜色体系:采用语义化颜色命名,利用 $r() 实现自动适配
  3. 深色模式适配:覆盖颜色资源、媒体资源、状态栏三个维度
  4. 国际化:通过 zh_CN/en_US 等语言目录实现多语言
  5. 性能优化:预缓存常用资源,避免频繁调用资源接口

最佳实践:始终使用 $r() 语法引用资源,避免硬编码色值和字符串。这样当未来需要支持更多语言或主题时,只需添加资源文件,无需修改业务代码。


如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!


相关资源:

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐