第71篇 | HarmonyOS SharedData:双镜照片如何变成系统可分享对象
第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 只保存分享需要的最小字段
interface LocalShareItem {
sourcePath: string;
title: string;
description: string;
baseType: string;
thumbnailPath?: string;
}
照片分享项和视频素材分享项分开构造
buildRecordShareItems 面向单条双镜记忆记录,会同时考虑 backPath 和 frontPath;buildVideoPhotoShareItems 面向成片选择,会遍历多条记录并取每条记录的后置照片作为素材。两个函数输出同一种 LocalShareItem,但输入场景不同。
这就是一个值得学习的拆分方式:业务选择逻辑可以不同,但输出模型保持统一。相册详情分享、近场分享和一键成片分享都能接到后面的 SharedData 构建函数,代码既不绕,也便于排查。

不同业务入口最终都输出 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 根据扩展名和基础类型生成系统分享记录
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 集中展示系统分享面板并处理异常
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抛出。
今日练习
- 给
LocalShareItem增加一个临时thumbnailPath,观察图片预览是否使用缩略图。 - 把一个无扩展名路径传入
getShareFileExtension,验证回退类型。 - 尝试把
selectionMode改为多选模式,记录分享面板行为变化。
训练营里的每一篇都建议按同一个节奏复盘:先看页面行为,再回到源码定位状态和服务层,最后自己改一个很小的参数验证结果。这样写文章时不会停留在 API 名词,读者也能沿着真实工程把功能跑通。
更多推荐



所有评论(0)