我是兰瓶Coding,一枚刚踏入鸿蒙领域的转型小白,原是移动开发中级,如下是我学习笔记《零基础学鸿蒙》,若对你所有帮助,还请不吝啬的给个大大的赞~

前言

先坦白,我曾信过“一套资源通吃所有设备”的美梦。后来被现实教育:屏幕密度打我左脸,语言长度打我右脸,夜间模式再给我来一脚。想活下去,只能老老实实地把**资源限定符、覆盖优先级、动态主题/资源引用、国际化/本地化(i18n/l10n)**这一整套做扎实。本文不端着,用人话 + 可跑/可改的 ArkTS 片段,把“多密度/多语言/夜间模式与样式体系”拆开讲清楚,顺便塞进一堆血泪经验,省你两倍返工。🙂

一、资源版图:密度、语言、外观(夜间)三线作战

1) 资源分类与命名习惯

  • 字符串resources/…/base/element/string.json(键值对);
  • 图片媒体resources/…/base/media/(位图/矢量);
  • 样式/主题resources/…/base/profile/(主题色、字号、圆角、阴影等 Token);
  • 颜色表resources/…/base/element/color.json
  • 维度/字号resources/…/base/element/float.json / integer.json

口号:把“视觉决定的东西”全部资源化,ArkTS 里只做“选择”,不硬编码魔法数字。

2) 资源限定符(qualifiers)常见组合

  • 语言/地区zh, zh_CN, en, en_US
  • 密度/尺寸:如 sdpi/mdpi/hdpi/xhdpi1.0x/2.0x/3.0x(按你的构建体系而定)
  • 外观light / dark
  • 方向/形态(可选):land/port、圆角/刘海之类的设备特性

一个典型的资源树(节选):

resources/
├─ en/        # 英文通用
│  └─ base/element/string.json
├─ en_US/     # 英美差异文案
│  └─ base/element/string.json
├─ zh_CN/     # 简体中文
│  └─ base/element/string.json
├─ dark/      # 夜间覆盖(颜色/部分图片)
│  ├─ base/element/color.json
│  └─ base/media/ic_logo.svg
├─ 2.0x/      # 2x 图资源
│  └─ base/media/...
└─ base/      # 默认兜底
   ├─ element/{string,color,float}.json
   └─ media/...

二、覆盖优先级:到底谁“说了算”?

核心原则:更具体的优先级更高;环境更匹配的先选;匹配层级自上而下。”

可以把选择看成一个多维匹配排序(伪代码思想):

Candidates = 所有同名资源(同 key)
给每个候选计算匹配度向量:
  Match = [
    LocaleMatch(语言/地区完全匹配>仅语言匹配>默认),
    AppearanceMatch(dark/light 精确匹配>无),
    DensityMatch(等于>最近邻>无),
    DeviceMatch(特性匹配>无),
    Specificity(限定符数量多>少)
  ]
按 Match 逐项降序排序,取第一名

经验条:语言 > 外观 > 密度 > 其他,一般能解释 95% 的“为什么拿到的不是我想要的”。

三、动态主题与资源引用:跑起来才叫真本事

1) 用 Token 组织样式(可暗可亮)

颜色 Token(base + dark 覆盖)

// resources/base/element/color.json
{
  "color_primary": "#0A59F7",
  "color_bg": "#FFFFFF",
  "color_text": "#1A1A1A"
}
// resources/dark/element/color.json
{
  "color_bg": "#0F1115",
  "color_text": "#EDEDED"
}

尺寸 Token

// resources/base/element/float.json
{
  "radius_l": 16,
  "space_m": 12,
  "font_title": 20
}

ArkTS 组件使用资源引用 $r()

// src/main/ets/components/Card.ets
@Component
export struct Card {
  @Prop title: string;
  build() {
    Column() {
      Text(this.title)
        .fontSize($r('app.float.font_title'))
        .fontWeight(FontWeight.SemiBold)
        .fontColor($r('app.color.color_text'))
    }
    .padding($r('app.float.space_m'))
    .borderRadius($r('app.float.radius_l'))
    .backgroundColor($r('app.color.color_bg'))
  }
}

重点:所有视觉量都从资源里拿,夜间切换时系统只需重新解析资源,不必改组件逻辑。

2) 动态切换主题(浅→深)

通常由系统外观事件驱动(或在设置中手动切换)。思路是监听外观变化 → 通知全局刷新

