在这里插入图片描述

HarmonyKit | 鸿蒙新特性实践:ToolCard 统一卡片布局设计迭代

卡片的困境

工具卡片看起来是最简单的 UI 组件——一个图标、一个标题、一行描述。但当你的网格里有 10 张卡片,每张卡片的描述文字长度从 8 个字到 21 个字不等时,“简单"就变成了"棘手”。

HarmonyKit 的卡片设计经历了两轮迭代。第一轮是"先让它能跑"——每个卡片自适应内容高度,结果 2 列网格中同行两张卡片高度不齐,参差感严重。第二轮是"让它整齐"——引入了固定高度、统一间距和主题色体系,10 张卡片的视觉效果终于统一。

项目仓库:https://atomgit.com/VON-/harmony-kit

迭代一:宽度自适应 + 高度自适应

最初的设计很简单——卡片宽度设为 '44%'(相对于 GridItem),高度不指定,靠内容自然撑开:

Column() {
  Text(icon).fontSize(32)
  Text(name).fontSize(14)
  Text(description).fontSize(11).maxLines(1)
}
.width('44%')
.padding(20)

这个方案在工具只有 5 个时看起来还凑合。但当工具扩展到 10 个,描述文字的差异性暴露了出来:

  • “文本与 Base64 互相转换”:10 个汉字
  • “二进制、八进制、十进制、十六进制互转”:17 个汉字

在 11px 的字体下,17 个汉字在 44% 屏幕宽度的卡片中会换行。而 10 个汉字的不会。结果就是:换行的卡片比不换行的卡片高出一行的高度。在 2 列网格中,同一行的两张卡片高度不一致,底部参差不齐。

在这里插入图片描述

迭代二:固定高度 + 柔和阴影 + 主题色

第二版设计做了三个核心改动:

1. 固定高度 158vp

.height(158)
.justifyContent(FlexAlign.Center)

158vp 的选择是计算出来的:

  • top padding: 20vp
  • 图标圆形: 48vp
  • 图标底边距: 12vp
  • 名称文字行高: ~20vp
  • 名称底边距: 4vp
  • 描述文字(最多两行): 11px × 1.5 行距 × 2 行 ≈ 33vp
  • bottom padding: 20vp
  • 合计: 约 157vp → 取整 158vp

justifyContent(FlexAlign.Center) 让短内容的卡片垂直居中,而不是贴顶。这样即使内容只有一半高度,卡片视觉上也平衡。

2. 图标设计:文字胜于图形

HarmonyKit 的图标不是 PNG 文件,而是一个带半透明底色圆的等宽文本:

Stack() {
  Circle()
    .width(48).height(48)
    .fill(this.tool.color + '18');

  Text(this.tool.icon)
    .fontSize(18)
    .fontWeight(FontWeight.Bold)
    .fontColor(this.tool.color)
    .fontFamily('monospace');
}

this.tool.color + '18' 这个技巧值得展开。color 是十六进制颜色字符串如 '#007aff'。追加 '18' 变成了 '#007aff18'——在十六进制中,最后两位是 alpha 通道(00-FF)。18 约等于 9% 的不透明度。这个半透明底色圆圈为卡片提供了柔和的视觉锚点,比纯色填充精致得多。

使用等宽字体文本作为图标,好处是:

  • 零资源文件——不需要 PNG/SVG 文件,不增加 APK 体积
  • 随字号缩放——字体缩放时图标自动跟随
  • 动态换色——每个工具用自己的主题色,不需要为每种颜色准备一份图标资源
  • 等宽字体保障了对齐——{}</> 虽然宽度不同,但 monospace 保证了每个字符占据相同宽度

3. 主题色体系

10 个工具各有独立的主题色:

