SwiftUI实现图片沿路径移动并持续翻转+路径跟随动画
问题描述
我想要实现图片沿半圆路径移动,移动过程中自身持续翻转,同时路径随图片前进逐步显示的动画效果。目前已借助SwiftUI-Lab实现了图片沿路径定向,现在需要解决两个核心需求:
- 图片沿路径移动时持续翻转
- 实现路径跟随图片逐步显示的动画效果
用户当前实现代码:
struct ContentView: View { @State private var animate = false var body: some View { GeometryReader(content: { geometry in ZStack(alignment: .topLeading) { SemiCircle() .stroke(style: StrokeStyle(lineWidth: 2, dash: [10, 15])) .frame(width: geometry.size.width, height: geometry.size.height) Image(systemName: "paperplane.fill").resizable().foregroundColor(Color.red) .rotationEffect(.degrees(45)) .rotationEffect(.degrees(180)) .frame(width: 50, height: 50).offset(x: -25, y: -25) .modifier(FollowEffect(pct: animate ? 0 : 1, path: SemiCircle.semiCirclePath(in: CGRect(x: 0, y: 0, width: geometry.size.width, height: geometry.size.height)), rotate: true)) .onAppear { withAnimation(Animation.linear(duration: 3.0).repeatForever(autoreverses: false)) { animate.toggle() } } } .frame(alignment: .topLeading) }) .padding(50) } } struct SemiCircle: Shape { func path(in rect: CGRect) -> Path { SemiCircle.semiCirclePath(in: rect) } static func semiCirclePath(in rect: CGRect) -> Path { var path = Path() let center = CGPoint(x: rect.midX, y: rect.midY) let radius = min(rect.width, rect.height) / 2 path.addArc(center: center, radius: radius, startAngle: .degrees(0), endAngle: .degrees(180), clockwise: true) return path } } struct FollowEffect: GeometryEffect { var pct: CGFloat = 0 let path: Path var rotate = true var animatableData: CGFloat { get { return pct } set { pct = newValue } } func effectValue(size: CGSize) -> ProjectionTransform { if !rotate { let pt = percentPoint(pct) return ProjectionTransform(CGAffineTransform(translationX: pt.x, y: pt.y)) } else { let pt1 = percentPoint(pct) let pt2 = percentPoint(pct - 0.01) let angle = calculateDirection(pt1, pt2) let transform = CGAffineTransform(translationX: pt1.x, y: pt1.y).rotated(by: angle) return ProjectionTransform(transform) } } func percentPoint(_ percent: CGFloat) -> CGPoint { // percent difference between points let diff: CGFloat = 0.001 let comp: CGFloat = 1 - diff // handle limits let pct = percent > 1 ? 0 : (percent < 0 ? 1 : percent) let f = pct > comp ? comp : pct let t = pct > comp ? 1 : pct + diff let tp = path.trimmedPath(from: f, to: t) return CGPoint(x: tp.boundingRect.midX, y: tp.boundingRect.midY) } func calculateDirection(_ pt1: CGPoint,_ pt2: CGPoint) -> CGFloat { let a = pt2.x - pt1.x let b = pt2.y - pt1.y let angle = a < 0 ? atan(Double(b / a)) : atan(Double(b / a)) - Double.pi return CGFloat(angle) } }
解决方案
一、实现图片移动时的持续翻转
利用animate状态的动画进度,给图片添加同步的旋转动画,让其在移动过程中持续翻转:
修改ContentView中的图片部分,新增基于animate的旋转效果:
Image(systemName: "paperplane.fill").resizable().foregroundColor(Color.red) .rotationEffect(.degrees(45)) .rotationEffect(.degrees(180)) // 新增持续翻转动画,3秒内旋转3圈,可根据需求调整圈数 .rotationEffect(.degrees(animate ? 360 * 3 : 0)) .frame(width: 50, height: 50).offset(x: -25, y: -25) .modifier(FollowEffect(pct: animate ? 0 : 1, path: SemiCircle.semiCirclePath(in: CGRect(x: 0, y: 0, width: geometry.size.width, height: geometry.size.height)), rotate: true)) .onAppear { withAnimation(Animation.linear(duration: 3.0).repeatForever(autoreverses: false)) { animate.toggle() } }
二、实现路径跟随图片逐步显示的动画
通过给路径添加可动画的修剪进度属性,同步图片的移动进度来控制路径显示范围:
- 修改
SemiCircle结构体,添加修剪进度属性并支持动画:
struct SemiCircle: Shape { var trimPct: CGFloat = 0 // 控制路径显示的进度 func path(in rect: CGRect) -> Path { SemiCircle.semiCirclePath(in: rect) .trimmedPath(from: 0, to: trimPct) // 根据进度修剪路径 } static func semiCirclePath(in rect: CGRect) -> Path { var path = Path() let center = CGPoint(x: rect.midX, y: rect.midY) let radius = min(rect.width, rect.height) / 2 path.addArc(center: center, radius: radius, startAngle: .degrees(0), endAngle: .degrees(180), clockwise: true) return path } // 让trimPct成为可动画属性 var animatableData: CGFloat { get { trimPct } set { trimPct = newValue } } }
- 在
ContentView中绑定路径的修剪进度和animate状态:
SemiCircle(trimPct: animate ? 1 : 0) // 图片进度从1到0,路径进度对应从0到1 .stroke(style: StrokeStyle(lineWidth: 2, dash: [10, 15])) .frame(width: geometry.size.width, height: geometry.size.height)
完整修改后的代码
struct ContentView: View { @State private var animate = false var body: some View { GeometryReader(content: { geometry in ZStack(alignment: .topLeading) { SemiCircle(trimPct: animate ? 1 : 0) .stroke(style: StrokeStyle(lineWidth: 2, dash: [10, 15])) .frame(width: geometry.size.width, height: geometry.size.height) Image(systemName: "paperplane.fill").resizable().foregroundColor(Color.red) .rotationEffect(.degrees(45)) .rotationEffect(.degrees(180)) .rotationEffect(.degrees(animate ? 360 * 3 : 0)) .frame(width: 50, height: 50).offset(x: -25, y: -25) .modifier(FollowEffect(pct: animate ? 0 : 1, path: SemiCircle.semiCirclePath(in: CGRect(x: 0, y: 0, width: geometry.size.width, height: geometry.size.height)), rotate: true)) .onAppear { withAnimation(Animation.linear(duration: 3.0).repeatForever(autoreverses: false)) { animate.toggle() } } } .frame(alignment: .topLeading) }) .padding(50) } } struct SemiCircle: Shape { var trimPct: CGFloat = 0 func path(in rect: CGRect) -> Path { SemiCircle.semiCirclePath(in: rect) .trimmedPath(from: 0, to: trimPct) } static func semiCirclePath(in rect: CGRect) -> Path { var path = Path() let center = CGPoint(x: rect.midX, y: rect.midY) let radius = min(rect.width, rect.height) / 2 path.addArc(center: center, radius: radius, startAngle: .degrees(0), endAngle: .degrees(180), clockwise: true) return path } var animatableData: CGFloat { get { trimPct } set { trimPct = newValue } } } struct FollowEffect: GeometryEffect { var pct: CGFloat = 0 let path: Path var rotate = true var animatableData: CGFloat { get { return pct } set { pct = newValue } } func effectValue(size: CGSize) -> ProjectionTransform { if !rotate { let pt = percentPoint(pct) return ProjectionTransform(CGAffineTransform(translationX: pt.x, y: pt.y)) } else { let pt1 = percentPoint(pct) let pt2 = percentPoint(pct - 0.01) let angle = calculateDirection(pt1, pt2) let transform = CGAffineTransform(translationX: pt1.x, y: pt1.y).rotated(by: angle) return ProjectionTransform(transform) } } func percentPoint(_ percent: CGFloat) -> CGPoint { let diff: CGFloat = 0.001 let comp: CGFloat = 1 - diff let pct = percent > 1 ? 0 : (percent < 0 ? 1 : percent) let f = pct > comp ? comp : pct let t = pct > comp ? 1 : pct + diff let tp = path.trimmedPath(from: f, to: t) return CGPoint(x: tp.boundingRect.midX, y: tp.boundingRect.midY) } func calculateDirection(_ pt1: CGPoint,_ pt2: CGPoint) -> CGFloat { let a = pt2.x - pt1.x let b = pt2.y - pt1.y let angle = a < 0 ? atan(Double(b / a)) : atan(Double(b / a)) - Double.pi return CGFloat(angle) } }
内容的提问来源于stack exchange,提问作者batman
相关产品推荐
相关产品推荐

