如何为已有翻转动画的SwiftUI闪卡视图添加滑动过渡效果?
闪卡左右滑动切换动画实现方案
问题描述
我开发了一个闪卡功能,参考现有代码实现了卡片翻转动画,但目前SwiftUI相关技术能力有限。现在希望为视图中的卡片添加滑动过渡效果:点击前进按钮时卡片从右侧滑至屏幕中央,点击后退按钮时从左侧滑至中央,请问是否可行?
实现方案
当然可行,只需通过以下几步修改现有代码即可实现:
- 添加方向枚举与状态变量:用来记录当前卡片切换的方向(前进/后退)
- 为卡片视图添加过渡动画:根据切换方向设置对应的滑入过渡效果
- 包裹按钮点击的动画逻辑:在切换卡片计数器时,同步更新方向并触发动画
- 重置卡片翻转状态:切换卡片时重置翻转相关的状态,避免显示异常
修改后的完整代码
import SwiftUI // 新增:定义滑动方向枚举 enum SlideDirection { case left, right, none } struct flashCardActivity: View { @State var flashCardObj: flashCardObject @State var flipped = false @State var animate3d = false @State var showButtonSet = false @State var counter: Int = 0 // 新增:记录滑动方向 @State private var slideDirection: SlideDirection = .none var body: some View { VStack { progressBar(counter: self.$counter, totalCards: flashCardObj.words.count) .padding(.top, 175) .padding(.bottom, 80) Spacer() // 修改:添加过渡动画与动画包裹 cardView(flipped: self.$flipped, animate3d: self.$animate3d, counter: self.$counter, flashCardObj: self.$flashCardObj) .transition( slideDirection == .right ? .move(edge: .trailing) : slideDirection == .left ? .move(edge: .leading) : .identity ) rightWrongButtonSet().padding(.top, 20) saveToMyListButton().padding(.top, 25) nextPreviousButtonSet(fcO: self.$flashCardObj, counter: self.$counter, slideDirection: $slideDirection, flipped: $flipped, animate3d: $animate3d) .offset(y:50) }.offset(y:-80) } } struct cardView: View { @Binding var flipped: Bool @Binding var animate3d: Bool @Binding var counter: Int @Binding var flashCardObj: flashCardObject var body: some View{ ZStack() { flashCardItal(counter: $counter, fcO: $flashCardObj).opacity(flipped ? 0.0 : 1.0) flashCardEng(counter: $counter, fcO: $flashCardObj).opacity(flipped ? 1.0 : 0.0) } .modifier(FlipEffect(flipped: $flipped, angle: animate3d ? 180 : 0, axis: (x: 1, y: 0))) .onTapGesture { withAnimation(Animation.linear(duration: 0.8)) { self.animate3d.toggle() } } } } struct FlipEffect: GeometryEffect { var animatableData: Double { get { angle } set { angle = newValue } } @Binding var flipped: Bool var angle: Double let axis: (x: CGFloat, y: CGFloat) func effectValue(size: CGSize) -> ProjectionTransform { DispatchQueue.main.async { self.flipped = self.angle >= 90 && self.angle < 270 } let tweakedAngle = flipped ? -180 + angle : angle let a = CGFloat(Angle(degrees: tweakedAngle).radians) var transform3d = CATransform3DIdentity; transform3d.m34 = -1/max(size.width, size.height) transform3d = CATransform3DRotate(transform3d, a, axis.x, axis.y, 0) transform3d = CATransform3DTranslate(transform3d, -size.width/2.0, -size.height/2.0, 0) let affineTransform = ProjectionTransform(CGAffineTransform(translationX: size.width/2.0, y: size.height / 2.0)) return ProjectionTransform(transform3d).concatenating(affineTransform) } } struct flashCardItal: View { @Binding var counter: Int @Binding var fcO: flashCardObject var body: some View{ Text(fcO.words[counter].wordItal) .font(Font.custom("Marker Felt", size: 40)) .foregroundColor(Color.black) .frame(width: 325, height: 250) .background(Color.teal) .cornerRadius(20) .shadow(radius: 10) .padding(.bottom, 30) .padding([.leading, .trailing], 10) .zIndex(1) } } struct flashCardEng: View { @Binding var counter: Int @Binding var fcO: flashCardObject var body: some View{ VStack{ Text(fcO.words[counter].wordEng) .font(Font.custom("Marker Felt", size: 40)) .foregroundColor(Color.black) .padding(.bottom, 30) .padding([.leading, .trailing], 10) Text(fcO.words[counter].gender.rawValue) .font(Font.custom("Marker Felt", size: 30)) .foregroundColor(Color.black) .padding(.top, 2) .padding([.leading, .trailing], 10) } .frame(width: 325, height: 250) .background(Color.teal) .cornerRadius(20) .shadow(radius: 10) } } struct rightWrongButtonSet: View{ var body: some View{ HStack{ Button(action: { }, label: {Image("cancel") .resizable() .scaledToFit() .frame(width: 65, height: 65) }).padding(.leading, 80) Spacer() Button(action: { }, label: {Image("checked") .resizable() .scaledToFit() .frame(width: 65, height: 65) }).padding(.trailing, 80) } } } struct saveToMyListButton: View{ var body: some View{ Button(action: {}, label: {Text("Save to My List")}) } } // 修改:更新按钮视图,添加方向绑定与状态重置 struct nextPreviousButtonSet: View{ @Binding var fcO: flashCardObject @Binding var counter: Int @Binding var slideDirection: SlideDirection @Binding var flipped: Bool @Binding var animate3d: Bool var body: some View{ HStack{ Button(action: { if counter > 0 { withAnimation(.easeInOut(duration: 0.5)) { slideDirection = .left counter -= 1 // 重置翻转状态 flipped = false animate3d = false } } }, label: {Image(systemName: "arrow.backward").resizable() .bold() .scaledToFit() .frame(width: 65, height: 65) .foregroundColor(Color.black) }).padding(.leading, 90) Spacer() Button(action: { if counter < fcO.words.count - 1 { withAnimation(.easeInOut(duration: 0.5)) { slideDirection = .right counter += 1 // 重置翻转状态 flipped = false animate3d = false } } }, label: {Image(systemName: "arrow.forward").resizable() .bold() .scaledToFit() .frame(width: 65, height: 65) .foregroundColor(Color.black) }).padding(.trailing, 90) } } } struct progressBar: View { @Binding var counter: Int let totalCards: Int var body: some View { VStack { Text(String(counter) + "/" + String(totalCards)).offset(y:20) ProgressView("", value: Double(counter), total: Double(totalCards)) .frame(width: 300).cornerRadius(10) .scaleEffect(x: 1, y: 4) } } } // 补充原代码缺失的定义,保证可编译 enum Gender: String { case masculine, feminine, neuter } struct Word { let wordItal: String let wordEng: String let gender: Gender } struct flashCardObject { let words: [Word] static let Food = flashCardObject(words: [ Word(wordItal: "Pizza", wordEng: "Pizza", gender: .feminine), Word(wordItal: "Pasta", wordEng: "Pasta", gender: .feminine) ]) } struct flashCardActivity_Previews: PreviewProvider { static var previews: some View { flashCardActivity(flashCardObj: flashCardObject.Food) } }
关键修改说明
- 新增
SlideDirection枚举,标记卡片滑入的方向 - 在主视图中添加
slideDirection状态变量,控制过渡动画的触发方向 - 为卡片视图绑定过渡效果,根据方向选择从左侧或右侧滑入
- 修改按钮点击逻辑,用
withAnimation包裹状态更新,确保动画流畅执行,同时重置卡片翻转状态,保证新卡片以正面显示 - 补充了原代码缺失的
flashCardObject相关定义,让代码可以直接编译运行
内容的提问来源于stack exchange,提问作者mzm0227
相关产品推荐
相关产品推荐

