HarmonyOS趣味相机实战第27篇ArkUI相册Grid筛选、稳定Key与空状态闭环摘要相册列表的难点不在“把数组画出来”而在状态变化后仍然可预测切换“仅水印照片”时计数与卡片要同步删除元素不能复用错卡片筛选无结果和相册本身为空要给出不同引导从详情返回后列表不能跳动数据增长后不能在每个组件中重复过滤。本文基于D:/APP/1quweixiangji的Index.ets围绕visibleAlbum()、albumFilterWatermarkOnly、ArkUIGrid、ForEach键函数、空状态和照片卡片操作展开。我们会把原始状态、派生数据、展示计数和交互出口串成一条闭环并给出适合相册列表的测试与性能检查方法。工程环境项目当前实现开发语言ArkTSUI 框架ArkUI列表容器Grid GridItem列数1fr 1fr列/行间距12 vp原始数据album: CapturedPhoto[]筛选状态albumFilterWatermarkOnly唯一标识photo.id一、先区分原始状态和派生状态相册原始状态只有两类Stateprivatealbum:CapturedPhoto[][];StateprivatealbumFilterWatermarkOnly:booleanfalse;“当前可见照片”可以由这两项计算得到不必再维护第三个可变数组privatevisibleAlbum():CapturedPhoto[]{if(!this.albumFilterWatermarkOnly){returnthis.album;}returnthis.album.filter((photo:CapturedPhoto){returnphoto.watermark?.enabledtrue;});}若同时维护filteredAlbum保存、删除、恢复数据时就必须更新两个数组遗漏一个分支就会让计数和卡片不一致。派生函数让原始数据保持单一事实来源。二、可选链表达旧数据兼容历史照片可能没有watermark字段。筛选条件使用photo.watermark?.enabledtrue它明确表示只有水印对象存在且启用时才进入筛选结果。不要写photo.watermark!.enabled非空断言只消除了编译提示并不能修复旧数据。也不要用photo.watermark ! undefined代替 enabled 判断因为关闭水印拍摄的照片仍可能保存一份enabled: false的快照。三、筛选动作只修改一个布尔状态privatetoggleAlbumFilter():void{this.albumFilterWatermarkOnly!this.albumFilterWatermarkOnly;this.captureStatusTextthis.albumFilterWatermarkOnly?相册仅显示水印照片:相册已显示全部照片;}按钮文字展示“点击后可执行的动作”还是“当前状态”需要团队统一。项目当前用Button(this.albumFilterWatermarkOnly?全部:筛选)即当前在水印模式时显示“全部”告诉用户可切回全部。为了提高可访问性可以增加状态描述或使用带选中态的筛选图标但不能只依赖颜色变化传达状态。四、计数必须来自同一份派生结果顶部显示Text(今日拍摄${this.visibleAlbum().length}张)Grid 也遍历visibleAlbum()因此筛选后数字与卡片数量天然一致。常见错误是计数读取album.length列表却读取过滤数组导致页面显示“今日拍摄 12 张”屏幕上只有 3 张。文案可以更精确privatealbumCountText(visibleCount:number):string{if(this.albumFilterWatermarkOnly){return带水印照片${visibleCount}张;}return本地照片${visibleCount}张;}若createdAt只保存月日时分严格意义上无法可靠判断“今日”文案应改为“本地照片”或持久化时间戳后再做当天筛选。五、避免一次构建中重复过滤当前 Builder 多次调用visibleAlbum()计数一次、空状态一次、Grid 一次。数据上限只有 60 条成本很低若未来扩展到数千张应在一次构建逻辑中复用结果或把派生值放在受控状态管理层。不建议仅为了缓存而新增容易过期的State filteredAlbum。更稳妥的是在数据或筛选变化时显式刷新只读视图privaterefreshAlbumView():void{this.albumViewthis.albumFilterWatermarkOnly?this.album.filter((photo:CapturedPhoto)photo.watermark?.enabledtrue):this.album.slice();}如果数组很小保持纯派生函数反而更简单。优化必须由实际耗时和数据规模驱动。六、Grid列定义要稳定项目使用双列Grid(){// items}.columnsTemplate(1fr 1fr).columnsGap(12).rowsGap(12).width(100%)1fr 1fr会让两列均分剩余宽度配合外层左右 18 vp padding 和 12 vp gap。照片卡片内部应使用明确的宽高比例如Stack(){// thumbnail and overlays}.width(100%).aspectRatio(0.82)固定宽高比能避免标题长度、加载状态或角标出现后改变卡片高度引发列表抖动。七、ForEach必须提供业务稳定Key项目写法ForEach(this.visibleAlbum(),(photo:CapturedPhoto){GridItem(){this.AlbumPhotoCard(photo)}},(photo:CapturedPhoto)this.photoKey(photo))键函数当前为privatephotoKey(photo:CapturedPhoto):string{return${photo.id}-${photo.status}-${photo.createdAt};}Key 决定 ArkUI 是否复用已有节点。没有稳定键或使用数组下标时删除中间一张照片可能让后续卡片复用错误的内部状态。八、Key应稳定还是随状态变化把status和createdAt放进键字段变化时会让 ArkUI 把它视为新节点并重建。若卡片状态很轻这可以确保展示刷新但真正的实体标识通常只需photo.idprivatephotoKey(photo:CapturedPhoto):string{returnphoto.id;}选择标准Key策略优点风险仅id最大化节点复用语义稳定子组件需正确响应属性更新id status状态变化时强制重建编辑状态可能丢失数组下标写法简单插入删除后错位复用整对象序列化看似唯一不稳定、成本高照片 ID 已由时间与序列生成时优先使用id。只有明确需要重置组件内部状态时才把版本字段加入 Key。九、空相册与筛选无结果不是同一种状态项目根据筛选状态显示不同文案if(this.visibleAlbum().length0){Text(this.albumFilterWatermarkOnly?没有带水印照片:还没有保存的照片)Text(this.albumFilterWatermarkOnly?切换为全部可以查看普通照片。:拍一张带水印的照片保存后会出现在这里。)}但两个状态的主操作也应不同相册为空按钮“去拍照”切到拍摄 Tab。筛选无结果但相册有数据按钮“查看全部”关闭筛选。可以写成Button(this.albumFilterWatermarkOnly?查看全部:去拍照).onClick((){if(this.albumFilterWatermarkOnly){this.albumFilterWatermarkOnlyfalse;}else{this.activeTab0;}})空状态的操作必须直接解决当前原因而不是一律跳到拍照页。十、空状态也需要稳定占位项目为空状态设置固定高度Column({space:10}){// title, description, action}.width(100%).height(260).alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)这样从空状态切换到 Grid 时页面顶部结构不会完全塌陷。描述设置maxLines(2)和居中对齐可避免窄屏溢出。注意不要把整个页面区块都做成浮动卡片。空状态是列表内容的一种表现视觉上应与相册区域连续不需要再嵌套多层卡片。十一、删除后状态需要同步收敛删除照片privateasyncdeleteAlbumPhoto(photoId:string):Promisevoid{this.albumawaitPhotoAlbumService.deletePhoto(photoId);if(this.albumPreviewPhotothis.albumPreviewPhoto.idphotoId){this.albumPreviewPhotonull;}this.captureStatusText已从本地相册删除照片;}更新原始album后visibleAlbum()会自动得出新列表。若删除的是当前预览对象还要关闭详情防止页面继续操作已不存在的数据。异步删除期间建议禁用当前卡片操作并在失败时保留卡片。不要在服务确认前先从 UI 数组移除除非实现了失败回滚。十二、详情预览应持有标识还是对象当前状态保存StateprivatealbumPreviewPhoto:CapturedPhoto|nullnull;优点是渲染简单缺点是相册数组更新后详情可能持有旧对象副本。更严格的做法是只保存 IDStateprivatealbumPreviewPhotoId:string;privatecurrentAlbumPreview():CapturedPhoto|undefined{returnthis.album.find((photo:CapturedPhoto)photo.idthis.albumPreviewPhotoId);}对于只读详情保存对象副本通常足够如果详情支持编辑状态ID 派生查询能避免主列表与详情数据分叉。十三、从历史照片恢复拍摄配置项目支持复用水印privatereuseAlbumPhoto(photo:CapturedPhoto):void{this.selectedTemplatephoto.watermark?.template??work;this.watermarkEnabledphoto.watermark?.enabled??true;this.customPlacephoto.watermark?.locationText??;this.customNotephoto.watermark?.note??;this.activeTab0;this.captureStatusText已将模板恢复到拍摄页;}这段代码对旧数据使用默认值并只恢复配置不修改历史照片。交互完成后切换到拍摄页形成“找到照片 - 复用模板 - 再拍一张”的闭环。若水印关闭且快照不存在需要定义默认策略。当前enabled ?? true会让无快照照片恢复为开启水印产品应确认是否符合预期。十四、滚动容器与Grid职责分离项目外层使用Scroll(){Column({space:18}){this.AlbumHeader()this.AlbumSummary()this.AlbumDocumentEntryCard()this.AlbumGridOrEmpty()}}.scrollable(ScrollDirection.Vertical).scrollBar(BarState.Off)Grid 作为内容参与外层整体滚动适合只有几十条的本地相册。如果数据扩展到数千条应评估懒加载容器避免一次构建全部卡片。不要同时让外层 Scroll 和内层 Grid 各自垂直滚动否则手势竞争和高度测量会变得复杂。十五、排序必须在数据层有明确定义PhotoAlbumService.persistPhoto()把新照片放到数组首部因此页面自然按最新优先展示。不要在每次 Builder 执行时原地调用this.album.sort(...)sort()会修改状态数组可能在构建阶段触发不可预测行为。需要排序时使用副本returnthis.album.slice().sort((a:CapturedPhoto,b:CapturedPhoto)b.createdAtTimestamp-a.createdAtTimestamp);当前createdAt是显示字符串不适合可靠排序。建议同时持久化数值时间戳把格式化留给 UI。十六、不要把真实图片存进Preferences列表Grid 卡片需要缩略图但 Preferences 适合存元数据不适合存 PixelMap 或 Base64。扩展真实相册时可让CapturedPhoto保存interfaceCapturedPhotoAsset{id:string;mediaUri:string;thumbnailUri?:string;width:number;height:number;}卡片按 URI 异步加载缩略图详情再加载原图。列表项要提供固定比例占位和失败状态避免图片加载改变 Grid 尺寸。十七、可访问性和大字体检查相册卡片往往同时放图片、角标、标题和操作。发布前检查筛选状态不只通过深浅颜色表达。“”按钮有可理解的无障碍描述。触控目标至少保持稳定尺寸。标题和地点最多两行并有省略。系统字体放大后卡片操作不会重叠。删除操作与普通点击有足够区分并需确认。对纯符号按钮界面可保持简洁但应提供 accessibilityText 或等价语义属性。十八、测试矩阵场景可见数量空状态操作相册为空、全部模式0还没有保存的照片去拍照相册为空、水印模式0没有带水印照片查看全部5张中2张有水印2不显示显示两张筛选时删除最后一张水印图0没有带水印照片查看全部删除中间卡片n-1视结果而定Key不串卡旧照片无watermark不进入水印结果不崩溃可查看全部恢复历史模板数量不变不变跳拍摄页连续保存61张最多60不显示最新优先键稳定性测试可以记录每个卡片子组件的照片 ID删除第二项后确认第三项仍绑定原 ID而不是继承第二项的内部状态。十九、性能观测点数据量增加时优先测量visibleAlbum()每次执行耗时。筛选开关到首帧更新的耗时。Grid 首屏创建的卡片数量。缩略图解码峰值内存。删除一项后实际重建的节点数量。不要先用复杂缓存掩盖问题。若瓶颈来自原图解码缓存过滤数组不会有效应先生成缩略图并使用懒加载。二十、发布前验收清单原始相册与筛选布尔值是单一事实来源。筛选条件兼容无水印字段的旧数据。计数、空状态和 Grid 使用同一派生结果。ForEach 使用稳定业务 ID不使用数组下标。空相册与筛选无结果提供不同操作。删除当前详情照片后会关闭详情。异步删除失败时不会误报成功。Grid 卡片有稳定比例和加载占位。Preferences 只保存元数据不保存图片 Base64。窄屏、大字体和长文案下无重叠。总结一个可靠的 ArkUI 相册列表需要先控制状态数量album保存原始记录筛选开关保存用户意图visibleAlbum()负责派生展示结果。计数、空状态和 Grid 都消费同一结果才能避免页面自相矛盾稳定业务 Key 则保证插入、删除和筛选后组件不会错位复用。在此基础上再区分“相册为空”和“筛选无结果”的操作处理详情引用、异步删除、缩略图加载与大字体布局相册才能从演示用双列网格变成可长期维护的本地内容入口。