前两篇已经把 SlideDrop 的两条基础链路做稳了。
01 解决“碰到哪个窗口、窗口里的哪个位置”,最终把tap_20261002_01锁定到slot_03;02 则把素材封装、UDMF 多记录、幂等提交和重复触碰去重串起来,让seq31只提交一次,seq32在 800ms 窗口里被明确拒绝。
第三篇继续同一个slidedrop_board_01,但我故意把接收端变复杂。
这次 Board 不再以 100% 比例静止在窗口里,而是:
缩放到 125% 横向滚动 96vp 纵向滚动 60vp BoardRect 重新布局系统侧仍然给到一个窗口级触点:
812 / 260如果继续用第一篇的:
touch - boardRect直接命中 slot,结果一定会偏。
所以 03 真正要解决的是:精准分享提供的是窗口坐标,SlideDrop 需要的是当前业务画布坐标。两者中间必须有一条可解释、可回归的坐标变换链。
本轮固定数据:
taskId: tap_20261002_03 targetWindowId: slidedrop_board_01 viewport: 1200 × 750vp boardRect: x=180 y=84 w=900 h=590 scale: 1.25 scrollOffset: x=96 y=60 touchPoint: x=812 y=260 localPoint: x=632 y=176 scrolledPoint: x=728 y=236 logicalPoint: x=582.4 y=188.8 normalized: x=0.809 y=0.400 geometryVersion: 17 targetSlot: slot_02 payload: image_20261002_19.webp payloadSize: 3.2MB transformCost: 8ms status: REMAPPED_LOCKED一、第一篇的坐标算法,在缩放以后为什么会错
01 的 Board 没有缩放,也没有滚动。
那时:
window coordinate → 减去 BoardRect → local coordinate → DropZone足够。
现在 Board 被放大到:
1.25x并且 Scroll 容器已经滚动:
96 / 60如果仍然只做:
localX = 812 - 180 localY = 260 - 84得到:
632 / 176这个值只是“触点在当前可视 Board 外框里的位置”,不是逻辑画布坐标。
真正要落槽,还要把滚动偏移加回去,再除以缩放比例。
二、坐标变换必须固定顺序
当前 SlideDrop 的顺序是:
Window Coordinate → Board Local → + Scroll Offset → ÷ Scale → Logical Board Coordinate → DropZoneResolver代码:
exportinterfaceTransformInput{touchX:numbertouchY:numberboardX:numberboardY:numberscrollX:numberscrollY:numberscale:number}exportfunctiontoLogicalPoint(input:TransformInput):{localX:numberlocalY:numberlogicalX:numberlogicalY:number}{constlocalX=input.touchX-input.boardXconstlocalY=input.touchY-input.boardYconstscrolledX=localX+input.scrollXconstscrolledY=localY+input.scrollYreturn{localX,localY,logicalX:scrolledX/input.scale,logicalY:scrolledY/input.scale}}本轮:
local: 632 / 176 + scroll: 728 / 236 ÷ 1.25: 582.4 / 188.8最终 logical point 落在slot_02。
三、为什么滚动偏移是“加回去”,不是减掉
这个地方最容易写反。
页面已经向右滚动 96vp,相当于用户当前看到的是逻辑画布更靠右的一段。
窗口触点632表示当前可视区域里的位置。
要还原到完整逻辑画布:
632 + 96 = 728然后再除以 1.25。
如果写成:
632 - 96命中结果会往左漂。
所以我没有把这段换算散在页面里,而是集中放进CoordinateTransformCoordinator。
坐标方向一旦固定,后面所有投递入口都复用。
四、Geometry Version 解决“布局刚变化,事件就到了”的竞争
真正跑起来以后,还有一个比数学更麻烦的问题:
BoardRect 更新了 DropZone 列表还没更新 触碰事件到了如果 Board 用 version 17,Zones 还是 version 16,计算本身没错,几何关系却已经不一致。
所以BoardGeometryStore改成带版本的完整快照:
exportinterfaceBoardGeometrySnapshot{version:numberboardRect:{x:numbery:numberwidth:numberheight:number}scale:numberscrollOffset:{x:numbery:number}zones:DropZone[]}只要 Board、Scale、Scroll、Zones 任意一项变化,就生成新的 version。
本轮:
geometryVersion=17解析过程中禁止混用两个版本。
五、事件锁定目标时,要把 geometryVersion 一起保存
TARGET_LOCKED不应该只保存:
slot_02而应该保存:
slot_02 geometryVersion=17 logicalPoint=582.4/188.8原因是后续传输可能花 100ms 以上。
这期间用户如果又缩放一次 Board:
17 → 18Commit 前就知道当前目标上下文已经变化。
这时不能直接沿用旧结果。
可以:
重新解析也可以:
拒绝旧投递具体产品策略后面再定,但至少上下文变化是可检测的。
六、normalized 这次换成逻辑画布比例
第一篇 normalized 用的是整个窗口:
touchX / viewportWidth touchY / viewportHeight03 开始我更关心业务画布里的比例位置。
当前逻辑 Board 约为:
720 × 472所以:
582.4 / 720 ≈ 0.809 188.8 / 472 ≈ 0.400得到:
0.809 / 0.400这个值用于:
日志 回归 跨尺寸比较但仍然不会替代真实 DropZone 几何。
七、DropZone 本身也要定义在逻辑坐标系里
如果 slot 的 Rect 直接使用屏幕像素,那么缩放一次就要重写所有 slot。
所以从第三篇开始:
DropZone Rect统一定义在 Logical Board Coordinate。
例如:
constzones:DropZone[]=[{id:'slot_01',rect:{x:0,y:0,width:360,height:236}},{id:'slot_02',rect:{x:360,y:0,width:360,height:236}},{id:'slot_03',rect:{x:0,y:236,width:360,height:236}},{id:'slot_04',rect:{x:360,y:236,width:360,height:236}}]当前 logical point:
582.4 / 188.8命中:
slot_02八、缩放手势和触碰事件不能同时改 Geometry
还有一种竞争:
用户正在 pinch zoom 精准碰一碰事件同时到达如果 Scale 正在 1.23、1.24、1.25 连续变化,哪个才是投递时真正的画布?
QuickDrop 当前规则:
缩放进行中 → Geometry 状态 = TRANSFORMING 缩放稳定 → Geometry version + 1 → READYPrecise Share 事件只有在:
READY时才做最终锁定。
否则返回:
GEOMETRY_TRANSFORMING让用户稍后重新碰。
比在动画中间猜一个比例更可靠。
九、滚动同样需要稳定快照
Scroll 高频回调很多。
如果每一帧都生成一个 geometryVersion,日志会被刷爆。
所以 Scroll 只更新内存中的 transient offset;在:
scroll stop或者短时间稳定后再提交新的 GeometrySnapshot。
本轮最终:
scroll=96/60 version=17是稳定态。
这条思路和 QuickDock 里“DragMove 不持久化,DragEnd 才写入”很像。
十、DevEco 图必须把变换链完整打印出来
开发图:
HiLog:
taskId=tap_20261002_03 windowTouch= 812 / 260 boardRect= 180 / 84 / 900 / 590 local= 632 / 176 scroll= 96 / 60 scrolled= 728 / 236 scale= 1.25 logical= 582.4 / 188.8 normalized= 0.809 / 0.400 geometryVersion= 17 targetSlot= slot_02 transformCost= 8ms status= REMAPPED_LOCKED只打印最终slot_02不够。
坐标链一旦出错,需要知道到底是:
BoardRect 错 Scroll 错 Scale 错 Zone 错十一、运行图重点不是“slot_02 发亮”,而是所有中间值一致
最终运行图:
同一套数据:
window: 812/260 local: 632/176 scrolled: 728/236 logical: 582.4/188.8 slot: slot_02底部流程把每一步都展示出来。
最终状态:
REMAPPED_LOCKED它表示的不只是“命中一个槽位”,而是:
在 125% 缩放和 96/60 滚动之后,系统窗口触点仍然能准确映射到业务画布里的同一个语义位置。
十二、布局尺寸改变后,不复用旧 BoardRect
这一篇还模拟了一次窗口变窄。
只要onAreaChange或对应布局测量结果变化,BoardGeometryStore 就会提交新快照。
旧 version 不再用于新事件。
当前工程原则:
一次解析 只读取一次 snapshot 解析中途 不再次读 Store这样不会出现:
上半段用 version17 下半段用 version18这种最难排查的混合状态。
十三、边界触点继续采用唯一归属规则
缩放以后边界问题更明显。
逻辑坐标如果刚好是:
x=360会落在左右两个 slot 分界。
SlideDrop 仍采用:
左闭右开 上闭下开Board 最右、最下边界例外。
这条规则统一放进 Resolver,不让每个 DropZone 自己写<=。
十四、03 的 8ms 只代表坐标变换成本
transformCost=8ms包含:
读取 GeometrySnapshot 窗口到局部坐标 滚动补偿 缩放逆变换 DropZone 命中 锁定状态不包含:
文件读取 跨设备传输 Board Commit性能口径继续和 01、02 保持分段。
后面 06 才会把:
target resolve payload pack transfer commit分别统计成完整链路。
十五、坐标算法必须允许单元测试
第三篇的 Transform 函数刻意写成纯函数。
输入:
touch board scroll scale输出:
logical不直接依赖 UI 组件。
这样可以固定测试:
scale=1 scroll=0 scale=1.25 scroll=96/60 scale=0.8 scroll=0/200精准投递的核心数学如果只能靠实机手工碰,维护成本会很高。
十六、缩放为 0 或异常值要直接拒绝
虽然正常 UI 不会出现:
scale=0但 Coordinator 仍然做防御:
if(snapshot.scale<=0){return{ok:false,reason:'INVALID_SCALE'}}不能让坐标变换产生:
Infinity NaN再进入 DropZoneResolver。
业务边界越早拦,后面状态越干净。
十七、为什么 03 没有直接把图片提交进去
因为这一篇的主问题是:
坐标是否还准确image_20261002_19.webp / 3.2MB只是统一测试素材。
只要REMAPPED_LOCKED成立,02 的 UDMF + Commit 链可以继续复用。
这就是连载连续性的意义:
01 坐标基础 02 提交基础 03 升级坐标变换而不是每篇重新写一套 Demo。
十八、第三篇最后固定八组回归
第一组,scale=1 / scroll=0,结果和 01 一致。
第二组,scale=1.25 / scroll=96/60,命中 slot_02。
第三组,scale=0.8,坐标逆变换正确。
第四组,Geometry version 不一致,拒绝解析。
第五组,缩放进行中,返回GEOMETRY_TRANSFORMING。
第六组,触点在 Board 外,返回OUTSIDE_BOARD。
第七组,边界点按唯一归属规则只命中一个 slot。
第八组,窗口尺寸变化后,旧 snapshot 不再被新事件复用。
全部通过后:
REMAPPED_LOCKED才算成立。
十九、下一篇:坐标已经准确,真正难题变成“大文件别一次全读”
04 会把素材从:
3.2MB WebP换成:
84.6MB 视频目标仍然是 SlideDrop。
但接收端不会先把完整视频全部读进内存再开始传。
我会把:
UDMF 元数据 预览图 文本 真实大文件拆开,让 Board 先拿到可展示信息,再按需读取真实文件。
同时故意在第 9 个分片制造一次失败,从 32MB checkpoint 继续恢复。
二十、坐标链里还要处理“页面缩放”和“内容缩放”不是一回事
实际页面里有两种看起来很像的缩放。
一种是整个页面因为窗口尺寸变化而重新布局;另一种是 Board 自己的内容缩放,例如用户双指把素材板放到 125%。
前者改变的是:
boardRect viewport后者改变的是:
scale logical board如果把两个值混在一起,就会出现一个很隐蔽的问题:页面变窄以后,开发者为了“看起来缩小了”,直接把boardRect.width当成业务 scale。这样触点换算虽然能得出数字,真正逻辑画布的几何却已经被破坏。
所以 GeometrySnapshot 里明确保留:
viewport geometry board display rect business scale scroll offset四个层级。
窗口缩放只更新显示矩形;用户缩放才更新业务 scale。
二十一、嵌套滚动容器不能只记一层 scrollOffset
第三篇当前 Board 只在一层 Scroll 里,因此:
96 / 60就够用。
真正产品里常见的结构是:
页面整体纵向 Scroll → Board 横向 Scroll → Board 内部画布 Pan如果未来 SlideDrop 加工具栏和侧边资源区,只保存最内层 offset 一定会出错。
所以 Coordinator 没有把 scroll 定义成两个裸数字,而是准备成 TransformChain:
exportinterfaceScrollTransform{x:numbery:numbersource:string}exportinterfaceTransformChain{scrolls:ScrollTransform[]scale:number}计算时按容器层级顺序累加。
当前只有:
BoardScroll 96 / 60以后增加页面 Scroll,不需要推倒整个接口。
二十二、GeometrySnapshot 应该是不可变对象
精准投递事件到达以后,我不会让 Resolver 继续引用一个会被 UI 实时修改的对象。
而是:
const snapshot = geometryStore.snapshot()拿到一份不可变拷贝。
整个 8ms 解析过程都使用它。
这样即使用户正在继续滚动画布,新事件会得到新 version,旧事件不会在中途被新数据污染。
这是并发和 UI 高频变化场景里很重要的工程习惯:
一次业务判断,只消费一次稳定快照。
二十三、坐标数值还要统一 vp / px 口径
跨设备开发里最容易出现的错误之一,是某一层返回 px,另一层以为是 vp。
第三篇当前所有业务几何明确统一成:
vp系统事件如果不是同一口径,Adapter 必须在进入PreciseShareEvent前完成转换。
业务层不再判断:
这个值到底是 px 还是 vpHiLog 也会在字段名或文档里明确口径。
坐标算法本身不难,最难的是单位混用时产生的“看起来只偏几十像素”的问题。
二十四、投递命中以后还要保留视觉反馈延迟预算
用户碰到 slot_02 后,真正跨设备文件传输可能还没开始。
但目标高亮必须尽快出现。
所以 03 的 8ms 解析完成以后,先更新:
slot highlight TARGET_LOCKED再进入后续 UDMF / Transfer。
这条顺序让“精准”先被用户感知到。
如果等文件传完才高亮目标,用户会觉得:
我刚才到底碰中了没有坐标准确是一部分,反馈时机同样是交互质量的一部分。
二十五、Geometry 错误要保留原始输入,方便复盘
解析失败时,SlideDrop 不只记录:
NO_DROP_ZONE还会保留:
windowTouch boardRect scale scroll version因为坐标类问题非常依赖现场。
如果日志只剩一个错误码,下一次页面布局已经变化,几乎无法复现。
最终诊断记录会做脱敏和大小控制,但工程调试阶段必须能还原当时的 TransformChain。
二十六、触点在动画过程中到达时,我更愿意“拒绝一次”而不是“猜一次”
这是这一篇比较明确的产品取舍。
页面正在执行缩放动画或布局过渡时:
GeometryState=TRANSFORMING精准分享事件如果到达,SlideDrop 返回可重试提示。
不会取动画中间某一帧的 geometry 去猜。
原因是“精准投递”最重要的是结果可预测。
一次让用户重新碰,比把素材错误插入另一个槽位,再让用户手动撤销更可接受。
二十七、第三篇完成后,坐标层已经可以独立复用
现在 SlideDrop 的位置处理已经不依赖“碰一碰”本身。
只要输入:
window coordinate geometry snapshot同一套 Transform + DropZoneResolver 也可以服务:
鼠标拖拽 手写笔投递 跨设备拖拽 普通本地拖拽这就是为什么我没有把代码命名成:
TapToShareSlotResolver而使用更通用的坐标变换语义。
精准碰一碰只是目前第一个数据入口。
二十八、03 最终验收里我还会看 100 次随机点命中率
除了固定八组测试,我还会在逻辑 Board 内生成 100 个可预测测试点。
每个点预先知道属于哪个 zone。
用相同的:
scale scroll boardRect经过正向变换生成窗口触点,再走逆向解析。
最终要求:
100 / 100回到原 zone。
这种测试比手工点四个格子更容易发现:
边界 浮点误差 舍入 单位问题。
第六篇最终回归会把这类坐标准确率正式纳入指标。
二十九、为什么这一篇值得保留所有中间数值
很多技术文章喜欢最后只留:
slot_02但工程排查最有价值的恰恰是:
632 / 176 728 / 236 582.4 / 188.8 0.809 / 0.400这些数值让我们知道每一步是否符合预期。
坐标问题最忌讳“黑盒正确”。
只要中间链路可见,哪怕以后加旋转矩阵、嵌套 Scroll,也仍然可以逐层验证。
这也是 SlideDrop 后续能继续扩到复杂画布的基础。
参考资料
- HarmonyOS 7 全场景能力:碰一碰·精准分享:https://developer.huawei.com/consumer/cn/features/all-scenario
- ArkUI / Input Kit 坐标体系术语:https://developer.huawei.com/consumer/cn/doc/HarmonyOS-Guides/input-kit-glossary
- ArkUI 帧率与状态更新优化建议:https://developer.huawei.com/consumer/cn/doc/best-practices/bpta-zhenlv