HarmonyOS 7 新特性10:碰一碰深潜——轻碰坐标真值、四段换算与区域命中的双端实测

1、引言

篇 4 写碰一碰的时候没有对端设备,整篇按降级预案写成了机制篇:单机能测的注册、数据准备、能力登记、坐标换算全测了,唯独回调里的轻碰坐标(coordinate)留了白——没有真实的碰,就没有真实的坐标,对着 d.ts 的空想不算数。

这篇把白补上了。对端到位:Mate 60 Pro 当发送方、MatePad Edge 切 PC 形态当接收方,两台设备真机互碰,接收侧回调里两次拿到坐标真值:(1481,1135) 和 (1483,1245)。两组数一比就有话说——X 几乎没动、Y 差了 110,正是两次碰点一上一下的真实反映。同时发送侧的 coordinate 恒为 undefined,把「坐标只在 PC/2in1/Tablet 接收侧提供」这条文档说法砸成了实测事实。

坐标到手只是第一步。轻碰坐标的业务价值在命中判定:碰了屏幕上的哪个卡片、哪个按钮、哪块画布区域,决定分享的数据落到哪、触发什么逻辑。从屏幕坐标走到业务控件,中间要过四段换算,每一段都有自己的坐标系和单位标准,错一段就全盘皆错。本篇把四段换算的数学、componentUtils.getRectangleById 的字段职责、区域匹配与像素匹配的正反写法、容差设计一次讲透。

环境声明:发送端 Mate 60 Pro(ALN-AL80,麒麟 9000S,HarmonyOS 7.0.0.107 / API 26 Release);接收端 MatePad Edge(QXS-W00,3120×2080,同 API 26,PC 形态自由窗口);DevEco Studio 26.0.0(SDK 26.0.0.105 Release);实测工程轻碰台(com.qingkouwei.lightknock)。数据边界先交代清楚:两组坐标样本证明的是「坐标随碰位变化、落在目标窗口内、单位与屏幕物理坐标系一致」,不是统计学意义的命中成功率分档——那需要几十上百次受控碰击,本篇不冒充。

2、效果展示与双端读数

互碰发生时双端诊断页的实测读数,先看图再看账。

pad 接收侧(dataReceive 两次触发):

HarmonyOS 7 新特性10:碰一碰深潜——轻碰坐标真值、四段换算与区域命中的双端实测-1.png

L1 接收侧坐标真值 screen=(1481,1135)
L4 幂等观察:dataReceive 第 1 次触发,间隔序列 [—]
L3 receive resolve elapsed=0ms
L1 接收侧坐标真值 screen=(1483,1245)
L4 幂等观察:dataReceive 第 2 次触发,间隔序列 [9225ms]
L3 receive resolve elapsed=2ms

手机发送侧(knockShare 两次触发):

HarmonyOS 7 新特性10:碰一碰深潜——轻碰坐标真值、四段换算与区域命中的双端实测-2.png

L1 发送侧 coordinate=undefined(判空分支命中,走默认放置位语义)
L4 幂等观察:knockShare 第 2 次触发,间隔序列 [17325ms]
L2 share resolve elapsed=1ms

三笔数据逐条对:

数据项实测值说明
接收侧坐标 #1(1481, 1135)屏幕左上原点,落在诊断窗口内
接收侧坐标 #2(1483, 1245)ΔX=2px、ΔY=110px,随碰位下移
发送侧坐标undefined(恒)knockShare 回调不给坐标,判空分支必走
触发间隔9225ms(接收侧)两次独立物碰,非重复派发
API 调用时延share 1-2ms / receive 0-2ms仅应用层调用段,不含无线传输

两个细节值得盯住。其一,ΔX 只有 2px 而 ΔY 有 110px——手机是竖着贴上去的,顶端接触点在水平方向天然稳定、垂直方向随手势滑动,这个分布特征做命中判定时要心里有数:垂直方向的容差要比水平方向给得宽。其二,时延账必须切段记:share/receive 的 resolve 只覆盖「应用调用系统 API」这一段(0-2ms),文件本体的无线传输在系统进程里异步走,篇 11 会讲这段账怎么对不上。

3、Kit 与 API:坐标从哪来、命中靠什么

3.1 轻碰坐标的接口面

坐标挂在华为分享(Share Kit)的 harmonyShare 回调目标对象上,d.ts 原文(@hms.collaboration.harmonyShare.d.ts):

