图库在折叠屏上遇到一个很容易被忽略的矛盾:宽度变了,卡片数量和位置当然要变,但用户刚选中的照片不该跟着丢。用两列和四列演示都不难,麻烦的是下一次状态更新究竟以什么为身份依据。如果把列表下标当作唯一身份,筛选、排序、重排会让“第六格”不再对应原来的那张照片。

这次只解决一个范围明确的问题:图库原有十二条本地演示记录,选择 PIC-006;容器从预设的 392vp 切换到 740vp,列数从两列改为四列,选中数量仍为一,状态记为 PRESERVED。这是 FoldGallery 项目的布局模型,不宣称已经在某款折叠屏真机测得窗口宽度。

一、先把宽度变化和用户选择分成两件事

不少示例会在屏幕尺寸变化时重新创建整个图片数组,然后把选中状态写在每张卡片对象内部。这样一来,刷新数据源或创建新数组就容易把局部状态冲掉。我更倾向于让图片记录只保存图片的稳定业务编号、标题和预览资源,选择属于页面层的一项独立状态。布局层关注列数;选择层只关注编号,二者不互相修改。

这里特别要区分“列数换了”和“数据顺序换了”。前者可以不改变原始数组;后者可能改变数组下标。因此 selectedIndex = 5 只在演示最初的顺序里碰巧成立。实际业务还可能插入云端同步来的图片、删除重复照片、按拍摄时间重排。只要身份是 PIC-006,选中映射才有可追踪的依据。

测试素材固定为十二个编号:PIC-001 到 PIC-012。图片预览可以是本地资源,这里不引入图片解码、媒体权限、索引扫描等其他问题。它们涉及额外生命周期,反而会干扰我们判断真正的选中态问题。项目结构只需要 pages/GalleryPage.ets 与可供复用的 model/GalleryItem.ets。图中的开发目录和运行页也是这套预设信息的演示,不是 IDE 已编译的证据。

二、先为数据定义稳定键,再谈页面重绘

第一段代码处理身份问题。id 是业务主键,而不是从当前位置推导出的下标;预览图使用项目资源,既不虚构系统相册返回值,也不依赖运行时访问外部图片。对真正的相册应用,图片媒体标识的持久性还需要按照媒体库文档另行确认,不能把这个 Demo 的静态编号直接当作跨设备永久 ID。

interface GalleryItem {
  id: string;
  title: string;
  preview: Resource;
}

const DEMO_ITEMS: GalleryItem[] = [
  { id: 'PIC-001', title: '湖边晨光', preview: $r('app.media.pic01') },
  { id: 'PIC-002', title: '城市建筑', preview: $r('app.media.pic02') },
  { id: 'PIC-003', title: '群山湖泊', preview: $r('app.media.pic03') },
  { id: 'PIC-004', title: '林间小路', preview: $r('app.media.pic04') },
  { id: 'PIC-005', title: '街道夜景', preview: $r('app.media.pic05') },
  { id: 'PIC-006', title: '海岛风光', preview: $r('app.media.pic06') },
  { id: 'PIC-007', title: '城市远景', preview: $r('app.media.pic07') },
  { id: 'PIC-008', title: '旧城夕阳', preview: $r('app.media.pic08') },
  { id: 'PIC-009', title: '湖面薄雾', preview: $r('app.media.pic09') },
  { id: 'PIC-010', title: '岛屿剪影', preview: $r('app.media.pic10') },
  { id: 'PIC-011', title: '河岸黄昏', preview: $r('app.media.pic11') },
  { id: 'PIC-012', title: '山城灯光', preview: $r('app.media.pic12') }
];

这一层没有任何宽度判断。将来要把卡片顺序改成时间倒序,也应该在排序逻辑中重排数组,不应重置 selectedId。如果本地资源不存在,构建阶段就应当暴露资源引用问题;不能靠在文章里写“已加载成功”来替代资源检查。本例图中照片只是绘制出的 UI 内容,不对应真实的 app.media.picXX 文件。

三、让响应式列数交给 GridRow,状态由业务编号持有

华为官方的响应式栅格指南说明,GridRow/GridCol 会按断点自动调整布局;GridRow 的列数可以针对不同断点配置。这里明确将小、中等宽度映射为两列,将较宽容器映射为四列,而不是写死所有设备只看一次宽度。另一个官方性能规则指出,ForEach 需要提供稳定的 keyGenerator,这正好是我们要锁住的身份边界。

@Entry
@Component
struct GalleryPage {
  @State private selectedId: string = 'PIC-006';
  @State private items: GalleryItem[] = DEMO_ITEMS;

  build() {
    Column({ space: 12 }) {
      Text('FoldGallery 折叠图库').fontSize(22)
      Text(`选中:${this.selectedId}`)
      GridRow({ columns: { xs: 2, sm: 2, md: 4, lg: 4 } }) {
        ForEach(this.items, (item: GalleryItem) => {
          GridCol({ span: 1 }) {
            Column({ space: 6 }) {
              Image(item.preview).width('100%').height(100)
              Text(item.id)
              Text(item.title).fontSize(12)
            }
            .border({ width: this.selectedId === item.id ? 2 : 0,
              color: '#078768' })
            .onClick(() => { this.selectedId = item.id; })
          }
        }, (item: GalleryItem) => item.id)
      }
      .width('100%')
    }
    .padding(12)
  }
}