// src/main/ets/app/ThemeBus.ets
export type Appearance = 'light' | 'dark'
let listeners: Array<(mode: Appearance) => void> = []
let current: Appearance = 'light'

export function onAppearanceChange(cb: (m: Appearance) => void) {
  listeners.push(cb)
  cb(current) // 首次同步
}

export function setAppearance(m: Appearance) {
  if (m === current) return
  current = m
  listeners.forEach(fn => fn(m))
}
// src/main/ets/pages/Settings.ets
import { setAppearance } from '../app/ThemeBus'

@Entry
@Component
export default struct Settings {
  @State mode: string = 'light'
  build() {
    Column() {
      Text('Appearance')
      Select([{value:'light', text:'Light'}, {value:'dark', text:'Dark'}])
        .selected(this.mode)
        .onSelect((v) => { this.mode = v; setAppearance(v as any) })
    }.padding(24)
  }
}

实操建议:不要在代码里 if-else 写两套色值;让资源系统干“换皮”的活,你只发“切主题”的命令。

四、国际化/本地化:不是“翻译两行”那么简单

1) 文案键与占位规范

  • 统一小写蛇形home_title, btn_confirm, msg_network_error
  • 占位使用命名参数"invite_message": "邀请 {name} 加入 {team}"
  • 避免拼接:“邀请” + 名字 + “加入” + 团队 —— 这种等着被 RTL 和折行教育。

字符串示例:

// resources/zh_CN/element/string.json
{
  "home_title": "首页",
  "invite_message": "邀请 {name} 加入 {team}",
  "countdown_label": "{n} 秒后开始"
}
// resources/en/element/string.json
{
  "home_title": "Home",
  "invite_message": "Invite {name} to join {team}",
  "countdown_label": "Starting in {n} s"
}

ArkTS 安全插值:

// src/main/ets/i18n/format.ts
export function t(key: string, vars: Record<string, any> = {}): string {
  // 伪代码:从资源拿字符串
  let s = getStringResource(key) // 类似 $r('app.string.xxx')
  Object.keys(vars).forEach(k => {
    s = s.replace(new RegExp(`\\{${k}\\}`, 'g'), String(vars[k]))
  })
  return s
}
// 使用
Text(t('invite_message', { name: '王小明', team: 'Design' }))

2) 占位长度变更 & 自适应布局

  • 最少 30% 扩展冗余:英文→德语常常“暴长”,键名/按钮预留宽度。
  • 长文案断行:使用 maxLines + textOverflow(Ellipsis);要点按钮可双行。
  • 栅格/自适应:避免固定宽度;多用 Flex/Grid 与约束。
// 按钮示例:文案过长可自动换行
Button(t('action_primary'))
  .width('100%')
  .maxLines(2)
  .textOverflow({ overflow: TextOverflow.Ellipsis })

3) 复数/量词(Plural)与性别化

如果框架没有内建 plural,自己封一个极简规则(示意):

// src/main/ets/i18n/plural.ts
type Rules = { zero?: string; one?: string; other: string }
export function plural(n: number, r: Rules) {
  if (n === 0 && r.zero) return r.zero.replace('{n}', String(n))
  if (n === 1 && r.one)  return r.one.replace('{n}', String(n))
  return r.other.replace('{n}', String(n))
}

// 使用
Text(plural(3, {
  one: t('you_have_one_message', {}),
  other: t('you_have_n_messages', { n: 3 })
}))

l10n 提醒:阿拉伯语等 RTL 方向要加镜像检查;图标箭头、进度条方向别忘了跟着翻。

4) 伪本地化(Pseudo-Localization)早下手

本地化前先“假装翻译”,把字符拉长加重音,尽早暴露布局问题。

// dev-only pseudo
function pseudo(s: string) {
  const map: Record<string,string> = { a:'á', e:'ē', i:'ī', o:'ō', u:'ū' }
  return '⟦' + s.replace(/[aeiou]/g, m => map[m]) + '⟧⟧'
}

五、样式体系:从“主题色”走到“设计 Token”

把视觉语言抽象为 Design Tokens(可主题化、可扩展):

// resources/base/profile/theme.json
{
  "brand": {
    "primary": "$color:color_primary",
    "success": "#16a34a",
    "danger": "#ef4444"
  },
  "text": {
    "title": { "size": "$float:font_title", "weight": 600, "color": "$color:color_text" },
    "body":  { "size": 16, "weight": 400, "color": "$color:color_text" }
  },
  "radius": { "lg": "$float:radius_l" },
  "space":  { "m": "$float:space_m" }
}

