本篇 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 以内为xs480 至 840 vp 为sm840 至 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 bpxs span12 cards4。点击第一张卡的查看按钮预期出现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 LayoutGridRow/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 编译或设备测试。