代码中的 ForEach 第三个参数明确返回 item.id,这比“索引加上标题”稳定。GridCol 的 span 固定为一,是把当前栅格列宽交给容器管理;不是把一个图片卡片硬塞进整行。Image 的高度只是演示排版参数,真实图库应结合裁剪策略、缩略图缓存和无障碍文字另做处理。列表很大时,要再评估懒加载与组件复用,这里十二条静态数据不会得出任何性能结论。

要注意 GridRow 的断点以实际布局环境为准。假如窗口外层还有侧边栏,容器分配宽度可能小于窗口宽度;不要把模拟数据 740vp 当作所有宽屏的唯一真值。正式项目应该明确宽度来源,并把断点逻辑与业务状态解耦。ForEach 的键稳定不代表组件所有临时编辑状态都会自动跨生命周期保留,复杂表单仍需业务模型接管。

四、把重排过程写成可检查的状态契约

这个 Demo 不冒充系统折叠事件,而是以固定输入演练一次前后对照:初始容器宽度 392vp、两列;当前选中 PIC-006;模拟展开后宽度 740vp、四列;选中 PIC-006;可选数量始终为一。图中 FG-707 是演示任务号。我们关心的是重新布局之后的身份是否保留,不把界面上看见的顺序当成身份本身。

如果要在日志里表达这个过程,建议先确定一组固定字段。以下不是系统框架提供的事件 API,而是可以接到工程日志模块里的应用自定义诊断函数。输出给调试页面之前还应避免写入真实用户相册路径和定位信息。

function describeLayout(widthVp: number, selectedId: string): string {
  const cols: number = widthVp >= 600 ? 4 : 2;
  return `FG-707 width=${widthVp}vp cols=${cols} selected=${selectedId}`;
}

// 由模型驱动的示例轨迹,不是来自真实折叠事件回调
const before: string = describeLayout(392, 'PIC-006');
const after: string = describeLayout(740, 'PIC-006');
console.info(`[FoldGallery] ${before}`);
console.info(`[FoldGallery] ${after}`);
console.info('[FoldGallery] FG-707 selection=PRESERVED');

这一段故意采用最普通的数字比较,只用来生成文中约定的模型轨迹,绝不能把 600 理解为官方统一的折叠阈值。产品实际使用 GridRow 响应式行为时应以组件的当前断点和容器空间为准。诊断轨迹应在状态确实更新后记录,不应因用户点了“展开演示”按钮就提前宣布恢复成功。

五、数据不丢,还要防止边界误判

用户取消选中后怎么办?这里的选择状态应被设置为空串或显式的未选状态,不应通过更换列数间接取消。被选照片从数据集中删除时,也不应该把旧编号偷偷映射到占据相同位置的新照片。可以在新数据写入后核对是否仍存在 selectedId,若不存在就提示“所选照片已移除”,让用户自行选择,而不是静默选中相邻项。

异步拉取新的列表也有类似风险。先进入页面的旧请求可能在新的筛选请求之后完成,覆盖更新后的列表。正确做法是为数据请求分配代次或可取消标记,只有最新请求可以刷新画面。页面离开时需要停止继续修改已失效页面的状态;真实图片对象或文件句柄若由业务层创建,还需要在其所有者确定后释放。本轮只保存 Resource 引用,不负责创建 PixelMap,不能凭空声称进行了图片内存释放。

还有一个经常被忽略的边界:保留选中和保留滚动位置是两种验收项。本文只保证选中主键的逻辑一致,不承诺变化后那张照片一定出现在同一屏幕位置。如果需要精确的可见锚点恢复,应另做 Scroller 与条目测量策略,不能把本例伪装成已解决滚动问题。这也避免与之前书架滚动恢复主题重复。

六、验证口径和可以继续扩展的地方

这轮图示显示的四列图片、PIC-006 选中边框、FG-707、PRESERVED、十二条素材,以及 392vp → 740vp 全部采用统一模型数据。它证明的是文稿与示意素材遵循同一契约,并不证明系统通知顺序、UI 真正重绘耗时或华为设备上的窗口折叠体验。发布前至少要测:两列到四列、四列回两列、选中后排序、删除选中项、页面切后台再回来、异步数据晚到六种情形。

我会把这个案例看作“稳定键在布局变化中的价值”而非折叠屏全适配解决方案。工程上最有价值的取舍,是让数据标识负责身份、GridRow 负责排版、页面状态负责选中。三者职责不混在一起,才能在后续加筛选、同步和图片加载时定位错误,而不是一出问题就重建整个页面。

官方依据:华为《响应式栅格布局 GridRow/GridCol》(2026-09-08):https://developer.huawei.com/consumer/en/doc/harmonyos-guides/arkts-layout-development-grid-layout ;华为《@performance/foreach-args-check》(2026-01-27):https://developer.huawei.com/consumer/en/doc/harmonyos-guides/ide-foreach-args-check 。上述页面用于核对组件定位与键生成原则,未替代实际工程编译和设备验收。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