1. 前言

同一张明信片,白天想用浅色天空,夜间想换成深色山景;放到二合一设备上,还想多留一点横向风景。第一反应可能是在页面里加几个判断,再给 Image 传不同路径。图少时还能记住,页面一多,条件就容易散到各个角落。

这次把选择交给资源目录。页面只引用 app.media.postcard,标题只引用 app.string.postcard_scene。名字不变,不同限定符目录提供不同内容,让资源管理器根据运行设备和外观配置匹配。

2. 限定符目录

目录

作用

图片与文案

base

默认版本

默认日景

dark

深色外观版本

默认夜景

2in1

二合一设备版本

二合一日景

2in1-dark

二合一设备的深色版本

二合一夜景

每个目录下面都有 media/postcard.svg 和对应的 element/string.json。限定符属于目录名,资源文件名仍然相同,不需要把文件改成 postcard_pc_dark 之类的一长串名字。

base 不能省。它负责提供基础资源,也承担无法匹配到特定版本时的回退。特定目录只放需要变化的内容,不必把应用全部资源复制四遍;复制得越多,后面改一个公共文案越容易遗漏。

本例组合目录使用 2in1-dark,设备类型限定符在前,颜色模式限定符在后。目录名不是随意拼接的标签,顺序不符合资源规则会在打包阶段报错。遇到这种错误,应先核对限定符目录,而不是改 Image 的引用来绕过去。

示例里的二合一版本还增加了前景树木,默认版本使用水面细节。日夜版本改变天空和远山颜色,但仍保留相同的资源用途。这样,不同资源不是为了凑目录而复制四份同样的文件,读代码和看运行结果都能理解它们各自负责的变化。

可以把资源引用想成一张索引卡:页面写的是用途相同的名字,目录补充适用条件。以后更换二合一夜景,只修改那一份对应文件,引用它的页面保持原样。这个组织方式的好处,在第四张图出现时就能看出来——页面没有再增加第四个文件路径判断。

2.1 四组同名资源

下面按目录列出本例使用的文件。默认字符串表保留模板原有的应用名称和描述,增加 postcard_scene;不要只留下新增文案,把入口标签等原资源删掉。其他限定目录只定义需要覆盖的场景文案即可。

resources/base/media/postcard.svg:

<svg xmlns="http://www.w3.org/2000/svg" width="1000" height="550" viewBox="0 0 1000 550"><rect width="1000" height="550" fill="#C7DFDB"/><circle cx="790" cy="120" r="55" fill="#EED499"/><path d="M0 360L220 170L480 390L720 200L1000 350V550H0Z" fill="#6D9D83"/><path d="M0 450L280 330L560 470L820 330L1000 420V550H0Z" fill="#496F62"/><path d="M450 550L620 360L680 385L570 550Z" fill="#DBD3B1"/><ellipse cx="250" cy="475" rx="170" ry="25" fill="#8CB4AB"/></svg>

resources/base/element/string.json:

{
  "string": [
    {
      "name": "module_desc",
      "value": "module description"
    },
    {
      "name": "EntryAbility_desc",
      "value": "description"
    },
    {
      "name": "EntryAbility_label",
      "value": "四时明信片"
    },
    {
      "name": "postcard_scene",
      "value": "默认日景 · 山野来信"
    }
  ]
}

resources/dark/media/postcard.svg:

<svg xmlns="http://www.w3.org/2000/svg" width="1000" height="550" viewBox="0 0 1000 550"><rect width="1000" height="550" fill="#182D47"/><circle cx="790" cy="120" r="55" fill="#E8E9CA"/><path d="M0 360L220 170L480 390L720 200L1000 350V550H0Z" fill="#34556B"/><path d="M0 450L280 330L560 470L820 330L1000 420V550H0Z" fill="#496F62"/><path d="M450 550L620 360L680 385L570 550Z" fill="#DBD3B1"/><ellipse cx="250" cy="475" rx="170" ry="25" fill="#8CB4AB"/></svg>