JSON 格式化    #007aff  蓝色   — Apple 的系统蓝
Base64 编解码  #34c759  绿色   — 编解码=安全=绿色
时间戳转换    #ff9500  橙色   — 时间=温暖=橙色调
URL 编解码    #5856d6  紫色   — URL=网页链接=紫色
哈希计算      #ff3b30  红色   — 哈希=指纹=独一无二=红色
UUID 生成器   #30b0c7  青色   — UUID=ID=冷静=青色
颜色转换      #af52de  粉紫   — 颜色=光谱=粉紫
进制转换      #ff6482  粉红   — 进制=数学=活泼=粉红
正则测试器    #30d158  翠绿   — 正则=模式=精确=翠绿
文本统计      #ff9f0a  琥珀   — 统计=数字=琥珀

配色原则:

  • 相邻工具的颜色在色环上拉开至少 60 度,防止混淆
  • 冷色调(蓝/青/绿/紫)和暖色调(橙/红/粉红/琥珀)交错排列
  • 每个颜色在白色背景上的对比度 >= 4.5:1,保证可读性

卡片的阴影设计

.shadow({
  radius: 10,
  color: '#0d000000',
  offsetX: 0,
  offsetY: 2
})

阴影参数经过了多次微调:

  • radius: 10:足够大的模糊半径让阴影看起来柔和,不像是"硬边框"
  • color: '#0d000000':纯黑色 5% 不透明度。比默认的 #1a000000(10%)更轻,在白色背景上若有若无
  • offsetY: 2:轻微偏下,模拟顶光照下物体的自然投影

这个阴影组合在浅灰色背景(#f5f5f5)上创造了微妙的"浮起"感——卡片似乎在背景上方 1-2mm 处悬浮。但如果背景也是白色,阴影效果会被弱化。这就是为什么主页背景必须是浅灰而非纯白。

名称文字的单行截断

Text(this.tool.name)
  .fontSize(14)
  .maxLines(1)
  .textOverflow({ overflow: TextOverflow.Ellipsis })

工具名称都是精心控制在 10 个字符以内的短文本。“JSON 格式化”“Base64 编解码”“时间戳转换”——最长的是"时间戳转换"(5 个字),在 14px 字体下约 70px 宽。卡片宽度约 170px,足够容纳。但为了防御未知的长名称,仍然设置了 maxLines(1) + 省略号截断。

描述文字的两行限制

Text(this.tool.description)
  .fontSize(11)
  .maxLines(2)
  .lineHeight(16)
  .textOverflow({ overflow: TextOverflow.Ellipsis })
  .textAlign(TextAlign.Center)

maxLines(2) 配合 lineHeight(16) 让描述区域最多占据 32vp 的高度。超过两行的文字被截断为省略号。居中对齐让描述在卡片宽度内水平居中,视觉上更平衡。

为什么是 2 行而不是 1 行?因为有的描述比较长(“二进制、八进制、十进制、十六进制互转"需要两行才能完整展示)。如果限制为 1 行,用户只能看到被截断的"二进制、八进制、十进制…”——丢失了关键信息。

卡片的交互反馈

HarmonyKit 的卡片在点击时提供路由跳转:

.onClick(() => {
  this.getUIContext().getRouter().pushUrl({ url: this.tool.routerPath });
})

使用 this.getUIContext().getRouter() 而非全局 router.pushUrl()——这是鸿蒙路由 API 演进后的推荐写法。UI 上下文绑定的路由实例在多窗口场景下行为正确,而全局 router 可能路由到错误的窗口。

Grid 布局的配合

ToolCard 的 width('100%') 配合 Grid 的 columnsTemplate('1fr 1fr')

Grid() {
  ForEach(TOOL_LIST, (tool: ToolItem) => {
    GridItem() {
      ToolCard({ tool: tool })
    }
  })
}
.columnsTemplate('1fr 1fr')
.columnsGap(12)
.rowsGap(12)

Grid 平均分配两列,每列宽度 = (screenWidth - 左右padding - gap) / 2。卡片的 width('100%') 填充 GridItem 的全部可用宽度。columnsGaprowsGap 定义卡片之间的间距——12vp 是一个经验值,既不显得拥挤,也不浪费空间。

项目仓库:https://atomgit.com/VON-/harmony-kit

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