ArkUI GridRow+GridCol:展陈仪表卡片断点占列与窄屏触达保护【鸿蒙心迹】
本篇 Demo 名称 PanelCaliper,页面 PanelDashboard,演示任务 PD-2112。所有指标为静态虚构演示值:温度 23.5 °C、湿度 48 %、客流 128 人、设备 6 台;既无现场传感器,也没有真实的馆内生产监控连接。文章附图为独立绘制的示意图,不是开发工具或真机测试证据。
一、屏幕变窄时,“能放下”不代表“能操作”
第一次画这个展陈看板,我把四张指标卡片摆成四列。浏览器式预览里看上去很整齐,切到窄屏的设计稿就不对劲了:标题挤成两行,数值下面的单位被折断,右下角“查看”按钮只剩一个很窄的触摸区域。应用能把四张卡硬塞在一行,恰恰说明布局缺少一套明确的占列策略。这里无需引入传感器数据或 AI 能力,仅仅是一个常见的多设备 ArkUI 栅格问题。
我把目标重写成两条可以验收的约束。第一,卡片身份与内容不随栅格变化而变化;温度永远是那张温度卡,不因为换行变成第一行第一个数据。第二,手机单列优先保证触达,宽屏才提升并排密度。这让问题从“哪个页面看起来更漂亮”变成“某个窗口宽度下,12 列网格该分给每张卡几列”。
本篇不接前面批次出现过的折叠屏路由、Tabs 草稿或异步校验逻辑。PanelCaliper 只关注 GridRow 与 GridCol 的布局职责,尤其是占列、断点、间距和按钮的最小尺寸。示例使用 360 vp 的手机设计宽度,期望进入 xs 档,每张卡占 12/12 列,四张卡排成四行。其它宽度只是方案推演,并非经过各种设备实测的断言。

