【鸿蒙开发实战】HarmonyOS 系统分享教程:统一拖拽、接收分享与拉起分享面板
HarmonyOS 系统分享教程:统一拖拽、接收分享与拉起分享面板
一、概述
系统分享是几乎所有应用都会用到的刚需功能。HarmonyOS 提供了 @kit.ShareKit 中的 systemShare 模块,支持应用作为分享的接收方和发送方参与系统分享流程
本教程以「云星图」项目为实战案例,讲解三个方向的完整实现:
| 方向 | 场景 | 核心 API |
|---|---|---|
| 接收分享 | 系统分享面板 -> 本应用(接收图片上传) | systemShare.getSharedData |
| 发起分享 | 本应用 -> 系统分享面板(分享图片到其他应用) | ShareController.show |
| 拖拽接收 | 拖拽文件到应用区域(PC/平板端接收图片) | unifiedDataChannel.startDataLoading |
整体数据流向:
二、前置准备
2.1 依赖导入
// 接收分享 & 发起分享
import { systemShare } from '@kit.ShareKit';
import { uniformTypeDescriptor as utd } from '@kit.ArkData';
// 发起分享:下载 + 文件操作
import { http } from '@kit.NetworkKit';
import { fileIo as fs, fileUri } from '@kit.CoreFileKit';
// 拖拽接收
import { unifiedDataChannel } from '@kit.ArkData';
// 通用
import { common, Want } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
2.2 UTD 标准化数据类型
系统分享基于 UTD(Uniform Type Descriptor)统一数据类型来匹配发送方和接收方。常用类型:
| UTD 常量 | 对应内容 | 说明 |
|---|---|---|
UniformDataType.IMAGE |
通用图片 | 所有图片类型的父类型 |
UniformDataType.PLAIN_TEXT |
纯文本 | 文本内容 |
UniformDataType.HYPERLINK |
超链接 | URL 链接 |
UniformDataType.VIDEO |
视频 | 视频文件 |
UniformDataType.GENERAL_FILE |
通用文件 | 任意文件 |
细粒度类型:可以通过文件扩展名获取更精确的 UTD 类型,帮助系统精准匹配接收应用:
// 根据文件扩展名获取细粒度 UTD 类型
const utdType = utd.getUniformDataTypeByFilenameExtension('.jpg', utd.UniformDataType.IMAGE);
// 返回 'general.jpg',比通用 'general.image' 更精确
三、接收分享:系统分享面板 -> 本应用
3.1 module.json5 配置
接收分享的第一步是在 module.json5 的 skills 中声明应用支持的分享数据类型。系统分享面板会根据这些声明来筛选可接收的应用。
{
"name": "EntryAbility",
"skills": [
// ... 其他 skill(如启动入口)
{
"actions": [
"ohos.want.action.sendData" // 声明接收分享数据
],
"uris": [
{
"scheme": "file",
"utd": "general.jpg",
"maxFileSupported": 50 // 一次最多接收 50 张 jpg
},
{
"scheme": "file",
"utd": "general.png",
"maxFileSupported": 50
},
{
"scheme": "file",
"utd": "general.jpeg",
"maxFileSupported": 50
}
]
}
]
}
配置说明:
ohos.want.action.sendData:声明本 Ability 可接收分享数据uris中的utd:声明支持的文件类型,系统会按 UTD 类型匹配maxFileSupported:一次分享可接收的最大文件数量- 每种图片格式需单独声明,不能只写一个通用的
general.image
3.2 EntryAbility 中处理分享数据
当用户在系统分享面板中选择本应用后,系统会通过 Want 携带分享数据启动应用。需要在 onCreate 和 onNewWant 中都处理:
// EntryAbility.ets
import { systemShare } from '@kit.ShareKit';
export default class EntryAbility extends UIAbility {
private uploadList: UploadItem[] = [];
async onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): Promise<void> {
// ... 其他初始化
this.handleShareData(want); // 冷启动时处理分享
}
onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
this.uploadList = []; // 每次新分享,清空旧数据
this.handleShareData(want); // 热启动时处理分享
}
}
为什么要两个入口都处理?
3.3 解析分享数据
handleShareData 是核心处理方法,从 Want 中提取分享的文件记录:
private handleShareData(want: Want) {
if (!want) {
console.error('[分享] want 为空,不处理');
return;
}
systemShare.getSharedData(want)
.then((data: systemShare.SharedData) => {
if (!data) {
console.error('[分享] 分享数据为空');
return;
}
const records = data.getRecords() || [];
if (records.length === 0) {
console.log('[分享] 无分享文件记录');
return;
}
console.log(`[分享] 收到分享文件数量:${records.length}`);
// 每次处理分享前清空列表
this.uploadList = [];
// 遍历分享记录
records.forEach((record: systemShare.SharedRecord) => {
if (!record || !record.uri) {
console.warn('[分享] 无效的分享记录,uri 为空');
return;
}
const shareUri = record.uri;
// 将分享的图片 URI 转为上传项
const timestamp = Date.now() + Math.random().toString(36).substr(2, 9);
const newItem: UploadItem = {
id: timestamp,
uri: shareUri,
status: 'idle',
progress: 0,
fileName: `分享_${this.uploadList.length + 1}`,
cosPath: `img/${timestamp}.jpg`,
uploadTime: new Date().toLocaleString(),
isHistory: false
};
this.uploadList.push(newItem);
});
// 跳转到上传页,传入分享数据
AppStorage.setOrCreate(StateKeys.FIRST_LEVEL_INDEX, 1); // 1 = 上传页
AppStorage.setOrCreate(StateKeys.UPLOAD_DATA, this.uploadList);
AppStorage.setOrCreate(StateKeys.SELECT_PAGE, 'upload');
})
.catch((error: BusinessError) => {
console.error(`Failed to getSharedData. Code: ${error.code}, message: ${error.message}`);
});
}
分享数据解析的完整流程:
关键点:
record.uri是文件的 URI(file://...或datashare://...),可直接用于上传- 每次处理前清空 uploadList,避免上一次分享的残留数据混入
- 通过
AppStorage传递数据 + Tab 索引,驱动页面跳转和列表渲染
四、发起分享:拉起系统分享面板
4.1 封装 ShareHelper
将"下载网络图片 + 构造分享数据 + 拉起面板"封装为通用工具类:
// ShareHelper.ets
import { http } from '@kit.NetworkKit';
import { fileIo as fs, fileUri } from '@kit.CoreFileKit';
import { systemShare } from '@kit.ShareKit';
import { uniformTypeDescriptor as utd } from '@kit.ArkData';
import { common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
export interface ShareImageOptions {
title?: string;
description?: string;
onStart?: () => void;
onSuccess?: () => void;
onError?: (error: BusinessError) => void;
}
export class ShareHelper {
static async shareImageFromUrl(
context: common.UIAbilityContext,
imageUrl: string,
options?: ShareImageOptions
): Promise<void> {
const title = options?.title ?? '分享图片';
const description = options?.description ?? '来自云星图的图片分享';
if (options?.onStart) {
options.onStart();
}
try {
// 1. 下载图片到缓存目录
const cacheDir = context.cacheDir;
const fileName = `share_${Date.now()}.jpg`;
const filePath = `${cacheDir}/${fileName}`;
const httpRequest = http.createHttp();
const response = await httpRequest.request(imageUrl, {
method: http.RequestMethod.GET,
expectDataType: http.HttpDataType.ARRAY_BUFFER,
connectTimeout: 60000,
readTimeout: 60000
});
if (response.responseCode !== http.ResponseCode.OK) {
throw new Error('图片下载失败');
}
// 2. 写入缓存文件
const arrayBuffer = response.result as ArrayBuffer;
const file = await fs.open(filePath, fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE);
await fs.write(file.fd, arrayBuffer);
await fs.close(file.fd);
// 3. 构造分享数据
const utdType = utd.getUniformDataTypeByFilenameExtension('.jpg', utd.UniformDataType.IMAGE);
const uri = fileUri.getUriFromPath(filePath);
const shareData = new systemShare.SharedData({
utd: utdType,
uri: uri,
title: title,
description: description
});
// 4. 拉起系统分享面板
const controller = new systemShare.ShareController(shareData);
await controller.show(context, {
selectionMode: systemShare.SelectionMode.SINGLE,
previewMode: systemShare.SharePreviewMode.DETAIL
});
if (options?.onSuccess) {
options.onSuccess();
}
} catch (err) {
const error = err as BusinessError;
console.error('ShareHelper error:', JSON.stringify(error));
if (options?.onError) {
options.onError(error);
}
}
}
}
发起分享的完整流程:
关键细节:
- 必须先下载到本地:
ShareController需要本地文件 URI,不能直接传网络 URL fileUri.getUriFromPath:将文件系统路径(如/data/.../share_xxx.jpg)转换为跨应用可访问的file://URI- 细粒度 UTD:用
getUniformDataTypeByFilenameExtension('.jpg', IMAGE)而非通用IMAGE,系统面板能更精准地筛选可接收的应用 SelectionMode.SINGLE:单选模式,用户只能选择一个目标应用SharePreviewMode.DETAIL:详细预览模式,分享面板会展示图片缩略图和标题描述
4.2 页面中调用
在图片预览页的分享菜单中调用 ShareHelper:
// PicView.ets
async shareCurrentImage() {
const uiContext = this.getUIContext();
const context = uiContext.getHostContext() as common.UIAbilityContext;
const promptAction = uiContext.getPromptAction();
const currentImageUrl = this.imgUrls[this.currentIndex];
if (!currentImageUrl) {
promptAction.showToast({ message: '暂无图片可分享' });
return;
}
await ShareHelper.shareImageFromUrl(context, currentImageUrl, {
title: '分享图片',
description: '来自云星图的图片分享',
onStart: () => {
promptAction.showToast({ message: '正在准备分享...' });
},
onError: (err) => {
promptAction.showToast({ message: `分享失败:${err.message || '请重试'}` });
}
});
}


分享菜单 UI:
@Builder
ShareMenuBuilder() {
Menu() {
MenuItem({ content: '复制链接' })
.onClick(() => { this.copyCurrentImageLink(); })
MenuItem({ content: '复制为Markdown' })
.onClick(() => { this.copyAsMarkdown(); })
MenuItem({ content: '复制为HTML' })
.onClick(() => { this.copyAsHtml(); })
MenuItem({ content: '系统分享' })
.onClick(() => { this.shareCurrentImage(); })
}
.radius(15)
}
五、拖拽接收:PC/平板端接收图片
在 PC 和平板上,用户可以通过拖拽文件到应用区域来导入图片。这使用 unifiedDataChannel 模块处理拖拽数据。
5.1 拖拽事件处理
// NormalUpLoad.ets
import { unifiedDataChannel, uniformTypeDescriptor } from '@kit.ArkData';
import { fileUri } from '@kit.CoreFileKit';
// 在组件的拖拽区域配置事件
.onDragEnter((event: DragEvent) => {
// 拖拽进入:高亮区域
this.getUIContext()?.animateTo({ curve: curves.springMotion() }, () => {
this.isDragOver = true;
});
})
.onDragLeave((event: DragEvent) => {
// 拖拽离开:取消高亮
this.getUIContext()?.animateTo({ curve: curves.springMotion() }, () => {
this.isDragOver = false;
});
})
.onDrop((event: DragEvent) => {
if (this.isAllUploading) return;
const context = this.context;
const destDir = context.distributedFilesDir;
const destUri = fileUri.getUriFromPath(destDir);
// 拖拽数据接收进度监听器
const progressListener = (
progress: unifiedDataChannel.ProgressInfo,
dragData: unifiedDataChannel.UnifiedData | null
) => {
if (!dragData) return;
const records = dragData.getRecords();
if (records.length === 0) return;
records.forEach((record) => {
// 按类型过滤:只接收图片
if (record.getType() === uniformTypeDescriptor.UniformDataType.IMAGE) {
const image = record as unifiedDataChannel.Image;
const imageUri = image.imageUri;
const realFileName = this.getFileNameFromUri(imageUri);
const newItem: UploadItem = {
id: Date.now() + Math.random().toString(36).substr(2, 9),
uri: imageUri,
status: 'idle',
progress: 0,
fileName: realFileName,
cosPath: `img/${realFileName}`,
uploadTime: this.formatDateTime(new Date()),
isHistory: false
};
this.uploadList.push(newItem);
}
});
};
// 配置接收参数
const options: unifiedDataChannel.GetDataParams = {
destUri: destUri,
fileConflictOptions: unifiedDataChannel.FileConflictOptions.OVERWRITE,
progressIndicator: unifiedDataChannel.ProgressIndicator.DEFAULT,
dataProgressListener: progressListener,
};
try {
event.startDataLoading(options);
} catch (e) {
const err = e as BusinessError;
this.message = `拖拽接收失败: ${err.message}`;
} finally {
this.isDragOver = false;
}
}, { disableDataPrefetch: true })
拖拽接收的处理流程:
关键细节:
distributedFilesDir:拖拽接收的文件会被复制到分布式文件目录,跨应用可访问FileConflictOptions.OVERWRITE:同名文件覆盖写入ProgressIndicator.DEFAULT:展示系统默认进度条disableDataPrefetch: true:禁用数据预取,避免大文件拖拽时卡顿- 类型过滤:通过
record.getType()判断是否为图片,只接收IMAGE类型
5.2 拖拽视觉反馈
通过 isDragOver 状态控制区域样式,提供拖拽视觉反馈:
.padding(this.isPadOrPC() ? 24 : 20)
.width(this.isPadOrPC() ? "80%" : "92%")
.height(this.isPadOrPC() ? "70%" : "75%")
// 拖拽悬停时蓝色半透明背景 + 虚线边框
.backgroundColor(this.isDragOver ? 'rgba(0, 122, 255, 0.2)' : 'rgba(255, 255, 255, 0.15)')
.border({
width: this.isDragOver ? 2 : 0,
color: '#007DFF',
style: BorderStyle.Dashed // 虚线边框
})
效果如下:

六、附加:剪贴板复制
除了系统分享面板,图片预览页还提供了三种剪贴板复制方式,适合快速分享链接:
// 复制纯链接
async copyCurrentImageLink() {
const currentImageUrl = this.imgUrls[this.currentIndex];
const pasteData = pasteboard.createData(
pasteboard.MIMETYPE_TEXT_PLAIN, currentImageUrl
);
const systemPasteboard = pasteboard.getSystemPasteboard();
await systemPasteboard.setData(pasteData);
// Toast: 链接已复制
}
// 复制为 Markdown
async copyAsMarkdown() {
const markdown = ``;
const pasteData = pasteboard.createData(
pasteboard.MIMETYPE_TEXT_PLAIN, markdown
);
await pasteboard.getSystemPasteboard().setData(pasteData);
// Toast: Markdown 已复制
}
// 复制为 HTML
async copyAsHtml() {
const html = `<img src="${this.imgUrls[this.currentIndex]}" />`;
const pasteData = pasteboard.createData(
pasteboard.MIMETYPE_TEXT_PLAIN, html
);
await pasteboard.getSystemPasteboard().setData(pasteData);
// Toast: HTML 已复制
}
三种复制格式对比:
七、三种方式对比
| 接收分享 | 发起分享 | 拖拽接收 | |
|---|---|---|---|
| 方向 | 系统面板 -> 本应用 | 本应用 -> 系统面板 | 外部拖拽 -> 本应用 |
| 入口 | EntryAbility.onCreate/onNewWant |
页面按钮点击 | 组件 onDrop 事件 |
| 核心 API | systemShare.getSharedData |
ShareController.show |
event.startDataLoading |
| 数据载体 | SharedRecord (含 uri) |
SharedData (含 uri/utd) |
unifiedDataChannel.Image |
| 需要 module.json5 配置 | 是(skills + utd 声明) | 否 | 否 |
| 适用设备 | 手机/平板/PC | 手机/平板/PC | 主要 PC/平板 |
| 文件来源 | 其他应用分享的文件 URI | 网络下载到缓存 | 拖拽的文件 URI |
三种接收方式的数据处理对比:
八、常见问题与最佳实践
8.1 常见问题
| 问题 | 原因 | 解决 |
|---|---|---|
| 分享面板中看不到本应用 | module.json5 未声明对应 UTD 类型 |
在 skills 中添加 ohos.want.action.sendData + 对应 utd |
| 热启动时分享数据不更新 | 只在 onCreate 处理,未处理 onNewWant |
两个入口都调用 handleShareData |
ShareController.show 报错 |
传入了网络 URL 而非本地文件 URI | 先下载到 cacheDir,用 fileUri.getUriFromPath 转换 |
| 接收的图片 URI 无法上传 | URI 权限问题或已过期 | 接收后尽快使用,不要长时间存储 URI |
| 拖拽只接收了部分文件 | progressListener 中未遍历所有 records |
确保遍历 dragData.getRecords() 全部记录 |
maxFileSupported 不生效 |
声明了数量但系统仍传入更多 | 该字段为建议值,代码中仍需做数量校验 |
8.2 最佳实践
-
UTD 类型用细粒度:用
getUniformDataTypeByFilenameExtension('.jpg', IMAGE)而非通用IMAGE,系统面板能更精准匹配。 -
冷热启动都处理:
onCreate处理冷启动,onNewWant处理热启动,两者缺一不可。 -
分享前清空旧数据:每次
handleShareData前清空uploadList,避免上次分享残留混入。 -
下载文件用 cacheDir:分享用的临时文件放
context.cacheDir,系统会自动清理,不需要手动删除。 -
拖拽类型过滤:
onDrop中按record.getType()过滤,只处理预期类型,忽略非图片文件。 -
拖拽视觉反馈:
onDragEnter/onDragLeave配合isDragOver状态,提供高亮 + 虚线边框反馈,提升用户体验。 -
提供多种分享格式:除了系统分享面板,同时提供链接复制、Markdown 复制、HTML 复制,覆盖不同使用场景。
九、总结
系统分享是应用间数据传递的基础能力,本教程介绍了三个方向的完整实现:
- 接收分享:
module.json5声明 UTD 类型 ->systemShare.getSharedData解析Want-> 遍历SharedRecord提取文件 URI -> 转为业务对象 - 发起分享:下载网络图片到缓存 ->
fileUri转换为file://URI ->new SharedData构造分享数据 ->ShareController.show拉起系统面板 - 拖拽接收:
onDragEnter/Leave/Drop事件 ->event.startDataLoading启动接收 ->progressListener回调中遍历unifiedDataChannel记录 -> 按类型过滤提取图片
开发建议:
- 接收分享必须同时在
onCreate和onNewWant中处理 - 发起分享前必须将网络资源下载为本地文件,不能直接传 URL
- UTD 类型尽量用细粒度,提升系统匹配精准度
- 拖拽接收要做类型过滤,避免非预期文件混入
- 提供多种分享格式(面板 + 剪贴板),覆盖不同使用场景
更多推荐


所有评论(0)