在这里插入图片描述

📖 引言

经过前九篇的学习,我们已经掌握了 ArkTS 语言、声明式 UI、组件、状态、路由和列表。但你有没有发现一个问题:我们的代码里有很多硬编码的字符串、颜色值、尺寸数字?

比如:

Text('民族图鉴')
  .fontSize(20)
  .fontColor('#333333')

这样写有什么问题?

  1. 不支持国际化:要做英文版怎么办?把所有文件改一遍?
  2. 不支持深色模式:深色模式下 #333333 的文字就看不见了
  3. 不支持多设备:手机和平板的字号、间距应该一样吗?
  4. 不好维护:要改个主题色,得找所有地方改,还容易漏

这就是为什么需要资源管理——把字符串、颜色、尺寸、图片从代码中抽离出来,统一管理,系统自动匹配最合适的资源。

本文第三篇我们讲过资源目录结构,但那只是入门。这一篇我们将深入讲解:vp/fp/lpx 这些单位到底是什么?深色模式怎么适配?国际化(i18n)怎么实现?多设备适配有哪些策略?

以「民族图鉴」项目为载体,带你搞懂鸿蒙资源管理的方方面面。


🎯 学习目标

完成本文后,你将能够:

  • ✅ 深入理解 vp、fp、px、lpx 等单位的区别与原理
  • ✅ 掌握字符串、颜色、尺寸资源的定义与引用
  • ✅ 理解限定符匹配的完整算法与优先级
  • ✅ 学会国际化(i18n)的完整实现方案
  • ✅ 掌握深色模式适配的方法与最佳实践
  • ✅ 理解多设备适配的策略(手机/平板/折叠屏)
  • ✅ 写出规范、易维护、可扩展的资源代码

💡 需求分析

为什么需要资源管理?

在代码里直接写字符串、颜色、尺寸,小项目没问题,项目一大,问题就来了:

1. 国际化难

  • 要支持英文了,每个文件里找中文字符串替换?
  • 漏了几个?不知道。
  • 以后再加个藏语、维语呢?不敢想。

2. 主题切换难

  • 要做深色模式了,每个颜色值都要改?
  • 有些颜色漏改了,浅色模式好好的,深色模式文字看不清。
  • 以后再加个主题色呢?

3. 多设备适配难

  • 手机和平板的字号、间距、布局都不一样,怎么适配?
  • 折叠屏展开和折叠,界面怎么变?

4. 维护成本高

  • 设计说"主色调从 #E74C3C 改成 #C0392B",你得全项目搜一遍。
  • 漏改一个,UI 就不统一。

资源管理就是解决这些问题的——把可变的部分抽离出来,集中管理,系统自动匹配。

「民族图鉴」的资源管理

「民族图鉴」项目是一个中等规模的应用,资源管理需求很典型:

需求 说明
双语支持 中文 + 英文
深色模式 跟随系统或手动切换
多设备 手机 + 平板适配
主题色 每个民族有自己的代表色

让我们一步步来看怎么实现。


🛠️ 核心实现

步骤1:尺寸单位——vp/fp/px/lpx 深度解析

1.1 为什么需要多种单位?

如果只有 px(像素),那问题就来了:

  • 同样是 20px,在 1080p 的屏幕上看起来很小,在 720p 的屏幕上看起来很大
  • 不同设备的像素密度不一样,同样的像素数,物理大小差很多

所以我们需要与密度无关的单位——不管屏幕分辨率多高,视觉大小差不多。

1.2 各单位详解
单位 全称 说明 适用场景
vp virtual pixel 虚拟像素,与屏幕密度无关 绝大多数尺寸(宽高、间距、圆角)
fp font pixel 字体像素,与密度和字号设置都有关 字体大小
px pixel 物理像素 极少数需要精确像素的场景
lpx logical pixel 逻辑像素,针对折叠屏和大屏 平板/折叠屏适配,布局相关

vp(virtual pixel)

  • 最常用的单位,90% 的尺寸都用 vp
  • 1vp ≈ 1/160 英寸(物理尺寸)
  • 不同密度的屏幕上,视觉大小差不多
  • 类似 Android 的 dp、iOS 的 point