二、先固定数据,不让排列顺序替业务背锅
演示任务 PD-2112 只有四张卡:温度 23.5 °C、湿度 48 %、客流 128 人、设备 6 台,各有 M-01 到 M-04 的业务键。不要把这个键写成“第几列”:窗口一变宽,行列坐标改变,业务含义仍然不变。数据模型只存读数与单位,展示布局通过断点规则派生,两者才不会互相污染。
下面的代码负责把断点名称翻译成 12 列网格的占列值,同时给文档中的验收口径提供可计算的结果。它不是读设备硬件型号,也不猜测折叠态,而是接受由布局容器实际报告的断点。注意栅格层面定义占列,并不等于自动保证了内容的触控高度,后面还要单独限制按钮尺寸。
class PanelMetric {
id: string;
label: string;
amount: string;
unit: string;
constructor(id: string, label: string, amount: string, unit: string) {
this.id = id;
this.label = label;
this.amount = amount;
this.unit = unit;
}
}
const DEMO_METRICS: PanelMetric[] = [
new PanelMetric('M-01', '温度', '23.5', '°C'),
new PanelMetric('M-02', '湿度', '48', '%'),
new PanelMetric('M-03', '客流', '128', '人'),
new PanelMetric('M-04', '设备', '6', '台')
];
function spanByBreakPoint(bp: string): number {
if (bp === 'lg') { return 3; }
if (bp === 'md') { return 4; }
if (bp === 'sm') { return 6; }
return 12;
}
function itemsPerRow(bp: string): number {
return 12 / spanByBreakPoint(bp);
}
这段算法只为演示把断点映射得更直观:xs→12、sm→6、md→4、lg→3,分别意味着一行能放 1、2、3、4 张卡。真实 GridRow 的断点选择不应该由此函数独立决定,两套判断一旦各用不同宽度,页面就会出现“日志说两列、画面实际一列”的矛盾。我的做法是以 onBreakpointChange 的值为观察信号,函数仅用于显示与审计,不作为另外一套手写布局引擎。
三、在状态模型中记住当前档位,而不是记住一台手机
过去做适配时最容易走弯路的写法,是在业务里判断“是不是折叠屏”“是不是平板”,然后人为切换四套页面。窗口可能分屏、悬浮或改变大小,设备类型判断并不能代表真实可用宽度。本例统一使用同一份数据,同一个 PanelDashboard,同时让 GridRow 自己完成跨断点布局。
下一段把业务数据、当前布局档位和选中的卡片分开保存。选中逻辑不触发任何采集请求,点击“查看”仅把 selectedLabel 改为对应名称,以便验证四张卡在任何排列下都保留业务身份。记录日志时使用任务编号、断点和占列值,这些信息比“页面好像变窄了”更适合团队排查。
@Entry
@Component
struct PanelDashboard {
@State currentBp: string = 'xs';
@State selectedLabel: string = '温度';
private cards: PanelMetric[] = DEMO_METRICS;
private selectMetric(item: PanelMetric): void {
this.selectedLabel = item.label;
console.info('PD-2112 OPEN ' + item.label);
}
private updateBreakPoint(bp: string): void {
this.currentBp = bp;
console.info('PD-2112 GRID bp=' + bp +
' span=' + spanByBreakPoint(bp) +
' cards=' + this.cards.length);
}
private gridCaption(): string {
return '断点 ' + this.currentBp + ' · 每行 ' +
itemsPerRow(this.currentBp) + ' 张';
}
这里的初始化 xs 是为了演示截图可重现,不代表程序启动时已经测到了 360 vp;真实值由容器回调纠正。数据用 id 作为键,选中名称只用于 UI 展示;产品化时可同时保存 selectedId,防止同名卡片造成歧义。没有网络监听器,也就没有额外异步回调要销毁;布局回调由组件生命周期管理。如果以后加上定时采集、设备订阅或图表渲染,必须分别在页面退出时取消,不可把“UI 会回收”理解成“后台业务自然停止”。
四、十二列只是几何系统,交互尺寸还得自己管
实际布局选择一套很保守的区间:480 vp 以内为 xs,480 至 840 vp 为 sm,840 至 1200 vp 为 md,更宽的窗口按 lg 策略。这里写的是 Demo 的断点配置,不是华为对所有设备类别的官方硬性宽度定义。横向与纵向间距都设为 12 vp,卡片最少高 148 vp,其中“查看”按钮高 48 vp;这组数值属于工程设计约束,不是无障碍合格证书。
下面的构建代码与前两段放在同一个 .ets 文件:组件结构体在上一段中尚未闭合。关键是每张卡都包在 GridCol 内,并明确配置四档 span;不要再通过手工算屏幕像素去修改卡片宽度。按钮和数值文本在卡片内部拥有独立尺寸,换行不应通过压低按钮高度来换取空间。
build() {
Column({ space: 12 }) {
Text('PanelCaliper · PD-2112').fontSize(16)
Text('PanelDashboard').fontSize(24).fontWeight(FontWeight.Bold)
Text(this.gridCaption()).fontColor('#965E40')
GridRow({
columns: 12,
gutter: { x: 12, y: 12 },
breakpoints: {
value: ['480vp', '840vp', '1200vp'],
reference: BreakpointsReference.WindowSize
}
}) {
ForEach(this.cards, (item: PanelMetric) => {
GridCol({ span: { xs: 12, sm: 6, md: 4, lg: 3 } }) {
Column({ space: 8 }) {
Text(item.label).fontSize(16)
Text(item.amount + ' ' + item.unit)
.fontSize(26).fontWeight(FontWeight.Bold)
Button('查看').height(48).width('100%')
.onClick(() => this.selectMetric(item))
}
.padding(12).height(148).width('100%')
.backgroundColor('#FFF8F3').borderRadius(14)
}
}, (item: PanelMetric) => item.id)
}.onBreakpointChange((bp: string) => this.updateBreakPoint(bp))
Text('已选择:' + this.selectedLabel)
Text('静态演示值 · 非真实采集').fontSize(12)
}.padding(16).width('100%').height('100%')
}
}
GridCol 的 span 是占据网格列数,并不是直接写一张卡要宽多少个物理像素。窗口横向尺寸和系统字体缩放一起变化时,即使断点没有切换,内部文本仍可能换行,因此对信息密度的验收不能仅检查一张全尺寸海报。给“查看”按钮设置 48 vp 高是最基本的交互留白,实际无障碍体验还应检查语义标签、焦点顺序、屏幕阅读器朗读和系统字体放大。固定高度 148 vp 同样可能需要在大字体下增高,示例不能被误认为覆盖了所有适配场景。