Token 到组件的映射(ArkTS):

// src/main/ets/ui/Tokens.ts
export const Tokens = {
  color: {
    primary: $r('app.color.color_primary'),
    text: $r('app.color.color_text'),
    bg: $r('app.color.color_bg')
  },
  radius: { lg: $r('app.float.radius_l') },
  space: { m: $r('app.float.space_m') },
  font: { title: $r('app.float.font_title') }
}
// src/main/ets/ui/Headline.ets
import { Tokens } from './Tokens'

@Component
export struct Headline {
  @Prop text: string
  build() {
    Text(this.text)
      .fontSize(Tokens.font.title)
      .fontWeight(FontWeight.SemiBold)
      .fontColor(Tokens.color.text)
  }
}

升级姿势:以后设计改主色/圆角,直接改 Token,所有组件同步变;还想上品牌节日皮肤?再加一套 dark-festival 覆盖就行。

六、密度与图片策略:位图少放、矢量优先

  • 优先 SVG/矢量(能矢量别位图):动态图标/主题跟随更稳,体积更友好。
  • 位图走多密度:只放用得到的 2x/3x 桶,谨慎放 4x
  • 插图/背景大图:考虑 WebP/AVIF;按主题分桶(light/dark 各一份,别在代码里调滤镜)。
  • CI 做重复检查:资源哈希 + 体积阈值报警。

七、启动与运行时:按需加载资源包(可选)

  • 首包只带基础语言/密度;进入非主语种时提示下载语言包(或静默增量)。
  • 夜间资源可合并在首包;若主题皮肤很多,做“主题包”按需拉取。
  • 缓存与淘汰:最近使用的语言/主题常驻;冷门资源 LRU 淘汰。

八、质量保障清单(落地就用)

  1. 伪本地化:构建 dev 变体自动启用。
  2. Dark/Light Diff:自测页一次性预览两套外观差异(截图对比)。
  3. 密度模拟:强制渲染到不同 DPR 的虚拟设备。
  4. 文案扫描:未资源化的硬编码文案报警;重复 key 失败。
  5. 体积报告:每次合并输出 资源分类体积 Top N
  6. 可访问性:对比度(AA/AAA)、动态字体(80%–130%)拉满测试。

九、示例:同一组件在多语言/夜间“自动对味儿”

// src/main/ets/pages/Demo.ets
import { t } from '../i18n/format'
import { Tokens } from '../ui/Tokens'
import { Card } from '../components/Card'

@Entry
@Component
export default class Demo {
  build() {
    Column() {
      Text(t('home_title'))
        .fontSize(Tokens.font.title)
        .fontWeight(FontWeight.Bold)
        .fontColor(Tokens.color.text)
        .margin({ bottom: Tokens.space.m })

      Card({ title: t('invite_message', { name: 'Alice', team: 'Design' }) })
    }
    .padding(Tokens.space.m)
    .backgroundColor(Tokens.color.bg)
  }
}

你改系统到 Dark,就能看到背景/文字色自动切换;你把语言换到英文,文案跟着走;你把字体放大 120%,布局也不会炸。 这才叫“主题化 + 资源系统”联手把复杂性挡在了外面。

十、常见翻车场景与止损

  • 在代码里写死色值/尺寸 👉 一年后你会痛恨自己。全部资源化
  • 字符串拼接 👉 RTL/折行崩溃。统一占位并在 JSON 里完整句子。
  • 夜间用滤镜临时救火 👉 图像细节糊掉。准备 dark 专用资源
  • 按钮宽度固定 👉 英德西语直接溢出。自适应 + 预留 30%
  • 密度包全塞 👉 首包暴涨。按需密度 + 矢量优先
  • 没有体积/文案扫描 👉 问题总在发布当天才发现。CI 早报警

结语:主题不是“换色板”,是“换脑袋”的能力

当你的 App 真正把资源限定符与覆盖优先级理顺、把主题 Token用到位、把i18n/l10n 流程嵌进 CI/CD,你会发现:新皮肤上线只是改几行 JSON;新语言支持只是加一份资源;夜间模式不过是多一个覆盖层
  这不是“皮肤系统”,这是应对不确定性的工程化防线。下次设计师兴致勃勃地说“要不要来一套春季限定?”你只需要微微一笑:“给我半天,下午茶前提测。”😉

(未完待续)

Logo

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

更多推荐