本文主线以 HarmonyOS 6.0 新增能力为切口: clarifyNonShare (6.0.2(22) 起)、手机 ↔ PC/2in1 碰一碰与沙箱接收、绑定窗口的 capability 注册(6.0.0(20) 起)。碰一碰基础能力可回溯到 5.0,文中只作铺垫,一笔带过。文中代码是为说明问题自行编写的完整示例,API 名称、枚举取值与版本号等事实性信息均标注官方出处;涉及真机传输表现的部分已注明,未做任何实测数据编造。
本文主线以 HarmonyOS 6.0 新增能力为切口: clarifyNonShare (6.0.2(22) 起)、手机 ↔ PC/2in1 碰一碰与沙箱接收、绑定窗口的 capability 注册(6.0.0(20) 起)。碰一碰基础能力可回溯到 5.0,文中只作铺垫,一笔带过。文中代码是为说明问题自行编写的完整示例,API 名称、枚举取值与版本号等事实性信息均标注官方出处;涉及真机传输表现的部分已注明,未做任何实测数据编造。
用户手掌在另一台手机背面一攥,手里的文件就飞过去了;对着电脑隔空一抓,手机内容就落进 PC。
用户手掌在另一台手机背面一攥,手里的文件就飞过去了;对着电脑隔空一抓,手机内容就落进 PC。
听着像调一个系统接口的事。V哥打开 Share Kit 文档准备抄示例,第一眼就发现方向不对: 网上的代码几乎都只写 harmonyShare.on('knockShare', cb) ,却没人告诉你这俩事件到底差在哪、回调里不立刻发数据会怎样、PC 端为什么要多传一个 windowId。 这三件事,才是这篇要讲清楚的重点。
碰一碰和隔空传送,在代码里就是两个事件名,都由 harmonyShare 抛出:
| 事件 | 触发方式 | 起始版本 | 说明 | | --- | --- | --- | --- | | knockShare | 设备背部轻贴(碰一碰) | 5.0.0(12) | 基础能力,5.0 时代就有 | | gesturesShare | 空中手势(隔空传送) | 6.0.0(20) | 6.0 才开放 ,是本文的切口之一 |
这就是第一个原创判断: 想落在 6.0 以上,隔空传送( gesturesShare )本身就是现成切口 ——它是 6.0.0(20) 才开放的。碰一碰虽老,但从 6.0.0(20) 起多了一个"带窗口注册"的能力重载,手机 ↔ PC/2in1 的沙箱接收也从这个版本起可用。所以写新功能时,建议两个事件一起注册,覆盖"贴"和"抓"两种动作。
官方在指南里对碰一碰的描述很直接:当宿主应用收到系统发出的碰一碰事件回调,可能因某些原因无法发起分享时,需及时终止,避免用户长时间等待( 碰一碰内容分享 )。这句话其实是全文的纲:回调里你必须"给个说法"。
规划表里常写"注册必须绑 windowId",但翻 API 参考会发现这事没那么绝对。 harmonyShare.on 有两种重载 :
SendCapabilityRegistry 是 6.0.0(20) 才出生的注册配置项,字段有俩: windowId (指定可轻贴的窗口)和 sendOnly: boolean (默认 false,双端都为 true 时双向分享被拦截, 仅碰一碰支持 )。
V哥的自创判断:手机单窗口可以不绑,PC/2in1/Tablet 必须绑。 手机一个应用基本就一个前台窗口,系统知道往哪发;但 PC 上可能开了好几个窗口,你不告诉系统"可轻贴的是主窗口",轻贴就可能落到不可见的窗口上——这正是规划表那句"不绑就等着窗口不可见时乱回调"的真实含义,只不过它不是语法报错,而是多窗口场景下的体验错乱。官方在 off 的说明里也明确写道:推荐在 PC/2in1 和 Tablet 上使用带 windowId 的注册方式。
所以V哥封装时把两种注册分开:手机走 registerOnPhone ,PC/2in1 走先取 windowId 再绑定的 registerOnWindow (见第五节代码)。
回调拿到的 SharableTarget 只是个"执行句柄",真正要发的东西得自己装进 systemShare.SharedData 。最常用的是链接分享,utd 类型填 utd.UniformDataType.HYPERLINK :
import
{ uniformTypeDescriptor
as
utd }
from
'@kit.ArkData'
;
import
{ systemShare, harmonyShare }
from
'@kit.ShareKit'
;
import
{ fileUri }
from
'@kit.CoreFileKit'
;
const
data =
new
systemShare.
SharedData
({
utd
: utd.
UniformDataType
.
HYPERLINK
,
content
:
'https://yourapp.example.com/video/20260910'
,
title
:
'碰一碰分享卡片标题'
,
description
:
'碰一碰分享卡片描述'
});
data.
thumbnailUri
= fileUri.
getUriFromPath
(
this
.
context
.
filesDir
+
'/poster.jpg'
);
title + description + thumbnailUri 三个字段会决定卡片模板(纯图片 / 沉浸式大卡 / 白卡上下),官方有专门的设计指南。 一个隐蔽坑 :预览图建议走沙箱 fileUri ,别直接塞 PixelMap 或外部不可读路径;云端大图来不及下载时,可以先发核心数据、后用 updateShareData 补图,避免卡在下载上超时。
链接分享真正的价值在"落地":utd 配成 general.hyperlink 后,交给 App Linking 接手(见第七节),应用没装也能跳。
这是全文最该抄进架构评审的一节。官方在最佳实践页对碰一碰的措辞是:
收到碰一碰分享事件回调后,需尽快调用 sharableTarget.share() 方法发起分享,超过 3 秒可能会失败。
收到碰一碰分享事件回调后,需尽快调用 sharableTarget.share() 方法发起分享,超过 3 秒可能会失败。
( 分享 App Linking 直达应用 )
V哥的提炼:回调里你只有三条路,且必须在窗口期内选一条——发、澄清、拒绝,缺一不可"给说法"。 clarifyNonShare 和 reject 同样是有效答复,别只在能发时才响应,异常时也得回。
| 方法 | 起始版本 | V哥什么时候用它 |
| --- | --- | --- |
| share(data) | 5.0.0(12) | 当前界面确有可分享内容,装好 SharedData 直接发 |
| clarifyNonShare({message}) | 6.0.2(22) | 界面本身不支持分享(如空白页),礼貌告知并引导去可分享页 |
| reject(errorCode) | 5.0.3(15) | 网络/下载失败等 genuinely 出错,上报错误码让用户知道原因 |
自创口诀:三秒不回话,系统直接判超时——数据要提前备好,不能等用户点了再现拼。 这意味着分享数据(链接、标题、缩略图)最好在进页面时就预备好,回调里只取不拼;任何耗时 IO(云端下载、压缩)都别堵在回调里。
clarifyNonShare 是 6.0.2(22) 才补上的"温和终止":它和 reject 的定位完全不同。
下面是V哥自写的封装,把注册、三秒兜底、三选一都收在一个 KnockShareController 里:
import
{ harmonyShare, systemShare }
from
'@kit.ShareKit'
;
import
{ uniformTypeDescriptor
as
utd }
from
'@kit.ArkData'
;
import
{ fileUri }
from
'@kit.CoreFileKit'
;
import
{
window
}
from
'@kit.ArkUI'
;
import
{ common }
from
'@kit.AbilityKit'
;
import
{
BusinessError
}
from
'@kit.BasicServicesKit'
;
const
RESPOND_DEADLINE_MS
=
3000
;
export
interface
SharePayload
{
link
:
string
;
title
:
string
;
description
:
string
;
thumbPath?:
string
;
}
export
class
KnockShareController
{
private
context
: common.
UIAbilityContext
;
private
watchdog?:
number
;
constructor
(
context: common.UIAbilityContext
) {
this
.
context
= context;
}
registerOnPhone
(
handler
:
(
t: harmonyShare.SharableTarget
) =>
void
):
void
{
const
cb
= (
t: harmonyShare.SharableTarget
) =>
this
.
guard
(handler, t);
harmonyShare.
on
(
'knockShare'
, cb);
harmonyShare.
on
(
'gesturesShare'
, cb);
}
async
registerOnWindow
(
handler
:
(
t: harmonyShare.SharableTarget
) =>
void
):
Promise
<
void
> {
const
win =
await
window
.
getLastWindow
(
this
.
context
);
const
windowId = win.
getWindowProperties
().
id
;
const
cb
= (
t: harmonyShare.SharableTarget
) =>
this
.
guard
(handler, t);
harmonyShare.
on
(
'knockShare'
, { windowId }, cb);
harmonyShare.
on
(
'gesturesShare'
, { windowId }, cb);
}
private
guard
(
handler
:
(
t: harmonyShare.SharableTarget
) =>
void
,
target
: harmonyShare.
SharableTarget
):
void
{
this
.
watchdog
=
setTimeout
(
() =>
{
target.
clarifyNonShare
({
message
:
'分享准备超时,请稍后重试'
});
},
RESPOND_DEADLINE_MS
)
as
unknown
as
number
;
handler
(target);
}
send
(
target
: harmonyShare.
SharableTarget
,
payload
:
SharePayload
):
void
{
const
data =
new
systemShare.
SharedData
({
utd
: utd.
UniformDataType
.
HYPERLINK
,
content
: payload.
link
,
title
: payload.
title
,
description
: payload.
description
});
if
(payload.
thumbPath
) {
data.
thumbnailUri
= fileUri.
getUriFromPath
(payload.
thumbPath
);
}
target.
share
(data)
.
then
(
() =>
this
.
clearWatchdog
())
.
catch
(
(
e: BusinessError
) =>
this
.
terminate
(target, e));
}
private
terminate
(
target
: harmonyShare.
SharableTarget
,
e
:
BusinessError
):
void
{
this
.
clearWatchdog
();
if
(e.
code
=== harmonyShare.
SharableErrorCode
.
DOWNLOAD_ERROR
) {
target.
reject
(harmonyShare.
SharableErrorCode
.
DOWNLOAD_ERROR
);
}
else
{
target.
clarifyNonShare
({
message
:
'当前内容暂不支持分享'
});
}
}
private
clearWatchdog
():
void
{
if
(
this
.
watchdog
!==
undefined
) {
clearTimeout
(
this
.
watchdog
);
this
.
watchdog
=
undefined
;
}
}
unregisterOnPhone
():
void
{
this
.
clearWatchdog
();
harmonyShare.
off
(
'knockShare'
);
harmonyShare.
off
(
'gesturesShare'
);
}
async
unregisterOnWindow
():
Promise
<
void
> {
this
.
clearWatchdog
();
const
win =
await
window
.
getLastWindow
(
this
.
context
);
const
windowId = win.
getWindowProperties
().
id
;
harmonyShare.
off
(
'knockShare'
, { windowId });
harmonyShare.
off
(
'gesturesShare'
, { windowId });
}
}
注意 off 时必须传和 on 一致的事件名与(PC 端的) windowId / callback ,否则注销不掉,残留监听会和其它应用的碰一碰打架。
6.0.0(20) 起,碰一碰不再只是手机对手机—— 手机 ↔ PC/2in1 也能碰,且接收端走沙箱 。这条链路是 6.0 最值得写的新切口:
传输表现(速率、稳定性、机型差异)V哥没法在这里编—— 以真机实测为准 。文档明确这类近场能力要求双端都具备 SystemCapability.Collaboration.HarmonyShare ,且建议双方登录同一华为账号或已在信任设备列表。
链接分享的最后一公里是"对端没装你的应用怎么办"。官方给的方案是 App Linking:
所以 content 里那条链接别随便写——它既是分享内容,也是落地路由。配上 module.json5 里的 skills (scheme 用 https、配 entity.system.browsable 、 domainVerify: true ),端到端才算通。
本文涉及的事实性信息(API 名称、枚举取值、版本号、官方约束)来自以下官方文档,文中的结构、代码示例、决策流程与自检清单为本人整理编写:
最后一句 :这两个能力真正的难点不在"注册"那一行,而在回调之后那 3 秒——你得提前把数据备好、把异常分支接全、把生命周期对齐。想清楚这三件事,剩下的就是 on 一下、 share 一下。
转载声明:本文为转载整理,原文版权归原作者及原站点所有。
原文来源:Juejin HarmonyOS
原文作者:威哥爱编程;如有侵权,请联系管理员删除。