第86篇 | HarmonyOS 去年今日和附近记忆:照片记录如何触发故地重游

第86篇讲“故地重游”。双镜记忆相机不是只把照片放进相册,还会在用户回到曾经拍摄的位置附近时,把相关回忆卡片提示出来。这个体验由地图记忆点、距离计算、周年记录匹配和通知触发共同完成。

这一篇重点看 anniversaryRecallRecordId、anniversaryRecallMemoryId 和 updateAnniversaryRecall。它们把“附近位置”和“过去照片”连接起来,让照片记录重新变成当前场景里的提示。

本篇目标

  • 理解附近记忆为什么需要位置距离和时间线共同判断。
  • 掌握 anniversaryRecallRecordId 和 memoryId 如何组成回忆 key。
  • 看清回忆卡片如何打开相册指定记忆。
  • 理解回忆提醒和通知触发之间的关系。

对应源码位置

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

照片记录可以重新唤起场景

运行路径图里,用户不是主动搜索旧照片,而是在地图场景里接近曾经拍摄的位置。应用把位置距离、记忆点和相册记录组合起来,提示“你在这里记录过”。

这个体验比普通相册列表更有作品感。它把照片从静态记录变成当前位置的上下文,也给通知回流和 LiveView 留下入口。

地图页根据当前位置和历史照片记录触发故地重游卡片

地图页根据当前位置和历史照片记录触发故地重游卡片

状态字段保存当前命中的回忆

anniversaryRecallRecordIdanniversaryRecallMemoryId 分别保存照片记录和地图记忆点。两者合在一起才是一次完整的回忆命中。

这种设计避免只用 recordId 或只用 memoryId 造成歧义。用户可能在同一地点拍过多组照片,也可能多个记忆点关联相似位置,组合 key 更稳。

页面用 recordId、memoryId 和提示文案保存当前命中的回忆

页面用 recordId、memoryId 和提示文案保存当前命中的回忆

  @State private videoPreviewFrameIndex: number = 0;
  @State private galleryNoticeText: string = '照片会自动进入相册';
  @State private anniversaryRecallRecordId: string = '';
  @State private anniversaryRecallMemoryId: string = '';
  @State private anniversaryRecallText: string = '回到曾经拍摄的位置附近时,这里会自动提醒相关回忆';
  @State private dismissedAnniversaryRecallKey: string = '';
  @State private vaultUnlocked: boolean = false;
  @State private vaultAuthBusy: boolean = false;
  @State private vaultSelectedId: string = '';

公开记录参与匹配

回忆匹配应该只看公开相册记录,保险箱照片不能因为用户接近某个地点就自动暴露。getAnniversaryRecallRecord 从公开记录中找当前命中的 recordId,保证边界清楚。

这也是隐私体验的一部分。故地重游很温柔,但前提是它不会把私密照片变成主动提醒。

回忆卡片通过公开相册记录和组合 key 确定当前提醒

回忆卡片通过公开相册记录和组合 key 确定当前提醒

    if (this.anniversaryRecallRecordId.length === 0) {
      return undefined;
    }
    return this.getPublicGalleryRecords().find((record: GalleryMoment) => record.id === this.anniversaryRecallRecordId);
  }

  private getAnniversaryRecallKey(): string {
    if (this.anniversaryRecallRecordId.length === 0 || this.anniversaryRecallMemoryId.length === 0) {
      return '';
    }
    return `${this.anniversaryRecallMemoryId}_${this.anniversaryRecallRecordId}`;
  }

  private isAnniversaryRecallDismissed(): boolean {
    const recallKey = this.getAnniversaryRecallKey();
    return recallKey.length > 0 && recallKey === this.dismissedAnniversaryRecallKey;
  }

更新回忆时同时触发通知

updateAnniversaryRecall 先判断距离是否超过阈值,再找到符合条件的历史记录。命中后,它会更新 recordId、memoryId 和提示文案,并在发生变化时触发场景回忆通知。

注意这里的 changed 判断。只有当前命中的回忆变化,才需要发布通知,避免用户停留在同一地点时不断被重复打扰。

updateAnniversaryRecall 匹配距离、更新文案,并在变化时发布通知

