SwiftUI中实现UIKit风格抛纸动画的最优方法问询
在SwiftUI中实现UIKit风格的抛纸动画的最佳方案
我需要实现从A点到B点的抛射动画(无弹跳),作为用户操作的反馈(类似相册添加照片的效果)。之前尝试用.rotationEffect()实现,但动画生硬且无法自定义点位,需要能定义起点、顶点、终点的自然抛纸动画。
我尝试的SwiftUI代码(未达到预期效果)
struct KeyframeAnimation: View { @State private var ascend = false @State private var ascendDescend = false @State private var descend = false var body: some View { ZStack{ VStack{ ZStack{ Image(systemName: "circle.fill") .font(.title) .offset(x: -157) .foregroundColor(Color.red) .rotationEffect(.degrees(ascend ? 17: 0)) .rotationEffect(.degrees(ascendDescend ? 145: 0)) .rotationEffect(.degrees(descend ? 18: 0)) .onAppear{ withAnimation(Animation.easeInOut(duration: 0.5)){ self.ascend.toggle() } withAnimation(Animation.easeInOut(duration: 1)){ self.ascendDescend.toggle() } withAnimation(Animation.easeInOut(duration: 2).delay(8)){ self.descend.toggle() } } } } } } }
目标UIKit参考代码(已实现预期效果)
@IBAction func startAnimation(_ sender: Any) { myView = UIView(frame: CGRect(x: pointA.center.x - 25, y: pointA.center.y - 25, width: 50, height: 50)) myView?.backgroundColor = .blue myView?.layer.cornerRadius = myView!.frame.height / 2 view.addSubview(myView!) UIView.animateKeyframes(withDuration: 1.5, delay: 0.0, options: [.calculationModeCubic], animations: { UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.2, animations: { self.myView!.center = CGPoint(x: self.pointA.center.x, y: self.pointA.center.y) }) UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 0.9, animations: { self.myView!.center = CGPoint(x: self.pointB.center.x + 75, y: self.pointB.center.y - 50 ) self.myView!.transform = CGAffineTransform(scaleX: 0.75, y: 0.75) }) UIView.addKeyframe(withRelativeStartTime: 0.1, relativeDuration: 0.7, animations: { self.myView!.center = CGPoint(x: self.pointB.center.x, y: self.pointB.center.y) self.myView!.transform = CGAffineTransform(scaleX: 0.2, y: 0.2) }) }, completion: { _ in self.myView?.removeFromSuperview() }) }

SwiftUI实现方案
方案1:iOS 17+ 用KeyframeAnimator(推荐)
利用SwiftUI 17新增的KeyframeAnimator可以精准复刻UIKit的关键帧动画逻辑,支持自定义路径和时序:
struct PaperTossAnimation: View { // 封装动画状态:位置、缩放比例 struct AnimationState { var position: CGPoint = .zero var scale: CGFloat = 1.0 } @State private var isAnimating = false // 替换为实际布局中的A、B、顶点坐标(可通过GeometryReader获取) let pointA = CGPoint(x: 100, y: 300) let pointB = CGPoint(x: 300, y: 500) let vertexPoint = CGPoint(x: 350, y: 200) var body: some View { ZStack { // 起点标记(可选,用于调试) Circle() .fill(.red) .frame(width: 20, height: 20) .position(pointA) // 终点标记(可选,用于调试) Circle() .fill(.green) .frame(width: 20, height: 20) .position(pointB) // 抛射的视图 Circle() .fill(.blue) .frame(width: 50, height: 50) .keyframeAnimator(initialValue: AnimationState(position: pointA, scale: 1.0), trigger: isAnimating) { content, state in content .position(state.position) .scaleEffect(state.scale) } keyframes: { _ in // 位置关键帧:从A到顶点再到B KeyframeTrack(\.position) { CubicKeyframe(vertexPoint, duration: 0.6) CubicKeyframe(pointB, duration: 0.9) } // 缩放关键帧:同步缩小 KeyframeTrack(\.scale) { LinearKeyframe(0.75, duration: 0.6) LinearKeyframe(0.2, duration: 0.9) } } .onAppear { isAnimating = true } // 动画结束后隐藏视图 .opacity(isAnimating ? 1 : 0) } .frame(maxWidth: .infinity, maxHeight: .infinity) } }
方案2:兼容iOS 16及以下版本
通过状态变量切换动画阶段,结合withAnimation模拟关键帧时序:
struct LegacyPaperTossAnimation: View { @State private var animationPhase = 0 let pointA = CGPoint(x: 100, y: 300) let pointB = CGPoint(x: 300, y: 500) let vertexPoint = CGPoint(x: 350, y: 200) var body: some View { ZStack { Circle().fill(.red).frame(width:20, height:20).position(pointA) Circle().fill(.green).frame(width:20, height:20).position(pointB) Circle() .fill(.blue) .frame(width: 50, height: 50) .position(currentPosition) .scaleEffect(currentScale) .onAppear { startAnimation() } } .frame(maxWidth: .infinity, maxHeight: .infinity) } // 根据当前阶段返回对应位置 private var currentPosition: CGPoint { switch animationPhase { case 0: return pointA case 1: return vertexPoint case 2, 3: return pointB default: return pointB } } // 根据当前阶段返回对应缩放比例 private var currentScale: CGFloat { switch animationPhase { case 0: return 1.0 case 1: return 0.75 case 2, 3: return 0.2 default: return 0.2 } } private func startAnimation() { // 第一阶段:移动到顶点 withAnimation(.easeInOut(duration: 0.6)) { animationPhase = 1 } // 第二阶段:移动到终点,与第一阶段衔接 withAnimation(.easeInOut(duration: 0.9).delay(0.6)) { animationPhase = 2 } // 动画结束后隐藏视图 DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { withAnimation { animationPhase = 3 } } } }
关键优化点
- 若需要获取实际布局中的A、B点坐标,可使用
GeometryReader包裹父视图,读取目标视图的frame或position - 调整关键帧的
duration和路径点坐标,可以优化动画的自然度,匹配UIKit中的relativeDuration参数 - 如需添加旋转效果,可在动画闭包中加入
rotationEffect的关键帧,同步调整旋转角度
内容的提问来源于stack exchange,提问作者fs_tigre
相关产品推荐
相关产品推荐

