使用拖动手势中断 SwiftUI 动画

Tim*_*len 5 swift swiftui swiftui-animation ios17

苹果今年比以往任何时候都更容易在执行拖动手势后平滑地制作视图动画,自动使用拖动手势的最终速度作为动画的初始速度。这是一个简单的例子:

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)
    }
}
Run Code Online (Sandbox Code Playgroud)

在此输入图像描述

但是,此代码缺乏的是通过在动画完成之前启动新的拖动手势来平滑中断圆圈动画的能力。相反,圆圈将立即完成动画,跳离您的手指:

在此输入图像描述

这是完全有道理的,因为我们要回到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)
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

这适用于大多数用例。然而,这总是将圆的中心放在手指所在的位置,而不考虑手势在圆上的哪个位置开始。换句话说,我们在确定动画中断时圆的位置这一目标方面尚未取得任何进展。

在此输入图像描述

我们可以使用手势中断动画并找出中断时圆圈的位置吗?

编辑:这就是我想要实现的目标:

在此输入图像描述

Hop*_*psa 1

让我们使用一个单独的变量来跟踪拖动时圆的位置 (dragOffset),并使用一个单独的变量来将圆动画回到其初始位置 (animationOffset)。我们将分别使用这两个变量来实现平滑的行为。

struct ContentViewC: View {
    var body: some View {
        ZStack {
            Color.white.edgesIgnoringSafeArea(.all)
            DraggableCircleView()
        }
    }
}

struct DraggableCircleView: View {
@GestureState private var dragOffset: CGSize = .zero
@State private var animationOffset: CGSize = .zero

var body: some View {
    VStack {
        Circle()
            .frame(width: 200, height: 200)
            .foregroundColor(.blue)
            .offset(animationOffset) // Use animationOffset for smooth animation
            .offset(dragOffset) // Use dragOffset for smooth dragging
            .gesture(DragGesture()
                .updating($dragOffset, body: { (value, dragOffset, _) in
                    dragOffset = value.translation
                })
                .onEnded { value in
                    // Save the final drag position in animationOffset
                    animationOffset = value.translation
                    withAnimation(.easeOut(duration: 1.5)) {
                        // Animate the circle back to its initial position
                        animationOffset = .zero
                    }
                }
            )
    }
}}
Run Code Online (Sandbox Code Playgroud)

在这段代码中,我们使用dragOffset在拖动过程中更新圆的位置,并使用animationOffset在拖动结束时实现平滑的动画。这种分离应该可以防止弹跳效果,并在将圆拖动并动画回到中心时为您提供更流畅的体验。

当视图层次结构中有两个 .offset 修饰符时,SwiftUI 会将它们组合起来并将总偏移量应用到视图。

您可以将 2x 偏移量替换为可读性:

.offset(CGSize(width: animationOffset.width + dragOffset.width,
                                         height: animationOffset.height + dragOffset.height))
Run Code Online (Sandbox Code Playgroud)

但 2x 偏移看起来更干净。