HarmonyOS趣味相机实战第26篇ArkUI拍照结果预览、PixelMap释放与操作分流摘要相机调用capture()成功只是拍照流程的中点。用户还需要看结果、核对水印、决定重拍、保存到相册或转成图片文档。这个短暂的结果页同时持有大体积 PixelMap、照片元数据和多个异步操作如果关闭与保存都能重复触发很容易出现资源未释放、同一照片重复落盘、页面跳转错乱或空白预览。本文基于D:/APP/1quweixiangji的Index.ets从状态建模、Stack图层合成、空资源占位、文字溢出、操作分流、PixelMap 所有权和防重入几个方面复盘拍照结果预览。最终目标是形成一个明确状态机一张预览图只有一个拥有者每条退出路径只提交一次副作用并且都能回收图像资源。工程环境与涉及对象项目配置开发语言ArkTSUIArkUI 声明式组件图像对象kit.ImageKitPixelMap照片元数据CapturedPhoto本地相册PhotoAlbumService图片文档PhotoDocumentService结果弹层ResultPreviewDialog预览图高度250 vp一、结果页不是一个普通弹窗它承担四类职责展示本次真实拍摄的 PixelMap。展示与拍摄时一致的水印快照。提供重拍、转文档、保存三条互斥出口。释放不再使用的图像资源并更新主页面状态。因此不要把所有逻辑直接写进三个按钮的onClick。页面只表达交互资源和数据操作放在具名方法里更容易审查每条路径是否完整。二、用两个状态表达图像与元数据项目保留StateprivatepreviewPhoto:CapturedPhoto|nullnull;StateprivatepreviewPhotoPixelMap:image.PixelMap|nullnull;两者含义不同previewPhoto是轻量元数据控制弹层是否出现。previewPhotoPixelMap是需要显式释放的原生图像资源。拍照成功后的赋值顺序应让 UI 不观察到不完整状态this.previewPhotoPixelMapcaptureState.photoPixelMap;this.previewShotIndex1;this.previewPhotoPhotoAlbumService.createPhoto(this.previewShotIndex,[],无滤镜,无相框,标准模式,real,${captureState.message}·${this.resolutionLabel()},0,标准,0,this.resolutionLabel(),this.watermarkSnapshot());先准备 PixelMap再让previewPhoto触发弹层可以减少第一帧只出现占位背景的概率。三、弹层显示条件必须唯一页面根Stack根据状态挂载结果预览build(){Stack(){this.AppTabs()if(this.previewPhoto){this.ResultPreviewDialog()}}}previewPhoto ! null是唯一显示条件。关闭时只要先把该状态清空弹层就会离开组件树。不要同时维护showDialog与previewPhoto两个布尔/对象状态否则可能出现“弹层已隐藏但资源仍被持有”或“弹层显示却没有照片”的非法组合。如果业务确实需要加载态可改为显式联合状态typePreviewPhaseidle|loading|ready|submitting;状态越清晰按钮禁用与资源释放越容易对应。四、用Stack分离底图与语义覆盖层结果图采用StackStack(){if(this.previewPhotoPixelMap!null){Image(this.previewPhotoPixelMap).width(100%).height(250).objectFit(ImageFit.Cover).borderRadius(22)}else{Column().width(100%).height(250).backgroundColor(#1B1B24).borderRadius(22)}this.ResultStatusOverlay()this.ResultWatermarkOverlay()}.width(100%).height(250).clip(true)底图、状态文案和水印是三个独立图层。这样的好处是PixelMap 不可用时仍有稳定尺寸水印文本不参与图像旋转或镜像所有子层受统一裁剪不会越过圆角区域。五、固定尺寸避免异步资源造成跳动图像加载前后都设置 250 vp 高度外层也固定 250 vp。若占位只写背景色而不写高度PixelMap 到达时弹层会突然增高三个操作按钮整体下移。稳定布局至少需要外层 Stack 高度 图片高度 占位高度 图片宽度 100% objectFit 策略明确 clip 在统一父容器执行对于拍照结果ImageFit.Cover能填满区域但会裁剪边缘。若用户需要核对完整构图应改为Contain并为留白提供中性色背景。选择 Cover 还是 Contain 是产品决策不能依赖默认值。六、水印使用拍摄快照而不是当前输入框项目只在拍摄时记录WatermarkSnapshot结果页读取照片上的快照if(this.watermarkEnabledthis.previewPhoto?.watermark){Column(){Blank()Row(){Column({space:2}){Text(this.previewPhoto.watermark.title)Text(this.previewPhoto.watermark.locationText)}.layoutWeight(1)Column({space:2}){Text(this.previewPhoto.watermark.timeText)Text(this.previewPhoto.watermark.note)}}}}如果结果页直接绑定customPlace或currentTimeText用户在异步等待期间修改模板预览文案可能与照片记录不一致。以previewPhoto.watermark为准能保证“看到的结果”和“最终保存的元数据”来自同一拍摄时刻。条件也应优先读取快照自身constwatermarkthis.previewPhoto?.watermark;if(watermark?.enabledtrue){// render snapshot}这样当前页面总开关变化不会影响已经拍下的结果。七、长文案必须有溢出策略地点和备注来自用户输入长度不可预测。状态摘要已经使用Text(this.previewPhoto?.captureSummary??this.captureStatusText).fontSize(11).fontColor(#FFFFFF).maxLines(1).textOverflow({overflow:TextOverflow.Ellipsis})水印区域也应设置边界Text(watermark.note).fontSize(11).fontColor(#E9F1ED).maxLines(2).textOverflow({overflow:TextOverflow.Ellipsis})不能只限制字体大小而不限制布局。极长连续字符可能挤压时间列推荐给左右列明确layoutWeight并对文字设置最大行数。八、PixelMap所有权必须写进代码结构项目集中释放预览图privateasyncreleasePreviewPixelMap():Promisevoid{constpixelMap:image.PixelMap|nullthis.previewPhotoPixelMap;this.previewPhotoPixelMapnull;if(pixelMap!null){try{awaitpixelMap.release();}catch(_){}}}先把成员设为null再await release()有两个作用UI 立即失去对旧对象的引用。第二次进入释放方法时不会重复释放同一对象。这相当于一次所有权转移局部变量临时接管释放责任成员状态不再拥有该资源。九、关闭路径必须同时清理两类状态重拍与右上角关闭都调用同一方法privateasyncclosePreview():Promisevoid{this.previewPhotonull;awaitthis.releasePreviewPixelMap();}不要在两个按钮中分别写一份清理代码。重复代码很容易让一个路径只清理元数据、另一个路径只释放图片。页面销毁时也要兜底asyncaboutToDisappear():Promisevoid{awaitthis.closePreview();awaitCameraPreviewService.release();}具体生命周期签名按工程 API 版本调整但原则是离开页面后不能继续持有大图资源。十、保存到相册的提交顺序项目保存流程privateasyncsavePreviewPhoto():Promisevoid{if(!this.previewPhoto){return;}this.albumawaitPhotoAlbumService.persistPhoto(this.previewPhoto);this.previewPhotonull;awaitthis.releasePreviewPixelMap();this.activeTab1;this.captureStatusText照片已保存到本地相册;}顺序体现了提交语义先确认存在照片再等待持久化成功后清理临时状态最后跳转相册并更新提示。若持久化抛错预览仍保留用户可以重试不会发生数据没保存但弹层已经消失。十一、转文档是另一条互斥出口privateasyncconvertPreviewPhotoToDocument():Promisevoid{if(!this.previewPhoto){return;}this.documentsawaitPhotoDocumentService.convertPhoto(this.previewPhoto);this.previewPhotonull;awaitthis.releasePreviewPixelMap();this.activeTab2;this.captureStatusText已生成照片文档;}它与保存相册共享清理步骤但业务副作用不同。可以抽取只处理退出的私有方法不能把两个服务调用粗暴合并privateasyncfinishPreview(targetTab:number):Promisevoid{this.previewPhotonull;awaitthis.releasePreviewPixelMap();this.activeTabtargetTab;}服务调用成功后再调用finishPreview错误时保持当前预览。十二、按钮必须防止异步重复提交用户快速连点“保存到相册”两次两个异步函数都可能在第一轮清空状态前读取到同一个previewPhoto。需要提交锁StateprivatepreviewSubmitting:booleanfalse;privateasyncsavePreviewPhoto():Promisevoid{if(!this.previewPhoto||this.previewSubmitting){return;}this.previewSubmittingtrue;try{this.albumawaitPhotoAlbumService.persistPhoto(this.previewPhoto);awaitthis.finishPreview(1);this.captureStatusText照片已保存到本地相册;}catch(error){this.captureStatusText保存失败请重试;}finally{this.previewSubmittingfalse;}}三个操作按钮在提交期间都应禁用避免保存与转文档同时发生Button(保存到相册).enabled(!this.previewSubmitting).onClick(()this.savePreviewPhoto())十三、不要让按钮文字挤坏三列布局项目使用Row({ space: 10 })与三个layoutWeight(1)按钮。三列在窄屏上可用但要检查较大字体和本地化文本。可选策略主操作“保存”占两份权重次操作使用图标或短文本。小屏改为两行布局第一行两个次操作第二行主操作。按钮固定最小高度不用文字撑开。使用系统字体放大 1.3 倍做真机检查。按钮视觉层级应与风险对应保存使用主色重拍与转文档使用次色。关闭按钮不能和保存按钮同等突出。十四、错误反馈要留在当前上下文持久化失败时不应直接关闭弹层。推荐保留照片并显示可重试提示catch(error){hilog.error(DOMAIN,TAG,persist preview failed: %{public}s,JSON.stringify(error));this.captureStatusText保存失败请稍后重试;}日志记录错误类型不记录水印备注、地点或 PixelMap 内容。用户看到的是可执行建议开发者看到的是诊断信息两者职责分开。十五、状态机比零散布尔值更容易验证结果预览可以画成以下状态idle - capturing - ready - discarding - idle - saving - album - converting - documents - failed - ready每次只允许一个转换。特别是saving与converting不能并发discarding后旧异步任务不能再把状态写回页面。若页面复杂度继续增长可以把 phase、photo 和 pixelMap 包进一个受控模型而不是增加多个互相依赖的布尔变量。十六、资源与数据的测试矩阵场景元数据PixelMap页面结果拍照成功创建 preview持有一次显示弹层右上角关闭清空release 一次回拍摄页点击重拍清空release 一次回拍摄页保存成功写入 saved 后清空release 一次切相册保存失败保留保留显示重试转文档成功创建文档后清空release 一次切文档页转文档失败保留保留显示重试连点保存只提交一次release 一次无重复记录页面退出清空release 一次无资源泄漏可以为释放器注入计数代理验证每个 PixelMap 最多释放一次服务层用 fake 实现控制成功、失败和延迟。十七、UI验收清单PixelMap 到达前后弹层尺寸不跳动。Cover 裁剪符合产品预期关键内容不被误裁。水印读取拍摄时快照不跟随当前模板变化。地点、备注和摘要超长时不会覆盖按钮。三个出口在提交期间互斥且不可重复点击。保存失败或转文档失败时预览仍可重试。关闭、重拍、保存、转文档都会进入统一清理流程。页面退出有 PixelMap 释放兜底。日志不包含图片、地点和备注等用户内容。大字体与窄屏下按钮仍可识别和点击。十八、常见问题定位现象高概率原因修复点连拍后内存持续上涨旧 PixelMap 未释放统一 release 方法点击两次出现两条照片异步提交未加锁previewSubmitting保存失败后照片消失清理发生在 await 之前成功后再 finish水印内容突然变化结果页读取当前输入状态读取 WatermarkSnapshot空白图导致布局塌陷占位没有稳定尺寸Stack/Image/占位同高文字越过图片边界缺少 clip 或 maxLines父容器裁剪与溢出策略总结拍照结果页的核心不是把 PixelMap 放进Image而是把资源所有权和用户决策建模清楚。previewPhoto控制弹层存在previewPhotoPixelMap由统一释放器管理水印来自拍摄快照保存、转文档和重拍是互斥状态转换任何失败都保留可重试上下文。当每条出口都能回答“数据是否已提交、PixelMap 由谁释放、页面跳到哪里、重复点击如何处理”结果预览就不再是容易泄漏的临时弹窗而是相机主链路中可测试、可维护的一段事务流程。