HarmonyOS 7 新特性4:碰一碰——双机实测轻碰坐标与 Share Kit 卡片数据准备
HarmonyOS 7 新特性4:碰一碰——双机实测轻碰坐标与 Share Kit 卡片数据准备
1、引言
两台设备顶端轻轻一碰,这边屏幕上的内容就出现在那边——碰一碰(knock share)把「分享」这件要从联系人列表里挑半天的事,简化成了一次物理接触。26.0.0 给它加了个很关键的增量:手机跟 PC/2in1 或者平板互碰时,被碰的那一方能从事件回调里拿到轻碰坐标,坐标系以屏幕左上角为原点。这个坐标的用途很实在——决定分享的数据落在对方屏幕的哪个位置,而不是闷头丢到应用的默认角落。
碰一碰整条链路需要两台设备,但有意思的是,碰之前要做的事、碰之后能解析的东西,大部分一台设备就能验证。我把接口面拆成五组小实验挨个跑了一遍:回调注册、数据准备、能力登记、坐标换算、引导资产,能测的全测干净;真正碰起来之后,坐标到底长什么样、哪一侧拿得到,也在手上两台真机(Mate 60 Pro 配 MatePad Edge)上拿到了第一手读数。
环境声明:HarmonyOS 7.0.0.107 (API 26) Release,实测工程轻碰台(com.qingkouwei.lightknock);DevEco Studio 26.0.0(SDK 26.0.0.105 Release),手机 Mate 60 Pro(ALN-AL80,麒麟 9000S)做发送侧、平板 MatePad Edge(QXS-W00,麒麟 X90A)做接收侧。
2、效果展示与诊断台结构
轻碰台是诊断台的形态(这个系列里有先例:轻规划冲突台、轻探能力台),把能单独验证的环节都做成了一键能点的按钮,每个按钮点下去读数直接显示在页面上。互碰的部分则专门做了个探针页,两台设备各开一个,一边注册成发送角色、一边注册成接收角色,碰完读数就出来了。
准备面五个按钮全跑完的效果:注册成功、76B 的演示文本落到沙箱、SharedData 回读 records=1、windowId=65 登记和解绑都正常、坐标换算命中 true、引导目录读到 1 项。

