资讯动态

ArkUI PanGesture+Stack:标注框拖拽坐标映射与取消回滚【鸿蒙心迹】

发布时间:2026/10/14 2:33:49 来源:尧图企业网站定制
本文的项目、操作过程、界面、HiLog及数字均为可复现的演示设计并非已经在 DevEco Studio、模拟器或真机验证通过的结论。示例以 ArkUI API 语义为依据实际落地应按目标 SDK 重新编译核验。一、一个拖得动却存不准的标注框在离线巡检应用里模型先给出目标区域复核人员还要用手指微调。产品经理看的是“这个框能不能移动”我更关心移动以后最终写进报告的坐标到底是哪一套。若把触点的屏幕坐标直接当成原图坐标保存画面看着没有问题导出以后却可能落到另一个对象上。尤其换到折叠屏、多窗口或横竖屏模式同一张图显示的尺寸改变了错误会一下子放大。这篇只收拾一个很窄、但容易被忽略的工程边界手势位移属于显示区域标注数据属于原图坐标系。Demo 叫AnchorCanvas页面为AnnotationPage任务AC-2212。示意原图宽一千二百、高八百绘制区固定为三百六十乘二百四十 vp。起始框的位置是(240,160)大小(300,200)同一框在示意绘制区内对应左上角(72,48)、大小(90,60)。后续所有数据、图片与日志均使用这组约定避免配图和代码各讲各的。很多项目会一开始就把移动回调接到服务器但我建议先把手势约束留在本地识别成功后复制起始框移动期间生成临时框手指正常抬起才提交被系统取消则回到起始值。操作过程没有网络请求不靠回调延迟模拟正常流程。这样才容易解释什么时候可以丢弃状态什么时候已经形成一次有效编辑。二、先把“屏幕增量”翻译成“原图增量”本例手势在绘制区内产生的累计偏移是横向45vp、纵向30vp。由于示意绘制区与原图宽高比都为三比二因此每移动一 vp原图坐标增加三分之十。计算结果为原图横向增加一百五十、纵向增加一百最终框位置应该是(390,260)。如果只把四十五和三十直接加到原图框上数据会变成(285,190)那不是精度误差而是坐标系根本没对上。下面这个独立函数解决的就是坐标映射与边界钳制。它不接触 ArkUI 状态便于通过普通输入输出测试确认四条边界。画面缩放后必须更新viewWidthVp、viewHeightVp如果实际使用objectFit等比留白还要先减去图片自身的左右、上下留白不能继续沿用整个组件尺寸。interface MarkBox { x: number; y: number; w: number; h: number } interface ViewSize { widthVp: number; heightVp: number } function clampBox(origin: MarkBox, dxVp: number, dyVp: number, view: ViewSize): MarkBox { const logicalW: number 1200; const logicalH: number 800; if (view.widthVp 0 || view.heightVp 0 || !Number.isFinite(dxVp) || !Number.isFinite(dyVp)) { return { ...origin }; } const x: number origin.x dxVp * logicalW / view.widthVp; const y: number origin.y dyVp * logicalH / view.heightVp; return { x: Math.max(0, Math.min(logicalW - origin.w, x)), y: Math.max(0, Math.min(logicalH - origin.h, y)), w: origin.w, h: origin.h }; }这里的origin是按下并识别成功时复制的起始框后续计算不能拿每一帧的当前框继续累加因为offsetX、offsetY在该用法下被当成累计位移。重复相加会产生肉眼可见的加速漂移。边界计算保留宽高不允许拖动时把正方形拉成长方形。若业务改成缩放或旋转必须把中心点、旋转角和包围盒约束单独建模这个简单函数不处理透视变换。三、用 PanGesture 明确完成与取消的差别下一段放进页面是本例真正接触系统能力的部分。PanGesture管手势识别Stack负责把透明可拖动框叠在占位底图上。这里通过onActionStart固定起点onActionUpdate只刷新临时框onActionEnd才置为COMMITTEDonActionCancel恢复起点。声明的五 vp 是手势识别门槛不等于每次位置更新只能移动五 vp也不要把它写成触控采样精度。Entry Component struct AnnotationPage { State mark: MarkBox { x: 240, y: 160, w: 300, h: 200 }; State phase: string READY; private startMark: MarkBox { x: 240, y: 160, w: 300, h: 200 }; private updateMove(dx: number, dy: number): void { this.mark clampBox(this.startMark, dx, dy, { widthVp: 360, heightVp: 240 }); this.phase DRAGGING; } build() { Column({ space: 14 }) { Text(AnchorCanvas | AC-2212) Stack({ alignContent: Alignment.TopStart }) { Rect().width(360).height(240).fill(#E4E9F7) Rect() .width(this.mark.w * 360 / 1200) .height(this.mark.h * 240 / 800) .fill(Color.Transparent) .stroke(#F59A23) .strokeWidth(3) .position({ x: this.mark.x * 360 / 1200, y: this.mark.y * 240 / 800 }) .gesture(PanGesture({ fingers: 1, direction: PanDirection.All, distance: 5 }) .onActionStart(() { this.startMark { ...this.mark }; this.phase DRAGGING; }) .onActionUpdate((e: GestureEvent) { this.updateMove(e.offsetX, e.offsetY); }) .onActionEnd((e: GestureEvent) { this.updateMove(e.offsetX, e.offsetY); this.phase COMMITTED; }) .onActionCancel(() { this.mark { ...this.startMark }; this.phase CANCELLED; })) }.width(360).height(240) Text(状态${this.phase}) Text(原图坐标(${this.mark.x}, ${this.mark.y})) } } }MarkBox的整体对象替换会触发状态刷新startMark只保存本轮操作的基线不需要参与渲染。这里把onActionEnd再调用一次updateMove是为了防止最后一段位移没有落在上一帧的更新事件里。onActionCancel不发保存请求也不制造一次提交。页面销毁时没有手势之外的定时器和订阅需要释放如果产品以后接入网络提交必须再加会话令牌和在页面退出时停止或忽略未完成请求不能照抄这个纯本地示例就声称处理了异步释放。这张 DevEco 白色主题画面是设计示意左侧AnchorCanvas工程树中间是clampBox与updateMove右侧模拟器示例显示COMMITTED、原图坐标(390,260)。底部示意 HiLog 中的deltaVP(45,30)是为验收事先约定的输入不是工具真实抓到的手指事件。把这条线画清楚读者才能明白“演示数据”与“测得数据”之间的差别。四、提交数据只认原图不认当前宽度示意画面已经变成(390,260)但报告不该保存三百六十宽画布上的坐标(117,78)。正确做法是保存逻辑矩形再视需要保存归一化坐标。本例归一化后的左上角为(0.325,0.325)宽高均为0.25。这个四元组在更大的平板绘制区内仍能重新投影如果存的是 vp那么下一次打开就需要猜上次窗口到底多宽。下面的函数用于提交前最后一次验证并明确把边界条件挡在持久层之外。请注意它是业务负载构造函数不会自动写数据库更不是某个系统自带的“标注保存 API”。状态只有处于COMMITTED才允许调用避免一边拖动一边产生大量半成品记录。interface MarkPayload { taskId: string; x: number; y: number; w: number; h: number; } function makePayload(box: MarkBox, phase: string): MarkPayload | null { if (phase ! COMMITTED || box.w 0 || box.h 0 || box.x 0 || box.y 0 || box.x box.w 1200 || box.y box.h 800) { return null; } return { taskId: AC-2212, x: box.x / 1200, y: box.y / 800, w: box.w / 1200, h: box.h / 800 }; }提交时必须把taskId与页面当前对象 ID 一并核对。倘若画面加载了第二张图前一张图的框不能复用相同逻辑坐标后直接上传。现在的示例只有一个固定标注框多框情况下还需让每个框持有稳定的框 ID而不是依靠数组下标识别目标。业务如果允许镜像、旋转或原图带 EXIF 朝向映射还要先做坐标矩阵反变换不能把(0.325,0.325)当作通用万能公式。五、按一组输入验收比盯着动画更可靠我们约定一次操作任务AC-2212进入页面AnnotationPage初始矩形(240,160,300,200)用户从正确的框区域开始拖动手势累计移动(45vp,30vp)正常抬起后显示COMMITTED最终矩形(390,260,300,200)。紧接着重复一遍相同拖动但在中途发送取消事件结果必须回到本次手势开始前的位置不能误认为每次取消都回到应用启动时的旧坐标。手机纯屏示意图保留了时间、通信图标和电量数字仅用于检查文章数据对齐。验收还要补上负数位移、越界拖到四角、绘制区宽度为零、连续多次拖动、图片等比留边五类用例。尤其宽度为零时公式会产生无穷大本例选择返回起始框而不是把异常坐标传给页面。越界时采用钳制视觉上会停在边缘开发者应该给明确的交互提示。项目扩展到多窗口后测量出的绘制区尺寸可能在一次手势过程中发生变化。若仍按手势开始时的倍率计算中间帧拖动方向会出现突然跳变。实际产品应在手势开始时冻结本轮换算比例窗口尺寸变化则中止当前交互并重建绘制布局随后再允许用户重新开始。标注内容一般也不宜在手势更新事件里重复落库频繁写入既耗费资源也会让取消语义失效。最后留一个产品判断拖动中的视觉位置和已提交的业务位置不是同一件事取消恢复的是当前手势基线不是数据库里随便拿来的一条旧记录。这套方法适合可见的单框平移不覆盖多点缩放、旋转裁剪或复杂父子组件手势竞争。把这几个边界写在实现旁边比“拖拽顺畅”四个字更能帮助下一位接手的人判断代码是否还能继续扩展。参考资料华为开发者联盟《PanGesture》API 参考https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/ts-basic-gestures-pangesture-V5官方自定义手势事件字段参考https://developer.huawei.com/consumer/en/doc/harmonyos-references-V14/ts-gesture-customize-judge-V14。本文未经目标 SDK 编译与真机测试。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