第71篇 | HarmonyOS SharedData:双镜照片如何变成系统可分享对象

第 71 篇专门拆 SharedData。前一篇我们知道近场回调最终会调用 buildSharedData,但在真实项目里,构造分享数据不只是把文件路径塞进去。双镜照片有前后两张图,有标题、地点、时间、缩略图和媒体类型,系统分享面板需要这些信息才能展示出正确的预览。

这一篇从 LocalShareItem 这个中间结构开始,讲清楚项目为什么要先建立自己的分享项模型,再转换成 ShareKit/SystemShare 使用的 SharedRecord。理解这层适配后,后续扩展视频分享、批量照片分享、保险箱导出都会更自然。

本篇目标

  • 理解 LocalShareItem 在业务记录和系统分享记录之间的适配作用。
  • 掌握图片和视频分享项的构造差异。
  • 理解 UTD 类型、fileUri 和 thumbnailUri 在分享预览中的作用。
  • 学会用同一套 showSystemSharePanel 支撑不同来源的分享入口。

对应源码位置

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

从视频管理页看分享数据的扩展性

双镜记忆相机并不只分享静态照片,后续还会把照片生成短片,再通过系统分享交给其他应用或系统相册能力。正因为有照片、视频、近场分享、系统分享多种出口,项目需要一层稳定的中间模型,避免每个入口都重新拼一遍路径、类型和描述。

运行页面里的视频管理入口说明了这点:用户看到的是照片成片能力,工程里却要让同一批记录既能生成 MP4,也能作为图片组交给系统分享面板。LocalShareItem 就是这条链路的最小通用结构。

视频管理页和系统分享共用同一套数据封装思路

视频管理页和系统分享共用同一套数据封装思路

业务中间层只保留必要字段

LocalShareItem 没有把整条 GalleryMoment 塞给系统,而是只保留分享需要的字段:源文件路径、标题、描述、基础媒体类型,以及可选缩略图路径。这样的中间层既轻量,也能屏蔽业务记录里大量与分享无关的状态。

在训练营项目里,这个接口还有一个隐含好处:以后如果增加视频、LivePhoto 或压缩后的导出文件,只要继续填充 LocalShareItem,后面的 buildSharedRecord 就可以复用,不需要在每个按钮里判断媒体类型。

LocalShareItem 只保存分享需要的最小字段

LocalShareItem 只保存分享需要的最小字段

interface LocalShareItem {
  sourcePath: string;
  title: string;
  description: string;
  baseType: string;
  thumbnailPath?: string;
}

照片分享项和视频素材分享项分开构造

buildRecordShareItems 面向单条双镜记忆记录,会同时考虑 backPath 和 frontPath;buildVideoPhotoShareItems 面向成片选择,会遍历多条记录并取每条记录的后置照片作为素材。两个函数输出同一种 LocalShareItem,但输入场景不同。

这就是一个值得学习的拆分方式:业务选择逻辑可以不同,但输出模型保持统一。相册详情分享、近场分享和一键成片分享都能接到后面的 SharedData 构建函数,代码既不绕,也便于排查。

不同业务入口最终都输出 LocalShareItem 数组

不同业务入口最终都输出 LocalShareItem 数组

  private buildRecordShareItems(record: GalleryMoment): Array<LocalShareItem> {
    const candidates: Array<LocalShareItem> = [
      {
        sourcePath: record.backPath,
        title: '',
        description: `${record.createdLabel} / ${record.place}`,
        baseType: utd.UniformDataType.IMAGE
      },
      {
        sourcePath: record.frontPath,
        title: '',
        description: `${record.createdLabel} / ${record.memoryTitle}`,
        baseType: utd.UniformDataType.IMAGE
      }
    ];
    const shareItems: Array<LocalShareItem> = [];
    const seenPaths: Array<string> = [];
    for (const candidate of candidates) {
      if (candidate.sourcePath.length === 0 || seenPaths.includes(candidate.sourcePath)) {
        continue;
      }
      seenPaths.push(candidate.sourcePath);
      shareItems.push(candidate);
    }
    return shareItems;
  }

  private buildVideoPhotoShareItems(records: Array<GalleryMoment>): Array<LocalShareItem> {
    const shareItems: Array<LocalShareItem> = [];
    const seenPaths: Array<string> = [];
    for (const record of records) {
      if (record.backPath.length === 0 || seenPaths.includes(record.backPath)) {
        continue;
      }
      seenPaths.push(record.backPath);
      shareItems.push({
        sourcePath: record.backPath,
        title: `${record.place} ${record.createdLabel}`,
        description: `${record.memoryTitle} / ${record.place}`,
        baseType: utd.UniformDataType.IMAGE
      });
    }
    return shareItems;
  }

文件扩展名决定更准确的媒体类型

系统分享并不只看文件路径,它还关心媒体类型。项目先从路径中提取扩展名,如果路径缺失扩展名,就根据基础类型回退到 .mp4.jpg。随后通过 utd.getUniformDataTypeByFilenameExtension 得到更精确的 UTD。