fp(font pixel)

  • 专门用于字体大小
  • 不仅和屏幕密度有关,还和系统字号设置有关
  • 用户在系统设置里调大字号,fp 单位的字就会变大
  • 这是鸿蒙的"无障碍"特性之一

💡 为什么字体要用 fp? 因为有些用户视力不好,会在系统设置里把字体调大。如果你用 vp 写死字号,用户调系统设置也没用,这就不友好了。用 fp 的话,字体会随系统设置缩放,更人性化。

1.3 单位换算

vp 和 px 的关系

px = vp × 屏幕密度(DPI / 160)

比如:

  • 160 DPI 的屏幕:1vp = 1px
  • 320 DPI 的屏幕:1vp = 2px
  • 480 DPI 的屏幕:1vp = 3px

fp 和 vp 的关系

fp = vp × 字体缩放比例

正常情况下(系统字体默认大小),1fp ≈ 1vp。
如果用户把系统字体调大到 1.5 倍,1fp = 1.5vp。

1.4 「民族图鉴」中的单位使用
// 尺寸用 vp
.width(100)          // 100vp
.height(100)         // 100vp
.padding(16)         // 16vp
.borderRadius(8)     // 8vp
.margin({ top: 12 }) // 12vp

// 字体用 fp
.fontSize(16)        // 16fp
.fontSize(20)        // 20fp

⚠️ 注意:在 ArkTS 中,数值属性(如 width、height、fontSize)默认单位就是 vp/fp,你不需要写单位。数字类型的尺寸值默认就是 vp(普通尺寸)或 fp(字体)。

1.5 什么时候用什么单位?
场景 推荐单位 原因
控件宽高 vp 与密度无关,视觉一致
间距(padding/margin) vp 同上
圆角、边框 vp 同上
字体大小 fp 随系统字号缩放,无障碍友好
线条粗细 vp 细线条在高清屏更细
精确像素对齐 px 极少需要,比如 1px 的细线

步骤2:元素资源——string/color/float

2.1 字符串资源(string.json)

把所有用户可见的文字都放到字符串资源里,不要硬编码在代码中。

定义

// resources/base/element/string.json
{
  "string": [
    {
      "name": "app_name",
      "value": "民族图鉴"
    },
    {
      "name": "tab_home",
      "value": "首页"
    },
    {
      "name": "tab_encyclopedia",
      "value": "百科"
    },
    {
      "name": "search_hint",
      "value": "搜索民族名称、拼音..."
    },
    {
      "name": "population",
      "value": "人口"
    },
    {
      "name": "region",
      "value": "地区"
    }
  ]
}

引用

// 代码中引用
Text($r('app.string.app_name'))
  .fontSize(20)

// 用在属性里也可以
Text($r('app.string.search_hint'))

带参数的字符串

{
  "string": [
    {
      "name": "total_count",
      "value": "共 %d 个民族"
    },
    {
      "name": "greeting",
      "value": "你好,%s!"
    }
  ]
}
2.2 颜色资源(color.json)

颜色资源是主题切换的基础。把所有颜色都定义成资源,深色模式只要提供另一套颜色值就行了。

定义

// resources/base/element/color.json
{
  "color": [
    {
      "name": "primary_color",
      "value": "#E74C3C"
    },
    {
      "name": "text_primary",
      "value": "#1A1A1A"
    },
    {
      "name": "text_secondary",
      "value": "#666666"
    },
    {
      "name": "text_hint",
      "value": "#999999"
    },
    {
      "name": "background_color",
      "value": "#F5F5F5"
    },
    {
      "name": "card_background",
      "value": "#FFFFFF"
    },
    {
      "name": "divider_color",
      "value": "#EEEEEE"
    }
  ]
}

引用

Text('文字内容')
  .fontColor($r('app.color.text_primary'))
  .fontSize(16)

Column()
  .backgroundColor($r('app.color.background_color'))
  .padding(16)
2.3 尺寸资源(float.json)

尺寸也可以定义成资源,方便统一调整和多设备适配。

定义

