import SwiftUI import UIKit // 胶卷帧数据模型 struct FilmFrame: Identifiable { let id = UUID() let imageName: String let systemImage: String let svgName: String // 用于展示的SVG名称 } // 动画阶段枚举 enum AnimationPhase { case accelerating // 加速阶段 case uniform // 匀速阶段 case exiting // 退出阶段(上下胶卷外移,中间胶卷消失) case stationary // 静止阶段 } // 主动画协调器 - 用于同步所有胶卷动画 class AnimationCoordinator: ObservableObject { @Published var shouldStartExiting = false // 控制所有胶卷是否开始退出 } // 动画控制器 class FilmAnimationController: NSObject, ObservableObject { @Published var offset: CGPoint = .zero @Published var scale: CGFloat = 1.0 @Published var opacity: Double = 1.0 @Published var isPresented: Bool = true @Published var phase: AnimationPhase = .accelerating let originalRotation: Double let reelIdentifier: UUID // 用于标识不同胶卷 var currentSpeed: CGFloat = 0 var lastUpdateTime: CFTimeInterval = 0 var startTime: CFTimeInterval = 0 var uniformStartTime: CFTimeInterval = 0 var exitStartTime: CFTimeInterval = 0 var displayLink: CADisplayLink? var targetFrameIndex: Int // 目标帧索引 // 配置参数 let frames: [FilmFrame] let verticalOffset: CGFloat // 初始垂直偏移 let exitDistance: CGFloat // 退出时的移动距离 let direction: Direction let initialSpeed: CGFloat let maxSpeed: CGFloat let accelerationDuration: Double let frameWidth: CGFloat let spacing: CGFloat let frameCycleWidth: CGFloat let isMiddleReel: Bool weak var coordinator: AnimationCoordinator? // 弱引用协调器 enum Direction { case left case right var multiplier: CGFloat { switch self { case .left: return -1 case .right: return 1 } } } init(frames: [FilmFrame], rotationAngle: Double, verticalOffset: CGFloat, exitDistance: CGFloat, direction: Direction, coordinator: AnimationCoordinator, isMiddleReel: Bool = false, initialSpeed: CGFloat = 100, maxSpeed: CGFloat = 600, accelerationDuration: Double = 4, frameWidth: CGFloat = 100, spacing: CGFloat = 10) { self.frames = frames self.originalRotation = rotationAngle self.verticalOffset = verticalOffset self.exitDistance = exitDistance self.direction = direction self.initialSpeed = initialSpeed self.maxSpeed = maxSpeed self.accelerationDuration = accelerationDuration self.frameWidth = frameWidth self.spacing = spacing self.isMiddleReel = isMiddleReel self.coordinator = coordinator self.reelIdentifier = UUID() // 设置目标帧为最后一帧 self.targetFrameIndex = frames.count - 1 let baseFramesCount = CGFloat(frames.count) let adjustedFramesCount = isMiddleReel ? baseFramesCount * 1.5 : baseFramesCount self.frameCycleWidth = adjustedFramesCount * (frameWidth + spacing) super.init() // 监听协调器的退出信号 NotificationCenter.default.addObserver(self, selector: #selector(startExitingWhenTriggered), name: NSNotification.Name("StartExiting"), object: nil) } // 当协调器发出信号时开始退出 @objc private func startExitingWhenTriggered() { if self.phase == .uniform { startExitAnimation() } } func startAnimation() { offset.x = -frameCycleWidth / (isMiddleReel ? 1.5 : 1) offset.y = verticalOffset currentSpeed = initialSpeed startTime = CACurrentMediaTime() lastUpdateTime = CACurrentMediaTime() displayLink = CADisplayLink(target: self, selector: #selector(updateAnimation(_:))) displayLink?.add(to: .main, forMode: .common) } func stopAnimation() { displayLink?.invalidate() displayLink = nil NotificationCenter.default.removeObserver(self) } // 开始退出动画 func startExitAnimation() { phase = .exiting exitStartTime = CACurrentMediaTime() if isMiddleReel { // 中间胶卷:直接消失 withAnimation(.easeIn(duration: 0.6)) { self.opacity = 0.0 } DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) { self.isPresented = false } } else { // 上下胶卷:向外移动并淡出 withAnimation(.easeInOut(duration: 1.2)) { self.offset.y = self.verticalOffset + self.exitDistance self.opacity = 0.0 } DispatchQueue.main.asyncAfter(deadline: .now() + 1.2) { self.isPresented = false } } } @objc private func updateAnimation(_ displayLink: CADisplayLink) { let currentTime = CACurrentMediaTime() let deltaTime = currentTime - lastUpdateTime lastUpdateTime = currentTime guard isPresented else { return } switch phase { case .accelerating: let elapsedTime = currentTime - startTime let progress = min(CGFloat(elapsedTime) / CGFloat(accelerationDuration), 1.0) currentSpeed = initialSpeed + (maxSpeed - initialSpeed) * progress if progress >= 1.0 { phase = .uniform uniformStartTime = currentTime NotificationCenter.default.post(name: NSNotification.Name("ReelEnteredUniform"), object: reelIdentifier) } let distance = currentSpeed * CGFloat(deltaTime) * direction.multiplier offset.x += distance resetOffset() case .uniform: let uniformTimeElapsed = currentTime - uniformStartTime if uniformTimeElapsed >= 3.0 { if isMiddleReel { NotificationCenter.default.post(name: NSNotification.Name("AllReelsReadyToExit"), object: nil) } } else { let distance = currentSpeed * CGFloat(deltaTime) * direction.multiplier offset.x += distance resetOffset() } case .exiting, .stationary: break } } private func resetOffset() { let cycleThreshold = isMiddleReel ? frameCycleWidth * 1.2 : frameCycleWidth * 2 if direction == .left { while offset.x < -cycleThreshold { offset.x += frameCycleWidth / (isMiddleReel ? 1.5 : 1) } } else { while offset.x > 0 { offset.x -= frameCycleWidth } } } } // 胶卷视图 struct FilmReelView: View { @StateObject private var animationController: FilmAnimationController let zIndex: Double init(frames: [FilmFrame], rotationAngle: Double, verticalOffset: CGFloat, exitDistance: CGFloat, direction: FilmAnimationController.Direction, coordinator: AnimationCoordinator, isMiddleReel: Bool = false, zIndex: Double = 0) { _animationController = StateObject(wrappedValue: FilmAnimationController( frames: frames, rotationAngle: rotationAngle, verticalOffset: verticalOffset, exitDistance: exitDistance, direction: direction, coordinator: coordinator, isMiddleReel: isMiddleReel )) self.zIndex = zIndex } var body: some View { if animationController.isPresented { let containerWidth = animationController.frameCycleWidth * 4 ZStack { HStack(spacing: animationController.spacing) { let repeatCount = animationController.isMiddleReel ? 4 : 3 ForEach(Array(repeating: animationController.frames, count: repeatCount).flatMap { $0 }) { frame in filmFrameView(frame: frame) } } .offset(x: animationController.offset.x, y: animationController.offset.y) } .frame(width: containerWidth) .rotationEffect(.degrees(animationController.originalRotation)) .scaleEffect(animationController.scale) .opacity(animationController.opacity) .zIndex(zIndex) .onAppear { animationController.startAnimation() } .onDisappear { animationController.stopAnimation() } } else { EmptyView() } } private func filmFrameView(frame: FilmFrame) -> some View { ZStack { RoundedRectangle(cornerRadius: 4) .stroke(Color.black, lineWidth: 2) .background(Color(white: 0.9, opacity: 0.7)) Group { if let uiImage = UIImage(named: frame.imageName) { Image(uiImage: uiImage) .resizable() } else { Image(systemName: frame.systemImage) .resizable() } } .aspectRatio(contentMode: .fit) .padding(6) .foregroundColor(animationController.isMiddleReel ? .red : .blue) } .frame(width: animationController.frameWidth, height: 150) .shadow(radius: 3) } } // 抖动效果修饰器 struct ShakeEffect: GeometryEffect { var amount: CGFloat = 10 var shakesPerUnit: CGFloat = 3 var animatableData: CGFloat func effectValue(size: CGSize) -> ProjectionTransform { ProjectionTransform(CGAffineTransform(translationX: amount * sin(animatableData * .pi * shakesPerUnit), y: 0)) } } // 主SVG视图 - 带抖动反转效果 struct MainSVGView: View { @Binding var isVisible: Bool @State private var scale: CGFloat = 0.1 @State private var opacity: Double = 0.0 @State private var showFront: Bool = true @State private var shakeAmount: CGFloat = 0 @State private var rotation: Double = 0 let svgName: String // SVG资源名称 var body: some View { ZStack { if showFront { // 正面:缩略图风格的SVG SVGImage(svgName: "IP") .aspectRatio(contentMode: .fit) .padding() .background(Color.white) .cornerRadius(12) .shadow(radius: 15) .rotation3DEffect(.degrees(rotation), axis: (x: 0, y: 1, z: 0)) } else { // 反面:完整展示的SVG SVGImage(svgName: svgName) .aspectRatio(contentMode: .fit) .padding() .background(Color.white) .cornerRadius(12) .shadow(radius: 15) .rotation3DEffect(.degrees(rotation), axis: (x: 0, y: 1, z: 0)) } } .scaleEffect(scale) .opacity(opacity) .modifier(ShakeEffect(animatableData: shakeAmount)) .onAppear { // 入场动画:由小变大并带有轻微抖动 withAnimation(.easeOut(duration: 1.5).delay(0.3)) { self.scale = 1.0 self.opacity = 1.0 self.shakeAmount = 1 // 触发轻微抖动 } // 抖动结束后重置 DispatchQueue.main.asyncAfter(deadline: .now() + 1.8) { withAnimation(.linear(duration: 0.5)) { self.shakeAmount = 0 } } // 延迟后自动翻转,带有抖动效果 DispatchQueue.main.asyncAfter(deadline: .now() + 2.3) { // 开始抖动 withAnimation(.easeInOut(duration: 0.5)) { self.shakeAmount = 2 // 增强抖动 } // 抖动中开始反转 DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { withAnimation(.easeInOut(duration: 1.0)) { self.rotation = 180 // 完成翻转 } // 翻转完成后停止抖动 DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) { withAnimation(.linear(duration: 0.5)) { self.shakeAmount = 0 self.showFront = false } } } } } .edgesIgnoringSafeArea(.all) } } // 主视图 struct Box7: View { @StateObject private var coordinator = AnimationCoordinator() @State private var showMainSVG = false @State private var readyReelsCount = 0 private let totalReels = 3 // 总胶卷数量 // 胶卷帧数据 - 使用svgName替代fullImageName private let filmFrames: [FilmFrame] = [ FilmFrame(imageName: "frame1", systemImage: "photo", svgName: "IP1"), FilmFrame(imageName: "frame2", systemImage: "camera", svgName: "IP2"), FilmFrame(imageName: "frame3", systemImage: "video", svgName: "IP3"), FilmFrame(imageName: "frame4", systemImage: "movie", svgName: "IP4"), FilmFrame(imageName: "frame5", systemImage: "film", svgName: "IP5"), FilmFrame(imageName: "frame6", systemImage: "photo.on.rectangle", svgName: "IP6"), FilmFrame(imageName: "frame7", systemImage: "photo.circle", svgName: "IP7"), FilmFrame(imageName: "frame8", systemImage: "film.frame", svgName: "IP") // 最后一帧使用"IP" SVG ] var body: some View { ZStack(alignment: .center) { // 背景 LinearGradient( gradient: Gradient(colors: [.gray.opacity(0.1), .gray.opacity(0.2)]), startPoint: .top, endPoint: .bottom ) .edgesIgnoringSafeArea(.all) // 所有胶卷 // 上边胶卷 - 向上移动退出 FilmReelView( frames: filmFrames, rotationAngle: 6, verticalOffset: -120, exitDistance: -250, direction: .right, coordinator: coordinator, zIndex: 2 ) // 中间胶卷 - 直接消失 FilmReelView( frames: filmFrames, rotationAngle: 0, verticalOffset: 0, exitDistance: 0, direction: .left, coordinator: coordinator, isMiddleReel: true, zIndex: 1 ) // 下边胶卷 - 向下移动退出 FilmReelView( frames: filmFrames, rotationAngle: -6, verticalOffset: 120, exitDistance: 250, direction: .right, coordinator: coordinator, zIndex: 2 ) // 主SVG - 在胶卷退出后出现,使用SVGImage(svgName: "IP") if showMainSVG { MainSVGView( isVisible: $showMainSVG, svgName: filmFrames.last!.svgName // 传递"IP" SVG名称 ) .zIndex(10) // 确保在最上层 } } .onAppear { // 监听胶卷进入匀速阶段的通知 NotificationCenter.default.addObserver(forName: NSNotification.Name("ReelEnteredUniform"), object: nil, queue: .main) { _ in readyReelsCount += 1 if readyReelsCount >= totalReels { print("所有胶卷已准备就绪") } } // 监听开始退出的通知 NotificationCenter.default.addObserver(forName: NSNotification.Name("AllReelsReadyToExit"), object: nil, queue: .main) { _ in coordinator.shouldStartExiting = true NotificationCenter.default.post(name: NSNotification.Name("StartExiting"), object: nil) // 胶卷开始退出后,准备显示主SVG DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) { showMainSVG = true } } } .onDisappear { NotificationCenter.default.removeObserver(self) } .padding(.horizontal, 60) .padding(.vertical, 80) } } // 预览 struct Box7_Previews: PreviewProvider { static var previews: some View { Box7() .previewDevice("iPhone 13") } }