如何通过拖拽手势平滑中断SwiftUI动画?
实现拖拽手势中断动画时的平滑无跳变交互
Apple在2023年WWDC中大幅简化了拖拽手势后视图的平滑动画实现,可自动将拖拽手势的最终速度作为动画初始速度。以下是基础示例:
struct DraggableCircle: View { @State private var offset: CGSize = .zero var body: some View { let gesture = DragGesture(minimumDistance: 0) .onChanged { gesture in offset = gesture.translation } .onEnded { _ in withAnimation(.bouncy(duration: 2)) { offset = .zero } } Circle() .frame(width: 200, height: 200) .offset(offset) .gesture(gesture) } }

但这段代码存在缺陷——当动画尚未结束时触发新的拖拽手势,无法平滑中断现有动画,圆形会瞬间完成动画并从手指位置跳开:

问题根源在于:手势结束时我们直接将offset设回.zero,完全没有考虑动画中断时圆形的实际屏幕位置。
有一种替代方案是直接用手势位置定位圆形,确保它始终跟随手指,但这种方法会强制圆形中心对齐手指位置,完全忽略了手势起始于圆形的哪个区域,也没解决动画中断时获取实时位置的核心问题:
struct DraggableCircle_v2: View { @State private var position: CGPoint? var body: some View { let gesture = DragGesture(minimumDistance: 0) .onChanged { gesture in position = gesture.location } .onEnded { _ in withAnimation(.bouncy(duration: 2)) { position = nil } } GeometryReader { geo in let center = CGPoint( x: geo.size.width / 2, y: geo.size.height / 2 ) Circle() .frame(width: 200, height: 200) .position(position ?? center) .gesture(gesture) } } }

正确实现:平滑中断动画+无跳变拖拽
要实现目标效果,核心是在拖拽触发时获取视图的动画实时位置,以此作为新拖拽的起始基准,同时确保动画可被中断。以下是完整实现代码:
struct DraggableCircle_Smooth: View { // 记录视图的目标偏移量(动画过程中会逐渐变化) @State private var targetOffset: CGSize = .zero // 拖拽过程中的临时偏移量,手势结束后自动重置 @GestureState private var dragOffset: CGSize = .zero var body: some View { let gesture = DragGesture(minimumDistance: 0) .updating($dragOffset) { value, state, _ in // 基于当前动画的实时位置(targetOffset)加上新拖拽的平移量 state = targetOffset + value.translation } .onEnded { value in // 更新目标偏移为拖拽结束时的位置 targetOffset = targetOffset + value.translation // 启动回到原点的可中断动画 withAnimation(.bouncy(duration: 2).interruptible()) { targetOffset = .zero } } Circle() .frame(width: 200, height: 200) // 拖拽时用实时偏移,动画时用目标偏移 .offset(dragOffset != .zero ? dragOffset : targetOffset) .gesture(gesture) } }
方案原理
@GestureState的作用:存储拖拽过程中的临时偏移量,手势结束后会自动重置,避免干扰后续动画状态- 实时位置衔接:拖拽开始时,
updating闭包基于当前targetOffset(即动画中断时的实时位置)计算新的拖拽偏移,确保圆形从当前位置平滑跟随手指,不会跳变 - 可中断动画:使用
.interruptible()修饰动画,允许新的拖拽手势直接中断正在进行的动画,无需等待动画完成 - 状态分离:
targetOffset负责记录动画的目标位置,dragOffset负责拖拽过程中的实时位置,两者配合实现无缝衔接

内容的提问来源于Stack Exchange,提问作者Tim Vermeulen
相关产品推荐
相关产品推荐