updateAnniversaryRecall 匹配距离、更新文案,并在变化时发布通知

  private updateAnniversaryRecall(memory: MemorySpot, distanceMeters: number): void {
    if (distanceMeters > this.anniversaryMatchDistanceMeters) {
      this.clearAnniversaryRecall();
      return;
    }

    const anniversaryRecord = this.getAnniversaryRecordForMemory(memory);
    if (!anniversaryRecord) {
      this.clearAnniversaryRecall();
      return;
    }

    const recallTime = this.formatVisitRecallTime(anniversaryRecord.createdAt);
    const distanceText = this.formatDistance(distanceMeters);
    const nextText = `你在 ${recallTime} 记录过这里,现在距离约 ${distanceText},可以打开当时的照片继续回看那一刻。`;
    const changed = this.anniversaryRecallRecordId !== anniversaryRecord.id ||
      this.anniversaryRecallMemoryId !== memory.id;
    this.anniversaryRecallRecordId = anniversaryRecord.id;
    this.anniversaryRecallMemoryId = memory.id;
    this.anniversaryRecallText = nextText;
    if (changed) {
      this.galleryFocusMemoryId = memory.id;
      this.gallerySelectedId = anniversaryRecord.id;
      this.galleryNoticeText = nextText;
      void this.publishSceneRecallNotification(memory, anniversaryRecord, nextText);
    }
  }

回忆卡片打开相册指定记忆

openAnniversaryRecallInGallery 会把 tab 切到 gallery,设置 focus memory id,并把提示文案写到相册 notice。用户从地图看到回忆后,可以自然回到相册查看当时照片。

这就是“故地重游”的闭环:地图负责触发,相册负责承接,照片记录负责展示内容。不要让提醒只停留在卡片上,要给用户一个明确的下一步。

回忆卡片点击后切换到相册并聚焦对应记忆

回忆卡片点击后切换到相册并聚焦对应记忆

  private openAnniversaryRecallInGallery(): void {
    const anniversaryRecord = this.getAnniversaryRecallRecord();
    if (!anniversaryRecord) {
      return;
    }
    this.galleryDetailReturnTarget = '';
    this.gallerySelectedId = anniversaryRecord.id;
    this.selectedGalleryGroupKey = this.buildGalleryRecordGroupKey(anniversaryRecord);
    this.galleryUserNoteDraft = this.getRecordUserNote(anniversaryRecord);
    if (this.anniversaryRecallMemoryId.length > 0) {
      this.galleryFocusMemoryId = this.anniversaryRecallMemoryId;
    }
    this.galleryNoticeText = this.anniversaryRecallText;
    this.galleryMediaTab = 'photo';
    this.resetGalleryDetailViewer();
    this.galleryViewMode = 'detail';
    void this.startGalleryAntiPeepProtection();
    this.switchTab('gallery');

隐私边界和回忆触发验证

故地重游看起来是推荐体验,本质上也涉及隐私边界。通知和页面卡片只应该使用公开相册里的地点、时间和脱敏标题,不能把保险箱照片、隐藏视频或完整文件路径带到通知栏。这样即使通知被截屏,用户也不会暴露敏感内容。

数据来源 是否参与回忆提醒 原因
公开相册记录 可以参与 用户已经在普通相册中可见
保险箱记录 不参与通知栏提醒 通知栏不可控,可能被旁人看到
无定位记录 不参与附近触发 无法计算距离,避免误提醒
已删除记录 不参与 防止旧快照复活已删除内容

回归测试要覆盖时间和空间两个维度:同一天历史记录、附近距离命中、无定位记录、保险箱记录。命中后再检查通知文案和页面卡片是否只包含安全字段。这样文章不仅说明“怎么触发”,也说明“哪些内容不能触发”。

实现上可以把筛选条件放在回忆匹配之前,而不是发布通知时才临时判断。先过滤敏感数据,再计算距离和周年匹配,能降低遗漏风险,也让后续调试更清楚:没有提醒是因为不满足条件,而不是通知模块失败。

工程检查清单

  • 回忆提醒只使用公开相册记录,不能暴露保险箱内容。
  • recordId 和 memoryId 组合成回忆 key,避免重复或误命中。
  • 距离超过阈值时要清理回忆状态。
  • 提醒必须能回流到相册指定记忆,而不是只显示一段文案。

今日练习

  1. anniversaryMatchDistanceMeters 调小,观察回忆触发频率变化。
  2. 模拟同一地点多条记录,思考 recordId 和 memoryId 如何避免歧义。
  3. 把回忆卡片点击链路画成“地图 -> 相册 -> 记录”的流程图。

完成练习后,建议把“看到的页面结果”和“源码里的状态字段”写在同一张小表里。这个习惯会让后续排查同步、通知、权限和多设备体验时更快定位问题,也能让文章从概念讲解变成真正可复现的工程笔记。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