【共创季稿事节】鸿蒙原生 ArkTS 布局方式之 PanGesture 拖拽手势布局:从原理到实战

发布时间:2026/7/2 3:30:43

【共创季稿事节】鸿蒙原生 ArkTS 布局方式之 PanGesture 拖拽手势布局:从原理到实战 一、引言1.1 为什么是 PanGesture如果说点击Tap是移动端交互的名词那么拖拽Pan就是动词——它代表了用户最直觉的操作把东西从一个地方移到另一个地方。从滑动列表、拖拽排序到地图平移、图片浏览PanGesture平移拖拽手势几乎无处不在。在 HarmonyOS NEXT 的 ArkUI 框架中PanGesture 是实现这些交互的底层基础设施。1.2 拖拽交互的四大应用场景场景 示例 核心能力自由拖拽 浮动窗口、悬浮球、桌面小组件 任意方向跟随手指滑动手势 列表滑动删除、卡片滑动消失 水平/垂直方向判定拖拽排序 列表项重排、网格拖动换位 位置计算 交换动画边界约束 抽屉面板、底部弹出层 位置限制 回弹效果1.3 本文目标深入理解 PanGesture 的设计原理与完整 API掌握从新手拖拽到生产级拖拽的完整技术栈理解三种边界模式无限制/约束/回弹的实现思路学会处理拖拽过程中的常见坑点与性能优化二、ArkUI 手势体系回顾2.1 从触摸到手势的完整链路在深入 PanGesture 之前有必要理解 ArkUI 处理触摸事件的完整流程用户手指触摸屏幕↓① 触摸事件Touch Event→ onTouch 回调Down / Move / Up↓② 命中测试Hit Test→ 确定触摸点落在哪个组件上↓③ 手势识别Gesture Recognition→ GestureRecognizer 判断是否为特定手势↓④ 手势绑定回调→ onActionStart / onActionUpdate / onActionEnd↓⑤ 状态更新与 UI 重绘→ State 变量变化 → build() 重新执行PanGesture 就处于第③④步之间——它识别用户的平移操作并通过回调把偏移信息传递给开发者。2.2 PanGesture 在整个手势家族中的位置Gesture基础手势├── TapGesture 单击/双击/多指点击├── LongPressGesture 长按├── PanGesture 拖拽/平移 ← 本文焦点├── SwipeGesture 滑动快速擦过├── PinchGesture 捏合缩放├── RotationGesture 旋转└── GestureGroup 组合手势├── Exclusive 互斥├── Parallel 并行└── Race 竞速PanGesture 与 SwipeGesture 的区别PanGesture 关注持续的位置变化而 SwipeGesture 关注快速滑动的方向判定。三、PanGesture API 深度解析3.1 完整 API 签名以下 API 签名基于 HarmonyOS NEXTAPI 12SDK 6.1.1gesture.d.ts 中的实际声明interface PanGestureInterface extends GestureInterface {// 构造函数两种重载 (options?: PanGestureOptions): PanGestureInterface;(options?: PanGestureHandlerOptions): PanGestureInterface;// 生命周期回调 onActionStart(event: (event: GestureEvent) void): PanGestureInterface; onActionUpdate(event: (event: GestureEvent) void): PanGestureInterface; onActionEnd(event: (event: GestureEvent) void): PanGestureInterface; onActionCancel(event: () void): PanGestureInterface;}// 配置参数declare class PanGestureOptions {fingers?: number; // 手指数量默认 1direction?: PanDirection; // 拖动方向distance?: number; // 最小拖动距离vp默认 5}enum PanDirection {ALL 0, // 所有方向默认HORIZONTAL 1, // 仅水平VERTICAL 2, // 仅垂直NONE 3, // 禁用}3.2 参数详解direction — 拖动方向值 含义 典型场景PanDirection.All默认 任意方向 自由拖拽PanDirection.Horizontal 仅水平方向 左右滑动、横向滑动删除PanDirection.Vertical 仅垂直方向 上下滚动、下拉刷新PanDirection.None 禁用 临时关闭拖拽distance — 最小触发距离类型number单位 vp默认值5作用防止手指轻微抖动被误判为拖拽策略手指移动超过此距离后手势才进入已识别状态fingers — 手指数量类型number默认值1范围1 ~ 10典型值1单指拖拽、2双指平移3.3 回调事件详解onActionStart — 拖拽开始.onActionStart((event: GestureEvent) {// event.timestamp: 触发时间戳// event.offsetX: 累计 X 偏移此时接近 0// event.offsetY: 累计 Y 偏移// event.fingerList: 参与手势的手指信息})典型用途重置状态、记录起点、触发进入拖拽模式的 UI 变化放大、变色、更新阴影。onActionUpdate — 拖拽更新核心回调.onActionUpdate((event: GestureEvent) {// event.offsetX: 从手势起点开始的累计 X 偏移vp正数向右// event.offsetY: 从手势起点开始的累计 Y 偏移vp正数向下// event.velocity: 当前速度vp/s// event.velocityX: X 轴速度分量// event.velocityY: Y 轴速度分量// event.timestamp: 当前帧时间戳})这是整个拖拽交互中最关键的代码路径——每一帧都会被调用60fps在其中更新 State 变量以驱动 UI 重新布局。onActionEnd — 拖拽结束.onActionEnd((event: GestureEvent) {// event.offsetX: 最终的累计 X 偏移// event.offsetY: 最终的累计 Y 偏移// event.velocity: 松手时的速度})典型用途提交最终位置、触发回弹动画、更新统计。onActionCancel — 拖拽取消.onActionCancel(() {// 无 event 参数重载1// event: GestureEvent重载2})触发条件来电中断、手势被父容器拦截、应用进入后台。3.4 ⚠️ 命名变更从 V1 到 V2 的迁移如果你是第一次接触 HarmonyOS NEXT API 12请注意以下关键变更旧命名API 11 及更早 新命名API 12onDragStart((event) {}) onActionStart((event) {})onDragUpdate((event) {}) onActionUpdate((event) {})onDragEnd((event) {}) onActionEnd((event) {})event.getOffsetX() event.offsetX属性而非方法event.getOffsetY() event.offsetY属性而非方法迁移原因HarmonyOS NEXT 统一了所有手势的回调命名风格采用了统一的事件模型。所有手势Tap/LongPress/Pan/Swipe/Pinch/Rotation都使用 onActionStart / onActionUpdate / onActionEnd 三件套。四、Demo 代码逐层剖析4.1 项目结构与路由{“src”: [“pages/PanGestureDemo”]}唯一的入口文件 PanGestureDemo.ets 包含 475 行代码结构清晰PanGestureDemo.ets (475行)├── enum DragBoundary ← 三种拖拽模式枚举├── Component PanGestureDemo ← 主组件│ ├── State 变量8个 ← 响应式状态│ ├── build() ← UI 声明│ │ ├── 标题区│ │ ├── Stack 拖拽舞台│ │ │ ├── GridLineRow×9 ← 背景参考网格│ │ │ └── 可拖拽卡片 ← 核心交互区│ │ │ └── PanGesture ← 手势绑定│ │ ├── 信息面板│ │ └── 底部按钮栏│ └── 私有方法├── Builder InfoRow ← 全局构建器└── Component GridLineRow ← 子组件4.2 八个 State 变量的设计哲学State private cardX: number 0; // 位置 XState private cardY: number 0; // 位置 YState private isDragging: boolean false; // 拖拽状态State private cardScale: number 1.0; // 缩放反馈State private boundaryMode: DragBoundary; // 模式State private dragCount: number 0; // 次数统计State private dragVelocity: number 0; // 速度State private totalDistance: number 0; // 总路程为什么需要这么多变量ArkUI 采用细粒度响应式更新——每个 State 变量都是独立的反应源。当我们在 onActionUpdate 中修改 cardX 时只有依赖 cardX 的 UI 部分会重新渲染。这种设计避免了整棵树的重绘在 60fps 的拖拽场景中至关重要。4.3 非响应式变量优化关键private lastOffsetX: number 0;private lastOffsetY: number 0;这两个变量没有用 State 装饰因为它们只用于内部计算增量差值不直接影响 UI。如果错误地将它们设为 State每次拖拽更新都会触发两次 UI 重绘一次更新 lastOffset一次更新 cardX导致性能下降 50%。4.4 核心手势绑定50 行代码读懂全貌.gesture(PanGesture({direction: PanDirection.All, // 所有方向distance: 5, // 5vp 阈值fingers: 1 // 单指}).onActionStart((event: GestureEvent) {this.isDragging true;this.lastOffsetX 0;this.lastOffsetY 0;this.cardScale 1.1; // 放大反馈}).onActionUpdate((event: GestureEvent) {// 关键用增量差值而非绝对偏移const deltaX: number event.offsetX - this.lastOffsetX;const deltaY: number event.offsetY - this.lastOffsetY;this.lastOffsetX event.offsetX;this.lastOffsetY event.offsetY;// 位置累加 this.cardX deltaX; this.cardY deltaY; // 速度记录 this.dragVelocity event.velocity; }) .onActionEnd((event: GestureEvent) { if (this.boundaryMode DragBoundary.SPRING_BACK) { this.getUIContext()?.animateTo(...); // 回弹动画 } this.isDragging false; this.cardScale 1.0; this.dragCount; this.totalDistance Math.abs(event.offsetX) Math.abs(event.offsetY); }) .onActionCancel(() { this.isDragging false; this.cardScale 1.0; }))为什么用增量差值而非直接用 event.offsetevent.offsetX 是从手势识别起点不是组件初始位置的累计偏移。如果直接使用// ❌ 错误每次 onActionUpdate 直接赋值this.cardX event.offsetX; // 位置会跳变假设用户第一次拖到 100px松手。第二次再拖offset 从 0 开始累加但 cardX 还是 100这样位置会瞬间跳到 0→100→200… 产生跳变。正确的做法是// ✅ 正确用增量差值累加const deltaX event.offsetX - this.lastOffsetX;this.lastOffsetX event.offsetX;this.cardX deltaX;这样不管用户拖拽多少次位置都是连续平滑的。4.5 三种拖拽模式详解模式一无限制UNBOUNDED卡片位置 手指位置无任何边界限制实现onActionUpdate 中直接赋值不做 clamp。效果卡片可以拖到屏幕外父容器设置了 .clip(true)超出部分被裁切。适用自由拖拽的悬浮窗、贴边吸附的悬浮球。模式二边界约束BOUNDEDmaxX 父容器宽度/2 - 卡片宽度/2newX clamp(newX, -maxX, maxX)if (this.boundaryMode DragBoundary.BOUNDED) {const halfW this.CARD_WIDTH / 2;const halfH this.CARD_HEIGHT / 2;const boundX this.parentWidth / 2 - halfW;const boundY this.parentHeight / 2 - halfH;newX Math.max(-boundX, Math.min(boundX, newX));newY Math.max(-boundY, Math.min(boundY, newY));}边界计算公式推导父容器中心点坐标(parentWidth/2, parentHeight/2)卡片左上角相对于父容器中心的偏移范围用 .offset() 定位时offsetX/Y 以组件原位置为基准所以约束范围是 [-parentWidth/2 cardWidth/2, parentWidth/2 - cardWidth/2]模式三回弹SPRING_BACK// onActionUpdate 中不做约束位置跟随手指// onActionEnd 中触发回弹动画.animateTo({ duration: 350, curve: curves.springMotion() }, () {this.cardX 0;this.cardY 0;});curves.springMotion() 是物理弹性曲线——它模拟了一个弹簧的阻尼振动让卡片在松手后产生弹回去的视觉效果。参数无法配置相比 iOS 的 spring()但默认效果已经足够自然。4.6 容器尺寸获取onAreaChange.onAreaChange((oldValue: Area, newValue: Area) {if (newValue typeof newValue.width ‘number’ typeof newValue.height ‘number’) {this.parentWidth newValue.width;this.parentHeight newValue.height;}})Area 类型的 width / height 是 Length 类型即 number | string所以需要做类型收窄。这里的处理是只接受 number 类型的尺寸值。4.7 视觉反馈设计属性 拖拽时 静止时 效果backgroundColor #FF6B35橙色 #4A90D9蓝色 颜色变化提示状态切换scale 1.1 1.0 轻微放大提供按住了的触感shadow.radius 24 10 阴影增大模拟抬升shadow.offsetY 8 4 投影更远增强立体感zIndex 10 1 拖拽时浮在其他元素之上这些是 Affordance可操作暗示设计——通过视觉变化告诉用户这个元素正在被你操控。4.8 信息面板与调试信息面板实时展示当前位置 (X, Y)拖拽状态静止/拖动中实时速度vp/s累计拖拽次数这在开发调试阶段非常有用——可以直观地看到 event.offsetX、event.velocity 等数值如何在拖拽过程中变化。4.9 底部按钮栏的实现四个按钮共用 TapGesture 实现点击切换——注意这里复用了上一篇文章的 TapGesture 知识。我们的拖拽页面并非只有 PanGesture而是 TapGesture PanGesture 协同工作。五、进阶从 Demo 到产品级拖拽5.1 带惯性的自由落体Fling真实世界的拖拽在松手后应该有惯性滑动.onActionEnd((event: GestureEvent) {const velocityX event.velocityX;const velocityY event.velocityY;// 根据速度计算惯性滑动距离const flingDistanceX velocityX * 0.3; // 阻尼系数const flingDistanceY velocityY * 0.3;this.getUIContext()?.animateTo({duration: 500,curve: curves.decelerate() // 减速曲线}, () {this.cardX flingDistanceX;this.cardY flingDistanceY;});})5.2 拖拽排序Drag-to-Reorder// 在 List 中使用拖拽排序List() {ForEach(this.items, (item: string, index: number) {ListItem() {Text(item)}.gesture(PanGesture({ direction: PanDirection.Vertical }).onActionUpdate((event) {// 计算拖拽偏移判断是否需要与相邻项交换位置this.handleReorder(index, event.offsetY);}))})}5.3 双指拖拽PanGesture({fingers: 2,distance: 10 // 双指需要更大的触发距离})5.4 与 PinchGesture 组合.gesture(GestureGroup(GestureMode.Parallel,PanGesture({ fingers: 1 }).onActionUpdate((event) {// 单指拖拽移动位置this.panImage(event.offsetX, event.offsetY);}),PinchGesture({ fingers: 2 }).onActionUpdate((event) {// 双指捏合缩放this.zoomImage(event.scale);})))5.5 与 onTouch 配合实现拖拽涟漪.onActionUpdate((event: GestureEvent) {// 从 fingerList 获取触摸点位置const finger event.fingerList[0];if (finger) {this.rippleX finger.x;this.rippleY finger.y;}})5.6 吸附效果Snap.onActionEnd(() {// 计算离最近锚点的距离const snapPoints [-100, 0, 100];const nearest snapPoints.reduce((prev, curr) Math.abs(curr - this.cardX) Math.abs(prev - this.cardX) ? curr : prev);this.getUIContext()?.animateTo({duration: 200,curve: curves.springMotion()}, () {this.cardX nearest;});})六、常见问题与坑点6.1 位置跳变第 4.4 节已详述现象松手后再次拖拽卡片位置跳变到起点。原因直接使用 event.offsetX 而非增量差值。修复用 deltaX event.offsetX - lastOffsetX 计算每帧增量。6.2 拖拽穿透手势被父容器拦截现象子组件上的 PanGesture 不触发或者触发不灵敏。原因父容器可能也有手势识别器或者父容器拦截了触摸事件。解决方案确保父容器没有 priorityGesture() 抢占手势使用 .hitTestBehavior(HitTestMode.None) 让父容器不参与命中测试如果父容器是 Scroll 或 List需要使用 .nestedScroll() 配置嵌套滚动6.3 拖拽与滚动冲突现象在可滚动容器Scroll/List内部拖拽时滚动和拖拽同时触发。解决方案PanGesture({direction: PanDirection.Horizontal, // 限定方向避免与垂直滚动冲突distance: 15 // 增大触发阈值})6.4 性能onActionUpdate 60fps 优化onActionUpdate 以 60fps 频率调用每帧只有 16ms 的执行时间。以下操作会导致掉帧❌ 在 update 闭包中执行 JSON.parse、正则匹配等❌ 在 update 闭包中创建新对象或闭包❌ 修改大量 State 变量虽然响应式是细粒度的但仍需布局计算✅ 推荐的做法只修改必要的 State 变量计算逻辑保持简单加减法避免在 update 中触发动画6.5 event.velocity 的数值范围event.velocity 的单位是 vp/s虚拟像素/秒。典型的拖拽速度范围操作 速度vp/s缓慢移动 200 ~ 500正常拖拽 500 ~ 2000快速滑动 2000 ~ 5000猛滑Fling 50006.6 onAreaChange 的 Length 类型interface Area {width: Length; // Length number | stringheight: Length;}onAreaChange 回调中的 width 和 height 是联合类型。当值为百分比字符串如 “100%”时typeof 检查会返回 string。我们的 Demo 中只处理了 number 类型对于 string 类型可以解析if (typeof newValue.width ‘number’) {this.parentWidth newValue.width;} else if (typeof newValue.width ‘string’) {// 解析百分比或从其他途径获取像素值}七、从 Demo 到生产最佳实践清单7.1 代码组织建议components/├── DraggableCard.ets ← 可复用的拖拽卡片组件├── DragContainer.ets ← 拖拽容器管理所有的拖拽行为├── SnapGrid.ets ← 吸附网格组件└── hooks/└── useDrag.ts ← 拖拽逻辑封装状态 手势7.2 手势配置常量化// gestureConfig.etsexport const PAN_CONFIG {FREE: { direction: PanDirection.All, distance: 5, fingers: 1 },HORIZONTAL: { direction: PanDirection.Horizontal, distance: 10, fingers: 1 },VERTICAL: { direction: PanDirection.Vertical, distance: 10, fingers: 1 },TWO_FINGER: { direction: PanDirection.All, distance: 10, fingers: 2 },} as const;7.3 拖拽参数可配置化interface DragConfig {axis?: ‘x’ | ‘y’ | ‘both’;bounds?: { minX: number; maxX: number; minY: number; maxY: number } | null;snapPoints?: { x: number[]; y: number[] };springBack?: boolean;inertia?: boolean;onDragStart?: () void;onDragEnd?: (position: { x: number; y: number }) void;}7.4 性能监控private frameCount: number 0;private lastFrameTime: number 0;.onActionUpdate((event) {this.frameCount;if (event.timestamp - this.lastFrameTime 1000) {console.info(PanGesture FPS: ${this.frameCount});this.frameCount 0;this.lastFrameTime event.timestamp;}// … 正常的更新逻辑})7.5 无障碍支持.gesture(PanGesture({ … })).accessibilityText(‘可拖拽的卡片当前位于(${this.cardX}, ${this.cardY})’).accessibilityLevel(‘auto’)八、与其他平台拖拽手势的对比特性 ArkUI (PanGesture) SwiftUI (DragGesture) Jetpack Compose (draggable/detectDragGestures)声明式 API ✅ gesture(PanGesture{}) ✅ gesture(DragGesture()) ✅ .draggable() / .pointerInput()回调命名 onActionStart/Update/End onChanged/onEnded onDragStart/onDrag/onDragEnd可配方向 ✅ PanDirection ✅ 无但可过滤 ✅ 通过 Orientation可配手指数 ✅ fingers: number ✅ minimumDistance ❌ 需自定义可配触发距离 ✅ distance: number ✅ minimumDistance ✅ detectDragGestures 内置速度信息 ✅ event.velocity ✅ value.velocity ✅ change.velocity边界回调 ❌ 需自行实现 ❌ 需自行实现 ❌ 需自行实现手势组合 ✅ GestureGroup ✅ Simultaneous/Sequenced ✅ forEachGesture核心差异ArkUI 的 PanGesture 在回调命名上采用了统一的 onAction 前缀与其他手势保持一致。而 SwiftUI 的 DragGesture 使用 onChanged/onEndedCompose 使用 onDrag。三种框架都支持方向、距离、手指数的配置但在命名风格和 API 结构上各有特点。九、结语9.1 核心收获通过这个 Demo我们完整地走通了 PanGesture 的从配置到触发的全链路PanGesture 配置方向/距离/手指数→ .gesture() 绑定到组件→ 用户手指触摸并滑动→ 命中测试判断触摸点在哪个组件上→ 手势识别器判断是否超过 distance 阈值→ onActionStart通知拖拽开始→ onActionUpdate × N60fps 实时追踪→ 更新 State → UI 重绘 → 卡片跟随手指→ onActionEnd松手→ 回弹动画 / 提交最终位置→ onActionCancel中断9.2 核心思维模型“拖拽的本质是状态同步——手指位置驱动组件位置。”手指的 物理世界 (screen position) 通过手势系统的 识别与量化 (PanGesture GestureEvent) 映射到组件的 响应式状态 (State cardX, cardY) 最终驱动界面的 布局渲染 (.offset { x: cardX, y: cardY })每一帧的 onActionUpdate 都是这个映射链路的一次同步。9.3 下一步技术探索与 Scroll / List 的嵌套手势处理nestedScroll()PanGesture AnimatedProperties 实现拖拽物理引擎PanGesture Grid 实现拖拽排序组件库GestureGroup(GestureMode.Race, TapGesture, PanGesture) 实现点按拖拽混合识别学习 SwipeGesture 快速滑动识别附录 A完整 Demo 代码/*PanGestureDemo.ets —— 鸿蒙原生 ArkTS 布局方式之 PanGesture 拖拽布局 核心技术 gesture() —— 将手势识别器绑定到组件PanGesture —— 拖拽/平移手势可配置方向、手指数量、最小拖动距离onActionUpdate —— 拖拽位置更新的回调实时获取手指偏移量 布局要点 PanGesture 识别用户在屏幕上的平移拖动操作通过 onActionUpdate 回调中的 event.offsetX / event.offsetY 实时获取拖拽偏移量配合 State 驱动组件位置变化实现「指哪打哪」的拖拽布局可配置direction拖动方向All / Horizontal / Vertical、distance触发距离阈值、fingers手指数量结合 onActionStart / onActionEnd 实现拖拽前后状态切换放大/阴影/回弹 API 说明HarmonyOS NEXT API 12 PanGesture 的回调统一为onActionStart((event: GestureEvent) void) 拖拽开始onActionUpdate((event: GestureEvent) void) 拖拽位置更新 ← 核心onActionEnd((event: GestureEvent) void) 拖拽结束onActionCancel(() void) 拖拽取消GestureEvent 的属性不是方法event.offsetX / event.offsetY 累计偏移量vpevent.velocity / event.velocityX / event.velocityY 速度event.timestamp 时间戳*/import { curves } from ‘kit.ArkUI’;/**拖拽约束区域的类型*/enum DragBoundary {UNBOUNDED,BOUNDED,SPRING_BACK}EntryComponentstruct PanGestureDemo {State private cardX: number 0;State private cardY: number 0;State private isDragging: boolean false;State private cardScale: number 1.0;State private boundaryMode: DragBoundary DragBoundary.UNBOUNDED;State private dragCount: number 0;State private dragVelocity: number 0;State private totalDistance: number 0;State private parentWidth: number 360;State private parentHeight: number 640;private readonly CARD_WIDTH: number 120;private readonly CARD_HEIGHT: number 120;private lastOffsetX: number 0;private lastOffsetY: number 0;build() {Column() {// 标题Text(‘PanGesture 拖拽手势布局演示’).fontSize(22).fontWeight(FontWeight.Bold).fontColor(Color.White).textAlign(TextAlign.Center).width(‘100%’).padding({ top: 16, bottom: 4 })// 模式描述 Text(this.getBoundaryDescription()).fontSize(13) .fontColor(this.boundaryMode DragBoundary.UNBOUNDED ? Color.Green : this.boundaryMode DragBoundary.BOUNDED ? Color.Orange : #00B4D8) .textAlign(TextAlign.Center).width(100%).margin({ bottom: 6 }) // 拖拽舞台 Stack() { // 背景网格 Column() { GridLineRow(); GridLineRow(); GridLineRow(); GridLineRow(); GridLineRow(); GridLineRow(); GridLineRow(); GridLineRow(); GridLineRow(); }.width(100%).height(100%) // --- 可拖拽卡片 --- Column() { Text(this.isDragging ? 拖拽中 : 拖拽我) .fontSize(16).fontColor(Color.White) .fontWeight(FontWeight.Medium).lineHeight(22) Text((${Math.round(this.cardX)}, ${Math.round(this.cardY)})) .fontSize(12).fontColor(Color.White).opacity(0.85).margin({ top: 4 }) } .width(this.CARD_WIDTH).height(this.CARD_HEIGHT) .backgroundColor(this.isDragging ? #FF6B35 : #4A90D9) .borderRadius(16) .shadow({ radius: this.isDragging ? 24 : 10, color: this.isDragging ? #FF6B3580 : #4A90D980, offsetY: this.isDragging ? 8 : 4 }) .offset({ x: this.cardX, y: this.cardY }) .scale({ x: this.cardScale, y: this.cardScale }) .zIndex(this.isDragging ? 10 : 1) // 核心PanGesture 绑定 .gesture( PanGesture({ direction: PanDirection.All, distance: 5, fingers: 1 }) .onActionStart(() { this.isDragging true; this.lastOffsetX 0; this.lastOffsetY 0; this.cardScale 1.1; }) .onActionUpdate((event: GestureEvent) { const deltaX event.offsetX - this.lastOffsetX; const deltaY event.offsetY - this.lastOffsetY; this.lastOffsetX event.offsetX; this.lastOffsetY event.offsetY; this.cardX deltaX; this.cardY deltaY; this.dragVelocity event.velocity; }) .onActionEnd((event: GestureEvent) { if (this.boundaryMode DragBoundary.SPRING_BACK) { this.getUIContext()?.animateTo( { duration: 350, curve: curves.springMotion() }, () { this.cardX 0; this.cardY 0; } ); } this.dragCount; this.totalDistance Math.abs(event.offsetX) Math.abs(event.offsetY); this.isDragging false; this.cardScale 1.0; }) .onActionCancel(() { this.isDragging false; this.cardScale 1.0; }) ) } .width(100%).layoutWeight(1) .backgroundColor(#16213e).borderRadius(16) .margin({ left: 16, right: 16 }).clip(true) .onAreaChange((_, newValue) { if (newValue typeof newValue.width number typeof newValue.height number) { this.parentWidth newValue.width; this.parentHeight newValue.height; } }) // 信息面板 Column() { Text(拖拽信息).fontSize(14).fontWeight(FontWeight.Bold) .fontColor(Color.White).width(100%) .textAlign(TextAlign.Center).padding({ bottom: 6 }) Column() { InfoRow(位置 X, ${this.cardX.toFixed(0)} vp) Divider().height(1).color(#ffffff11) InfoRow(位置 Y, ${this.cardY.toFixed(0)} vp) Divider().height(1).color(#ffffff11) InfoRow(状态, this.isDragging ? 拖动中 : ✓ 静止) Divider().height(1).color(#ffffff11) InfoRow(速度, ${this.dragVelocity.toFixed(1)} vp/s) Divider().height(1).color(#ffffff11) InfoRow(累计, ${this.dragCount} 次) }.width(100%).padding({ left: 16, right: 16 }) } .width(100%).backgroundColor(#1a1a3e).borderRadius(12) .padding({ top: 10, bottom: 10 }).margin({ left: 16, right: 16, top: 8 }) // 底部按钮 Row() { modeButton(无限制, DragBoundary.UNBOUNDED, #4A90D9) modeButton(边界约束, DragBoundary.BOUNDED, #FF6B35) modeButton(回弹模式, DragBoundary.SPRING_BACK, #00B4D8) Button(重置).height(36).backgroundColor(#E74C3C) .fontColor(Color.White).fontSize(12).borderRadius(18).layoutWeight(1) .margin({ left: 4 }) .gesture(TapGesture().onAction(() this.resetPosition())) } .width(100%).padding({ left: 16, right: 16, top: 8, bottom: 16 }) Text(用手指拖拽卡片体验 PanGesture 拖拽布局效果) .fontSize(12).fontColor(Color.Gray) .textAlign(TextAlign.Center).width(100%).padding({ bottom: 8 }) } .width(100%).height(100%).backgroundColor(#0f3460)}// 模式按钮辅助方法private modeButton(label: string, mode: DragBoundary, color: string) {Button(label).height(36).backgroundColor(this.boundaryMode mode ? color : ‘#333’).fontColor(Color.White).fontSize(12).borderRadius(18).layoutWeight(1).margin({ left: 4, right: 4 }).gesture(TapGesture().onAction(() this.switchBoundary(mode)))}private switchBoundary(mode: DragBoundary): void {this.boundaryMode mode;this.getUIContext()?.animateTo({ duration: 300, curve: curves.springMotion() },() { this.cardX 0; this.cardY 0; });}private resetPosition(): void {this.getUIContext()?.animateTo({ duration: 350, curve: curves.springMotion() },() {this.cardX 0; this.cardY 0; this.dragCount 0;this.totalDistance 0; this.dragVelocity 0;this.cardScale 1.0; this.isDragging false;this.lastOffsetX 0; this.lastOffsetY 0;});}private getBoundaryDescription(): string {const descs [‘★ 无限制模式卡片可拖到屏幕任意位置’,‘★ 边界约束模式卡片不能超出灰色区域’,‘★ 回弹模式松手后卡片弹性回到中心’];return descs[this.boundaryMode];}}Builderfunction InfoRow(label: string, value: string) {Row() {Text(label).fontSize(12).fontColor(Color.Gray)Text(value).fontSize(13).fontColor(Color.White).fontWeight(FontWeight.Medium)}.width(‘100%’).justifyContent(FlexAlign.SpaceBetween).padding({ top: 3, bottom: 3 })}Componentstruct GridLineRow {build() {Row() {Text(‘·’).fontSize(10).fontColor(Color.Gray).opacity(0.3)Text(‘·’).fontSize(10).fontColor(Color.Gray).opacity(0.3)Text(‘·’).fontSize(10).fontColor(Color.Gray).opacity(0.3)Text(‘·’).fontSize(10).fontColor(Color.Gray).opacity(0.3)Text(‘·’).fontSize(10).fontColor(Color.Gray).opacity(0.3)Text(‘·’).fontSize(10).fontColor(Color.Gray).opacity(0.3)}.width(‘100%’).layoutWeight(1).justifyContent(FlexAlign.SpaceEvenly)}}附录 B参考资料HarmonyOS NEXT 开发者文档 — ArkUI 手势处理PanGestureHarmonyOS NEXT 开发者文档 — 动画 APIHarmonyOS NEXT 开发者文档 — 显式动画animateToArkUI 手势事件 SDK 声明文件 — gesture.d.tsHarmonyOS NEXT 状态管理 — State 装饰器版权声明本文为 HarmonyOS NEXT 技术分享系列的第二篇遵循 CC BY-NC 4.0 协议。欢迎转载但请注明出处。

相关新闻