301 lines
10 KiB
Swift
301 lines
10 KiB
Swift
import SwiftUI
|
||
|
||
struct FilmStripView: View {
|
||
@State private var animate = false
|
||
// 使用SF Symbols名称数组
|
||
private let symbolNames = [
|
||
"photo.fill", "heart.fill", "star.fill", "bookmark.fill",
|
||
"flag.fill", "bell.fill", "tag.fill", "paperplane.fill"
|
||
]
|
||
private let targetIndices = [2, 5, 3] // 每条胶片最终停止的位置
|
||
|
||
var body: some View {
|
||
ZStack {
|
||
Color.black.edgesIgnoringSafeArea(.all)
|
||
|
||
// 三条胶片带
|
||
FilmStrip(
|
||
symbols: symbolNames,
|
||
targetIndex: targetIndices[0],
|
||
offset: 0,
|
||
stripColor: .red
|
||
)
|
||
.rotationEffect(.degrees(5))
|
||
.zIndex(1)
|
||
|
||
FilmStrip(
|
||
symbols: symbolNames,
|
||
targetIndex: targetIndices[1],
|
||
offset: 0.3,
|
||
stripColor: .blue
|
||
)
|
||
.rotationEffect(.degrees(-3))
|
||
.zIndex(2)
|
||
|
||
FilmStrip(
|
||
symbols: symbolNames,
|
||
targetIndex: targetIndices[2],
|
||
offset: 0.6,
|
||
stripColor: .green
|
||
)
|
||
.rotationEffect(.degrees(2))
|
||
.zIndex(3)
|
||
}
|
||
.onAppear {
|
||
withAnimation(
|
||
.timingCurve(0.2, 0.1, 0.8, 0.9, duration: 4.0)
|
||
) {
|
||
animate = true
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 单个胶片带视图
|
||
struct FilmStrip: View {
|
||
let symbols: [String]
|
||
let targetIndex: Int
|
||
let offset: Double
|
||
let stripColor: Color
|
||
@State private var animate = false
|
||
|
||
var body: some View {
|
||
GeometryReader { geometry in
|
||
let itemWidth: CGFloat = 100
|
||
let spacing: CGFloat = 8
|
||
let totalWidth = itemWidth * CGFloat(symbols.count) + spacing * CGFloat(symbols.count - 1)
|
||
|
||
// 胶片背景
|
||
RoundedRectangle(cornerRadius: 10)
|
||
.fill(stripColor.opacity(0.8))
|
||
.frame(height: 160)
|
||
.overlay(
|
||
// 胶片齿孔
|
||
HStack(spacing: spacing) {
|
||
ForEach(0..<symbols.count * 3, id: \.self) { index in
|
||
Circle()
|
||
.fill(Color.black)
|
||
.frame(width: 12, height: 12)
|
||
.offset(y: -75)
|
||
}
|
||
}
|
||
.frame(width: totalWidth * 3),
|
||
alignment: .leading
|
||
)
|
||
.overlay(
|
||
// 符号内容
|
||
HStack(spacing: spacing) {
|
||
ForEach(0..<symbols.count * 3, id: \.self) { index in
|
||
let actualIndex = index % symbols.count
|
||
|
||
ZStack {
|
||
RoundedRectangle(cornerRadius: 6)
|
||
.fill(Color.white)
|
||
.frame(width: itemWidth - 10, height: 100)
|
||
|
||
Image(systemName: symbols[actualIndex])
|
||
.font(.system(size: 30))
|
||
.foregroundColor(stripColor)
|
||
.shadow(radius: 2)
|
||
}
|
||
}
|
||
}
|
||
.offset(x: animate ? -CGFloat(targetIndex) * (itemWidth + spacing) - totalWidth : 0)
|
||
.frame(width: totalWidth * 3),
|
||
alignment: .leading
|
||
)
|
||
}
|
||
.frame(height: 180)
|
||
.onAppear {
|
||
DispatchQueue.main.asyncAfter(deadline: .now() + offset) {
|
||
withAnimation(
|
||
.timingCurve(0.2, 0.1, 0.8, 0.9, duration: 3.5)
|
||
) {
|
||
animate = true
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 增强版胶片视图(带边框和阴影)
|
||
struct EnhancedFilmStrip: View {
|
||
let symbols: [String]
|
||
let targetIndex: Int
|
||
let offset: Double
|
||
let stripColor: Color
|
||
@State private var animate = false
|
||
|
||
var body: some View {
|
||
GeometryReader { geometry in
|
||
let itemWidth: CGFloat = 110
|
||
let spacing: CGFloat = 10
|
||
let totalWidth = itemWidth * CGFloat(symbols.count) + spacing * CGFloat(symbols.count - 1)
|
||
|
||
ZStack {
|
||
// 胶片阴影
|
||
RoundedRectangle(cornerRadius: 12)
|
||
.fill(Color.black.opacity(0.3))
|
||
.frame(height: 170)
|
||
.offset(y: 5)
|
||
.blur(radius: 3)
|
||
|
||
// 胶片主体
|
||
RoundedRectangle(cornerRadius: 12)
|
||
.fill(stripColor)
|
||
.frame(height: 170)
|
||
.overlay(
|
||
// 胶片齿孔
|
||
HStack(spacing: spacing) {
|
||
ForEach(0..<symbols.count * 3, id: \.self) { index in
|
||
VStack {
|
||
Circle()
|
||
.fill(Color.black)
|
||
.frame(width: 14, height: 14)
|
||
.padding(.top, 8)
|
||
Spacer()
|
||
Circle()
|
||
.fill(Color.black)
|
||
.frame(width: 14, height: 14)
|
||
.padding(.bottom, 8)
|
||
}
|
||
.frame(height: 170)
|
||
}
|
||
}
|
||
.frame(width: totalWidth * 3),
|
||
alignment: .leading
|
||
)
|
||
|
||
// 符号内容
|
||
HStack(spacing: spacing) {
|
||
ForEach(0..<symbols.count * 3, id: \.self) { index in
|
||
let actualIndex = index % symbols.count
|
||
|
||
ZStack {
|
||
RoundedRectangle(cornerRadius: 8)
|
||
.fill(Color.white)
|
||
.frame(width: itemWidth - 15, height: 110)
|
||
.shadow(color: .black.opacity(0.2), radius: 3, x: 0, y: 2)
|
||
|
||
VStack {
|
||
Image(systemName: symbols[actualIndex])
|
||
.font(.system(size: 32, weight: .bold))
|
||
.foregroundColor(stripColor)
|
||
|
||
Text("\(actualIndex + 1)")
|
||
.font(.caption)
|
||
.foregroundColor(.gray)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
.offset(x: animate ? -CGFloat(targetIndex) * (itemWidth + spacing) - totalWidth : 0)
|
||
.frame(width: totalWidth * 3)
|
||
}
|
||
}
|
||
.frame(height: 180)
|
||
.onAppear {
|
||
DispatchQueue.main.asyncAfter(deadline: .now() + offset) {
|
||
withAnimation(
|
||
.timingCurve(0.2, 0.1, 0.8, 0.9, duration: 3.5)
|
||
) {
|
||
animate = true
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 预览
|
||
struct FilmStripView_Previews: PreviewProvider {
|
||
static var previews: some View {
|
||
FilmStripView()
|
||
}
|
||
}
|
||
|
||
// 使用增强版的视图
|
||
struct EnhancedFilmStripView: View {
|
||
@State private var animate = false
|
||
private let symbolNames = [
|
||
"camera.fill", "film.fill", "photo.fill", "heart.fill",
|
||
"star.fill", "bookmark.fill", "flag.fill", "bell.fill"
|
||
]
|
||
private let targetIndices = [2, 5, 3]
|
||
|
||
var body: some View {
|
||
ZStack {
|
||
LinearGradient(
|
||
gradient: Gradient(colors: [Color.blue.opacity(0.8), Color.purple.opacity(0.8)]),
|
||
startPoint: .topLeading,
|
||
endPoint: .bottomTrailing
|
||
)
|
||
.edgesIgnoringSafeArea(.all)
|
||
|
||
VStack(spacing: 30) {
|
||
Text("胶片动效展示")
|
||
.font(.title)
|
||
.fontWeight(.bold)
|
||
.foregroundColor(.white)
|
||
.padding(.top)
|
||
|
||
EnhancedFilmStrip(
|
||
symbols: symbolNames,
|
||
targetIndex: targetIndices[0],
|
||
offset: 0,
|
||
stripColor: .red
|
||
)
|
||
.rotationEffect(.degrees(4))
|
||
|
||
EnhancedFilmStrip(
|
||
symbols: symbolNames,
|
||
targetIndex: targetIndices[1],
|
||
offset: 0.4,
|
||
stripColor: .blue
|
||
)
|
||
.rotationEffect(.degrees(-2))
|
||
|
||
EnhancedFilmStrip(
|
||
symbols: symbolNames,
|
||
targetIndex: targetIndices[2],
|
||
offset: 0.8,
|
||
stripColor: .green
|
||
)
|
||
.rotationEffect(.degrees(3))
|
||
|
||
Button("重新播放") {
|
||
restartAnimation()
|
||
}
|
||
.padding()
|
||
.background(Color.white)
|
||
.foregroundColor(.blue)
|
||
.cornerRadius(10)
|
||
.padding()
|
||
}
|
||
}
|
||
.onAppear {
|
||
startAnimation()
|
||
}
|
||
}
|
||
|
||
private func startAnimation() {
|
||
withAnimation(
|
||
.timingCurve(0.2, 0.1, 0.8, 0.9, duration: 4.0)
|
||
) {
|
||
animate = true
|
||
}
|
||
}
|
||
|
||
private func restartAnimation() {
|
||
animate = false
|
||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||
startAnimation()
|
||
}
|
||
}
|
||
}
|
||
|
||
// 预览增强版
|
||
struct EnhancedFilmStripView_Previews: PreviewProvider {
|
||
static var previews: some View {
|
||
EnhancedFilmStripView()
|
||
}
|
||
} |