// 接收侧:ReceivableTarget.getInfo() 返回 ReceivableTargetInfo
// coordinate?: CoordinateInfo —— 可选字段,判空是义务不是选择
interface CoordinateInfo {
  screenX: number;  // 屏幕左上角为原点的 X 坐标,d.ts 注明「当前仅支持整数」
  screenY: number;  // 同上
}

// 订阅接收事件:capability 登记 windowId + utd 能力
// windowId 决定「哪个窗口被碰才算」,这是命中判定的第一道过滤
function on(event: 'dataReceive', capability: RecvCapabilityRegistry,
  callback: Callback<ReceivableTarget>): void;

三条语义要用实测校准:

  1. 坐标是非对称能力。d.ts 与开发指南说法一致:手机与 PC/2in1 或平板互碰时,坐标在 PC/2in1 或平板侧提供。发送侧(手机)的 knockShare 回调里 getInfo().coordinate 恒为 undefined——本篇实测两次触发两次 undefined,与篇 4 单机推演一致。写代码时发送侧别指望坐标,判空分支就是正常路径。
  2. 整数坐标。d.ts 明注 screenX/screenY「仅支持整数」,拿到手就是取整后的物理像素值,别指望亚像素精度,容差设计里这 1px 量化误差要算进去。
  3. windowId 是第一道过滤。注册时 RecvCapabilityRegistry.windowId 圈定了「哪个窗口被碰才触发回调」——碰桌面壁纸、碰别的应用窗口,dataReceive 根本不会响。命中判定还没开始,系统已经帮你把「碰没碰在我身上」过滤掉了。

3.2 命中判定的尺子:componentUtils

坐标进了门,量控件要靠 @ohos.arkui.componentUtils(API 10 起)。getRectangleById(id) 返回的 ComponentInfo 八字段,d.ts 原文职责如下:

字段类型d.ts 职责原文(意译)换算段
sizeSize组件尺寸区域边界
localOffsetOffset相对父组件偏移页面内布局
windowOffsetOffset相对窗口偏移段2:窗口坐标
screenOffsetOffset相对屏幕偏移段1:屏幕坐标
translateTranslateResult组件平移量变换修正
scaleScaleResult组件缩放变换修正
rotateRotateResult组件旋转变换修正
transformMatrix4Result4×4 仿射矩阵全量变换

给组件挂 .id('card_3'),一行 componentUtils.getRectangleById('card_3') 就拿到这把尺子。关键认知:screenOffset 和轻碰坐标同属屏幕参照系——命中判定不需要你手工把屏幕坐标换算到窗口再换算到页面,系统已经把每个组件的屏幕绝对位置给你了,直接同系比较。四段换算的价值不在「算」,在「懂」:懂得每段参照系是什么,才会在自由多窗、窗口拖动、横竖屏切换时知道哪些值会变、哪些不会。

4、逻辑流:四段换算与命中判定

碰在注册窗口外

碰在注册窗口内

screenX∈[left,right]±tol
screenY∈[top,bottom]±tol

区间外

物碰发生
手机顶端贴 pad 窗口

系统过滤层

dataReceive 不触发
windowId 过滤

回调派发
ReceivableTarget

段1 getInfo().coordinate
屏幕坐标 screenX/Y 整数 px
左上原点

段2 屏幕→窗口
减 window.getWindowProperties()
的 windowRect.left/top

段3 窗口→页面
减页面根容器偏移
自由多窗下窗口≠全屏

段4 页面→控件
getRectangleById(id)
取 screenOffset+size 直接同系比

区域匹配

命中:触发控件业务

未命中:走默认放置位

流程里最容易被低估的是段 2 和段 3。单机全屏应用里「屏幕=窗口=页面」,三段重合,换算写成恒等式也能跑对;一到 PC 形态自由多窗(本篇接收端正是这个形态),窗口有位置偏移、页面有根容器内边距,恒等式立刻翻车。实测坐标 (1481,1135) 是屏幕系读数,诊断窗口当时悬浮在桌面中部——如果拿它直接和页面内某控件的 localOffset 比,命中的是空气。

而段 4 给了条捷径:getRectangleById 的 screenOffset 已经是屏幕系,和轻碰坐标同参照系,直接比较就完成命中。四段换算的正确用法是理解链路、校验数据:当 screenOffset 和你的预期对不上时(窗口拖动后没刷新、缩放没算进去),逐段回查才知道断在哪一段。

