HarmonyOS 跨设备分享教程:隔空传送与碰一碰分享

一、概述

HarmonyOS Share Kit 提供了两种无接触/近场跨设备分享能力,允许应用在不拉起系统分享面板的情况下,直接将内容推送到对端设备:

能力 事件名 触发方式 适用场景
隔空传送 gesturesShare 手势感应(设备举至对端屏幕前方 20~40cm,握拳抓取) 跨设备推送图片、链接、配置
碰一碰分享 knockShare 两台设备物理轻触 近场名片、文件互传

本教程以「云星图」项目为实战案例,讲解三种典型用法:

  1. 隔空手势触发上传 – 手势不传数据,而是触发本机批量上传(NormalUpLoad
  2. 隔空/碰一碰分享配置链接 – 将图床配置以 AppLinking 推送到对端(CloudSetting
  3. 隔空/碰一碰分享图片 – 将当前查看的云端图片推送到对端设备(PicView

整体能力架构如下:

三种实战场景

管理器封装(common/basic)

HarmonyOS Share Kit

gesturesShare
隔空传送事件

knockShare
碰一碰事件

GestureShareManager
enable / disable

KnockManager
enable / disable

场景一:隔空触发上传
NormalUpLoad.ets
手势 -> 本机批量上传

场景二:分享配置链接
CloudSetting.ets
配置 -> AppLinking -> 推送

场景三:分享图片
PicView.ets
云端图片 -> 下载 -> 推送

二、前置知识

2.1 能力边界与运行要求

条件 要求
系统版本 HarmonyOS 5.0 及以上
硬件能力 两台设备均需支持 SystemCapability.Collaboration.HarmonyShare
账号条件 双方登录同一华为账号,或已在「信任设备」列表中互相添加
权限 无需额外权限,系统统一管控设备发现与连接

2.2 核心对象关系

harmonyShare.on('gesturesShare'/'knockShare', { windowId }, (target) => {
    // target: SharableTarget -- 本次传送的执行句柄
    // 3 秒内必须调用以下方法之一,否则系统判定超时
    target.share(shareData)              // 发起传送
    target.clarifyNonShare({ message })  // 当前无可分享内容
    target.reject(errorCode)             // 主动终止
})

手势/碰一碰触发后的处理流程:

是: target.share(data)

是: target.clarifyNonShare

是: target.reject

否: 超时

系统检测到手势/碰一碰

回调触发
传入 target: SharableTarget

3 秒内是否调用
target 方法?

发起传送
数据推送到对端

提示无可分享内容
本次不传数据

主动终止
上报错误码

系统判定超时
本次传送自动失败

2.3 依赖导入

import { harmonyShare, systemShare } from '@kit.ShareKit';
import { uniformTypeDescriptor as utd } from '@kit.ArkData';
import { window } from '@kit.ArkUI';
import { common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';

三、封装分享管理器

两种分享方式的监听注册/注销逻辑几乎一致,区别仅在于事件名(gesturesShare vs knockShare)。下面分别封装为静态管理器,供多个页面复用。

管理器的 enable / disable 内部流程:

disable()

isRegistered?

直接返回

harmonyShare.off
用同一 windowId 注销

清空 windowId / callback

isRegistered = false

enable(context, callback)

canIUse 能力检测

支持?

直接返回

已注册?

先 off 注销旧监听

window.getLastWindow
获取窗口 ID

harmonyShare.on
绑定 windowId 注册监听

isRegistered = true

3.1 隔空传送管理器

核心职责:能力检测 -> 获取窗口 ID -> 注册/注销监听 -> 回调分发。

// GestureShareManager.ets
import { harmonyShare } from '@kit.ShareKit';
import { window } from '@kit.ArkUI';
import { common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';

export interface GestureShareCallback {
  onGestureDetected?: (target: harmonyShare.SharableTarget) => void;
  onError?: (error: BusinessError) => void;
}

export class GestureShareManager {
  private static isRegistered: boolean = false;
  private static windowId: number | null = null;
  private static callback: GestureShareCallback | null = null;

  static enable(context: common.UIAbilityContext, callback: GestureShareCallback): void {
    // 1. 能力检测:不支持直接返回
    if (!canIUse('SystemCapability.Collaboration.HarmonyShare')) {
      return;
    }

    // 2. 已注册则先注销再重新注册,确保回调更新为最新
    if (GestureShareManager.isRegistered) {
      harmonyShare.off('gesturesShare', { windowId: GestureShareManager.windowId! });
      GestureShareManager.isRegistered = false;
    }

    GestureShareManager.callback = callback;

    // 3. 获取当前窗口 ID,绑定监听到具体窗口
    window.getLastWindow(context).then((windowClass) => {
      GestureShareManager.windowId = windowClass.getWindowProperties().id;
      harmonyShare.on('gesturesShare', { windowId: GestureShareManager.windowId }, (target) => {
        if (GestureShareManager.callback?.onGestureDetected) {
          GestureShareManager.callback.onGestureDetected(target);
        }
      });
      GestureShareManager.isRegistered = true;
    }).catch((err: BusinessError) => {
      if (GestureShareManager.callback?.onError) {
        GestureShareManager.callback.onError(err);
      }
    });
  }

  static disable(): void {
    if (!GestureShareManager.isRegistered || GestureShareManager.windowId === null) {
      return;
    }
    harmonyShare.off('gesturesShare', { windowId: GestureShareManager.windowId });
    GestureShareManager.isRegistered = false;
    GestureShareManager.windowId = null;
    GestureShareManager.callback = null;
  }

  static isEnabled(): boolean {
    return GestureShareManager.isRegistered;
  }
}

关键设计点:

  • 窗口绑定harmonyShare.on 的第二个参数是 { windowId },必须传入当前窗口 ID。这是因为系统需要知道哪个窗口在监听手势,以便在窗口不可见时自动暂停。
  • 先 off 再 onenable 被多次调用时(如 Tab 切回页面),先注销旧监听再注册新回调,避免回调过期。
  • 静态单例:全应用只需一个监听实例,页面切换时通过 enable/disable 切换回调即可。

3.2 碰一碰管理器

结构与隔空传送完全一致,仅事件名不同:

// KnockController.ets
export class KnockManager {
  private static isRegistered: boolean = false;
  private static windowId: number | null = null;
  private static callback: KnockShareCallback | null = null;

  static enable(context: common.UIAbilityContext, callback: KnockShareCallback): void {
    if (!canIUse('SystemCapability.Collaboration.HarmonyShare')) {
      return;
    }
    if (KnockManager.isRegistered) {
      return; // 碰一碰无需重复注册
    }
    KnockManager.callback = callback;
    window.getLastWindow(context).then((windowClass) => {
      KnockManager.windowId = windowClass.getWindowProperties().id;
      harmonyShare.on('knockShare', { windowId: KnockManager.windowId }, (target) => {
        if (KnockManager.callback?.onKnockDetected) {
          KnockManager.callback.onKnockDetected(target);
        }
      });
      KnockManager.isRegistered = true;
    }).catch((err: BusinessError) => {
      if (KnockManager.callback?.onError) {
        KnockManager.callback.onError(err);
      }
    });
  }

  static disable(): void {
    if (!KnockManager.isRegistered || KnockManager.windowId === null) return;
    harmonyShare.off('knockShare', { windowId: KnockManager.windowId });
    KnockManager.isRegistered = false;
    KnockManager.windowId = null;
    KnockManager.callback = null;
  }
}

四、实战一:隔空手势触发上传

这是最特殊的用法——隔空手势事件不用于传输数据,而是作为应用内操作触发器。在云星图中,用户在上传页添加好图片后,隔空握拳即可一键开始批量上传。虽然使用频率不高,但交互体验非常出色。

4.1 页面生命周期中注册/注销

// NormalUpLoad.ets
@State private isGestureUploadLocked: boolean = false;

async aboutToAppear(): Promise<void> {
  const context = this.getUIContext().getHostContext() as common.UIAbilityContext;
  GestureShareManager.enable(context, {
    onGestureDetected: (target) => {
      this.handleGestureUpload(target);
    },
    onError: (err) => {
      hilog.error(0x0000, 'NormalUpLoad', `隔空监听错误: ${err.message}`);
    }
  });
}

async aboutToDisappear(): Promise<void> {
  GestureShareManager.disable();
}

4.2 手势回调中触发上传

手势回调内部的处理决策流程:

手势触发
onGestureDetected(target)

uploadList
为空?

Toast: 请先添加图片

isAllUploading
或 isGestureUploadLocked?

Toast: 上传进行中,请稍后

isGestureUploadLocked = true

Toast: 隔空上传已触发

startBatchUpload()

finally: isGestureUploadLocked = false

对应代码:

private handleGestureUpload(target: harmonyShare.SharableTarget): void {
  const prompt = this.getUIContext().getPromptAction();

  // 前置校验:列表为空
  if (this.uploadList.length === 0) {
    prompt.showToast({ message: '请先添加图片' });
    return;
  }

  // 防重复触发:上传中或锁定期内忽略
  if (this.isAllUploading || this.isGestureUploadLocked) {
    prompt.showToast({ message: '上传进行中,请稍后' });
    return;
  }

  this.isGestureUploadLocked = true;
  prompt.showToast({ message: '隔空上传已触发' });

  // 复用已有的批量上传方法
  this.startBatchUpload().finally(() => {
    this.isGestureUploadLocked = false;
  });
}

设计要点:

  • isGestureUploadLocked 互斥锁:手势可能在短时间内连续触发(用户手抖),用独立锁保证同一时间只有一次上传在执行。
  • target 参数不使用:此场景下不需要向对端传数据,target 仅作为事件信号。但不能不处理 target——如果 3 秒内不调用 target.share() / target.clarifyNonShare() / target.reject(),系统会报超时。这里选择忽略(让系统自动超时),因为业务上不需要反馈。

提示:如果你的场景需要更严谨的处理,可以在回调内立即调用 target.clarifyNonShare({ message: '触发本机上传' }),告知系统本次不传数据。

4.3 Tab 切回时重新注册

用户可能切到其他 Tab 再切回来,此时需要重新绑定回调。整个页面生命周期与监听注册的关系如下:

监听状态

NormalUpLoad 页面生命周期

切走

切回上传页
onTabChanged

aboutToAppear

页面可见

用户切换 Tab

页面不可见
(不触发 aboutToDisappear)

重新 enable

aboutToDisappear

enable 注册手势

监听中

disable 注销

对应代码:

@StorageLink(StateKeys.FIRST_LEVEL_INDEX) @Watch('onTabChanged')
firstLevelIndex: number = 0;

private onTabChanged(): void {
  if (this.firstLevelIndex === 1) { // 1 = 上传页
    GestureShareManager.enable(this.context, {
      onGestureDetected: (target) => {
        this.handleGestureUpload(target);
      },
      onError: (err) => {
        hilog.error(0x0000, 'NormalUpLoad', `隔空监听错误: ${err.message}`);
      }
    });
    this.reInitCloudService();
  }
}

五、实战二:隔空/碰一碰分享配置链接

在设置页配置好图床后,用户可以通过隔空手势或碰一碰将配置以 AppLinking 链接推送到对端设备,对端打开链接即可自动导入配置。

端到端流程如下:

接收端

发送端(CloudSetting 页)

系统跨设备传输

用户配置好图床

隔空手势/碰一碰触发

configToImportData
配置序列化

encodeURIComponent
URL 编码

拼接 AppLinking URL

构造 SharedData
UTD: HYPERLINK

target.share(shareData)

系统收到链接

自动打开 AppLinking

EntryAbility.onNewWant

解析 URL config 参数

importDataToConfig
还原配置

StorageUtil.saveConfig
保存到本地

跳转设置页
表单自动填充

5.1 注册两种监听

// CloudSetting.ets
aboutToAppear() {
  const context = this.getUIContext().getHostContext() as common.UIAbilityContext;

  // 隔空传送
  GestureShareManager.enable(context, {
    onGestureDetected: this.handleGestureShare
  });

  // 碰一碰
  KnockManager.enable(context, {
    onKnockDetected: this.handleKnockShare
  });
}

aboutToDisappear() {
  GestureShareManager.disable();
  KnockManager.disable();
}

5.2 构造分享数据并推送

两种手势的回调逻辑完全一致——都是把当前配置编码为 AppLinking 链接,通过 target.share() 推送:

private handleGestureShare = (target: harmonyShare.SharableTarget) => {
  const appLinkingUrl = this.generateAppLinkingUrl();
  if (!appLinkingUrl) return;

  const shareData = new systemShare.SharedData({
    utd: utd.UniformDataType.HYPERLINK,   // 超链接类型
    content: appLinkingUrl,                // AppLinking 链接
    title: `${this.getProviderName()}配置导入`,
    description: '接收后自动导入配置'
  });

  target.share(shareData).then(() => {
    promptAction.showToast({ message: `${this.getProviderName()}配置已准备就绪` });
  }).catch((err: BusinessError) => {
    promptAction.showToast({ message: `配置准备失败:${err.message}` });
  });
};

// 碰一碰回调完全相同
private handleKnockShare = (target: harmonyShare.SharableTarget) => {
  this.handleGestureShare(target); // 直接复用
};

5.3 生成 AppLinking 配置链接

private generateAppLinkingUrl(): string | null {
  if (!this.isConfigComplete()) {
    promptAction.showToast({ message: '请先填写完整配置信息' });
    return null;
  }
  const exportData: ImportConfigData = configToImportData(this.config);
  const configJson = JSON.stringify(exportData);
  const encodedConfig = encodeURIComponent(configJson);
  return `https://你的域名/import?config=${encodedConfig}`;
}

对端设备收到链接后,系统会自动打开浏览器跳转到该 AppLinking,应用通过 EntryAbility.onNewWant 解析 URL 参数即可导入配置。

PS: 需要注意本示例代码中你的域名部分需要替换为自己的AppLink域名

六、实战三:隔空/碰一碰分享图片

在图片预览页,用户可以通过隔空手势或碰一碰将当前查看的云端图片推送到对端设备。由于图片在云端,需要先下载到本地缓存再分享。

分享图片的完整处理流程:

手势/碰一碰触发
target: SharableTarget

Toast: 正在准备传输...

当前图片 URL
存在?

返回,不处理

http.createHttp
下载云端图片

下载成功?

Toast: 传输失败

fs.open + fs.write
写入缓存文件

fileUri.getUriFromPath
转换为 file:// URI

utd.getUniformDataTypeByFilenameExtension
获取细粒度 UTD 类型

new SharedData
构造分享数据

target.share(shareData)

推送成功?

Toast: 准备完成

Toast: 传输失败

6.1 注册监听(页面可见时)

// PicView.ets
.onShown(() => {
  const context = this.getUIContext().getHostContext() as common.UIAbilityContext;

  GestureShareManager.enable(context, {
    onGestureDetected: this.handleGestureShare,
    onError: (err) => {
      hilog.error(0x0000, 'PicView', `隔空监听错误: ${err.message}`);
    }
  });

  KnockManager.enable(context, {
    onKnockDetected: this.handleKnockShare,
    onError: (err) => {
      hilog.error(0x0000, 'PicView', `碰一碰监听错误: ${err.message}`);
    }
  });
})
.onHidden(() => {
  GestureShareManager.disable();
  KnockManager.disable();
})

6.2 下载图片并推送

private handleGestureShare = (target: harmonyShare.SharableTarget) => {
  this.executeShare(target);
};

private handleKnockShare = (target: harmonyShare.SharableTarget) => {
  this.executeShare(target);
};

private async executeShare(target: harmonyShare.SharableTarget): Promise<void> {
  const currentImageUrl = this.imgUrls[this.currentIndex];
  if (!currentImageUrl) return;

  const uiContext = this.getUIContext();
  const promptAction = uiContext.getPromptAction();
  promptAction.showToast({ message: '检测到分享事件,正在准备传输...' });

  try {
    const context = uiContext.getHostContext() as common.UIAbilityContext;
    const cacheDir = context.cacheDir;
    const imageFileName = `share_${Date.now()}.jpg`;
    const imagePath = `${cacheDir}/${imageFileName}`;

    // 1. 下载云端图片到本地缓存
    const httpRequest = http.createHttp();
    const response = await httpRequest.request(currentImageUrl, {
      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(imagePath, fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE);
    await fs.write(file.fd, arrayBuffer);
    await fs.close(file.fd);

    // 3. 构造分享数据(必须用 fileUri 转换为跨应用可访问的 URI)
    const shareUri = fileUri.getUriFromPath(imagePath);
    const utdTypeId = utd.getUniformDataTypeByFilenameExtension('.jpg', utd.UniformDataType.IMAGE);
    const shareData = new systemShare.SharedData({
      utd: utdTypeId,
      uri: shareUri,
      title: '来自云星图的图片',
      description: '碰一碰/隔空分享'
    });

    // 4. 推送到对端
    await target.share(shareData);
    promptAction.showToast({ message: '准备完成' });
  } catch (error) {
    const err = error as BusinessError;
    promptAction.showToast({ message: `传输失败:${err.message || '请重试'}` });
  }
}

关键细节:

  • 必须先下载再分享target.share() 要求传入本地文件 URI,不能直接传网络 URL。
  • fileUri.getUriFromPath:将文件系统路径转换为 file:// URI,这是跨应用/跨设备可访问的格式。
  • UTD 类型用细粒度:用 getUniformDataTypeByFilenameExtension('.jpg', IMAGE) 而非通用 IMAGE,有助于对端系统精准匹配接收应用。

七、三种场景对比

隔空触发上传 分享配置链接 分享图片
使用事件 gesturesShare gesturesShare + knockShare gesturesShare + knockShare
target 用法 不调用(忽略超时) target.share(linkData) target.share(imageData)
UTD 类型 HYPERLINK IMAGE(细粒度 .jpg)
数据来源 本机配置序列化 云端下载到缓存
注册时机 aboutToAppear + Tab 切回 aboutToAppear onShown(页面可见)
注销时机 aboutToDisappear aboutToDisappear onHidden(页面隐藏)
防重机制 isGestureUploadLocked 互斥锁 无需(单次推送) 无需(单次推送)

三种场景在 target 处理上的差异对比:

手势/碰一碰触发
获得 target: SharableTarget

场景一:隔空触发上传

不调用 target 方法
(让系统自动超时)

执行本机批量上传

场景二:分享配置链接

配置序列化为 AppLinking

target.share(SharedData)
UTD: HYPERLINK

对端打开链接自动导入

场景三:分享图片

下载云端图片到缓存

构造 SharedData
UTD: IMAGE

target.share(SharedData)

对端接收图片文件

八、常见问题与最佳实践

8.1 常见问题

问题 原因 解决
回调不触发 设备不支持 / 未开启系统开关 canIUse 检测 + 引导用户到「设置 -> 快捷启动和手势 -> 隔空传送」
off() 注销无效 传入的 windowId 与 on() 时不一致 管理器中保存 windowId,off 时用同一值
分享超时失败 3 秒内未调用 target.share() 网络下载图片耗时较长时,先展示 Toast 再异步下载
Tab 切回后手势失效 页面未重新注册监听 监听 FIRST_LEVEL_INDEX 变化,切回时重新 enable
两台设备无法发现 未登录同一华为账号 / 未加信任设备 引导用户在「设置 -> 超级终端」中添加信任设备

8.2 页面生命周期配对速查

不同页面的注册/注销时机不同,需严格配对:

PicView(NavDestination)

onShown
enable x2

页面可见

onHidden
disable x2

CloudSetting(Tab 页)

aboutToAppear
enable x2

页面可见

aboutToDisappear
disable x2

NormalUpLoad(Tab 页)

切回

切走

aboutToAppear
enable

页面可见

@Watch Tab 切换

重新 enable

aboutToDisappear
disable

8.3 最佳实践

  1. 统一封装管理器:将 on/off 逻辑收敛到 GestureShareManager / KnockManager,页面只关心回调业务,不直接接触底层 API。

  2. 生命周期严格配对:每个 enable 必须有对应的 disable。推荐在 aboutToAppear/aboutToDisappearonShown/onHidden 中配对调用,避免页面销毁后回调仍被触发导致空指针。

  3. Tab 切换重新注册:ArkUI 的 Tab 页面切换不会触发 aboutToDisappear,需要用 @Watch 监听 Tab 索引变化,在切回时重新 enable 以刷新回调。

  4. 3 秒超时处理:如果回调内需要异步准备数据(如下载图片),建议先同步展示提示,异步完成后调用 target.share()。超过 3 秒系统会自动判定超时。

  5. 能力降级canIUse 返回 false 时,应隐藏相关功能入口或提供系统分享面板作为替代方案。

  6. UTD 类型选择

    分享内容 推荐 UTD
    纯文本 PLAIN_TEXT
    URL / AppLinking HYPERLINK
    图片文件 getUniformDataTypeByFilenameExtension('.jpg', IMAGE)
    视频文件 getUniformDataTypeByFilenameExtension('.mp4', VIDEO)

九、总结

跨设备分享是 HarmonyOS 分布式能力的典型应用,本教程介绍了三种实战场景:

  • 隔空手势触发上传:将手势事件作为本机操作触发器,不传数据,适合"挥手即执行"的交互
  • 分享配置链接:将配置序列化为 AppLinking,通过 target.share() 推送超链接,对端自动导入
  • 分享图片:下载云端图片到缓存,构造 SharedData 推送文件 URI

开发建议:

  1. 先用 canIUse 检测设备能力,不支持时优雅降级
  2. 封装统一管理器,页面只关心业务回调
  3. 生命周期严格配对 enable/disable,Tab 切换时用 @Watch 重新注册
  4. 注意 3 秒超时限制,异步准备数据时先展示提示
  5. 文件分享必须先转为本地 file:// URI,不能直接传网络地址
Logo

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

更多推荐