// resources/base/element/float.json
{
  "float": [
    {
      "name": "font_size_xs",
      "value": "12fp"
    },
    {
      "name": "font_size_sm",
      "value": "14fp"
    },
    {
      "name": "font_size_md",
      "value": "16fp"
    },
    {
      "name": "font_size_lg",
      "value": "18fp"
    },
    {
      "name": "font_size_xl",
      "value": "24fp"
    },
    {
      "name": "spacing_xs",
      "value": "4vp"
    },
    {
      "name": "spacing_sm",
      "value": "8vp"
    },
    {
      "name": "spacing_md",
      "value": "12vp"
    },
    {
      "name": "spacing_lg",
      "value": "16vp"
    },
    {
      "name": "spacing_xl",
      "value": "24vp"
    },
    {
      "name": "radius_sm",
      "value": "8vp"
    },
    {
      "name": "radius_md",
      "value": "12vp"
    },
    {
      "name": "radius_lg",
      "value": "16vp"
    }
  ]
}

引用

Text('标题')
  .fontSize($r('app.float.font_size_lg'))
  .fontWeight(FontWeight.Bold)

Column({ space: $r('app.float.spacing_md') }) {
  // ...
}
.padding($r('app.float.spacing_lg'))
.borderRadius($r('app.float.radius_md'))

💡 为什么尺寸也要做资源? 因为多设备适配的时候,平板的字号、间距都要比手机大。有了尺寸资源,只要在平板的限定符目录下放另一套 float.json 就行了,代码不用改。

2.4 资源命名规范

好的命名让资源一目了然,找的时候也方便。

字符串命名

  • 页面 + 组件 + 含义
  • 例:home_search_hintdetail_btn_favorite
  • 通用的可以不加前缀:confirmcancel

颜色命名

  • 用途 + 层级
  • 例:text_primarytext_secondarytext_hint
  • 不要用颜色名:red_colorblue_color(以后改成绿色怎么办?)

尺寸命名

  • 类型 + 大小级别
  • 例:font_size_smspacing_lgradius_md
  • 大小级别:xs < sm < md < lg < xl < xxl

「民族图鉴」的命名实践

// 好的命名:一看就知道是干什么的
$r('app.string.search_hint')       // 搜索框提示
$r('app.color.text_primary')       // 主要文字颜色
$r('app.float.spacing_lg')         // 大间距

// 不好的命名:不知所云
$r('app.string.text1')             // 什么文字?
$r('app.color.color1')             // 什么颜色?
$r('app.float.size1')              // 多大的尺寸?

步骤3:限定符匹配算法——多端适配的核心

3.1 限定符是什么?

限定符(Qualifier)是资源目录的后缀,用来表示"这套资源是给什么场景用的"。

比如:

  • base/:默认,所有场景都能用
  • zh/:中文环境用
  • en/:英文环境用
  • dark/:深色模式用
  • tablet/:平板设备用

完整的限定符类型(按优先级从高到低):

优先级 限定符类型 示例 说明
1 MCC/MNC mcc460_mnc00 移动国家码/网络码
2 语言+文字+地区 zh_Hans_CN 语言(小)、文字(中)、地区(国)
3 横竖屏 horizontal 横屏/竖屏
4 设备类型 phone, tablet, wearable 手机/平板/手表
5 屏幕密度 mdpi, hdpi, xhdpi, xxhdpi 像素密度
6 主题模式 dark, light 深色/浅色

优先级的意思是:匹配的时候,先看高优先级的限定符,再看低优先级的。

3.2 匹配算法详解

系统选择资源的过程,就像一个"找最合适的"的游戏:

当前设备状态:
  语言 = 中文
  主题 = 深色
  设备 = 手机

资源目录列表:
  1. base/              ← 默认
  2. zh/                ← 中文
  3. dark/              ← 深色
  4. zh_dark/           ← 中文 + 深色
  5. en/                ← 英文
  6. tablet/            ← 平板

匹配过程

第1步:列出所有候选目录
  → base, zh, dark, zh_dark, en, tablet

第2步:按优先级匹配
  语言是最高优先级(仅次于 MCC)
  保留有 zh 的:zh, zh_dark
  淘汰:base, dark, en, tablet

第3步:下一个优先级(主题模式)
  主题是深色
  在 zh 和 zh_dark 中选
  选 zh_dark → 匹配成功!