这一步会影响系统分享面板的预览和接收方处理方式。比如图片应该有缩略图,视频应该按视频类型展示。如果只是传一个模糊类型,接收端可能可以收到文件,但用户看到的预览体验会差很多。

buildSharedRecord 根据扩展名和基础类型生成系统分享记录

buildSharedRecord 根据扩展名和基础类型生成系统分享记录

  private getShareFileExtension(sourcePath: string, baseType: string): string {
    const lastDotIndex = sourcePath.lastIndexOf('.');
    if (lastDotIndex >= 0 && lastDotIndex < sourcePath.length - 1) {
      const extension = sourcePath.slice(lastDotIndex).replace(/[^.0-9a-zA-Z]/g, '').toLowerCase();
      if (extension.length > 1) {
        return extension;
      }
    }
    return baseType === utd.UniformDataType.VIDEO ? '.mp4' : '.jpg';
  }

  private getNearbyShareItems(): Array<LocalShareItem> {
    const shareRecord = this.getNearbyShareRecord();
    if (!shareRecord) {
      return [];
    }
    return this.buildRecordShareItems(shareRecord);
  }

  private getNearbyShareTargetText(): string {
    const shareRecord = this.getNearbyShareRecord();
    if (!shareRecord) {
      return '';
    }
    return `${shareRecord.place} / ${shareRecord.memoryTitle}`;
  }

  private getNearbyShareRecord(): GalleryMoment | undefined {
    if (this.activeTab === 'vault') {
      return this.getFeaturedVaultRecord();
    }
    if (this.activeTab === 'gallery') {
      return this.getFeaturedGalleryRecord();
    }
    return this.getFeaturedGalleryRecord();
  }

  private buildSharedData(items: Array<LocalShareItem>): systemShare.SharedData {
    if (items.length === 0) {
      throw new Error('');
    }
    const sharedData: systemShare.SharedData = new systemShare.SharedData(this.buildSharedRecord(items[0]));
    for (let index = 1; index < items.length; index++) {
      try {
        sharedData.addRecord(this.buildSharedRecord(items[index]));
      } catch (error) {
        const message = error instanceof Error ? error.message : JSON.stringify(error);
        throw new Error(`添加分享文件失败:${message}`);
      }
    }
    return sharedData;
  }

  private buildSharedRecord(item: LocalShareItem): systemShare.SharedRecord {
    const extension = this.getShareFileExtension(item.sourcePath, item.baseType);
    const preciseType = utd.getUniformDataTypeByFilenameExtension(extension, item.baseType);
    const sharedRecord: systemShare.SharedRecord = {
      utd: preciseType,
      uri: fileUri.getUriFromPath(item.sourcePath),
      title: item.title,
      description: item.description
    };
    if (item.baseType === utd.UniformDataType.IMAGE) {
      const thumbnailPath = item.thumbnailPath ?? item.sourcePath;
      sharedRecord.thumbnailUri = fileUri.getUriFromPath(thumbnailPath);
    }
    return sharedRecord;
  }

系统分享面板只接收 SharedData

showSystemSharePanel 是统一出口。它不关心数据来自相册详情、视频管理还是保险箱,只要求调用方给出 LocalShareItem 数组。函数内部构建 SharedData,再创建 ShareController 并展示系统分享面板。

统一出口还有一个工程收益:失败文案、选择模式、预览模式都集中管理。后面如果希望改成多选分享、改预览模式或增加埋点,不需要去每个按钮里找逻辑,只要调整这个函数即可。

showSystemSharePanel 集中展示系统分享面板并处理异常

showSystemSharePanel 集中展示系统分享面板并处理异常

  private async showSystemSharePanel(items: Array<LocalShareItem>): Promise<void> {
    if (items.length === 0) {
      throw new Error('');
    }

    try {
      const sharedData = this.buildSharedData(items);
      const controller: systemShare.ShareController = new systemShare.ShareController(sharedData);
      await controller.show(this.getAbilityContext(), {
        selectionMode: systemShare.SelectionMode.SINGLE,
        previewMode: systemShare.SharePreviewMode.DETAIL
      });
    } catch (error) {
      const err = error as BusinessError;
      throw new Error(`拉起系统分享面板失败:${err.message ?? err.code ?? 'unknown'}`);
    }
  }

工程检查清单

  • LocalShareItem 字段足够支撑图片、视频和缩略图。
  • 不同业务入口输出同一种分享项数组。
  • SharedRecord 里 URI 使用 fileUri.getUriFromPath 转换。
  • 系统分享面板的异常统一从 showSystemSharePanel 抛出。

今日练习

  1. LocalShareItem 增加一个临时 thumbnailPath,观察图片预览是否使用缩略图。
  2. 把一个无扩展名路径传入 getShareFileExtension,验证回退类型。
  3. 尝试把 selectionMode 改为多选模式,记录分享面板行为变化。

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

Logo

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

更多推荐