resources/dark/element/string.json:

{
  "string": [
    {
      "name": "postcard_scene",
      "value": "默认夜景 · 山野来信"
    }
  ]
}

resources/2in1/media/postcard.svg:

<svg xmlns="http://www.w3.org/2000/svg" width="1000" height="550" viewBox="0 0 1000 550"><rect width="1000" height="550" fill="#C7DFDB"/><circle cx="790" cy="120" r="55" fill="#EED499"/><path d="M0 360L220 170L480 390L720 200L1000 350V550H0Z" fill="#6D9D83"/><path d="M0 450L280 330L560 470L820 330L1000 420V550H0Z" fill="#496F62"/><path d="M450 550L620 360L680 385L570 550Z" fill="#DBD3B1"/><path d="M110 420L150 290L190 420ZM180 440L220 310L260 440Z" fill="#2E6054"/></svg>

resources/2in1/element/string.json:

{
  "string": [
    {
      "name": "postcard_scene",
      "value": "二合一日景 · 山野来信"
    }
  ]
}

resources/2in1-dark/media/postcard.svg:

<svg xmlns="http://www.w3.org/2000/svg" width="1000" height="550" viewBox="0 0 1000 550"><rect width="1000" height="550" fill="#182D47"/><circle cx="790" cy="120" r="55" fill="#E8E9CA"/><path d="M0 360L220 170L480 390L720 200L1000 350V550H0Z" fill="#34556B"/><path d="M0 450L280 330L560 470L820 330L1000 420V550H0Z" fill="#496F62"/><path d="M450 550L620 360L680 385L570 550Z" fill="#DBD3B1"/><path d="M110 420L150 290L190 420ZM180 440L220 310L260 440Z" fill="#2E6054"/></svg>

resources/2in1-dark/element/string.json:

{
  "string": [
    {
      "name": "postcard_scene",
      "value": "二合一夜景 · 山野来信"
    }
  ]
}

3. 固定资源名的页面实现

页面 pages/Index.ets 的全部代码如下。Image 与 Text 引用的资源名固定,外观按钮只改变应用颜色模式,选择匹配文件的工作仍然属于资源管理器。

import { deviceInfo } from '@kit.BasicServicesKit';
import { common, ConfigurationConstant } from '@kit.AbilityKit';

@Entry
@Component
struct Index {
  @State private sent: boolean = false;
  @State private dark: boolean = false;
  private toggle(): void {
    this.dark = !this.dark;
    (this.getUIContext().getHostContext() as common.UIAbilityContext).getApplicationContext().setColorMode(
      this.dark ? ConfigurationConstant.ColorMode.COLOR_MODE_DARK : ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT);
  }
  build() {
    Scroll() {
      Column({ space: 22 }) {
        Row() {
          Text('四时明信片').fontSize(28).fontWeight(FontWeight.Bold).fontColor('#384449');
          Blank();
          Text('POSTCARD').fontSize(12).letterSpacing(3).fontColor('#80938F');
        }.width('100%');
        Image($r('app.media.postcard')).width('100%').aspectRatio(1.8).objectFit(ImageFit.Contain)
          .backgroundColor('#DDE7DE').borderRadius(18).id('postcard');
        Text($r('app.string.postcard_scene')).fontSize(24).fontWeight(FontWeight.Bold).fontColor('#3F5E53').width('100%').id('scene');
        Text('同一张卡片,同一个资源名。设备类型与外观改变时,由资源管理器挑选匹配版本。')
          .fontSize(18).lineHeight(29).fontColor('#5F6F67').width('100%');
        Row({ space: 12 }) {
          Text(`运行设备类型:${deviceInfo.deviceType}`).fontSize(14).fontColor('#6A7C71').id('deviceType');
          Blank();
        }.width('100%');
        Button(this.dark ? '切换日景' : '切换夜景').id('sceneToggle').backgroundColor('#4B7360').onClick(() => this.toggle());
        Button(this.sent ? '已放入寄送清单' : '收下这张明信片').id('send').backgroundColor('#A2835D')
          .onClick(() => { this.sent = !this.sent; });
        Text('图形资源为示例内的 SVG,运行时按限定符自动选择。').fontSize(12).fontColor('#80938F').width('100%');
      }.padding(28).constraintSize({ maxWidth: 980 }).width('100%');
    }.width('100%').height('100%').backgroundColor('#F4F3EA');
  }
}

