HarmonyOS 系统分享教程:统一拖拽、接收分享与拉起分享面板

一、概述

系统分享是几乎所有应用都会用到的刚需功能。HarmonyOS 提供了 @kit.ShareKit 中的 systemShare 模块,支持应用作为分享的接收方发送方参与系统分享流程

本教程以「云星图」项目为实战案例,讲解三个方向的完整实现:

方向 场景 核心 API
接收分享 系统分享面板 -> 本应用(接收图片上传) systemShare.getSharedData
发起分享 本应用 -> 系统分享面板(分享图片到其他应用) ShareController.show
拖拽接收 拖拽文件到应用区域(PC/平板端接收图片) unifiedDataChannel.startDataLoading

整体数据流向:

拖拽接收

onDrop

外部拖拽图片

unifiedDataChannel
解析拖拽数据

转为 UploadItem
加入上传列表

发起分享

PicView 页面

ShareHelper
下载图片到缓存

构造 SharedData

ShareController.show
拉起系统分享面板

用户选择目标应用

接收分享

getSharedData

系统分享面板

EntryAbility
解析 SharedRecord

转为 UploadItem
跳转上传页

二、前置准备

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.json5skills 中声明应用支持的分享数据类型。系统分享面板会根据这些声明来筛选可接收的应用。

{
  "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 携带分享数据启动应用。需要在 onCreateonNewWant 中都处理:

// 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);   // 热启动时处理分享
  }
}

为什么要两个入口都处理?

否(冷启动)

是(热启动)

用户在系统分享面板选择本应用

应用是否
已在运行?

系统创建 Ability 实例

onCreate(want)
处理分享数据

系统复用已有实例

onNewWant(want)
清空旧数据 + 处理分享数据

handleShareData

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}`);
    });
}

分享数据解析的完整流程:

系统传入 Want

systemShare.getSharedData(want)

SharedData
为空?

返回,不处理

data.getRecords()
获取分享记录数组

记录数为 0?

返回,不处理

清空 uploadList

遍历每条 SharedRecord

record.uri
存在?

跳过该记录

构造 UploadItem
uri = record.uri

push 到 uploadList

还有更多
记录?

AppStorage 写入上传数据
跳转到上传页

关键点:

  • 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);
      }
    }
  }
}

发起分享的完整流程:

选择目标应用

取消分享

shareImageFromUrl(context, imageUrl)

onStart 回调
Toast: 正在准备分享...

http.createHttp 下载图片

下载成功?

onError 回调

fs.open + fs.write
写入 cacheDir/share_xxx.jpg

utd.getUniformDataTypeByFilenameExtension
获取细粒度 UTD: general.jpg

fileUri.getUriFromPath
路径转 file:// URI

new SharedData
构造分享数据

new ShareController(shareData)

controller.show(context, options)
拉起系统分享面板

用户操作

onSuccess 回调

关键细节:

  • 必须先下载到本地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 || '请重试'}` });
    }
  });
}

screenshot_20260722_145035_com.mengxinyuan.starmap

screenshot_20260722_145039_com.mengxinyuan.starmap
分享菜单 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 })

拖拽接收的处理流程:

用户拖拽文件到应用区域

onDragEnter
isDragOver = true
区域高亮

用户释放鼠标/手指

onDrop 触发

isAllUploading?
正在上传中?

直接返回,不处理

准备 distributedFilesDir
作为接收目标目录

event.startDataLoading(options)
启动数据接收

progressListener 回调
随进度持续触发

dragData.getRecords()
获取拖拽数据记录

record.getType()
是 IMAGE?

跳过该记录

取出 image.imageUri

构造 UploadItem
加入 uploadList

还有更多
记录?

onDragLeave
isDragOver = false
取消高亮

关键细节:

  • 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   // 虚线边框
})

效果如下:

统一拖拽2

六、附加:剪贴板复制

除了系统分享面板,图片预览页还提供了三种剪贴板复制方式,适合快速分享链接:

// 复制纯链接
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 = `![image](${this.imgUrls[this.currentIndex]})`;
  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 已复制
}

三种复制格式对比:

分享菜单

复制链接

复制为Markdown

复制为HTML

系统分享

纯文本 URL
https://xxx/img.jpg

![image](https://xxx/img.jpg)

<img src='https://xxx/img.jpg' />

下载图片 + 拉起
系统分享面板

七、三种方式对比

接收分享 发起分享 拖拽接收
方向 系统面板 -> 本应用 本应用 -> 系统面板 外部拖拽 -> 本应用
入口 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

三种接收方式的数据处理对比:

拖拽接收

DragEvent

startDataLoading(options)

progressListener 回调

dragData.getRecords()

record as Image
image.imageUri

构造 UploadItem

发起分享

网络图片 URL

http 下载到 cacheDir

fileUri 转为 file:// URI

new SharedData(utd, uri)

ShareController.show()

接收分享

系统传入 Want

getSharedData(want)

data.getRecords()

record.uri

构造 UploadItem

八、常见问题与最佳实践

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 最佳实践

  1. UTD 类型用细粒度:用 getUniformDataTypeByFilenameExtension('.jpg', IMAGE) 而非通用 IMAGE,系统面板能更精准匹配。

  2. 冷热启动都处理onCreate 处理冷启动,onNewWant 处理热启动,两者缺一不可。

  3. 分享前清空旧数据:每次 handleShareData 前清空 uploadList,避免上次分享残留混入。

  4. 下载文件用 cacheDir:分享用的临时文件放 context.cacheDir,系统会自动清理,不需要手动删除。

  5. 拖拽类型过滤onDrop 中按 record.getType() 过滤,只处理预期类型,忽略非图片文件。

  6. 拖拽视觉反馈onDragEnter/onDragLeave 配合 isDragOver 状态,提供高亮 + 虚线边框反馈,提升用户体验。

  7. 提供多种分享格式:除了系统分享面板,同时提供链接复制、Markdown 复制、HTML 复制,覆盖不同使用场景。

九、总结

系统分享是应用间数据传递的基础能力,本教程介绍了三个方向的完整实现:

  • 接收分享module.json5 声明 UTD 类型 -> systemShare.getSharedData 解析 Want -> 遍历 SharedRecord 提取文件 URI -> 转为业务对象
  • 发起分享:下载网络图片到缓存 -> fileUri 转换为 file:// URI -> new SharedData 构造分享数据 -> ShareController.show 拉起系统面板
  • 拖拽接收onDragEnter/Leave/Drop 事件 -> event.startDataLoading 启动接收 -> progressListener 回调中遍历 unifiedDataChannel 记录 -> 按类型过滤提取图片

开发建议:

  1. 接收分享必须同时在 onCreateonNewWant 中处理
  2. 发起分享前必须将网络资源下载为本地文件,不能直接传 URL
  3. UTD 类型尽量用细粒度,提升系统匹配精准度
  4. 拖拽接收要做类型过滤,避免非预期文件混入
  5. 提供多种分享格式(面板 + 剪贴板),覆盖不同使用场景
Logo

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

更多推荐