当前位置: 首页 > news >正文

第76篇 | HarmonyOS 保险箱详情页:私密照片如何浏览、恢复和导出

第76篇 | HarmonyOS 保险箱详情页:私密照片如何浏览、恢复和导出

第 76 篇讲保险箱详情页。私密照片解锁后不能只显示一个列表,用户还需要像普通相册一样查看前后镜头、滑动浏览、恢复公开相册、导出到系统相册或再次锁定。区别在于这些动作都必须在保险箱上下文里完成。

这一篇从详情查看器、选中记录、私密帧列表、恢复/导出按钮和锁定按钮五个角度拆代码。重点是理解:保险箱复用了普通相册的数据和预览能力,但它在入口、状态和操作上加了更严格的边界。

本篇目标

  • 理解保险箱详情页为什么要先检查vaultUnlocked
  • 掌握getFeaturedVaultRecordgetFeaturedVaultFrames如何支撑预览。
  • 理解私密照片恢复、导出、系统分享和锁定按钮的状态条件。
  • 学会让详情查看器复用普通相册帧数据,同时保留保险箱边界。

对应源码位置

  • superImage/entry/src/main/ets/pages/Index.ets

解锁后才进入私密照片详情

保险箱详情页的体验和普通相册相似:全屏黑底、可滑动查看前后镜头、顶部返回和页码、底部标题地点信息。但进入这个页面之前,用户必须先完成本地认证。

这种设计减少了重复开发。普通相册已经有前后镜头帧的组织方式,保险箱详情只需要在入口处做解锁校验,在页面表现上继续复用图像预览能力。隐私边界放在状态和入口上,而不是重写一套图片查看逻辑。

保险箱详情页在解锁后展示私密照片浏览能力

打开详情前先检查解锁状态

openVaultRecordViewer的第一行就是if (!this.vaultUnlocked) return。这行代码把入口挡在最前面:不管哪个 UI 元素误触发了打开详情,只要保险箱未解锁,就不会显示私密照片。

getFeaturedVaultRecord通过选中 id 找当前记录,找不到时回退到第一条私密记录;getFeaturedVaultFrames复用普通相册的getGalleryDetailFrames。这样保险箱既有自己的入口控制,又不用重复维护前后镜头帧构造逻辑。

openVaultRecordViewer 在未解锁时直接返回