工程结构很简单,一页代码加一个资产目录:
entry/src/main/
├── ets/pages/KnockLabPage.ets // 诊断台(K1-K5)
└── resources/rawfile/
├── knock_demo.txt // 演示数据源
└── knock_share_guide/ // 引导资产目录(官方约定的目录名)
这里单独说一下 knock_share_guide 这个目录:名字是官方约定死的,互碰触发时系统会从这个目录里取引导动图来播。妙就妙在——目录不存在的时候什么错都不报,构建正常、运行正常,只是碰的时候没有引导动画。这种「静默缺失」的资产最容易漏打进去,所以我把它做成了一个显式的检查项,打包完读一次,读不到就报警。
3、Kit 与 API:对称的双回调与坐标语义
接口都在 @kit.ShareKit 的 harmonyShare 命名空间里(同一个 kit 下还有个 systemShare,管的是分享数据本体)。碰一碰的接口面是一对对称的双回调:
| 侧 | 注册 | 回调参数 | 核心方法 |
|---|---|---|---|
| 发送侧 | on('knockShare', cb) | SharableTarget | share(SharedData) / reject() / updateShareData() |
| 接收侧 | on('dataReceive', registry, cb) | ReceivableTarget | receive(sandboxUri, cb) / reject() |
两边的 target 都带一个 getInfo(),返回 { coordinate?: CoordinateInfo },而 CoordinateInfo 就是简单的 { screenX, screenY }。发送侧和接收侧理论上都能拿到坐标——发送侧拿到的是「对方碰在我屏幕上的映射点」,接收侧拿到的是「我碰在对方屏幕上的映射点」,坐标系统一是屏幕左上角原点。注意 coordinate 是可选字段(coordinate?),在不支持轻碰坐标的设备或者场景下会是 undefined,所以取值之前必须判空——这是接口面写进签名里的第一个规矩,后面会看到它在真机上意味着什么。
接收侧的注册要多传一个参数 RecvCapabilityRegistry = { windowId: number, capabilities: [{ utd, maxSupportedCount }] },作用相当于告诉系统「我这个窗口,能收哪些 UTD 类型的数据,每种最多收几条」。windowId 从 window.getLastWindow 现取,utd 用 uniformTypeDescriptor 的常量,别手写字符串。
数据本体是 systemShare.SharedData:构造时收一条 SharedRecord(utd 必填,content/uri/title/label/description/thumbnail/thumbnailUri/extraData 都是可选),后续用 addRecord 追加、getRecords 回读。官方给了两条硬约束:分享数据的描述总大小不超过 200KB、记录数不超过 500。
版本上有一句话得说清楚,免得写错:碰一碰的基础能力从 HarmonyOS 5 就有了,26.0.0 新增的只是轻碰坐标——把它整个写成本版本新能力是不对的。
互碰之后对方屏幕上出现的是 Share Kit 卡片,有三种模板:纯图片、沉浸式大卡、白卡上下结构。卡片是系统渲染的,应用能插手的只有喂给它的素材:预览图推荐 3:4 比例、600×800 起步、最大 3000×4000——比例不对会被裁,尺寸不够会糊,这两个数就是素材的验收线。卡片跟坐标的关系也理一下:坐标决定卡片放在屏幕哪儿,模板决定卡片长什么样,两件事互相独立。
4、逻辑流
把碰一碰从注册到触发串一遍,先看准备面:
真碰起来之后,完整的一轮大概是这样:
这张图里有一行跟官方文档的印象不太一样:getInfo().coordinate 在发送侧真机上拿不到(undefined),得走判空降级——这不是推测,是下面 5.6 实测出来的。坐标的「放置」语义,实际只有接收侧那半边能用。
5、实战:一组准备面读数,加一次双机互碰
5.1 K1 注册与解绑跑通
on('knockShare', cb) 注册没有异常,off('knockShare', cb) 解绑也没有异常,一来一回跑通。注册成功当然不等于整条链路一定可用,但注册失败就一定不可用,所以它是个必要的地基。回调体里我把坐标读取的逻辑提前埋好——先 getInfo(),判空,再取 screenX/Y——这样真碰起来的时候读数会自动进日志。
5.2 K2 数据准备链
把 rawfile 里 76B 的演示文本用 fs.openSync + writeSync 写进沙箱,然后用 utd=general.plain-text(uniformTypeDescriptor 的常量)加上 uri 构造 SharedData,再用 getRecords 回读——records=1、uri 还在,资产到沙箱到分享结构这三段就串通了。至于 200KB/500 条那两条约束,单条 76B 根本够不着,构建这一步不会触发它们。
5.3 K3 能力登记
window.getLastWindow 实际拿到 windowId=65,用这个值加两条能力(plain-text 上限 10 条、image 上限 4 条)构造 RecvCapabilityRegistry,on('dataReceive', registry, cb) 注册成功。解绑的时候踩了个坑,值得单独说:off('dataReceive') 的第二个参数是 registry,不是 callback——完整签名是 off(event, capability, callback?),registry 必传、callback 反而可选。我一开始照着 knockShare 那种「对称」的直觉去传 callback,编译直接报 overload 不匹配。教训是:看着对称的两个回调,on/off 的签名得一个一个对着头文件核,别想当然。
5.4 K4 四段换算
轻碰坐标是屏幕级的,但你要落到某个控件上,中间隔着四段换算:屏幕坐标(左上原点)→ 窗口坐标(减掉窗口在屏幕上的偏移,全屏窗口偏移是 0、浮窗就得读 windowRect)→ 页面坐标(再减页面在窗口里的偏移,包括顶部避让区高度)→ 控件命中(落在哪个热区矩形里)。演示里屏幕坐标 (630,900) 换算下来命中了热区 [430,700,830,1100],返回 true。
这里有两个坑要特别留神。第一,坐标系是屏幕左上角原点(官方明确写了),跟 ArkUI 组件自己那套局部坐标完全不是一回事,中间哪一段换算错了,放置就偏一截。第二,判命中要用热区矩形,别精确匹配像素点。碰这个动作的物理精度天然在厘米级,你要求坐标正好落在某个点上根本不现实——官方博客也点名过像素匹配的脆弱。
把误差来源拆开看更清楚:第一段(屏幕→窗口)的误差来自取 windowRect 的时机,浮窗被拖动后 windowRect 会变,得在回调触发的那一刻现取、不能缓存;第二段(窗口→页面)的误差来自不同设备形态避让区和标题栏的高度差异,直板机有状态栏、PC 形态的标题栏在窗口内部,pageOffY 怎么取会随形态变;第三段(页面→控件)的误差其实不是换算误差,而是热区本身划得多大的问题——热区是业务决策,不是数学问题。前两段能测能修,最后一段只能声明边界:热区划得粗,换算再准也放不准,这正好是「用区域别用像素」这条的另一面。
5.5 K5 引导资产
getRawFileList('knock_share_guide') 返回 1 项,说明目录打进包了、也能读出来。真正上线时把里面的占位文件换成设计做好的动图就行,目录名和读取方式都不用动。这个检查的价值不在读到了什么,而在读不到的时候你能第一时间知道——毕竟系统对缺失这件事是完全沉默的。
5.6 双机互碰:坐标拿到了,还发现发送侧其实没有坐标
准备面做完,手上正好有 Mate 60 Pro 和 MatePad Edge,就把两台各开了一个探针页——手机做发送侧、平板做接收侧,两边点「注册双角色」,然后顶端一碰。这次互碰的收获比预想的大,有三条:
先说接收侧的坐标,屏幕左上角原点,直接拿到手。 平板的 dataReceive 回调里 getInfo().coordinate 返回了 screen=(1409,598)。碰的位置就在平板屏幕中部偏左上,跟实际对得上——也就是说 K4 那条四段换算链,输入端从此有了真值锚点,不用再拿假想坐标演示了。后来又碰了几次,读数里 (1856,723)、(1537,758) 这些值都是这么来的,每次碰的落点都不一样,但坐标语义一直稳定。
再说发送侧一个不起眼但很关键的发现:手机的 coordinate 是 undefined。 连着碰了好几次,手机作为发送侧,每次 getInfo().coordinate 都走到判空那条分支去。官方文档把 coordinate 写成可选字段、只说「不支持的设备为 undefined」,读起来像个边缘情况;真碰过才知道,手机当发送侧的时候系统就是不下发坐标。换句话说,「碰哪儿就放在哪儿」这个漂亮语义,实际上只在接收侧那一半成立;发送侧想放置,只能老老实实退到默认位置。这个不对称在文档里没有明说,是碰出来的。
第三条关于 resolve 的时延,以及它到底代表什么。 手机 share、平板 receive 的 resolve 都在 1-2ms 这个量级,快得像是秒传。但毫秒级的 resolve 只代表「系统受理了这个动作」,不代表文件真的传过去了。这提醒很值钱:别拿 resolve 成功当传输完成的证据,业务上判断「传完了」得看真正收到数据的那个回调,不是看 promise resolve。
平板接收侧的读数(坐标真值 + 第一次触发 + resolve 时延):

