import SwiftUI struct ReplayableFilmReelAnimation: View { // 控制动画状态的变量 @State private var animationProgress: CGFloat = 0 @State private var isCatching: Bool = false @State private var isDisappearing: Bool = false @State private var showReplayButton: Bool = false // 更长的胶卷图片数据(16帧) private let reelImages: [[String]] = [ (0..<16).map { "film1-\($0+1)" }, // 上方倾斜胶卷 (0..<16).map { "film2-\($0+1)" }, // 中间正胶卷 (0..<16).map { "film3-\($0+1)" } // 下方倾斜胶卷 ] // 两边胶卷的倾斜角度 private let topTiltAngle: Double = -7 // 上方胶卷左倾 private let bottomTiltAngle: Double = 7 // 下方胶卷右倾 // 最终要突出显示的图片索引 private let targetImageIndex: Int = 8 var body: some View { ZStack { // 深色背景增强胶片质感 Color(red: 0.08, green: 0.08, blue: 0.08).edgesIgnoringSafeArea(.all) // 胶卷层 - 中间正胶卷,上下各一个倾斜胶卷 ZStack { // 上方倾斜胶卷(向右移动) FilmReelView1(images: reelImages[0]) .rotationEffect(Angle(degrees: topTiltAngle)) .offset(x: calculateTopOffset(), y: -200) .opacity(isDisappearing ? 0 : 1) .zIndex(1) // 中间正胶卷(向左移动) FilmReelView1(images: reelImages[1]) .offset(x: calculateMiddleOffset(), y: 0) .scaleEffect(isCatching ? 1.03 : 1.0) .opacity(isDisappearing ? 0 : 1) .zIndex(2) // 下方倾斜胶卷(向右移动) FilmReelView1(images: reelImages[2]) .rotationEffect(Angle(degrees: bottomTiltAngle)) .offset(x: calculateBottomOffset(), y: 200) .opacity(isDisappearing ? 0 : 1) .zIndex(1) // 最终显示的图片 if isDisappearing { Image(reelImages[1][targetImageIndex]) .resizable() .scaledToFit() .frame(maxWidth: .infinity, maxHeight: .infinity) .padding() .transition(.opacity.combined(with: .scale)) .zIndex(3) } } // 重复播放按钮 if showReplayButton { Button(action: resetAndReplay) { ZStack { Circle() .fill(Color.black.opacity(0.7)) .frame(width: 60, height: 60) .shadow(radius: 10) Image(systemName: "arrow.counterclockwise") .foregroundColor(.white) .font(.system(size: 24)) } } .position(x: UIScreen.main.bounds.width - 40, y: 40) .transition(.opacity.combined(with: .scale)) .zIndex(4) } } .onAppear { startAnimation() } } // 重置并重新播放动画 private func resetAndReplay() { withAnimation(.easeInOut(duration: 0.5)) { showReplayButton = false isDisappearing = false isCatching = false animationProgress = 0 } // 延迟一小段时间后重新启动动画 DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { startAnimation() } } // 上方倾斜胶卷偏移量计算(向右移动) private func calculateTopOffset() -> CGFloat { let baseDistance: CGFloat = 1000 let speedFactor: CGFloat = 1.0 return baseDistance * speedFactor * progressCurve() } // 中间正胶卷偏移量计算(向左移动) private func calculateMiddleOffset() -> CGFloat { let baseDistance: CGFloat = -1100 let speedFactor: CGFloat = 1.05 return baseDistance * speedFactor * progressCurve() } // 下方倾斜胶卷偏移量计算(向右移动) private func calculateBottomOffset() -> CGFloat { let baseDistance: CGFloat = 1000 let speedFactor: CGFloat = 0.95 return baseDistance * speedFactor * progressCurve() } // 动画曲线:先慢后快,最后卡顿 private func progressCurve() -> CGFloat { if animationProgress < 0.6 { // 初期加速阶段 return easeInQuad(animationProgress / 0.6) * 0.7 } else if animationProgress < 0.85 { // 高速移动阶段 return 0.7 + easeOutQuad((animationProgress - 0.6) / 0.25) * 0.25 } else { // 卡顿阶段 let t = (animationProgress - 0.85) / 0.15 return 0.95 + t * 0.05 } } // 缓入曲线 private func easeInQuad(_ t: CGFloat) -> CGFloat { return t * t } // 缓出曲线 private func easeOutQuad(_ t: CGFloat) -> CGFloat { return t * (2 - t) } // 启动动画序列 private func startAnimation() { // 第一阶段:逐渐加速 withAnimation(.easeIn(duration: 3.5)) { animationProgress = 0.6 } // 第二阶段:高速移动 DispatchQueue.main.asyncAfter(deadline: .now() + 3.5) { withAnimation(.linear(duration: 2.5)) { animationProgress = 0.85 } // 第三阶段:卡顿效果 DispatchQueue.main.asyncAfter(deadline: .now() + 2.5) { withAnimation(.easeOut(duration: 1.8)) { animationProgress = 1.0 isCatching = true } // 卡顿后重合消失,显示目标图片 DispatchQueue.main.asyncAfter(deadline: .now() + 1.8) { withAnimation(.easeInOut(duration: 0.7)) { isDisappearing = true } // 显示重复播放按钮 DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { withAnimation(.easeInOut(duration: 0.3)) { showReplayButton = true } } } } } } } // 电影胶卷视图组件 struct FilmReelView1: View { let images: [String] var body: some View { HStack(spacing: 10) { ForEach(images.indices, id: \.self) { index in ZStack { // 胶卷边框 RoundedRectangle(cornerRadius: 4) .stroke(Color.gray, lineWidth: 2) .background(Color(red: 0.15, green: 0.15, blue: 0.15)) // 图片内容 Rectangle() .fill( LinearGradient( gradient: Gradient(colors: [.blue, .indigo]), startPoint: .topLeading, endPoint: .bottomTrailing ) ) .opacity(0.9) .cornerRadius(2) .padding(2) // 模拟图片文本 Text("\(images[index])") .foregroundColor(.white) .font(.caption2) } .frame(width: 90, height: 130) // 胶卷孔洞 .overlay( HStack { VStack(spacing: 6) { ForEach(0..<6) { _ in Circle() .frame(width: 6, height: 6) .foregroundColor(.gray) } } Spacer() VStack(spacing: 6) { ForEach(0..<6) { _ in Circle() .frame(width: 6, height: 6) .foregroundColor(.gray) } } } ) } } } } // 预览 struct ReplayableFilmReelAnimation_Previews: PreviewProvider { static var previews: some View { ReplayableFilmReelAnimation() } }