五、把四张静态卡片做成可复验的布局账本
本轮只设计一张手机窄屏演示:360 vp 对应 xs,每张 GridCol 占 12 列,四张卡按 M-01 温度、M-02 湿度、M-03 客流、M-04 设备的次序单列排布。因为卡片总高与页面可用高度存在限制,屏幕示意图允许下面的内容靠滚动查看;不能为了在一屏挤下四张卡就牺牲按钮。图中保留状态栏的时间、5G、Wi-Fi、信号和电量百分比,它们只是演示手机界面外观,不意味着 ArkUI 页面控制了系统状态栏。
对应的预期日志为 PD-2112 GRID bp=xs span=12 cards=4。点击第一张卡的查看按钮,预期出现 PD-2112 OPEN 温度,页面底部“已选择:温度”不需要重新计算任何采集数据。若窗口进入 sm,理论上每行两张;进入 md,理论上三张;进入 lg,理论上四张。这里的“理论上”很重要,因为本轮没有分别在平板、折叠屏、PC 窗口中完成设备级测试。

六、排查顺序先看宽度,再看列数,最后看卡片内容
如果出现一行多了半张卡,先检查是否另有外层容器给 GridRow 加了额外 padding,或者页面没有获取预期窗口宽度;再看 onBreakpointChange 是否实际传来了目标档位。不要一上来就把 span 改成固定像素宽,那会绕开栅格系统,也使后续布局维护变得更难。如果日志的断点和视觉布局一致,但按钮依然难点,问题通常在卡片自身的内容高度或触摸热区,而不是 GridRow 的列数。
第二个值得单独测的是方向变化与系统字体放大同时发生。横屏切竖屏会触发重排,字体扩大可能让单位或者标题换行;两者叠加以后,固定 148 vp 的卡片未必放得下所有内容。生产版本应为文字允许多行、按内容提高卡片高度,并在低宽度下优先隐藏装饰性元素,而不是隐藏关键数值。对于四张静态卡片,ForEach 没有性能压力;扩展到大量卡片时,再根据滚动、懒加载和局部刷新需求选择合适的容器。
第三个排查点是断点口径与日志口径保持一致。不要同时在构建函数外面写一份 500/900 的宽度判断,又在 GridRow 里写 480/840,否则只能获得两组互相打架的状态。本文的 itemsPerRow 函数只解释 currentBp,真正的布局执行完全交给 GridRow,便于把 UI 行为与业务选中状态独立验收。如果实际产品需要不同品牌设备的特殊能力,可以在此基础上另增设备能力层,但不应再复用一组不准确的“手机/平板”硬编码宽度。
七、演示完整,不等于设备适配已经验收
PanelCaliper 的交付标准其实很克制:宽度改变时卡片占列按规则变化,卡片 ID 和指标不变,窄屏查看按钮保持足够的基本高度,调试输出能解释当前布局档位。它没有真实馆内温湿度数据,没有图表引擎、后台订阅、自动监测,也没有跑过正式的无障碍测试。这样在把示例交给别人时,哪些可以从代码推导、哪些必须回到设备上再验一次,界限才足够清楚。相比“响应式布局已经适配所有机型”的说法,我更愿意留下这份可检验的占列合同与未覆盖边界。
参考资料:华为官方《Responsive Grid Layout(GridRow/GridCol)》指南:https://developer.huawei.com/consumer/en/doc/harmonyos-guides/arkts-layout-development-grid-layout;《GridCol》API 文档:https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V14/ts-container-gridcol-V14。接口调用仅经过官方文档对照,没有执行目标 SDK 编译或设备测试。
更多推荐





所有评论(0)