HarmonyOS 7 新特性10:碰一碰深潜——轻碰坐标真值、四段换算与区域命中的双端实测
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 两次触发):

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 两次触发):

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;
三条语义要用实测校准:
- 坐标是非对称能力。d.ts 与开发指南说法一致:手机与 PC/2in1 或平板互碰时,坐标在 PC/2in1 或平板侧提供。发送侧(手机)的
knockShare回调里getInfo().coordinate恒为 undefined——本篇实测两次触发两次 undefined,与篇 4 单机推演一致。写代码时发送侧别指望坐标,判空分支就是正常路径。 - 整数坐标。d.ts 明注 screenX/screenY「仅支持整数」,拿到手就是取整后的物理像素值,别指望亚像素精度,容差设计里这 1px 量化误差要算进去。
- windowId 是第一道过滤。注册时
RecvCapabilityRegistry.windowId圈定了「哪个窗口被碰才触发回调」——碰桌面壁纸、碰别的应用窗口,dataReceive 根本不会响。命中判定还没开始,系统已经帮你把「碰没碰在我身上」过滤掉了。
3.2 命中判定的尺子:componentUtils
坐标进了门,量控件要靠 @ohos.arkui.componentUtils(API 10 起)。getRectangleById(id) 返回的 ComponentInfo 八字段,d.ts 原文职责如下:
| 字段 | 类型 | d.ts 职责原文(意译) | 换算段 |
|---|---|---|---|
| size | Size | 组件尺寸 | 区域边界 |
| localOffset | Offset | 相对父组件偏移 | 页面内布局 |
| windowOffset | Offset | 相对窗口偏移 | 段2:窗口坐标 |
| screenOffset | Offset | 相对屏幕偏移 | 段1:屏幕坐标 |
| translate | TranslateResult | 组件平移量 | 变换修正 |
| scale | ScaleResult | 组件缩放 | 变换修正 |
| rotate | RotateResult | 组件旋转 | 变换修正 |
| transform | Matrix4Result | 4×4 仿射矩阵 | 全量变换 |
给组件挂 .id('card_3'),一行 componentUtils.getRectangleById('card_3') 就拿到这把尺子。关键认知:screenOffset 和轻碰坐标同属屏幕参照系——命中判定不需要你手工把屏幕坐标换算到窗口再换算到页面,系统已经把每个组件的屏幕绝对位置给你了,直接同系比较。四段换算的价值不在「算」,在「懂」:懂得每段参照系是什么,才会在自由多窗、窗口拖动、横竖屏切换时知道哪些值会变、哪些不会。
4、逻辑流:四段换算与命中判定
流程里最容易被低估的是段 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,走默认放置位语义');
}
};
三段设计取舍讲清楚:
- 容差不对称(TOL_X=8 / TOL_Y=24):不是拍的,是 2 组实测坐标的分布给的——垂直方向手势滑动量是水平的几十倍。样本少,这组数是起点不是终点,业务上线前该按自己的碰击样本重校。
- 多目标顺序遍历:ids 数组按业务优先级排,先命中先返回。控件有重叠时(浮层盖着卡片),遍历顺序就是层级语义,比算 z-order 便宜得多。
- 中间值全量带出:
detail把矩形四边和碰点都打进日志。命中判定是几何问题,几何问题的排查靠数不靠猜——一行日志比十次断点快。
5.3 真机对账
坐标从系统回调走到业务命中,跨了三个进程的边界,时序账用一张图确认:
实测坐标 (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 走同系比较,才是这条链的正确打开方式。命中判定的工程规矩就三条:区域匹配起步、容差按实测校准、命中与未命中路径都验证。在这里插入图片描述
更多推荐
所有评论(0)