鸿蒙 ArkUI Grid 实战:从固定宽度到响应式卡片的四次重构【鸿蒙心迹】 TL;DR:ArkUI Grid 的列数应由内容容器可用宽度与最小卡片宽度共同决定,而不是把设备宽度写死。本文给出四次重构的决策依据、代码与回归场景;示例尚未在目标 SDK 或真机完成编译运行。从固定宽度到响应式卡片:ArkUI Grid 的四次重构【鸿蒙心迹】一张卡片在手机上看起来“刚好”,并不代表它在分屏、平板或折叠态下也刚好。做列表页时,最容易写出的第一版通常是:两列Grid,每张卡片固定168vp宽;开发机预览正常,于是很快合入。随后问题会依次出现:窄窗口里两张卡片挤压文字,宽窗口里卡片孤零零地留出大片空白,分栏或多窗时又因为窗口宽度与实际内容区宽度不一致而错选列数。本文复盘一个商品/内容卡片栅格从“固定宽度”到“由内容空间决定列数”的四次改造。重点不是记住一个断点数字,而是建立一条可复用的约束:先保证卡片的最低可读宽度,再让容器剩余空间决定列数。读完可以得到一份单文件 ArkTS 最小示例、一套列数推导公式,以及一张用于回归的检查表。 适用范围:本文讨论 ArkUI 声明式 UI 中内容卡片的纵向Grid。示例用于说明布局决策,不涉及商品接口、图片缓存或真实性能数据。Grid 的行列模板、间距与滚动方向规则以当前官方文档为准;本文示例尚未在目标 SDK 编译或真机运行,文末列出补测步骤。先把“看起来不对”说清楚响应式卡片的故障常被描述为“适配有问题”,但这句话无法指导修复。把它拆成可观察的信号,判断会清晰很多:1. 卡片内的标题必须可读。它允许两行省略,但不能因为列数过多而缩成每行只剩两三个汉字。2. 图片、标题和操作区的高度应该属于卡片内部规则,不能由外层的某个固定宽度偶然撑出来。3. 页面左右留白、卡片间距和列数要一起计算。只改其中一项,经常会在另一种宽度下重新失衡。4. 当页面被导航栏、侧栏或多窗压缩时,真正影响卡片的是卡片所在容器的可用宽度,不是设备型号,也不必然是整个应用窗口宽度。这里有一个很实用的分工:Grid解决“有多少列、列之间多大间距、如何滚动”;GridItem解决“每一项装什么、内容如何填满格子”。官方的 创建网格(Grid/GridItem) 说明了Grid的子组件应为GridItem,并说明columnsTemplate、rowsTemplate、columnsGap、rowsGap的作用。把容器职责和卡片职责混写在一起,是后面四次返工的共同根源。第一次:固定 `vp`,问题不是“写死”这么简单第一版常见写法如下:Grid() { ForEach(this.cards, (card: Card) = { GridItem() { ProductCard({ card: card }) .width('168vp') } }, (card: Card) = card.id) } .columnsTemplate('1fr 1fr') .columnsGap(12) .padding({ left: 16, right: 16 })它的问题不只是168vp是常量。Grid已经把一行分成两份,卡片又在格子内声明固定宽度;于是卡片宽度和格子宽度形成两个互相独立的约束。当父容器变宽时,格子变宽而卡片不变,视觉上出现漂浮;当父容器变窄时,格子不足以容纳卡片,文字、图片和间距的挤压方式又取决于内部组件,排查很不稳定。固定值仍然可以存在,但它应该表达设计下限而不是最终宽度。例如“标题可接受两行、省略号和价格并列时,卡片主体不能小于 160vp”是一个设计约束;“无论容器多宽都等于 168vp”则把布局权交给了偶然的屏幕尺寸。两者看似只差一个数字,实际决定了后续是否能够扩展。这次重构的结论:删掉卡片根节点的固定宽度,让卡片以width('100%')填满其GridItem;最低宽度改为列数决策的输入,而不是写在卡片上。