5、实战:从坐标到命中的完整代码

5.1 环境与工程

系统 HarmonyOS 7.0.0.107(API 26 Release),IDE DevEco Studio 26.0.0(SDK 26.0.0.105),芯片:发送端麒麟 9000S、接收端麒麟平台(MatePad Edge)。工程为轻碰台扩展:接收侧回调里在坐标读数之后接一段命中判定,目标控件是诊断页上的日志滚动区(挂了稳定 id)。

5.2 命中判定实现

import { componentUtils } from '@kit.ArkUI';
import { harmonyShare } from '@kit.ShareKit';

// 命中容差:单位 px。水平 8、垂直 24 —— 不对称是实测分布给的:
// 手机竖贴时 ΔX=2px / ΔY=110px,垂直方向手势滑动量大,容差必须更宽
const TOL_X: number = 8;
const TOL_Y: number = 24;

interface HitResult {
  hit: boolean;
  targetId: string;
  // 把每段中间值全带出来,排查时不用加日志重跑
  detail: string;
}

// 区域匹配:唯一正确的命中写法
function hitTest(screenX: number, screenY: number, ids: string[]): HitResult {
  for (const id of ids) {
    try {
      const info = componentUtils.getRectangleById(id);
      // screenOffset 与轻碰坐标同属屏幕参照系,直接同系比较
      const left = info.screenOffset.x - TOL_X;
      const right = info.screenOffset.x + info.size.width + TOL_X;
      const top = info.screenOffset.y - TOL_Y;
      const bottom = info.screenOffset.y + info.size.height + TOL_Y;
      if (screenX >= left && screenX <= right &&
          screenY >= top && screenY <= bottom) {
        return { hit: true, targetId: id,
          detail: `rect=[${left},${top},${right},${bottom}] pt=(${screenX},${screenY})` };
      }
    } catch (e) {
      // id 不存在时 getRectangleById 抛错,跳过继续试下一个目标,
      // 不让单个失效 id 拖死整次命中判定
      continue;
    }
  }
  return { hit: false, targetId: '', detail: `pt=(${screenX},${screenY}) 无区域命中` };
}

// 接收回调里的用法(节选自 KnockLinkPage 扩展)
this.recvCb = (target: harmonyShare.ReceivableTarget): void => {
  const info = target.getInfo();
  if (info.coordinate !== undefined) {
    // 坐标真值先落日志再判定——实测工程规矩:原始读数永远第一优先级
    this.pushLog(`L1 接收侧坐标真值 screen=(${info.coordinate.screenX},${info.coordinate.screenY})`);
    const r = hitTest(info.coordinate.screenX, info.coordinate.screenY, ['log_area', 'btn_register']);
    this.pushLog(`L6 命中判定 hit=${r.hit} target=${r.targetId} ${r.detail}`);
  } else {
    // 发送侧/不支持坐标的场景:判空分支是正常路径不是异常
    this.pushLog('L1 coordinate=undefined,走默认放置位语义');
  }
};

三段设计取舍讲清楚:

  1. 容差不对称(TOL_X=8 / TOL_Y=24):不是拍的,是 2 组实测坐标的分布给的——垂直方向手势滑动量是水平的几十倍。样本少,这组数是起点不是终点,业务上线前该按自己的碰击样本重校。
  2. 多目标顺序遍历:ids 数组按业务优先级排,先命中先返回。控件有重叠时(浮层盖着卡片),遍历顺序就是层级语义,比算 z-order 便宜得多。
  3. 中间值全量带出:detail 把矩形四边和碰点都打进日志。命中判定是几何问题,几何问题的排查靠数不靠猜——一行日志比十次断点快。

5.3 真机对账

坐标从系统回调走到业务命中,跨了三个进程的边界,时序账用一张图确认:

业务分支 componentUtils (getRectangleById) 轻碰台接收侧 (dataReceive 回调) 系统碰一碰服务 (ShareManagerService) 业务分支 componentUtils (getRectangleById) 轻碰台接收侧 (dataReceive 回调) 系统碰一碰服务 (ShareManagerService) alt [命中] [未命中] ReceivableTarget(碰派发) getInfo().coordinate 实测 (1481,1135) pushLog L1 坐标真值 (原始读数第一优先级) getRectangleById('log_area') screenOffset+size (屏幕系,与坐标同参照系) hitTest 区域匹配 TOL_X=8 / TOL_Y=24 触发控件业务 L6 hit=true 默认放置位语义 L6 hit=false