手机发送侧的读数(coordinate=undefined 判空命中 + resolve 时延):

6、避坑
6.1 off 的不对称签名:dataReceive 解绑要带 registry
现象:解绑 dataReceive 时按 knockShare 那种写法传了个 callback 进去,编译报 No overload matches this call。
原因:on('dataReceive') 注册时参数里带了 registry,对应的 off 签名是 off(event, capability, callback?)——registry 是必传的第二个参数,callback 反而是可选的。看着对称的两个回调,off 的入参却不对称。
怎么办:注册时就把 registry 存成成员变量,解绑时直接复用;双回调的 on/off 签名逐个对着头文件核,别凭对称直觉写。
确认:换成传 registry 之后编译过了,登记—解绑在真机上跑通无异常。
6.2 coordinate 是可选字段,发送侧真机上就是 undefined
现象:回调体里直接 info.coordinate.screenX 的写法很危险——coordinate 的类型是 CoordinateInfo?,可能为 undefined。
原因:官方文档写了这是可选字段,但把它归到「不支持坐标的设备」那种边缘情况。真机互碰才看到另一面:手机作为发送侧时 coordinate 每次都是 undefined,系统压根没下发。这不是理论分支,是常态。
怎么办:取坐标前先判空,拿不到就走默认放置位的降级逻辑(屏幕中心或应用主区域都行)。设计上要接受一个事实——「碰哪儿放哪儿」这个卖点,实际只有被碰的那一侧能用;发起分享的一侧只能保证「碰了能放」,放哪儿是应用自己定的。
确认:判空分支提前埋在了回调体里,互碰时手机每次触发都走进这个分支并有日志读数,降级路径实打实跑起来了。
6.3 引导目录缺失是完全静默的
现象:knock_share_guide 目录忘打进包的时候,构建不报错、运行不报错,只是碰的时候引导动图不播。
原因:引导资产是系统按约定目录名懒加载的,取不到就跳过,不抛异常。
怎么办:这种静默缺失的资产不能指望系统提醒你,自己做启动自检——目录读一次,读不到就写日志、上诊断界面。我把这个检查做成了 K5 那个按钮。
确认:K5 正常返回 1 项,读取链路通。
7、总结
这一趟下来的交付物,说白了是一张核对过的碰一碰接口清单:对称的双回调配上不对称的 off 签名、可选的 coordinate 字段和它的屏幕左上角原点语义、registry 里 windowId 加 utd 的能力登记、SharedData 的构建与回读以及 200KB/500 条两条约束、四段坐标换算和「用区域别用像素」的落位,还有引导目录那种静默资产必须自建诊断。准备面五个按钮全绿,意味着真正碰起来之前该打的地基都打实了。
双机互碰最值钱的收获,其实不是接收侧那个坐标真值,而是发送侧那个 undefined。官方文档写 coordinate 是可选字段,你读一百遍「可能为 undefined」,都不如真碰一次看清楚到底哪一侧拿不到。做接口适配这种事,文档告诉你「有什么」,真机告诉你「实际给你什么」,两者对不上的地方往往才是产品的分水岭——碰一碰的「精准放置」听着很美,但它其实是单边能力,这个认知得在动手写业务之前就有。
顺带那条毫秒级 resolve 也别被骗了:它只说明系统接住了你的动作,不代表数据真的到了对面。凡是「resolve 就当成功」的地方,都值得回头问一句——完成真正的信号到底在哪个回调里。
更多推荐


所有评论(0)