HarmonyOS应用<民族图鉴>开发第10篇:资源管理——字符串/颜色/尺寸/图片的多端适配深度解析

📖 引言
经过前九篇的学习,我们已经掌握了 ArkTS 语言、声明式 UI、组件、状态、路由和列表。但你有没有发现一个问题:我们的代码里有很多硬编码的字符串、颜色值、尺寸数字?
比如:
Text('民族图鉴')
.fontSize(20)
.fontColor('#333333')
这样写有什么问题?
- 不支持国际化:要做英文版怎么办?把所有文件改一遍?
- 不支持深色模式:深色模式下 #333333 的文字就看不见了
- 不支持多设备:手机和平板的字号、间距应该一样吗?
- 不好维护:要改个主题色,得找所有地方改,还容易漏
这就是为什么需要资源管理——把字符串、颜色、尺寸、图片从代码中抽离出来,统一管理,系统自动匹配最合适的资源。
本文第三篇我们讲过资源目录结构,但那只是入门。这一篇我们将深入讲解: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_hint、detail_btn_favorite - 通用的可以不加前缀:
confirm、cancel
颜色命名:
- 用途 + 层级
- 例:
text_primary、text_secondary、text_hint - 不要用颜色名:
red_color、blue_color(以后改成绿色怎么办?)
尺寸命名:
- 类型 + 大小级别
- 例:
font_size_sm、spacing_lg、radius_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 → 匹配成功!
匹配规则总结:
- 按优先级从高到低匹配
- 每一级,优先选择完全匹配的
- 没有匹配的,就跳过这一级(降级)
- 最后都没匹配的,用 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 语言切换实现
「民族图鉴」支持手动切换语言,不是只能跟随系统。
实现思路:
- 用 @StorageLink 保存当前语言设置
- 应用内所有文本都用资源引用
- 切换语言时,更新全局状态
- 页面会自动刷新,显示新的语言
代码实现:
// 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 深色模式为什么重要?
深色模式不只是"好看"或者"酷",它有实际的好处:
- 护眼:夜间使用不刺眼
- 省电:OLED 屏幕黑色像素不发光,更省电
- 潮流:现在的应用基本都支持深色模式
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 响应式布局思路
除了尺寸,布局也要适配。比如:
- 手机:列表一列
- 平板:列表两列(网格)
实现思路:
- 获取当前设备类型(手机/平板)
- 根据设备类型选择不同的布局
- 或者用断点(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)。
原因:
- 多语言友好:如果你用中文当 key,那英文资源里的 key 也是中文,看着别扭
- 规范统一:业界惯例都是用英文 key
- 避免编码问题:中文 key 可能有编码问题(虽然现代系统不会,但还是避免的好)
- 自动补全方便:英文的变量名,IDE 补全更顺手
命名建议:
页面_模块_含义
home_search_hint 首页搜索框提示
detail_btn_favorite 详情页收藏按钮
settings_lang_title 设置页语言设置标题
问题2:颜色资源不要用颜色名命名
现象:red_color、blue_color、green_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/ ← 英文深色
系统会按优先级匹配:
- 先匹配语言 → 选 zh
- 再匹配主题 → 选 dark
- 所以最终选
zh_dark/目录下的资源
如果没有 zh_dark/,就会降级:
- 语言匹配 zh ✓
- 主题匹配 dark ✗ → 降级
- 最终用
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 等)
- 自定义组件的封装与复用
- 动画与交互效果
- 页面架构与数据流设计
- 完整的页面实战
🔗 相关链接
💡 提示:资源管理是应用的"基础设施"。前期做好了,后期加功能、加语言、加主题、加设备支持,都非常轻松。前期偷懒硬编码,后期要改的时候,就是一场灾难。从项目第一天起,就养成好的资源管理习惯——用户可见的文字用资源,主题色用资源,通用尺寸用资源。这是专业开发者和业余爱好者的重要区别之一。
更多推荐




所有评论(0)