【鸿蒙开发实战】HarmonyOS 跨设备分享教程:隔空传送与碰一碰分享
HarmonyOS 跨设备分享教程:隔空传送与碰一碰分享
一、概述
HarmonyOS Share Kit 提供了两种无接触/近场跨设备分享能力,允许应用在不拉起系统分享面板的情况下,直接将内容推送到对端设备:
| 能力 | 事件名 | 触发方式 | 适用场景 |
|---|---|---|---|
| 隔空传送 | gesturesShare |
手势感应(设备举至对端屏幕前方 20~40cm,握拳抓取) | 跨设备推送图片、链接、配置 |
| 碰一碰分享 | knockShare |
两台设备物理轻触 | 近场名片、文件互传 |
本教程以「云星图」项目为实战案例,讲解三种典型用法:
- 隔空手势触发上传 – 手势不传数据,而是触发本机批量上传(
NormalUpLoad) - 隔空/碰一碰分享配置链接 – 将图床配置以 AppLinking 推送到对端(
CloudSetting) - 隔空/碰一碰分享图片 – 将当前查看的云端图片推送到对端设备(
PicView)
整体能力架构如下:
二、前置知识
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) // 主动终止
})
手势/碰一碰触发后的处理流程:
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 内部流程:
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 再 on:
enable被多次调用时(如 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 手势回调中触发上传
手势回调内部的处理决策流程:
对应代码:
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 再切回来,此时需要重新绑定回调。整个页面生命周期与监听注册的关系如下:
对应代码:
@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 链接推送到对端设备,对端打开链接即可自动导入配置。
端到端流程如下:
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域名
六、实战三:隔空/碰一碰分享图片
在图片预览页,用户可以通过隔空手势或碰一碰将当前查看的云端图片推送到对端设备。由于图片在云端,需要先下载到本地缓存再分享。
分享图片的完整处理流程:
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 处理上的差异对比:
八、常见问题与最佳实践
8.1 常见问题
| 问题 | 原因 | 解决 |
|---|---|---|
| 回调不触发 | 设备不支持 / 未开启系统开关 | canIUse 检测 + 引导用户到「设置 -> 快捷启动和手势 -> 隔空传送」 |
off() 注销无效 |
传入的 windowId 与 on() 时不一致 |
管理器中保存 windowId,off 时用同一值 |
| 分享超时失败 | 3 秒内未调用 target.share() |
网络下载图片耗时较长时,先展示 Toast 再异步下载 |
| Tab 切回后手势失效 | 页面未重新注册监听 | 监听 FIRST_LEVEL_INDEX 变化,切回时重新 enable |
| 两台设备无法发现 | 未登录同一华为账号 / 未加信任设备 | 引导用户在「设置 -> 超级终端」中添加信任设备 |
8.2 页面生命周期配对速查
不同页面的注册/注销时机不同,需严格配对:
8.3 最佳实践
-
统一封装管理器:将
on/off逻辑收敛到GestureShareManager/KnockManager,页面只关心回调业务,不直接接触底层 API。 -
生命周期严格配对:每个
enable必须有对应的disable。推荐在aboutToAppear/aboutToDisappear或onShown/onHidden中配对调用,避免页面销毁后回调仍被触发导致空指针。 -
Tab 切换重新注册:ArkUI 的 Tab 页面切换不会触发
aboutToDisappear,需要用@Watch监听 Tab 索引变化,在切回时重新enable以刷新回调。 -
3 秒超时处理:如果回调内需要异步准备数据(如下载图片),建议先同步展示提示,异步完成后调用
target.share()。超过 3 秒系统会自动判定超时。 -
能力降级:
canIUse返回false时,应隐藏相关功能入口或提供系统分享面板作为替代方案。 -
UTD 类型选择:
分享内容 推荐 UTD 纯文本 PLAIN_TEXTURL / AppLinking HYPERLINK图片文件 getUniformDataTypeByFilenameExtension('.jpg', IMAGE)视频文件 getUniformDataTypeByFilenameExtension('.mp4', VIDEO)
九、总结
跨设备分享是 HarmonyOS 分布式能力的典型应用,本教程介绍了三种实战场景:
- 隔空手势触发上传:将手势事件作为本机操作触发器,不传数据,适合"挥手即执行"的交互
- 分享配置链接:将配置序列化为 AppLinking,通过
target.share()推送超链接,对端自动导入 - 分享图片:下载云端图片到缓存,构造
SharedData推送文件 URI
开发建议:
- 先用
canIUse检测设备能力,不支持时优雅降级 - 封装统一管理器,页面只关心业务回调
- 生命周期严格配对
enable/disable,Tab 切换时用@Watch重新注册 - 注意 3 秒超时限制,异步准备数据时先展示提示
- 文件分享必须先转为本地
file://URI,不能直接传网络地址
更多推荐


所有评论(0)