匹配规则总结

  1. 按优先级从高到低匹配
  2. 每一级,优先选择完全匹配的
  3. 没有匹配的,就跳过这一级(降级)
  4. 最后都没匹配的,用 base(默认)
3.3 限定符组合

多个限定符可以组合使用,用下划线连接,高优先级在前:

zh_dark         ← 中文 + 深色
en_light        ← 英文 + 浅色
zh_CN_tablet    ← 中文中国 + 平板
dark_tablet     ← 深色 + 平板

⚠️ 注意:限定符的顺序不能乱!必须按优先级从高到低排列。zh_dark 是对的,dark_zh 是错的,系统识别不了。


步骤4:国际化(i18n)——多语言支持

4.1 什么是国际化?

国际化(Internationalization,简称 i18n,因为 i 和 n 之间有 18 个字母),就是让应用支持多种语言。

核心思想:把文字从代码里抽出来,按语言分类存放,系统根据当前语言自动选择对应的文字。

4.2 多语言资源结构
resources/
├── base/                    ← 默认(中文)
│   └── element/
│       └── string.json
├── en/                      ← 英文
│   └── element/
│       └── string.json
├── zh_TW/                   ← 繁体中文(台湾)
│   └── element/
│       └── string.json
└── ...

中文资源

// resources/base/element/string.json
{
  "string": [
    { "name": "app_name", "value": "民族图鉴" },
    { "name": "tab_home", "value": "首页" },
    { "name": "tab_encyclopedia", "value": "百科" },
    { "name": "search_hint", "value": "搜索民族名称、拼音..." },
    { "name": "population", "value": "人口" },
    { "name": "region", "value": "地区" }
  ]
}

英文资源

// resources/en/element/string.json
{
  "string": [
    { "name": "app_name", "value": "Ethnic Chronicles" },
    { "name": "tab_home", "value": "Home" },
    { "name": "tab_encyclopedia", "value": "Encyclopedia" },
    { "name": "search_hint", "value": "Search by name or pinyin..." },
    { "name": "population", "value": "Population" },
    { "name": "region", "value": "Region" }
  ]
}

代码完全不用改,还是 $r('app.string.tab_home'),系统会自动根据当前语言选对应的字符串。

4.3 语言切换实现

「民族图鉴」支持手动切换语言,不是只能跟随系统。

实现思路

  1. 用 @StorageLink 保存当前语言设置
  2. 应用内所有文本都用资源引用
  3. 切换语言时,更新全局状态
  4. 页面会自动刷新,显示新的语言

代码实现

// models/EnumModels.ets
export enum AppLanguage {
  ZH_CN = 'zh-CN',
  EN = 'en'
}

// services/I18nService.ets
export class I18nService {
  private static instance: I18nService;
  private currentLanguage: AppLanguage = AppLanguage.ZH_CN;

  static getInstance(): I18nService {
    if (!I18nService.instance) {
      I18nService.instance = new I18nService();
    }
    return I18nService.instance;
  }

  setLanguage(lang: AppLanguage): void {
    this.currentLanguage = lang;
    // 保存到持久化存储
    // 通知所有页面刷新
  }

  getLanguage(): AppLanguage {
    return this.currentLanguage;
  }
}

// 页面中使用
@Entry
@Component
struct SettingsPage {
  @StorageLink('currentAppLanguage') currentLanguage: AppLanguage = AppLanguage.ZH_CN;

  build() {
    Column() {
      // 所有文字都用 $r(),语言变了自动更新
      Text($r('app.string.settings_language'))
        .fontSize(16)

      // 语言切换按钮...
    }
  }
}

💡 手动切换语言的实现原理:鸿蒙的资源系统是跟随系统语言的。如果要实现应用内手动切换,需要用 @StorageLink 保存语言状态,每个页面监听这个状态,切换时刷新。更完整的实现需要自定义 i18n 服务。

4.4 双语数据模型

「民族图鉴」的数据是内置的 Mock 数据,本身就是双语的:

// models/EthnicModels.ets
export interface EthnicGroup {
  id: string;
  name: string;              // 中文名
  nameEn: string;            // 英文名
  description: string;       // 中文简介
  descriptionEn: string;     // 英文简介
  language: string;          // 中文语言名
  languageEn: string;        // 英文语言名
  // ... 很多字段都是双语的
}

