HarmonyOS 7 ArkUI ContainerReader × Grid:折叠分栏图墙按容器宽度重排与选中态保持【鸿蒙心迹】
一、不是折叠屏变宽了,图墙就该多一列
图集工作台在手机上展示两列照片,展开折叠屏后显示三列,看起来是一个简单的响应式需求。真正容易被忽略的是:展开设备不等于图墙所在容器变宽。有的页面在左边保留筛选栏,右侧才是图片区域;有的页面打开详情抽屉,图墙甚至比折叠前更窄。继续依赖整个窗口宽度来算列数,会出现卡片被挤压、选中角标错位、右边缘被裁掉等问题。这里讨论的不是屏幕方向识别,而是容器内的局部断点。
示例项目命名为 FoldGallery,演示任务编号为 FG-101。图墙中有 P-101 到 P-112 的十二条模拟素材记录,其中 P-108 默认被选中。示意状态固定为容器宽度 512vp、2 列、已选 1 张。上述数值用于让文章和配图可对照,并非在任何真机上测得的性能或尺寸。

这次技术取舍是把“如何排列”与“选中了谁”拆开。布局只读取当前容器宽度或断点,业务选择只记录稳定素材 ID;两者不能以当前行列索引建立关联。旧项目最常见的错误是保存第八个可见卡片而不是保存 P-108,筛选或排序后第八个位置可能已变成另一张照片。
二、确认平台能力和工程边界
华为官方《容器断点(ContainerReader)》说明,该组件从 API 26.0.0 起提供,能够通过双向绑定的尺寸和断点状态观察自身容器,而不是推断设备物理屏幕宽度。它的 size 和 widthBreakpoint 都要求通过状态变量绑定,并使用 !! 建立双向绑定;父容器必须提供可确定的布局空间。多窗口和多形态开发指南同时强调要针对内容实际可用区域调整布局。本文只针对目标 SDK 支持该组件的环境,旧版本需要使用官方支持的其他布局响应方案,不能把此代码直接复制到所有项目。
先定义业务数据,不借助真实图库读取权限。每张素材至少有一个稳定 id,标题和背景颜色只是界面演示数据。图像缩略图可以在接入时再通过应用可访问的来源取得,本文不会冒充图库访问已完成。因为重点是断点变化,展示层采用纯色素材卡片,让状态变化比素材细节更清楚。
下面这一段解决容器测量与网格分栏的问题,保留官方示例采用的状态变量和双向绑定结构。这里用默认宽度断点映射:XS 一列、SM 两列、MD 三列、其他宽度四列,不把 512vp 当作系统写死的分界值。
import { ContainerReader, Size } from '@kit.ArkUI';
@Entry
@Component
struct GalleryPage {
@State containerSize: Size = { width: 0, height: 0 };
@State widthBp: WidthBreakpoint = WidthBreakpoint.WIDTH_MD;
@State selectedIds: string[] = ['P-108'];
private photos: string[] = ['P-101', 'P-102', 'P-103', 'P-104',
'P-105', 'P-106', 'P-107', 'P-108', 'P-109', 'P-110',
'P-111', 'P-112'];
columns(): string {
if (this.widthBp === WidthBreakpoint.WIDTH_XS) return '1fr';
if (this.widthBp === WidthBreakpoint.WIDTH_SM) return '1fr 1fr';
if (this.widthBp === WidthBreakpoint.WIDTH_MD) return '1fr 1fr 1fr';
return '1fr 1fr 1fr 1fr';
}
代码片段属于 GalleryPage 组件的前半部分,后面的 UI 和方法与它放在同一结构体中。Size 存的是容器测量结果,widthBp 是实际布局断点,两者是只供视图决策的信号。这里没有通过修改 containerSize 去控制宽度,因为双向绑定回传的是组件测量值,不是设置组件宽度的指令。若父级尺寸尚未确定,状态初值仍然为零,调试日志也不应把零当成真实窄屏。
三、稳定 ID 先于布局索引
为了验证展开、收起和筛选操作不会破坏选择结果,选中集必须与卡片位置脱钩。Grid 的重新排列可以让一个项目从第二行移到第三行,但点击这个项目时传递的依旧是它的唯一 ID。不要将 index 存为业务凭证;后续接入异步图片列表后,索引迁移更加频繁。
下面是选择状态操作。它解决重复点击时的切换,以及从已选集中移除素材的问题。这里使用新数组赋值,让状态的变化具有明确触发点,也便于在日志里对比前后值。
toggleSelected(photoId: string): void {
const exists: boolean = this.selectedIds.includes(photoId);
this.selectedIds = exists
? this.selectedIds.filter((id: string) => id !== photoId)
: [...this.selectedIds, photoId];
console.info(`FG-101 selected=${this.selectedIds.join(',')}`);
}
isSelected(photoId: string): boolean {
return this.selectedIds.includes(photoId);
}
这里真正重要的变量是 selectedIds 而不是显示序号。由于数据规模只有十二条,数组成员查询的开销可以接受;大规模图库可将选择 ID 放到业务层的集合模型,并维护稳定顺序的列表映射。console.info 为调试示例,实际发布时应根据日志管理要求收敛内容,尤其避免输出用户图片路径等敏感字段。
四、让单个图墙自己观察空间
主界面还要有筛选侧栏、页头和底部操作按钮。因此不能只把 ContainerReader 放在整个页面外层;它需要包住可变宽度的图墙区域。右侧视图的 Grid 根据断点切换 columnsTemplate,但不会更改 photos 和 selectedIds。固定宽度侧栏会由父级优先分配空间,然后剩余区域才交给容器测量。
下面这段是组件的渲染部分,使用 GridItem 显示素材 ID、选中标记和点击区域。为聚焦逻辑,真实项目中的图片解码、焦点态与无障碍说明需在界面完成后再补齐。
build() {
Row() {
Column() { Text('全部素材'); Text('已选') }.width(108)
ContainerReader({
size: this.containerSize!!,
widthBreakpoint: this.widthBp!!
}) {
Grid() {
ForEach(this.photos, (id: string) => {
GridItem() {
Column() {
Text(id).fontSize(16)
Text(this.isSelected(id) ? '✓ 已选中' : '点击选择')
.fontSize(12)
}
.height(108).width('100%')
.backgroundColor(this.isSelected(id) ? '#DDF4FF' : '#EEF2F8')
.onClick(() => this.toggleSelected(id))
}
}, (id: string) => id)
}
.columnsTemplate(this.columns())
.columnsGap(8).rowsGap(8)
.width('100%').height('100%')
}
.layoutWeight(1)
}
.height(380).width('100%')
}
}
ContainerReader 的尺寸来自外层 Row 的剩余宽度;在这份演示数据中,图片卡片数量固定,重排的是网格而非数据。代码里的 ForEach 提供显式 key,能降低重建节点时把错误状态带给另一条记录的风险。不要为容器额外强制设置与父容器矛盾的固定宽度,否则看到的断点只是人为设定的宽度,并不能反映窗口的实际变化。