private getFeaturedVaultRecord(): GalleryMoment | undefined { const vaultRecords = this.getVaultRecords(); const selected = vaultRecords.find((record: GalleryMoment) => record.id === this.vaultSelectedId); return selected ?? vaultRecords[0]; } private selectVaultRecord(recordId: string): void { this.vaultSelectedId = recordId; } private openVaultRecordViewer(recordId: string): void { if (!this.vaultUnlocked) { return; } this.vaultSelectedId = recordId; this.vaultDetailPhotoIndex = 0; this.vaultDetailVisible = true; } private closeVaultRecordViewer(): void { this.vaultDetailVisible = false; this.vaultDetailPhotoIndex = 0; } private getFeaturedVaultFrames(): Array<MediaPreviewFrame> { const record = this.getFeaturedVaultRecord(); if (!record) { return []; } return this.getGalleryDetailFrames(record); } private getVaultPreviewRecords(): Array<GalleryMoment> { const featuredRecord = this.getFeaturedVaultRecord(); if (!featuredRecord) { return []; } return this.getVaultRecords() .filter((record: GalleryMoment) => record.id !== featuredRecord.id) .slice(0, 3);

详情查看器复用 Swiper 浏览双镜帧

buildVaultDetailViewer使用Swiper遍历getFeaturedVaultFrames。每一帧先铺一层低透明度背景图,再用 contain 模式展示主体图,这样竖图、横图和双镜照片都能在黑底里保持可看性。

顶部的返回按钮和页码、底部的记忆标题和地点时间都来自当前记录。这个实现说明保险箱详情不是简单的图片弹窗,而是保留了双镜记忆的上下文信息。用户知道自己正在看哪一组私密记忆,也能在多帧之间切换。

buildVaultDetailViewer 使用 Swiper 展示私密照片帧

private buildVaultDetailViewer() { if (this.vaultDetailVisible && this.getFeaturedVaultRecord() && this.getFeaturedVaultFrames().length > 0) { Stack({ alignContent: Alignment.TopStart }) { Column() .width('100%') .height('100%') .backgroundColor('#000000') Swiper() { ForEach(this.getFeaturedVaultFrames(), (frame: MediaPreviewFrame) => { Stack({ alignContent: Alignment.Center }) { Image(frame.uri) .width('100%') .height('100%') .objectFit(ImageFit.Cover) .opacity(0.34) Image(frame.uri) .width('100%') .height('74%') .objectFit(ImageFit.Contain) .align(Alignment.Top) } .width('100%') .height('100%') .backgroundColor('#000000') }, (frame: MediaPreviewFrame) => `vault_${frame.id}`) } .width('100%') .height('100%') .index(this.vaultDetailPhotoIndex) .autoPlay(false) .loop(this.getFeaturedVaultFrames().length > 1) .indicator(this.getFeaturedVaultFrames().length > 1) .onChange((index: number) => { this.vaultDetailPhotoIndex = index; }) Column() { Row({ space: 12 }) { Button('\u8fd4\u56de') .height(40) .fontSize(13) .fontWeight(FontWeight.Medium) .fontColor('#FFF7E6') .backgroundColor('#80111317') .borderRadius(18) .onClick(() => { this.closeVaultRecordViewer(); }) Blank() if (this.getFeaturedVaultFrames().length > 1) { Text(`${this.vaultDetailPhotoIndex + 1}/${this.getFeaturedVaultFrames().length}`) .fontSize(12) .fontColor('#FFF7E6') .padding({ left: 12, right: 12, top: 8, bottom: 8 }) .backgroundColor('#80111317') .borderRadius(16) } } .width('100%') .padding({ left: 16, right: 16, top: this.getPageTopPadding(18) }) Blank() Column({ space: 5 }) { Text(this.getCompactMemoryTitle( (this.getFeaturedVaultRecord() as GalleryMoment).memoryTitle, (this.getFeaturedVaultRecord() as GalleryMoment).place )) .fontSize(17) .fontWeight(FontWeight.Bold) .fontColor('#FFF7E6') .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) Text(`${(this.getFeaturedVaultRecord() as GalleryMoment).place} / ${(this.getFeaturedVaultRecord() as GalleryMoment).createdLabel}`) .fontSize(12) .fontColor('#D8CBB2') .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) } .width('100%') .padding(14) .backgroundColor('#D8111317') .borderRadius(22) .margin({ left: 16, right: 16, bottom: this.getPageBottomPadding(18) }) .alignItems(HorizontalAlign.Start) } .width('100%') .height('100%') } .width('100%') .height('100%') .backgroundColor('#000000') } }

锁定态和解锁态是两个 UI 分支

buildEnhancedVaultTab里先判断是否有私密照片,再判断vaultUnlocked和当前记录。未解锁时显示认证按钮;解锁后才显示私密照片数量、马赛克网格和操作按钮。这比在每个按钮上单独隐藏更清晰。

状态分支写清楚后,后续维护会轻松很多。新增一个私密照片操作时,只需要放在解锁分支里,并根据 busy 状态控制按钮可用性。未解锁分支始终保持认证路径,不会意外露出私密内容。

保险箱未解锁时只显示认证和导入入口

