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

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 的全部可用宽度。columnsGap 和 rowsGap 定义卡片之间的间距——12vp 是一个经验值,既不显得拥挤,也不浪费空间。
更多推荐



所有评论(0)