使用方式

@Component
struct EthnicCard {
  @Prop ethnic: EthnicGroup;
  @StorageLink('currentAppLanguage') currentLanguage: AppLanguage = AppLanguage.ZH_CN;

  private isChinese(): boolean {
    return this.currentLanguage === AppLanguage.ZH_CN;
  }

  build() {
    Column({ space: 8 }) {
      // 根据当前语言显示对应文本
      Text(this.isChinese() ? this.ethnic.name : this.ethnic.nameEn)
        .fontSize(18)
        .fontWeight(FontWeight.Bold)

      Text(this.isChinese() ? this.ethnic.region : this.ethnic.regionEn)
        .fontSize(12)
        .fontColor('#999')
    }
  }
}

这是应用内数据的国际化方式,和系统资源的国际化是两回事。系统资源用限定符,业务数据要自己处理。


步骤5:深色模式适配

5.1 深色模式为什么重要?

深色模式不只是"好看"或者"酷",它有实际的好处:

  1. 护眼:夜间使用不刺眼
  2. 省电:OLED 屏幕黑色像素不发光,更省电
  3. 潮流:现在的应用基本都支持深色模式
5.2 深色模式资源

深色模式的实现,核心就是颜色资源

浅色模式的颜色放 base/element/color.json
深色模式的颜色放 dark/element/color.json

系统会根据当前主题模式自动选择。

深色颜色定义

// resources/dark/element/color.json
{
  "color": [
    {
      "name": "primary_color",
      "value": "#FF6B6B"          // 主色调亮一点,深色背景下醒目
    },
    {
      "name": "text_primary",
      "value": "#E0E0E0"          // 主文字:浅灰色(不是纯白,刺眼)
    },
    {
      "name": "text_secondary",
      "value": "#A0A0A0"          // 次要文字:中灰色
    },
    {
      "name": "text_hint",
      "value": "#707070"          // 提示文字:深灰色
    },
    {
      "name": "background_color",
      "value": "#121212"          // 背景:深灰(不是纯黑,太暗)
    },
    {
      "name": "card_background",
      "value": "#1E1E1E"          // 卡片背景:比背景亮一点
    },
    {
      "name": "divider_color",
      "value": "#333333"          // 分割线:深灰色
    }
  ]
}

代码还是一样的:

Text('文字')
  .fontColor($r('app.color.text_primary'))

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

完全不用改代码,系统自动选颜色。

5.3 深色模式的设计原则

