HarmonyOS 3D 拆解分析:图标升级与场景化改造
从 emoji 到 Unicode 符号,从通用预设到场景化卡片——一次"小改动大提升"的迭代。
前言
3D 拆解分析 App 的上一版 UI 已经有了基本的视觉层次——渐变 Hero Card、卡片式预设、弹簧滚动效果。但用了一段时间后发现两个问题:
- emoji 图标在不同设备上渲染不一致——有的设备上 emoji 有颜色,有的没有,大小也不统一
- 预设选项太抽象——“2×2 简单”"3×3 标准"这种命名对普通用户不友好,他们不知道该选哪个
这次迭代主要解决这两个问题:把 emoji 换成 Unicode 符号和系统图标,把通用网格预设改成场景化设备卡片。
首页:从网格预设到场景卡片
旧版 vs 新版
旧版的精度选择区:
[2×2 简单] [3×3 标准] [4×3 精细] [4×4 深度]
新版的场景选择区:
[📱 手机 ] [⌚ 手表 ]
[📋 平板/相机] [🖥 大件设备 ]

区别不只是文字变了——交互逻辑也变了。旧版用户需要自己判断"我的东西适合什么网格",新版直接告诉用户"拍手机选这个,拍手表选那个"。
场景卡片的实现
@Builder SceneCard(idx: number, name: string, info: string, icon: string) {
Column() {
Row() {
Text(icon).fontSize(22)
Blank()
// 单选按钮
Row() {}.width(22).height(22).borderRadius(11)
.backgroundColor(this.preset === idx ? A : '#E8E5F0')
.border({ width: 2, color: this.preset === idx ? A : '#DDD' })
if (this.preset === idx) {
Text('✓').fontSize(12).fontWeight(FontWeight.Bold).fontColor(Color.White)
.position({ x: 6, y: 3 })
}
}.width('100%').margin({ bottom: 10 })
Text(name).fontSize(16).fontWeight(FontWeight.Bold)
.fontColor(this.preset === idx ? A : T1).width('100%')
Text(info).fontSize(11).fontColor(T3).width('100%').margin({ top: 1 })
}
.layoutWeight(1).padding(14)
.backgroundColor(this.preset === idx ? AL : CW)
.borderRadius(16)
.border({ width: this.preset === idx ? 1.5 : 1,
color: this.preset === idx ? A + '30' : '#F0EEF6' })
.onClick(() => { this.preset = idx })
}
每个卡片包含:
- 左上角:设备图标(emoji)
- 右上角:单选按钮
- 底部:设备名称 + 网格信息

选中态的变化和之前一样——背景变浅紫、边框变紫色、单选按钮填充 + ✓。但这次加了一个细节:选中时设备名称也变成紫色(fontColor(this.preset === idx ? A : T1)),进一步强化选中反馈。
场景到网格的映射
用户选的是场景,但分析页面需要的是网格尺寸。映射逻辑在两个方法里:
private cols(): number {
return this.preset === 0 ? 2 : (this.preset === 1 ? 3 : 4)
}
private rows(): number {
return this.preset === 0 ? 2 : (this.preset <= 2 ? 3 : 4)
}
映射关系:
| 场景 | preset | cols | rows | 部件数 |
|---|---|---|---|---|
| 手机 | 0 | 2 | 2 | 4 |
| 手表 | 1 | 3 | 3 | 9 |
| 平板/相机 | 2 | 4 | 3 | 12 |
| 大件设备 | 3 | 4 | 4 | 16 |
手机用 2×2 是因为手机屏幕小,4 个部件足够展示结构。大件设备用 4×4 是因为需要更精细的拆解。
Header 图标:emoji → 文字
Stack() {
Column() {}.width(46).height(46).borderRadius(23)
.linearGradient({ angle: 135, colors: [[A, 0], ['#A78BFA', 1]] })
Text('拆').fontSize(20).fontWeight(FontWeight.Bold).fontColor(Color.White)
}
旧版用 🔧 emoji,新版用"拆"字。好处:
- 文字渲染在所有设备上一致
- "拆"字直接表达应用功能,比扳手图标更直观
- 渐变背景 + 白色文字的对比度比 emoji 高
Flow 步骤:编号 + 箭头
Row({ space: 0 }) {
this.Flow('1', '拍照上传', '拍摄物品正面\n清晰照片', '📸')
Column() {
SymbolGlyph($r('sys.symbol.chevron_right')).fontSize(14).fontColor(['#DDD'])
}.margin({ left: 4, right: 4 })
this.Flow('2', '智能切分', '自动网格划分\n识别部件区域', '🧩')
Column() {
SymbolGlyph($r('sys.symbol.chevron_right')).fontSize(14).fontColor(['#DDD'])
}.margin({ left: 4, right: 4 })
this.Flow('3', '3D 爆炸', '部件飞散展示\n结构一目了然', '💥')
}