图中的开发界面、示例代码和 HiLog 都是演示绘制,不是真实 DevEco Studio 截屏。为了让核对方便,日志设计为 FG-101 width=512 columns=2 selected=P-108。它说明的是想要观察的三个变量,并不证明模拟器已经按该顺序调用回调。实际接入时,需要在界面稳定后记录尺寸和断点,观察侧栏展开、抽屉展开、旋转和分屏四种操作。
五、把“看起来正确”变成可检查的状态
建议把调试步骤拆成两轮。第一轮保持同一个数据源,改变容器可用宽度:记录 containerSize.width、widthBp、columns() 的结果,确保每次列数变化都能解释。第二轮保持同一个选中 ID,做筛选、插入与删除:比较 selectedIds 中的 ID 是否仍然一致,检查被删素材是否需要显式从选中集中移除。前者属于布局,后者属于业务数据生命周期,两者不能混为一谈。
还有一个边界问题:折叠屏展开时整个窗口变宽,但右侧详情板也可能同时占去空间。此时页面级媒体查询会说“宽屏”,ContainerReader 观察到的图墙却可能只剩 512vp。本例示意图为两列并保持 P-108 被选中,符合局部测量优先于窗口猜测的原则。

这张 9:16 手机运行图展示的是专门绘制的预期 UI 状态,页头任务 FG-101、容器宽度 512vp、列数 2、选中 P-108 与上文保持一致。它不是经过真实设备验证的运行结果;真机显示宽度和实际可用区域取决于父容器、字体缩放以及设备窗口环境。
六、几条值得提前写入代码评审的约束
第一,切换列数不应顺带清空业务状态;如果某个卡片因为节点复用被高亮到其他 ID,应优先检查稳定 key 和状态绑定,而不是在布局回调里重置选择集。第二,实际图片展示常有异步缩略图请求:加载结果回来之前应该重新检查照片 ID,避免在重排时将旧缩略图绘制到新位置。
第三,局部断点既适用于折叠屏,也适用于应用内分栏、悬浮窗口和桌面多窗口;但是 ContainerReader 的支持版本需要单独核对,不能把它当作所有旧机型的通用组件。第四,建议把资源释放和任务取消放在图片服务层,而非写在 GridItem 的显示与隐藏事件中,避免滚动引发反复销毁。
这篇方案最后保留两个清晰边界:它没有接入真实系统相册,也没有宣称任何帧率或时延指标。能够从代码审查判断的是选中态以稳定 ID 表示、列数由容器断点决定;至于真机布局是否吻合,还需要在相应 API 环境下编译、调试和验证。实际做项目时,明确这两层责任比只看一张精美的适配图更重要。
参考:华为官方《容器断点(ContainerReader)》https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-layout-development-container-reader;多设备适配指南 https://developer.huawei.com/consumer/cn/multidevice/adaptive-apps/ 。
更多推荐





所有评论(0)