
FoldDropLab 的卡片拖拽在直板态完全正常展开并旋转后却总落到目标右下方。这个偏移不是一个常量而是密度、窗口原点、内容安全区和铰链补偿被混在了同一套坐标里。一、十七次失败都指向不同位置项目是一个跨栏资料板左侧素材列表右侧三个投放槽位。测试人员在折叠态拖 96 次没有问题展开到 840 × 720 vp、旋转 90°后却出现 17 次落点偏移。有时差 24 vp有时差 88 vp靠近铰链时还会多出 20 vp。直接给 x、y 减一个常量只能修好某一张截图。复盘日志才发现拖拽入口记录的是显示坐标像素槽位命中测试用的是内容局部 vp。中间还经历窗口迁移、状态栏安全区与双栏铰链空洞。旧实现只做了px / density相当于把五套坐标当成一套。这次不改拖拽样式也不改列表结构。任务drop_20261001_23只建立一条可追踪的换算链RAW → WINDOW_LOCAL → HINGE_ADJUSTED → CONTENT_LOCAL → DROP_ACCEPTED。页面叫CoordinateProbePage每次操作都显示原始坐标、快照版本、最终局部点和命中槽位。二、一次拖拽只使用一份几何快照最初我在每个onDrop中现查窗口尺寸和显示密度。问题是展开动画期间几何正在变化按下时拿到旧窗口原点抬起时已经是新密度计算结果自然不可复现。正确做法是手势开始时捕获一份不可变快照结束前不混用新参数窗口发生结构变化则取消本轮。下面的代码解决参数来源分散的问题。CoordinateSnapshot明确列出从显示到内容需要的全部几何数据并用version标记快照代次。exportinterfacePoint{x:number;y:number}exportinterfaceRect{x:number;y:number;width:number;height:number}exportinterfaceCoordinateSnapshot{version:numberdensity:numberrotation:0|90|180|270displaySizePx:Point windowOriginPx:Point contentOriginVp:Point hingeRectVp?:Rect targetPane:PRIMARY|SECONDARY}exportclassGeometrySnapshotStore{privatecurrent?:CoordinateSnapshotpublish(next:CoordinateSnapshot):void{if(next.density0)thrownewError(DENSITY_INVALID)this.currentObject.freeze({...next})}capture():CoordinateSnapshot{if(!this.current)thrownewError(GEOMETRY_NOT_READY)returnthis.current}invalidate():void{this.currentundefined}}快照在窗口尺寸、旋转、显示迁移或折叠状态变化完成后发布。capture()不复制半套字段调用方拿到的是同一版本。页面进入后台时调用invalidate()回来后必须等新几何到齐才允许拖拽。正式项目可由 Window Kit、Display 和布局测量回调填充这些字段Demo 把输入集中到适配器避免业务代码依赖多个监听器的先后顺序。三、换算顺序比公式更重要坐标映射分四步先用显示尺寸处理旋转再减窗口原点得到窗口局部像素之后按 density 转成 vp最后扣除内容原点并在目标位于第二栏时减去铰链占用。每一步都产生一份可记录的中间值。这段DropCoordinateMapper解决“结果错了却不知道错在哪一层”的问题。它返回轨迹而不只返回终点HiLog 因此能直接看到哪一步产生跳变。exportinterfaceCoordinateTrace{rotatedPx:Point windowVp:Point hingeAdjustedVp:Point contentVp:Point}exportclassDropCoordinateMapper{map(rawPx:Point,s:CoordinateSnapshot):CoordinateTrace{constrotatedthis.rotate(rawPx,s.rotation,s.displaySizePx)constwindowVp:Point{x:(rotated.x-s.windowOriginPx.x)/s.density,y:(rotated.y-s.windowOriginPx.y)/s.density}consthingeShifts.targetPaneSECONDARYs.hingeRectVp?s.hingeRectVp.width:0consthingeAdjustedVp:Point{x:windowVp.x-hingeShift,y:windowVp.y}return{rotatedPx:rotated,windowVp,hingeAdjustedVp,contentVp:{x:hingeAdjustedVp.x-s.contentOriginVp.x,y:hingeAdjustedVp.y-s.contentOriginVp.y}}}privaterotate(p:Point,r:number,size:Point):Point{if(r90)return{x:size.y-p.y,y:p.x}if(r180)return{x:size.x-p.x,y:size.y-p.y}if(r270)return{x:p.y,y:size.x-p.x}returnp}}这里的旋转函数针对 Demo 记录的原始显示坐标定义不应直接复制到所有输入 API不同事件可能已经提供窗口局部坐标重复旋转会得到第二次错误。正式接入时先确认事件坐标语义再决定从链路哪一层开始。换算中只使用本轮快照数据从 raw 逐层派生原始点永远不被覆盖方便重放。本次样本raw(1540, 990) px密度 2.75目标为 SECONDARY。经过旋转和窗口原点处理得到window(580, 360) vp扣除 20 vp 铰链占位及内容原点(24, 88)后最终content(536, 272) vp命中slot_B。图片和日志都使用这组值。四、窗口变化时宁可取消也不混算拖到一半折叠设备姿态变化是最难复现的一类边界。继续使用旧快照目标已经移动切到新快照手指起点又属于旧坐标系。两种做法都会让卡片跳跃。当前方案选择取消本轮提示用户重新拖动。下面的DropSession用快照版本锁定会话并保证结束或取消只结算一次。它解决的是生命周期与重复回调而不是几何公式。exportclassDropSession{privateactive:booleanfalseprivatesettled:booleanfalseprivatesnapshotVersion:number-1begin(snapshot:CoordinateSnapshot):void{this.activetruethis.settledfalsethis.snapshotVersionsnapshot.version}accept(raw:Point,snapshot:CoordinateSnapshot,mapper:DropCoordinateMapper):CoordinateTrace|undefined{if(!this.active||this.settled)returnundefinedif(snapshot.version!this.snapshotVersion){this.cancel(GEOMETRY_CHANGED)returnundefined}this.settledtruethis.activefalsereturnmapper.map(raw,snapshot)}cancel(reason:string):void{if(!this.active||this.settled)returnthis.settledtruethis.activefalsehilog.info(0x0000,FoldDropLab,drop cancelled:${reason})}}begin()在拖拽开始时执行accept()只允许第一次结算几何版本不同就进入GEOMETRY_CHANGED不修改业务列表。页面消失、窗口失焦和组件销毁前均调用cancel()同时注销窗口监听。正式产品还要释放拖拽预览使用的 PixelMap 或临时数据引用本文 Demo 的预览是纯色卡片没有额外图像资源。工程目录也体现这种分层geometry/GeometrySnapshotStore.ets管输入geometry/DropCoordinateMapper.ets做纯换算drag/DropSession.ets管会话pages/CoordinateProbePage.ets展示轨迹window/FoldWindowObserver.ets只负责把系统变化翻译成新快照。FoldWindowObserver还有一道稳定性处理窗口、显示与布局回调可能在同一轮变化中连续到达不能每来一个字段就发布快照。它先收集候选值等窗口尺寸和内容测量都完成后再递增 version。若 100 ms 内信息仍不完整页面保持GEOMETRY_UPDATING拖拽入口置灰。比起拿半新半旧的数据继续算短暂不可拖更容易向用户解释。槽位注册也不再保存组件对象。每个目标只上报targetId、局部 Rect 与 geometryVersion页面重排后旧 Rect 自动失效。列表刷新或 LazyForEach 复用组件时如果 targetId 已不存在落点返回TARGET_GONE不会把资料投给刚好复用了同一索引的新卡片。正式工程还要让业务层对投递命令做幂等校验坐标命中成功不等于数据写入一定成功。错误日志从单个偏移值改成完整 trace但线上不会逐次上传 raw 坐标。开发构建保留四层点位正式构建只聚合旋转、形态、误差区间和拒绝原因。这样既能判断某类设备是否集中偏移又不保留用户具体触点轨迹。若需要复现由测试样本中的合成坐标重放而不是采集真实用户手势。五、把偏移量变成可验收的证据测试不再只看卡片是否大致落进槽位而是准备 96 个固定样本覆盖 0°/90°/180°/270°、折叠与展开、主栏与副栏、窗口迁移前后。每个样本保存 raw、快照和期望内容坐标误差超过 1 vp 即失败。修复前偏移失败 17 次修复后为 0。24 个旋转样本全部通过12 次折叠状态切换全部按预期取消或重试映射计算 P95 为 0.34 ms。最终状态DROP_ACCEPTED命中目标slot_B。0.34 ms 只统计纯换算和命中测试不包含拖拽动画或跨窗口数据传输。96 个样本不是手工写 96 份页面。测试数据由rawPoint snapshot expectedTarget三部分组成换算器是纯函数因此可以在不启动窗口的情况下先跑单元测试。真机只验证系统回调提供的几何是否正确填进 snapshot以及 ArkUI 槽位测量是否与内容坐标同源。把算法和系统输入拆开后失败能快速归到“公式错”或“适配器错”。我还用红点叠层做了人工抽查原始触点用灰点窗口局部用蓝点最终内容点用红点。三点在普通直板态几乎重合在展开副栏会明显看到 20 vp 铰链修正。叠层不参与命中只读取 trace因此不会为了调试再次修改业务坐标。验收完成后关闭开关不进入发布页面。运行图顶部时间 23:52、电量 79%中间用一条细轨迹展示 RAW 到 CONTENT_LOCAL 的五步变化底部保留样本、误差和状态。红色标注只指出 20 vp 铰链补偿和slot_B命中点不遮住主要 UI。它承担的是调试证据不是装饰。最终 HiLog 中会同时出现snapshotVersion37与targetVersion37。这两个数字相等才允许进入DROP_ACCEPTED。以前日志只有 x、y看见偏移后无法判断参数来自哪个窗口状态现在即使截图不在也能从版本号确认拖拽是否跨过一次布局变化。对于被取消的会话日志只记录GEOMETRY_CHANGED不会伪造一个最终落点。应用恢复时也不直接沿用 version 37。aboutToAppear触发观察器重新采集版本递增到 38布局测量完成前状态为GEOMETRY_UPDATING。页面消失前注销系统监听和布局订阅清空 target registry并取消活动会话。遗漏任一项都可能让后台页面继续更新快照或让旧槽位留在命中集合中。六、几个不能用补偿常量掩盖的边界第一铰链不是永远位于正中也不是所有形态都占据可见宽度。只有布局确实为铰链留出间隔时才应用 shift。第二密度变化必须伴随快照版本变化不能在会话中偷偷替换。第三窗口迁移到外接屏后显示尺寸、旋转和 windowOrigin 都要重新取值。第四目标槽位的 Rect 必须和 contentVp 属于同一局部坐标系。若槽位来自全局测量应在注册时转换一次而不是拿全局 Rect 与局部点直接比较。第五拖拽预览的视觉位置和业务命中点可以不同不能因为预览阴影有偏移就修改真实指针坐标。还有一个产品取舍几何变化时取消拖拽会打断用户但它比把资料投到错误栏目更可控。可以保留拖拽对象 ID在窗口稳定后提供“重新拖动”提示却不要自动猜测新落点。涉及删除、发送或跨设备转移时错误命中比一次取消代价更高。浮窗与分屏还会引入窗口装饰区。不要假定 windowOrigin 就是内容左上角标题栏、状态栏和应用自己的工具栏分别属于不同层。当前示例的 contentOrigin 是(24, 88) vp来自布局测量不是写死的安全区。窗口尺寸变化后如果工具栏换行这个原点也会变化因此它和槽位 Rect 必须在同一个 version 中发布。对于跨应用拖拽事件可能只提供对方能够理解的数据不应把本地几何快照塞进 UDMF 载荷。坐标只用于当前接收窗口命中数据载荷负责内容与权限两者的生命周期不同。接收完成后及时释放临时预览资源和授权句柄命中失败也要走释放路径不能因为业务没有提交就跳过清理。上线前我又把调试页切到最小窗口、自由窗口和外接显示三种环境确认GEOMETRY_UPDATING不会误报成功。最终的验收标准不是红点看起来对齐而是相同样本在相同快照下始终得到同一内容坐标快照变化时则明确取消不产生半旧半新的结果。这也让后续维护更简单新增一种设备形态时只需要适配它如何产出 snapshot不必重写命中算法新增一种投放组件时只需要注册同坐标系的 Rect。系统变化与业务槽位被隔在两端中间的映射器保持纯净、可测也更容易发现真正的边界条件。这次修复没有发明新的手势。真正做的是承认坐标有层次并把每一次转换留下证据显示像素、窗口局部、vp、铰链调整、内容局部。只要链路中的单位和版本明确折叠、旋转和跨窗口就不再是一堆补偿常量而是可以重放、可以拒绝、可以验收的工程过程。