实测坐标 (1481,1135) 与 (1483,1245) 均落在诊断窗口日志区(屏幕系矩形约 [560,430]–[2560,1310])内,hitTest 返回 hit=true、target=log_area;把碰点人工平移到窗口外(用 dumpLayout 取桌面空白处坐标代入),返回 hit=false 走默认放置位分支。判定器两端行为都验证过,不是只测了命中路径。

6、避坑指南

坑1:像素匹配——官方博客点名的反面教材

现象:if (screenX === card.centerX && screenY === card.centerY) 这类写法,真机一次都命不中。

根因:轻碰坐标是整数化的物理像素读数,手指/机身的接触面有尺寸,系统给的点本身带量化误差(±1px)与手势偏移(实测 ΔY 达 110px)。要求像素级相等,等于要求用户每次都碰在同一个物理点上。

方案对比:

策略写法真机表现适用
像素匹配x===cx && y===cy命中率≈0无(反面教材)
区域匹配点在矩形内两次实测全命中默认选择
区域+不对称容差矩形外扩 TOL_X/TOL_Y命中且吸收手势滑动生产推荐

最佳实践:区域匹配起步,容差按实测碰击分布校准,水平垂直分开设。

验证:命中路径与未命中路径都要真机过一遍——只测命中的判定器,等于没测。

坑2:拿屏幕坐标直接比页面坐标

现象:单机全屏时命中正常,切 PC 形态自由多窗后命中全偏。

根因:轻碰坐标是屏幕参照系,而 localOffset 是父组件参照系、windowOffset 是窗口参照系。全屏时三系重合掩盖了混用;自由多窗下窗口有偏移,混用立刻偏出天际。

方案:统一参照系再比较。最省事的路径是 getRectangleById 的 screenOffset——它和轻碰坐标同为屏幕系,直接比。需要手工换算时才走「减 windowRect.left/top」的段 2 数学。

验证:把窗口拖到屏幕另一角再碰一次,命中结果应不变——参照系统一了,窗口在哪都一样。

坑3:单位标准——px 与 vp 差一个密度

现象:命中判定偏移量随设备不同而不同,3120×2080 的 pad 上偏得尤其离谱。

根因:轻碰坐标是物理像素(px),而 ArkUI 布局属性(width/height/padding)默认 vp。MatePad Edge 这类高密度屏上 1vp≈2px 以上,拿 vp 布局值和 px 坐标直接比,偏差按密度倍数放大。

方案:比较前统一到同一单位。getRectangleById 返回的 size/offset 与坐标同说法,优先用它做基准;自算布局时用 vp2px() 显式换算,别靠脑补密度。

验证:换一台不同密度的设备跑同一组判定,命中率不应随密度漂移。

坑4:发送侧等坐标——undefined 是常态不是故障

现象:手机端 knockShare 回调里读 coordinate,永远 undefined,误以为注册出了错。

根因:坐标是非对称能力,d.ts 与指南说法一致——只在 PC/2in1/Tablet 接收侧提供。发送侧(手机)拿不到坐标是设计如此。

方案:发送侧代码不依赖坐标做业务分支;判空分支写成正常路径(诊断页的 L1 打点日志两种分支都打了读数)。

验证:双端各碰一轮,接收侧有坐标、发送侧恒 undefined,即为正常。

7、总结

这篇把篇 4 留的白补上了:轻碰坐标从 d.ts 里的字段说明变成了两次真机读数 (1481,1135) 与 (1483,1245)。比读数本身更值钱的是三条被实测校准过的认知——坐标是非对称的(只给接收侧)、整数化的(px 说法,±1px 量化误差)、带手势分布的(垂直滑动量远大于水平,容差不该对称)。

四段换算的落点也清楚了:手工把坐标一层层减下去是下策,懂得每段参照系是什么、用 getRectangleById 的 screenOffset 走同系比较,才是这条链的正确打开方式。命中判定的工程规矩就三条:区域匹配起步、容差按实测校准、命中与未命中路径都验证。在这里插入图片描述

Logo

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

更多推荐