253 lines
8.8 KiB
Swift
253 lines
8.8 KiB
Swift
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()
|
||
}
|
||
}
|
||
|