从 emoji 到 Unicode 符号,从通用预设到场景化卡片——一次"小改动大提升"的迭代。

前言

3D 拆解分析 App 的上一版 UI 已经有了基本的视觉层次——渐变 Hero Card、卡片式预设、弹簧滚动效果。但用了一段时间后发现两个问题:

  1. emoji 图标在不同设备上渲染不一致——有的设备上 emoji 有颜色,有的没有,大小也不统一
  2. 预设选项太抽象——“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)
  }
})

每个部件卡片的结构:

  1. 编号圆圈(紫色背景 + 白色数字)
  2. 深度条(宽度和深度成正比)
  3. 名称 + 位置信息

编号圆圈让用户快速定位"第 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 开发中经常需要做的判断。

Logo

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

更多推荐