文章目录
-
- 前言
- [先认识 PanGesture](#先认识 PanGesture)
- [松手回弹:animateTo 上场](#松手回弹:animateTo 上场)
- 进阶:拖到边缘就吸附
- 一个完整可跑的例子
- 常见误区(小白必踩)
- 完整可运行示例:拖拽回弹小球
- 写在最后
前言

"拖一下松手,东西自己弹回原位"------这种交互在你每天用的 app 里到处都是:下拉刷新、卡片左滑删除、悬浮球归边。
它其实是"手势 + 动画"的组合拳。这篇文章我带你从零写一个可拖拽、松手回弹 的小方块,把 PanGesture 和 animateTo 怎么配合讲透。代码不多,但套路是通用的。
先认识 PanGesture
PanGesture 是"拖拽手势",监听手指在屏幕上滑动。它有几个回调:开始拖、拖的过程中、松手。
代码实现
typescript
@Local offsetX: number = 0
@Local offsetY: number = 0
build() {
Stack() {
Circle({ width: 60, height: 60 })
.fill('#4A90E2')
.offset({ x: this.offsetX, y: this.offsetY }) // 用 offset 控制位置
.gesture(
PanGesture()
.onActionStart(() => { /* 手指按下开始拖 */ })
.onActionUpdate((e) => {
this.offsetX = e.offsetX // 实时跟随手指
this.offsetY = e.offsetY
})
.onActionEnd(() => { this.backToOrigin() })
)
}
.width('100%')
.height('100%')
}

逐行拆解:
.offset({ x, y }):给组件一个偏移量,正数向右/下。我们用它来"移动"方块,而不是改布局。PanGesture().onActionUpdate((e) => ...):e.offsetX是相对拖动起点的位移。把它赋给offsetX,方块就实时跟着手指走。.onActionEnd(...):手指松开时触发,在这里让方块弹回。
关键点:拖动时不要包 animateTo,否则会有延迟感。只有松手回弹时才需要动画。这是新手最容易搞反的地方。
松手回弹:animateTo 上场
typescript
backToOrigin() {
animateTo({ duration: 300, curve: Curve.Spring }, () => {
this.offsetX = 0
this.offsetY = 0
})
}
松手后,用弹簧曲线把 offsetX/Y 动画回 0,方块就"嗖"地弹回起点。弹簧曲线让回弹有轻微过冲,手感更真实。
进阶:拖到边缘就吸附

很多场景不是弹回,而是"拖过一半就飞走,没过一半弹回"。加个判断:
核心代码
typescript
.onActionEnd(() => {
if (this.offsetX > 150) {
// 拖够了,飞出屏幕右侧
animateTo({ duration: 250 }, () => { this.offsetX = 400 })
} else {
// 没拖够,弹回
animateTo({ duration: 250, curve: Curve.Spring }, () => { this.offsetX = 0 })
}
})
offsetX > 150 是你自己定的"阈值",代表"用户拖得够远了,算他确认操作"。这种"阈值 + 二选一动画"的模式,在滑动删除、底部弹窗 dismiss 里都用得到。
一个完整可跑的例子
typescript
@Entry
@ComponentV2
struct DragDemo {
@Local x: number = 0
@Local y: number = 0
build() {
Stack() {
Circle({ width: 80, height: 80 })
.fill('#FF6B6B')
.offset({ x: this.x, y: this.y })
.gesture(
PanGesture()
.onActionUpdate((e: GestureEvent) => {
this.x = e.offsetX
this.y = e.offsetY
})
.onActionEnd(() => {
animateTo({ duration: 400, curve: curves.springMotion(0.5, 0.9) }, () => {
this.x = 0
this.y = 0
})
})
)
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
}
跑起来:按住红圆拖动,松手它带弹性地归位。这就是所有"拖拽回弹"交互的最小骨架。
常见误区(小白必踩)
| 误区 | 说明 |
|---|---|
拖动时也包 animateTo |
会导致跟手有延迟感。记住:拖动过程实时跟手(不动画),只有松手回弹才动画。 |
| 松手不回弹 | onActionEnd 里要用 animateTo 把位置动画回原点或用弹簧曲线弹回。 |
用布局属性代替 offset |
拖拽用 .offset() 控制偏移最顺,改 margin/position 反而麻烦。 |
下面这段代码可以直接复制到 DevEco Studio 里运行。建议你边读边敲,改一改文末「动手改一改」里的参数,亲眼看看效果。
完整可运行示例:拖拽回弹小球
显式动画配合手势,做一个「拖出去松手弹回」的跟手效果。
完整示例
typescript
// PanGesture 跟手拖动 + animateTo 松手回弹
@Entry
@Component
struct DragBall {
@State offsetX: number = 0
@State offsetY: number = 0
build() {
Stack() {
Circle({ width: 80, height: 80 })
.fill('#007DFF')
.translate({ x: this.offsetX, y: this.offsetY })
.gesture(
PanGesture()
.onActionUpdate((e) => { // 手指移动时实时跟手
this.offsetX = e.offsetX
this.offsetY = e.offsetY
})
.onActionEnd(() => { // 松手:弹回原点
animateTo({ duration: 400, curve: Curve.Spring }, () => {
this.offsetX = 0
this.offsetY = 0
})
})
)
}
.width('100%').height('100%')
}
}
你会看到什么:一个蓝色圆球。手指按住拖动,圆球实时跟随手指;松手后弹回中心位置,带弹簧回弹手感。
动手改一改:
- 把
onActionEnd里的目标改成offsetX = e.offsetX(需要缓存),做成「拖到哪停哪」。 - 把
Curve.Spring改成Curve.Friction,对比回弹与摩擦停下的差异。 - 给圆球加
.scale({ x: 1.1, y: 1.1 }),拖拽时放大,松手还原。
写在最后
手势 + 动画的套路就三步:手势里改位置(实时跟手)、松手时判断、用 animateTo 做过渡。记住"拖动不动画、回弹才动画",手感立刻专业。
建议你把阈值判断那段加上,体验一下"拖够飞走、没拖够弹回"------理解了这个,滑动删除你就能自己写出来了。