资源文件放对目录,并不代表页面一定使用了它。若 Image 仍然写着 rawfile 下的固定路径,新增的限定版本就不会参与这次引用。排查时可以沿着 $r 的资源名,确认默认资源、特定目录和最终组件使用的是同一个名字。

还有一条很实用的区分:rawfile 适合按原始文件组织内容,资源限定符参与的是资源表匹配。把图片放进 rawfile,再在代码里拼出 dark 目录名,并不会自动获得这里的选择机制。需要哪一种能力,应先确定,再决定文件放置方式。

还要照顾资源尺寸和显示方式。本例使用固定比例的展示区域与 Contain,不裁掉图形边缘。真实照片或封面若要铺满区域,可以考虑 Cover,但需要检查主体是否被裁切。限定符只决定拿哪份资源,图片在容器里如何显示仍由组件属性决定。

3.1 设备条件与窗口尺寸

资源限定符适合“同一种用途,在不同运行条件下需要不同内容”的情况,例如外观配色、插画、图标和本地化文案。它不会自动把一列布局改成三列,也不会替页面判断当前自由窗口究竟有多宽。

二合一设备仍然可能打开一个很窄的窗口。设备类型没有变,2in1 资源仍会匹配,但排版应继续看容器尺寸。反过来,手机横屏后可用空间增大,也不意味着它变成了二合一设备。资源选择与布局适配可以配合使用,判断依据却不能混为一谈。

4. 日夜资源匹配结果

演示来自 HarmonyOS 6.1.1、API 24 的二合一模拟器。页面显示实际设备类型,再通过应用外观设置切换日景与夜景。下方文案会出现“二合一日景”或“二合一夜景”,便于确认拿到的是哪个资源版本,而不是只凭天空颜色猜测。

这些 SVG 是示例项目中的矢量资源,图形由源码定义。动图中的图片由应用实际加载,页面没有在切换按钮里给 Image 手动换文件路径。收藏状态单独保存,换景时仍然留在原处。

文案是一个很方便的观察点。给各个版本安排清楚的场景文字,运行后就能核对匹配结果。正式产品不一定要保留这些诊断信息,但开发阶段留下它们,比同时打开几张颜色相近的插画来回比较更省事。

本次在二合一模拟器上观察日夜组合资源。默认版本保留在 base 目录,作为其他支持设备匹配与回退的基础。验证不同设备时,可以继续读取系统报告的设备类型与匹配文案,沿着资源名、限定目录和最终显示结果核对选择过程。

如果运行时始终显示默认文案,先核对实际设备类型,再检查组合目录顺序与字符串资源名。不要为了让画面看起来正确,把显示文案手动改成“二合一版本”;那只会把用于检查匹配的线索也改掉。让诊断文字来自同一套资源管理流程,才能帮助定位问题。

图案和场景文字一起切换,提供了两条可以互相核对的线索:树木与水面的区别来自图形文件,日夜版本的文字来自字符串表。若文字变了而图没有变,就沿图片资源名继续查;两者都没变,再看当前外观与目录条件。这比只看“整体颜色似乎深了一点”更容易定位到具体文件。

5. 总结

最后回到最初那一行 Image:它始终只认识 postcard。限定条件放在目录,运行结果通过图片与文案共同确认,页面便能专心处理展示与收藏。需要增加新版本时,先明确是哪一种运行条件,再补对应资源,不必让每个页面各自决定应该去哪儿找图。

继续扩展时,可以加入语言、方向或密度等限定条件,但应按实际需求增加,避免准备一堆无法验证的组合。目录规则与回退方式见 华为资源分类与访问说明。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