从“碰一碰”触发器到空间坐标回调
HarmonyOS 7.0(API 26)的 Share Kit 针对“碰一碰”只作为触发信号、接收端不知道触碰区域的问题,新增了接收端轻碰位置信息能力。PC/2in1 或 Tablet 设备侧可以获取空间相对坐标系中的 x、y。这个坐标以接收端设备屏幕左上角为原点,属于像素级坐标。
过去,手机触碰 PC 或 Tablet 时,接收端只知道“有设备碰了我并要传数据”,不知道“具体碰了哪块区域”。典型场景是平板分屏两个独立应用,手机轻碰左侧屏幕区域,预期分享给左侧应用,但系统缺少空间坐标,只能全局弹窗让用户二次选择;另一个场景是手机触碰 PC 屏幕一角,开发者希望在这个触碰点精准渲染水波纹动效,而不是在屏幕正中心弹出对话框。
底层链路:NFC触发、BLE/UWB测距与坐标映射
原文把底层融合感知拆成三步,应用侧主要关心最终通过 Want 投递回来的坐标。
1. NFC 场强捕获(Trigger):NFC 线圈切割磁感线,唤醒系统级 ShareService。
2. BLE/UWB 空间测距(Ranging):NFC 握手通道建立后,双方快速交换 BLE MAC 或 UWB 会话密钥,接收端设备通过多天线阵列计算 AoA(Angle of Arrival,到达角)和精确测距。
3. 坐标系换算与投递(Mapping):系统底层结合设备物理尺寸、屏幕分辨率以及传感器物理偏移量(Offset),推算出相对屏幕的像素级 (x, y) 坐标。
这也解释了为什么单纯依靠 NFC 硬件不够:NFC 有效交互距离在 2-4cm,且主要位于设备背部特定区域的线圈,空间位置需要后续测距和映射补齐。应用侧无需自行实现底层坐标解算,重点是在接收端正确处理 Want 与 UI 响应。
接收端配置:module.json5声明分享意图
为了让系统知道应用有资格处理特定类型的数据并支持触碰位置接收,需要在 module.json5 的 skills 中配置 actions 与 uris。以接收图片为例:
- {
- "module": {
- "abilities": [
- {
- "name": "ShareEntryAbility",
- "srcEntry": "./ets/entryability/ShareEntryAbility.ets",
- "exported": true,
- "skills": [
- {
- "actions": ["ohos.want.action.sendData"],
- "uris": [
- { "scheme": "file", "type": "image/*" }
- ]
- }
- ]
- }
- ]
- }
- }
复制代码
配置中的 ohos.want.action.sendData 用来声明支持碰一碰分享意图,uris 中的 scheme 与 type 用来限定可接收的数据类型。
ArkTS入口解析坐标:onCreate与onNewWant
当手机触碰平板屏幕边缘时,平板上的系统级服务会拦截这一动作,提取数据与物理坐标,并通过 Want 将其传递给 ShareEntryAbility。这里重点关注 want.parameters 中的两个新增参数:ohos.extra.param.key.share.touch.x 和 ohos.extra.param.key.share.touch.y。
- import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
- import { hilog } from '@kit.PerformanceAnalysisKit';
- import { window } from '@kit.ArkUI';
- const TAG = 'ShareEntryAbility';
- export default class ShareEntryAbility extends UIAbility {
- onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
- hilog.info(0x0000, TAG, 'onCreate triggered by share');
- this.handleShareData(want);
- }
- onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
- hilog.info(0x0000, TAG, 'onNewWant triggered by share');
- this.handleShareData(want);
- }
- private handleShareData(want: Want) {
- if (want.action !== 'ohos.want.action.sendData') {
- return;
- }
- let uris = want.parameters?.['ability.params.stream'] as string[];
- let touchX = want.parameters?.['ohos.extra.param.key.share.touch.x'] as number;
- let touchY = want.parameters?.['ohos.extra.param.key.share.touch.y'] as number;
- if (touchX !== undefined && touchY !== undefined) {
- hilog.info(0x0000, TAG, `Device touched at coordinates: (${touchX}, ${touchY})`);
- AppStorage.setOrCreate('TouchPosX', touchX);
- AppStorage.setOrCreate('TouchPosY', touchY);
- AppStorage.setOrCreate('SharedUris', uris);
- } else {
- hilog.warn(0x0000, TAG, 'No touch coordinates received.');
- }
- }
- onWindowStageCreate(windowStage: window.WindowStage): void {
- windowStage.loadContent('pages/ShareReceivePage', (err) => {
- if (err.code) {
- hilog.error(0x0000, TAG, 'Failed to load the content.');
- return;
- }
- hilog.info(0x0000, TAG, 'Succeeded in loading the content.');
- });
- }
- }
复制代码
这里将坐标与共享 URI 写入 AppStorage,便于页面侧订阅。业务数据可以通过 ability.params.stream 获取,坐标则用于后续空间化反馈或路由。
UI响应:在触碰点渲染水波纹
接收到坐标后,可以在页面上做精准交互反馈。下面示例在触碰点生成一个涟漪展开动画,让用户感觉数据是从触碰位置流入的。
- import { Component, State, Watch } from '@kit.ArkUI';
- @Entry
- @Component
- struct ShareReceivePage {
- @StorageLink('TouchPosX') @Watch('onTouchPosChanged') touchX: number = 0;
- @StorageLink('TouchPosY') touchY: number = 0;
- @StorageLink('SharedUris') sharedUris: string[] = [];
- @State rippleScale: number = 0;
- @State rippleOpacity: number = 1;
- @State isRippleVisible: boolean = false;
- onTouchPosChanged() {
- if (this.touchX > 0 && this.touchY > 0) {
- this.isRippleVisible = true;
- this.rippleScale = 0;
- this.rippleOpacity = 1;
- animateTo({
- duration: 800,
- curve: Curve.EaseOut,
- onFinish: () => {
- this.isRippleVisible = false;
- }
- }, () => {
- this.rippleScale = 10;
- this.rippleOpacity = 0;
- });
- }
- }
- build() {
- Stack() {
- Column() {
- Text('等待接收碰一碰分享...')
- .fontSize(20)
- .fontColor('#666')
- if (this.sharedUris.length > 0) {
- Text(`已接收到 ${this.sharedUris.length} 个文件`)
- .margin({ top: 20 })
- .fontWeight(FontWeight.Bold)
- }
- }
- .width('100%')
- .height('100%')
- .justifyContent(FlexAlign.Center)
- if (this.isRippleVisible) {
- Circle({ width: 50, height: 50 })
- .fill(Color.Transparent)
- .stroke(Color.Blue)
- .strokeWidth(3)
- .position({ x: this.touchX - 25, y: this.touchY - 25 })
- .scale({ x: this.rippleScale, y: this.rippleScale })
- .opacity(this.rippleOpacity)
- }
- }
- .width('100%')
- .height('100%')
- .backgroundColor('#F5F5F5')
- }
- }
复制代码
Circle 的大小为 50,因此 position 中用 touchX - 25、touchY - 25 把圆心对齐到触碰物理坐标。ArkUI 显式动画负责缩放与透明度变化,动画结束后隐藏涟漪。
稳定性边界:非空校验与1秒防抖
如果发送端或接收端的硬件不支持精确位置解算,例如较老机型缺少多天线传感器或系统版本过低,touch.x 和 touch.y 可能为 undefined。工程实现中必须做好非空校验与防御性降级编程,缺少坐标时退回普通分享流程或屏幕中心反馈。
另一个常见问题是多重抖动事件。用户可能由于手抖,在 1 秒内触发多次 NFC 读写,使 onNewWant 被高频调用。若不加防御,会导致 UI 动效重叠、内存瞬间膨胀或底层通道 FD(文件描述符)句柄耗尽。原文给出的处理策略是:在入口层使用 Token Bucket 或时间戳比对进行防抖,若连续两次触碰事件间隔小于 1000ms,强制将多余事件 Drop 掉。
适配建议
从原文给出的路径看,Share Kit 新增的空间位置信息回调表面上只是多传了两个整数,背后反映的是操作系统对底层传感器网络的整合与系统级调度能力。适配时建议按以下顺序落地:先在 module.json5 声明 skills;再在 UIAbility 的 onCreate/onNewWant 中统一解析 Want;对 touch.x/touch.y 做非空判断;把坐标与共享数据投递到页面状态;最后在 ArkUI 侧按坐标渲染动效或做分屏路由,并在入口加防抖。
从“系统知道你碰了我”到“系统知道你碰了我的左下角(x:200, y:800)”,近场交互开始具备空间感知能力。对开发者而言,这类能力可以用于分屏应用精准分享、PC 角落水波纹动效以及更多基于触碰位置的跨设备交互。 |