步骤之间用 SymbolGlyph 的右箭头连接,比旧版的 Text('→') 更精致。箭头颜色 #DDD 很淡,不会抢视觉焦点。
底部 CTA:图标 + 文字
Button() {
Row() {
Text('📸 ').fontSize(18)
Text('拍照开始分析').fontSize(16).fontWeight(FontWeight.Bold).fontColor(Color.White)
}
}
.width('88%').height(52).backgroundColor(A).borderRadius(26)
.shadow({ radius: 14, color: A + '38', offsetY: 6 })
旧版按钮只有文字,新版加了 📸 图标。图标放在文字前面,引导用户理解按钮的功能。
分析工作台:符号化图标
网格选择器:⊞ 替代文字
Row() {
Text('⊞').fontSize(14).fontColor(T3).margin({ right: 6 })
ForEach(this.gps, (p: GP) => {
Text(p.label).fontSize(11)
.fontWeight(p.cols === this.cols && p.rows === this.rows ? FontWeight.Bold : FontWeight.Regular)
.fontColor(p.cols === this.cols && p.rows === this.rows ? '#FFFFFF' : T2)
.padding({ left: 10, right: 10, top: 5, bottom: 5 })
.borderRadius(12)
.backgroundColor(p.cols === this.cols && p.rows === this.rows ? A : '#F5F5F7')
.margin({ right: 6 })
.onClick(() => { this.cg(p.cols, p.rows) })
})
Blank()
// 部件列表开关
Row() {
Text('☰ ').fontSize(14).fontColor(A)
Text('部件').fontSize(11).fontWeight(FontWeight.Medium).fontColor(A)
}
.padding({ left: 10, right: 10, top: 5, bottom: 5 })
.backgroundColor(AL).borderRadius(12)
.onClick(() => { this.list = !this.list })
}
两个符号图标:
⊞(网格符号):替代"网格"文字,更紧凑☰(菜单符号):替代"部件"文字前的图标,表示列表/菜单
这些都是 Unicode 字符,不是 emoji,渲染一致性有保障。
播放按钮:图标 + 文字
Row() {
Text(this.on ? '⏸' : '▶').fontSize(14).fontColor(Color.White)
Text(this.on ? ' 收起' : ' 爆炸').fontSize(12)
.fontWeight(FontWeight.Medium).fontColor(Color.White)
}
.padding({ left: 14, right: 14, top: 8, bottom: 8 })
.borderRadius(18)
.backgroundColor(this.on ? '#FF6B6B' : A)
.onClick(() => { this.toggle() })
图标和文字组合的好处:
- 图标提供快速识别(▶ = 播放,⏸ = 暂停)
- 文字提供明确含义(“爆炸” = 开始分析,“收起” = 停止分析)
- 颜色区分状态(紫色 = 可播放,红色 = 可停止)
部件清单:编号圆圈
ForEach(this.pc, (c: PCard) => {
Row() {
Column() {
Text(c.idx.toString()).fontSize(11).fontWeight(FontWeight.Bold).fontColor(A)
}
.width(24).height(24).borderRadius(12)
.backgroundColor(AL)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
Row() {}.width(c.d * 42).height(5).borderRadius(3)
.backgroundColor(A + '50').margin({ left: 10, right: 10 })
Column() {
Text(c.n).fontSize(13).fontWeight(FontWeight.Medium).fontColor(T1)
Text(c.sub).fontSize(10).fontColor(T3)
}.alignItems(HorizontalAlign.Start).layoutWeight(1)
}
})
每个部件卡片的结构:
- 编号圆圈(紫色背景 + 白色数字)
- 深度条(宽度和深度成正比)
- 名称 + 位置信息
编号圆圈让用户快速定位"第 3 个部件在哪",深度条让用户一眼看出哪些部件更"深"。
图标选择的原则
这次迭代中图标的选择遵循了几个原则:
1. 优先用 Unicode 符号,其次用系统图标,最后用 emoji
⊞ (网格) → Unicode 符号,渲染一致
☰ (菜单) → Unicode 符号,渲染一致
Sys.symbol.chevron_right → 系统图标,矢量缩放
📸 (相机) → emoji,可能渲染不一致
2. 图标必须有对应的文字说明
纯图标按钮需要用户猜测功能。加了文字后(“部件”“爆炸”“收起”),按钮的意图一目了然。
3. 图标大小和文字大小匹配
Text('⊞').fontSize(14) // 图标
Text('部件').fontSize(11) // 文字
图标比文字大 3px,视觉上更协调。如果图标和文字一样大,图标会显得太小。
4. 颜色跟随语义
- 功能性图标(网格、部件):跟随品牌色
A - 状态性图标(播放/暂停):紫色=正常,红色=警告
- 装饰性图标(步骤箭头):淡灰色
#DDD
踩坑记录
1. SymbolGlyph vs Text 符号
// 系统图标(矢量,推荐)
SymbolGlyph($r('sys.symbol.chevron_right')).fontSize(14).fontColor(['#DDD'])
// Unicode 文本符号(也能用,但不是矢量)
Text('⊞').fontSize(14).fontColor(T3)
SymbolGlyph 是矢量图标,缩放不失真。Text 符号是字体字符,放大到一定程度会模糊。对于小尺寸(14-16px),两者差别不大;大尺寸建议用 SymbolGlyph。
2. emoji 的渲染问题
同一个 emoji 在不同设备上的渲染差异:
- iOS:彩色,有阴影
- Android:彩色,风格不同
- 鸿蒙:可能无彩色,大小不一
如果 emoji 只是装饰性的(如步骤图标),差异可以接受。如果是功能性图标(如导航按钮),建议用 SymbolGlyph 或 Unicode 符号。
3. 单选按钮的位置
Row() {}
.width(22).height(22).borderRadius(11)
.backgroundColor(this.preset === idx ? A : '#E8E5F0')
.border({ width: 2, color: this.preset === idx ? A : '#DDD' })
if (this.preset === idx) {
Text('✓').fontSize(12).fontWeight(FontWeight.Bold).fontColor(Color.White)
.position({ x: 6, y: 3 })
}
✓ 符号用 position 定位在圆圈中心。x: 6, y: 3 是经验值——需要根据字体大小和圆圈大小微调。不同字号的 ✓ 偏移量不同。
4. 场景卡片的宽度
Row({ space: 10 }) {
this.SceneCard(0, '手机', '3×3 网格 · 9 部件', '📱')
this.SceneCard(1, '手表', '3×3 网格 · 9 部件', '⌚')
}
.width('100%')
两个卡片用 Row({ space: 10 }) 排列,每个卡片内部用 layoutWeight(1) 等宽分配。如果卡片内容长度差异很大,需要确保 layoutWeight(1) 生效——前提是 Row 的宽度是 '100%'。
写在最后
这次迭代的改动量不大——换了图标、改了命名、调整了布局。但效果很明显:场景化卡片让用户更容易做选择,Unicode 符号让界面在所有设备上保持一致。
移动端的图标选择是个看似简单但实际很讲究的事情。emoji 方便但不一致,系统图标一致但数量有限,Unicode 符号居中但不够直观。根据使用场景选择合适的图标类型,是 UI 开发中经常需要做的判断。
更多推荐

所有评论(0)