Unicode 图标为什么适合原型、不适合长期数据
项目早期为了快速验证功能,曾经用 Unicode 字符表示图标:
☀ ☾ ✓ ♥ ♫
优点很明显:
- 不需要准备图片资源;
- Text 组件就能显示;
- 写几个字符即可覆盖原型。
但进入正式版本后,问题也会出现:
- 不同字体的字形差异很大;
- 某些字符在不同设备上基线不一致;
- 字符可能被渲染为彩色 Emoji;
- 图标粗细无法和系统风格统一;
- 持久化后很难知道“这个字符的业务含义是什么”;
- 更换图标体系时需要迁移旧数据。
因此 HarmonyOS 版本最终使用 SymbolGlyph 和稳定语义 Key。
一、磁盘里保存语义,不保存 Resource 对象
习惯模型中的图标字段是字符串:
export interface Habit {
id: string;
name: string;
icon: string;
color: string;
// 其他字段
}
但这里的字符串不再是“☾”,而是:
moon_z
figure_run
book
drop
checkmark_circle
这些值是应用自己的语义协议。
好处是:
- 不依赖某个具体系统资源对象能否 JSON 序列化;
- 业务数据可以跨版本保存;
- 将来替换成自定义图标时不必重写 Habit;
- 中英日切换不影响图标身份;
- 导出文件仍然可读。
二、通过一层映射连接业务 Key 与系统 Symbol
页面集中维护:
private iconResource(icon: string): Resource {
switch (normalizedIconKey(icon)) {
case 'sun_max':
return $r('sys.symbol.sun_max');
case 'face_smiling':
return $r('sys.symbol.face_smiling');
case 'water_waves':
return $r('sys.symbol.water_waves');
case 'moon_z':
return $r('sys.symbol.moon_z');
case 'figure_run':
return $r('sys.symbol.figure_run');
case 'book':
return $r('sys.symbol.book');
default:
return $r('sys.symbol.circle');
}
}
这层映射看起来比直接在页面写 $r() 多了一步,却带来清晰边界:
业务数据:moon_z
→ 映射层:sys.symbol.moon_z
→ UI:SymbolGlyph
系统资源名变化或设计决定替换图标时,只需调整映射层。
三、封装统一的图标 Builder
项目用一个 Builder 统一尺寸和颜色入口:
@Builder
private AppIcon(
icon: string,
size: number,
color: string
) {
SymbolGlyph(this.iconResource(icon))
.fontSize(size)
.fontColor([color])
}
圆形底图也进一步封装:
@Builder
private IconBadge(
icon: string,
size: number,
color: string,
diameter: number,
background: string
) {
Stack({ alignContent: Alignment.Center }) {
this.AppIcon(icon, size, color)
}
.width(diameter)
.height(diameter)
.backgroundColor(background)
.borderRadius(diameter / 2)
}
这样心情、标签、习惯和设置页不会各自维护一套 Symbol 样式。
四、旧 Unicode 数据必须在读取路径中兼容
应用一旦发布,磁盘数据就是需要长期兼容的协议。
不能把编辑器中的 Unicode 替换成新 Key 后,就假设旧用户数据自动变化。
项目提供归一化函数:
export function normalizedIconKey(icon: string): string {
switch (icon) {
case '☀': return 'sun_max';
case '≈': return 'water_waves';
case '☾': return 'moon_z';
case '▣': return 'briefcase';
case '↗': return 'figure_run';
case '◇': return 'drop';
case '▤': return 'book';
case '✓': return 'checkmark_circle';
case '♥': return 'heart';
case '♫':
case '♬': return 'music';
default: return icon;
}
}
显示图标和打开习惯编辑器时都先归一化:
this.habitIconDraft = normalizedIconKey(
habit?.icon ?? HABIT_ICONS[0]
);
这样旧数据仍能显示,用户下次保存习惯时会自然迁移到新的语义 Key。
五、懒迁移和一次性迁移怎么选
当前项目使用“读取兼容、编辑时迁移”的懒迁移。
适合:
- 旧格式数量很少;
- 映射完全确定;
- 不迁移也不影响显示;
- 数据规模小。
另一种方式是在 Repository 加载后一次性改写:
parsed.habits = parsed.habits.map((habit) => ({
…habit,
icon: normalizedIconKey(habit.icon)
}));
然后保存新版本。
一次性迁移适合:
- 后续代码不希望长期携带兼容分支;
- 新旧格式混用会造成错误;
- 迁移结果可验证;
- 失败时有回滚策略。
无论选择哪种,都不应该直接把未知值清空。
六、未知图标需要稳定回退
导入文件、测试版本或未来资源变化可能带来未知 Key。
映射层默认返回:
return $r('sys.symbol.circle');
回退图标的目标不是“看起来完美”,而是保证:
- 页面不会因为单个坏值崩溃;
- 用户仍能打开编辑器并重新选择;
- 导出和其他数据不受影响;
- 问题可以通过日志或测试定位。
七、图标颜色也应该是数据的一部分吗
当前习惯保存:
color: '#5E7CE2'
这让用户选择的颜色能够持久化,但也意味着设计系统调整时,旧数据仍保留旧色值。
可以有两种模型:
保存实际色值
适合允许用户自由选择颜色,导出结果直观。
保存语义色 Key
例如:
habit-blue
habit-mint
habit-gold
适合需要统一适配深色模式或未来更换主题的产品。
当前项目色板固定且规模较小,直接保存色值足够。如果后续增加深色模式和主题系统,语义色 Key 会更易维护。
八、Symbol 不能替代无障碍名称
一个“锁”图标对开发者很直观,但屏幕阅读器需要知道它代表“应用锁”还是“隐私政策”。
因此可点击 Symbol 应与明确文本、控件标签或无障碍描述结合,不能只依靠图形表达业务操作。
尤其要避免:
- 多个只显示图标的按钮没有名称;
- 颜色是区分状态的唯一方式;
- 选中态只改变图标粗细;
- 删除和归档使用过于相似的图形。
九、系统 Symbol 的测试清单
- 所有配置 Key 都能映射到资源;
- 未知 Key 显示回退圆形;
- 旧 Unicode 数据能打开和编辑;
- 不同字号下图标没有截断;
- 选中和未选中颜色对比清楚;
- 中文、英文、日文下布局一致;
- 真机系统版本包含使用到的 Symbol;
- 重要操作有文字或无障碍名称;
- 导出再导入后语义 Key 不丢失。
总结
从 Unicode 字符迁移到 SymbolGlyph,真正重要的不是“图标更好看”,而是建立了一套稳定协议:
这套模式同样适用于系统图标、自定义 SVG 和图片资源之间的后续替换。
本文案例来自“心晴手记(MoodMemoir)”HarmonyOS 版的心情、标签与习惯图标系统。
参考资料
- HarmonyOS 图标小符号 SymbolGlyph/SymbolSpan