if (this.getVaultRecords().length === 0) { Column({ space: 12 }) { Text('还没有私密照片') .fontSize(18) .fontWeight(FontWeight.Medium) .fontColor($r('app.color.ml_on_surface')) Button(this.mediaImportBusy ? '导入中...' : '导入系统相册') .height(42) .width('100%') .enabled(!this.mediaImportBusy) .fontSize(14) .fontWeight(FontWeight.Medium) .fontColor(this.getWarmActionTextColor()) .backgroundColor(this.getWarmActionBackgroundColor()) .borderRadius(18) .onClick(() => { void this.importSystemAlbumPhotos('vault'); }) Button('去相册选择') .height(42) .width('100%') .fontSize(14) .fontWeight(FontWeight.Medium) .fontColor(this.getSecondaryActionTextColor()) .backgroundColor(this.getSecondaryActionBackgroundColor()) .borderRadius(18) .onClick(() => { this.switchTab('gallery'); }) } .width('100%') .padding(18) .backgroundColor($r('app.color.ml_panel_glass')) .borderRadius(28) .alignItems(HorizontalAlign.Start) } else if (!this.vaultUnlocked || !this.getFeaturedVaultRecord()) { Column({ space: 18 }) { Stack({ alignContent: Alignment.Center }) { Circle() .width(118) .height(118) .fill('#263542') .stroke('#E9B65E') .strokeWidth(1) Circle() .width(82) .height(82) .fill('#050809') .stroke('#FFB86B') .strokeWidth(2) Text('锁') .fontSize(28) .fontWeight(FontWeight.Bold) .fontColor('#FFF1D2') } .width(128) .height(128) .shadow({ radius: 30, color: '#6619B8C7', offsetX: 0, offsetY: 0 }) Text('打开保险箱查看私密照片') .fontSize(22) .fontWeight(FontWeight.Bold) .fontColor($r('app.color.ml_on_surface')) .textAlign(TextAlign.Center) Text('查看私密内容需要验证身份') .fontSize(13) .lineHeight(20) .fontColor($r('app.color.ml_on_surface_variant')) .textAlign(TextAlign.Center) Button(this.vaultAuthBusy ? '认证中...' : '解锁保险箱') .height(48) .width('100%') .enabled(!this.vaultAuthBusy) .fontSize(15) .fontWeight(FontWeight.Medium) .fontColor(this.getWarmActionTextColor()) .backgroundColor(this.getWarmActionBackgroundColor()) .borderRadius(24) .onClick(() => { void this.unlockVaultWithFace(); }) Button(this.mediaImportBusy ? '导入中...' : '导入系统相册') .height(42) .width('100%') .enabled(!this.mediaImportBusy && !this.vaultAuthBusy) .fontSize(14) .fontWeight(FontWeight.Medium) .fontColor(this.getSecondaryActionTextColor()) .backgroundColor(this.getSecondaryActionBackgroundColor()) .borderRadius(18) .onClick(() => { void this.importSystemAlbumPhotos('vault'); }) Row({ space: 12 }) { Text('人脸识别') .fontSize(12) .fontColor($r('app.color.ml_on_surface')) .padding({ left: 14, right: 14, top: 8, bottom: 8 }) .backgroundColor(this.getDarkChipBackgroundColor()) .borderRadius(16) .onClick(() => { void this.unlockVaultWithFace(); }) Text('指纹识别') .fontSize(12) .fontColor($r('app.color.ml_on_surface')) .padding({ left: 14, right: 14, top: 8, bottom: 8 }) .backgroundColor(this.getDarkChipBackgroundColor()) .borderRadius(16) .onClick(() => { void this.unlockVaultWithFingerprint(); }) } } .width('100%') .padding({ left: 24, right: 24, top: 30, bottom: 24 }) .backgroundColor($r('app.color.ml_panel_glass')) .borderRadius(34) .border({ width: 1, color: '#5519B8C7' }) .alignItems(HorizontalAlign.Center) } else {

恢复、导出、分享和锁定都在解锁分支

解锁分支里的操作按钮很集中:恢复照片调用restoreRecordFromVault,导出到相册调用exportRecordToSystemAlbum,系统分享调用shareRecordWithSystemShare,锁定按钮调用lockVault。每个按钮都结合 busy 状态限制点击。

这组按钮体现了保险箱详情的完整闭环:用户可以把私密照片恢复公开,也可以导出或分享当前记录,操作完还能手动锁定。对实战文章来说,这比只展示解锁页更有价值,因为它覆盖了用户真正会反复使用的路径。

保险箱解锁后提供恢复、导出、系统分享和锁定操作

Row({ space: 10 }) { Button('恢复照片') .height(42) .layoutWeight(1) .fontSize(14) .fontWeight(FontWeight.Medium) .fontColor(this.getSecondaryActionTextColor()) .backgroundColor(this.getSecondaryActionBackgroundColor()) .borderRadius(18) .enabled(!this.mediaExportBusy && !this.systemShareBusy && !this.vaultAuthBusy) .onClick(() => { void this.restoreRecordFromVault((this.getFeaturedVaultRecord() as GalleryMoment).id); }) Button('导出到相册') .height(42) .layoutWeight(1) .fontSize(14) .fontWeight(FontWeight.Medium) .fontColor(this.getMutedActionTextColor()) .backgroundColor(this.getMutedActionBackgroundColor()) .borderRadius(18) .enabled(!this.mediaExportBusy && !this.systemShareBusy && !this.vaultAuthBusy) .onClick(() => { void this.exportRecordToSystemAlbum((this.getFeaturedVaultRecord() as GalleryMoment), 'vault'); }) } .width('100%') Row({ space: 10 }) { Button(this.systemShareBusy ? '分享中...' : '系统分享') .height(42) .layoutWeight(1) .fontSize(14) .fontWeight(FontWeight.Medium) .fontColor(this.getMutedActionTextColor()) .backgroundColor(this.getMutedActionBackgroundColor()) .borderRadius(18) .enabled(!this.mediaExportBusy && !this.systemShareBusy) .onClick(() => { void this.shareRecordWithSystemShare((this.getFeaturedVaultRecord() as GalleryMoment), 'vault'); }) Button('锁定保险箱') .height(42) .layoutWeight(1) .fontSize(14) .fontWeight(FontWeight.Medium) .fontColor(this.getMutedActionTextColor()) .backgroundColor(this.getMutedActionBackgroundColor()) .borderRadius(18) .enabled(!this.mediaExportBusy && !this.systemShareBusy) .onClick(() => { this.lockVault(); }) }

工程检查清单

  • 详情入口必须先判断vaultUnlocked
  • 私密照片帧复用普通相册详情帧构造。
  • 锁定态和解锁态用清晰 UI 分支隔离。
  • 恢复、导出和分享按钮都要受 busy 状态控制。

今日练习

  1. 在未解锁状态下调用openVaultRecordViewer,验证详情不会打开。
  2. 切换不同私密记录,观察getFeaturedVaultRecord的回退逻辑。
  3. 导出过程中连续点击恢复按钮,确认 enabled 条件能阻止并发操作。

训练营里的每一篇都建议按同一个节奏复盘:先看页面行为,再回到源码定位状态和服务层,最后自己改一个很小的参数验证结果。这样写文章时不会停留在 API 名词,读者也能沿着真实工程把功能跑通。

http://www.rkmt.cn/news/1496635.html

相关文章:

  • Kotlin单表达式函数在安卓开发中的精简艺术
  • 手把手教你用MATLAB复现圆柱绕流POD分解:从Brunton的代码到自己的流场图
  • AgentWatch MCP 服务说明文档
  • 基于 LlamaIndex + DeepSeek + Streamlit 搭建智能问答系统
  • 2026最新渭南市黄金回收价格一览表 回收避坑攻略靠谱商家推荐 - 余生黄金回收
  • UVM源码探秘:start_item的sequencer参数怎么用?解锁更灵活的sequence驱动方式
  • 10kV配网故障识别:波形分析全攻略
  • 【国产电脑python编译器配置】麒麟V10系统anaconda配置pycharm
  • 人工智能专业术语详解(I)
  • Vue3自定义指令实战:从拖拽到权限按钮,3个真实项目案例手把手教学
  • STM32F4实战:5分钟搞定CANopen快速SDO通信,读取节点数据就这么简单
  • 云南大学考研辅导班正规机构,全维度榜单推荐 - 推荐评测师
  • 弹窗交互:AlertDialog与CustomDialog的创建与关闭(11)
  • 【提示词工程】提示词工程笔记:从核心思想到实战代码
  • Got timeout reading communication packets解决方法
  • 微信投票小程序怎么用丨图文视频投票制作全过程(海投票实时更新) - 微信投票小程序
  • 告别编译焦虑!Windows 10下用LLVM-MinGW和Ninja一键搞定OLLVM-14.x(附成品下载)
  • 别再截图了!用Altium Designer 23原生功能导出PCB高清丝印图,5分钟搞定SW贴图素材
  • 通化黄金回收2026大盘价结算无套路攻略 - 润富黄金回收
  • 云南研学旅行包车公司排行:5家合规靠谱服务商盘点 - 奔跑123
  • 不只是混淆:手把手教你将OLLVM-14.x集成到Android Studio NDK,打造专属加固工具链
  • AI小助手开发与应用(下):API迁移实践与多性格交互引擎
  • 2026潍坊防水补漏哪家靠谱?正规公司排名及避坑价格指南 - 苏易修缮
  • 2026年高县水上乐园重磅开业:皮划艇比赛、无动力乐园、端午狂欢节全攻略 - 年度推荐企业名录
  • 信号分解算法选型指南:从EMD到VMD,如何根据你的数据特征避开模态混叠?
  • Elastic Agent独立模式避坑指南:从API密钥权限到服务启动的完整配置流程
  • 手把手教你用MATLAB复现圆柱绕流POD分解(附Brunton案例完整代码与避坑指南)
  • Web应用开发入门与实战总结
  • 青岛管道漏水检测哪家好?消防管道测漏 /TOP5 公司推荐,精准定位无盲拆,避坑不踩雷 - 速递信息
  • 用Cesium打造酷炫三维大屏:动态飞线、雷达扫描与天气特效的完整配置流程