颜色

  • 背景不要用纯黑(#000000),用深灰(#121212),更有层次感
  • 文字不要用纯白(#FFFFFF),用浅灰(#E0E0E0),不刺眼
  • 主色调可以稍微亮一点,在深色背景下更突出

层次

  • 浅色模式:白底 + 灰色卡片(卡片比背景深)
  • 深色模式:深灰底 + 浅一点的卡片(卡片比背景亮)
  • 层次感靠"亮度差"来体现

图片

  • 图片一般不用换
  • 但如果有图标(PNG),可能需要深色版本
  • 文字上面的图片,要加半透明遮罩,保证文字可读

「民族图鉴」的深色模式实践

// pages/EthnicDetailPage.ets
// 详情页顶部的封面图,上面有文字
Stack() {
  Image($rawfile(`coverImage/${this.ethnic.coverImage}`))
    .width('100%')
    .height(300)
    .objectFit(ImageFit.Cover)

  // 半透明渐变遮罩,保证文字在任何图片上都能看清
  // 从透明渐变到半透明黑
  LinearGradient(...)
    .width('100%')
    .height('100%')
    .opacity(0.4)

  Text(this.ethnic.name)
    .fontSize(24)
    .fontColor('#FFFFFF')  // 文字用白色,有遮罩就不怕
    .fontWeight(FontWeight.Bold)
}

💡 图片上的文字怎么办? 图片内容千奇百怪,有的亮有的暗,直接放文字可能看不清。最简单的办法:加个半透明的黑色遮罩,文字用白色。这样不管图片是什么样,文字都是清晰的。


步骤6:多设备适配

6.1 为什么需要多设备适配?

鸿蒙是全场景操作系统,应用可能运行在:

  • 手机(小屏)
  • 平板(大屏)
  • 折叠屏(可变屏)
  • 车机(特殊屏幕)
  • 智慧屏(大屏,远距离观看)

同样的界面,在不同尺寸的屏幕上,布局和字号应该不一样。

6.2 设备类型限定符

用限定符区分手机和平板:

resources/
├── base/                    ← 默认(手机)
│   └── element/
│       ├── color.json
│       └── float.json
└── tablet/                  ← 平板
    └── element/
        └── float.json        ← 平板的尺寸更大

平板尺寸资源

// resources/tablet/element/float.json
{
  "float": [
    // 平板字号大一点
    { "name": "font_size_sm", "value": "16fp" },   // 手机是 14fp
    { "name": "font_size_md", "value": "18fp" },   // 手机是 16fp
    { "name": "font_size_lg", "value": "20fp" },   // 手机是 18fp

    // 间距也大一点
    { "name": "spacing_md", "value": "16vp" },     // 手机是 12vp
    { "name": "spacing_lg", "value": "24vp" },     // 手机是 16vp

    // 卡片圆角也大一点
    { "name": "radius_md", "value": "16vp" },      // 手机是 12vp
  ]
}

代码还是一样的,系统自动根据设备类型选对应的尺寸。

6.3 响应式布局思路

除了尺寸,布局也要适配。比如:

  • 手机:列表一列
  • 平板:列表两列(网格)

实现思路

  1. 获取当前设备类型(手机/平板)
  2. 根据设备类型选择不同的布局
  3. 或者用断点(Breakpoint)响应式

简单的响应式实现

@Entry
@Component
struct EthnicListPage {
  @State isTablet: boolean = false;

  aboutToAppear(): void {
    // 获取屏幕宽度,判断是不是平板
    this.isTablet = this.isTabletDevice();
  }

  private isTabletDevice(): boolean {
    // 获取屏幕宽度
    // 大于 600vp 算平板
    return display.getWindowProperties().windowRect.width > 600;
  }

  build() {
    if (this.isTablet) {
      // 平板:两列网格
      Grid() {
        ForEach(this.ethnicList, (ethnic: EthnicGroup) => {
          GridItem() {
            EthnicCard({ ethnic: ethnic })
          }
        }, e => e.id)
      }
      .columnsTemplate('1fr 1fr')
    } else {
      // 手机:单列列表
      List({ space: 12 }) {
        ForEach(this.ethnicList, (ethnic: EthnicGroup) => {
          ListItem() {
            EthnicCard({ ethnic: ethnic })
          }
        }, e => e.id)
      }
    }
  }
}

💡 断点(Breakpoint):响应式设计的常用概念。屏幕宽度达到某个阈值(断点),布局就变一下。常用的断点:sm(小屏)、md(中屏)、lg(大屏)、xl(超大屏)。


⚠️ 常见问题与解决方案

问题1:字符串资源用中文 key 还是英文 key?

现象
字符串资源的 name 用中文还是英文?比如 搜索提示 还是 search_hint

答案:用英文(语义化的英文 key)。

原因

  1. 多语言友好:如果你用中文当 key,那英文资源里的 key 也是中文,看着别扭
  2. 规范统一:业界惯例都是用英文 key
  3. 避免编码问题:中文 key 可能有编码问题(虽然现代系统不会,但还是避免的好)
  4. 自动补全方便:英文的变量名,IDE 补全更顺手

命名建议

页面_模块_含义
home_search_hint       首页搜索框提示
detail_btn_favorite    详情页收藏按钮
settings_lang_title    设置页语言设置标题

问题2:颜色资源不要用颜色名命名

现象
red_colorblue_colorgreen_bg 这样的命名。

问题
以后设计改了,“红色"改成了"橙色”,那 red_color 这个名字就名不副实了。改名字吧,所有引用的地方都要改;不改吧,看着别扭。

正确做法:用用途命名,不用颜色命名。

// ❌ 不好:用颜色名
$r('app.color.red_color')
$r('app.color.blue_text')

// ✅ 好:用用途名
$r('app.color.primary_color')    // 主色调
$r('app.color.text_primary')     // 主要文字颜色
$r('app.color.danger_color')     // 危险/错误颜色
$r('app.color.success_color')    // 成功颜色

这样以后换颜色,只要改 value 就行了,name 不用动。


问题3:什么时候该用资源,什么时候直接写值?

现象
所有尺寸、颜色、字符串都要定义成资源吗?会不会太麻烦?

判断标准

场景 要不要做资源 原因
用户可见的文字 ✅ 必须 要国际化
主题相关的颜色 ✅ 必须 要深色模式
全局统一的尺寸 ✅ 建议 要多设备适配
只有一处用的颜色 ⚠️ 看情况 如果是主题相关的就做,临时的可以不做
内部测试的文字 ❌ 不用 用户看不见,不用国际化
临时调试的数值 ❌ 不用 用完就删了

经验法则

  • 用户能看到的文字 → 必须做字符串资源
  • 和主题相关的颜色 → 必须做颜色资源
  • 全局统一的间距、字号 → 建议做尺寸资源
  • 局部的、特殊的、一次性的 → 直接写值也没关系

💡 不要过度设计。什么都做成资源,也会很累。先把最核心的做了(用户可见的文字、主题色),其他的看着办。项目大了,需要的时候再加。


问题4:深色模式下图片太亮/太暗怎么办?

现象
深色模式下,图片太亮了,刺眼;或者太暗了,看不清。

解决方案

方案1:加半透明遮罩(最常用)

Stack() {
  Image(src)
    .width('100%')
    .objectFit(ImageFit.Cover)

  // 半透明黑色遮罩
  // 深色模式下可以调深一点
  Rect()
    .width('100%')
    .height('100%')
    .fillColor(Color.Black)
    .opacity(this.isDarkMode ? 0.5 : 0.3)
}

方案2:两套图片(麻烦,但效果最好)

resources/
├── base/media/
│   └── banner.png         ← 浅色模式用图
└── dark/media/
    └── banner.png         ← 深色模式用图

有 dark 限定符的 media 目录,深色模式下自动用里面的图片。

方案3:给图片上的文字加阴影

Text('文字')
  .fontColor('#FFFFFF')
  .textShadow({
    radius: 4,
    color: '#000000',
    offsetX: 0,
    offsetY: 1
  })

💡 「民族图鉴」用的是方案1 + 文字阴影。封面图上加个半透明渐变遮罩,文字再加个阴影,不管什么图片、什么模式,文字都是清晰的。简单有效。


问题5:多语言和深色模式同时生效吗?

现象
既是中文、又是深色模式,系统会怎么选资源?

答案:会的,限定符可以组合。

resources/
├── base/                    ← 默认
├── zh/                      ← 中文浅色
├── en/                      ← 英文浅色
├── dark/                    ← 中文深色(语言用 base 的中文)
├── zh_dark/                 ← 中文深色(完整匹配)
└── en_dark/                 ← 英文深色

系统会按优先级匹配:

  1. 先匹配语言 → 选 zh
  2. 再匹配主题 → 选 dark
  3. 所以最终选 zh_dark/ 目录下的资源

如果没有 zh_dark/,就会降级:

  1. 语言匹配 zh ✓
  2. 主题匹配 dark ✗ → 降级
  3. 最终用 zh/ 里的颜色 + dark/ 里的颜色?不对

实际上,系统会分别匹配每一种资源。字符串在 zh 里找,颜色在 dark 里找。不同类型的资源是分开匹配的。


📝 本章小结

核心知识点

本文深入讲解了鸿蒙的资源管理体系:

1. 尺寸单位

  • vp:虚拟像素,绝大多数尺寸用这个
  • fp:字体像素,随系统字号缩放,无障碍友好
  • px:物理像素,极少用
  • lpx:逻辑像素,大屏/折叠屏适配
  • 记住:尺寸用 vp,字体用 fp

2. 元素资源

  • 字符串资源:string.json,国际化的基础
  • 颜色资源:color.json,深色模式的基础
  • 尺寸资源:float.json,多设备适配的基础
  • 命名规范:用途 + 层级,不要用颜色名当 key

3. 限定符匹配算法

  • 限定符目录:zh、en、dark、tablet…
  • 匹配优先级:MCC > 语言 > 横竖屏 > 设备类型 > 密度 > 主题
  • 组合限定符:下划线连接,高优先级在前
  • 匹配规则:从高到低,匹配不上就降级,最后用 base

4. 国际化(i18n)

  • 不同语言的字符串放不同限定符目录
  • 代码用 $r(‘app.string.xxx’) 引用,系统自动匹配
  • 应用内切换语言:@StorageLink + 全局状态
  • 业务数据的双语:自己定义中英文字段,根据语言状态切换

5. 深色模式

  • 颜色资源是核心:base + dark 两套颜色
  • 设计原则:背景用深灰(不是纯黑),文字用浅灰(不是纯白)
  • 层次感:卡片比背景亮一点(浅色模式卡片比背景深)
  • 图片适配:半透明遮罩 + 文字阴影,保证文字可读

6. 多设备适配

  • 设备类型限定符:phone / tablet
  • 尺寸资源:平板字号大、间距大、圆角大
  • 响应式布局:判断屏幕宽度,选择不同布局
  • 断点设计:sm / md / lg / xl

最佳实践总结

用户可见文字必须用字符串资源

// ❌ 不要硬编码中文
Text('搜索民族...')

// ✅ 用资源引用
Text($r('app.string.search_hint'))

颜色必须用资源,不要硬编码色值

// ❌ 不要硬编码颜色
.fontColor('#333333')

// ✅ 用颜色资源
.fontColor($r('app.color.text_primary'))

尺寸尽量用资源,方便统一调整

// 推荐
.padding($r('app.float.spacing_lg'))
.fontSize($r('app.float.font_size_md'))

命名要语义化,不要用颜色名/中文

// ✅ 好的命名
primary_color     主色调
text_primary      主要文字
spacing_lg        大间距

// ❌ 不好的命名
red_color         红色(以后改橙色怎么办?)
text1             不知道是什么
搜索提示           中文 key,不规范

深色模式三件套:深灰背景 + 浅灰文字 + 图片遮罩

背景:#121212(不是纯黑)
文字:#E0E0E0(不是纯白)
图片:加半透明黑色遮罩,保证文字可读

资源适配用限定符,不要在代码里判断

// 不要这样
if (isDarkMode) {
  color = '#FFFFFF'
} else {
  color = '#333333'
}

// 应该这样
// 定义两套颜色资源
// 代码直接用 $r('app.color.text_primary')
// 系统自动匹配

入门基础篇总结

恭喜你!到这里,入门基础篇的 10 篇文章就学完了。让我们回顾一下:

第1篇:开篇——为什么选择鸿蒙原生开发
第2篇:环境搭建——DevEco Studio 安装与配置
第3篇:项目结构——工程目录与资源体系
第4篇:ArkTS 入门——类型系统与基础语法
第5篇:声明式 UI——ArkUI 核心概念
第6篇:基础组件——Text/Image/Button/Column/Row
第7篇:状态管理——@State/@Prop/@Link/@Storage
第8篇:路由导航——页面跳转与参数传递
第9篇:列表组件——List/Grid/ForEach 高效渲染
第10篇:资源管理——多端适配与国际化

你现在已经具备的能力

  • ✅ 理解鸿蒙应用的架构和开发流程
  • ✅ 熟练使用 ArkTS 进行类型安全的开发
  • ✅ 掌握声明式 UI 的开发范式
  • ✅ 能用基础组件构建常见界面
  • ✅ 管理组件状态和页面路由
  • ✅ 构建高性能的列表页面
  • ✅ 做多语言、深色模式、多设备适配

下一篇章预告:页面开发篇

  • 更多组件的深入学习(Tab、Dialog、Swiper 等)
  • 自定义组件的封装与复用
  • 动画与交互效果
  • 页面架构与数据流设计
  • 完整的页面实战

🔗 相关链接


💡 提示:资源管理是应用的"基础设施"。前期做好了,后期加功能、加语言、加主题、加设备支持,都非常轻松。前期偷懒硬编码,后期要改的时候,就是一场灾难。从项目第一天起,就养成好的资源管理习惯——用户可见的文字用资源,主题色用资源,通用尺寸用资源。这是专业开发者和业余爱好者的重要区别之一。

Logo

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

更多推荐