具有两个视图的 SwiftUI Card Flip 动画,其中一个嵌入在 Stack 中

Posted

技术标签:

【中文标题】具有两个视图的 SwiftUI Card Flip 动画,其中一个嵌入在 Stack 中【英文标题】:SwiftUI Card Flip animation with two views, one of which is embedded within a Stack 【发布时间】:2020-07-06 11:23:20 【问题描述】:

我正在尝试在两个视图之间创建“卡片翻转”动画:

视图“A”是CardView 内的LazyVGrid 视图“B”是自定义模式叠加视图

LazyVGrid 和视图“B”在一个 ZStack

具体来说,ContentView 的组织方式如下:

var body: some View 
    ZStack 
        NavigationView 
            ScrollView 
                LazyVGrid(columns: columns, spacing: 10) 
                    ForEach(model.events, id: \.self)  event in
                        SmallCardView(event: event)
                            .opacity(!showModal || event != modifiableEvent ? 1.0 : 0.0)
                    
                
            
        
        .brightness(self.showModal ? -0.1 : 0)
        .blur(radius: self.showModal ? 16 : 0)
        
        if self.showModal 
            AddEventView(
                showModal: $showModal,
                existingEvent: modifiableEvent,
            )
            .opacity(showModal ? 1.0 : 0.0)
            .padding(.horizontal, 16)
                    
    

我遇到了this SO 帖子,答案似乎很有希望,但是答案没有考虑到其中一个视图是否在堆栈/网格中,对我来说就是这种情况。所以,我的问题是,如果其中一个视图确实嵌入在堆栈或网格中,我该如何调整链接解决方案以使其按预期工作。

编辑:另外需要注意的是Views的大小和位置不同

我尝试将.modifier(FlipEffect(flipped: $showModal, angle: animate3d ? 180 : 0, axis: (x: 0, y: 1))) 添加到ZStackSmallCardView,但是都没有产生预期的结果。

谢谢!

编辑:为了清楚起见,我想在这两个视图之间以卡片翻转样式制作动画:

【问题讨论】:

【参考方案1】:

这个非常简单的结构应该可以帮助您理解所需的必要结构:

为此目的有一个特定的rotation3DEffect 修饰符。

struct ContentView: View 
    
    // What is the current status
    @State var flipped: Bool = false
    
    // Whats the initial "flip" degree
    @State var degrees: Double = 180.0
    
    @State var width: CGFloat = 200
    @State var height: CGFloat = 300
    
    var body: some View 
        ZStack 
            if flipped 
                //Cart Back
                CardBack(width: self.$width, height: self.$height)
                  
             else 
                //Cart front

                CardFront(width: self.$width, height: self.$height)
                 
            
        //Styling
        .background(Color.gray)
        .cornerRadius(20)
        .rotation3DEffect(.degrees(degrees), axis: (x: 0, y: 1, z: 0))
            
            // When tapped turn it around
        .onTapGesture 
            if self.flipped 
                self.flipped = false
                withAnimation 
                    self.degrees += 180
                    self.width = 200 // add other animated stuff here
                    self.height = 300
                
             else 
                self.flipped = true
                withAnimation 
                    self.degrees -= 180
                    self.width = 300 // add other animated stuff here
                    self.height = 500
                
            
        
    


struct CardBack: View 
    
    @Binding var width: CGFloat
    @Binding var height: CGFloat
    
    var body: some View 
        Rectangle().foregroundColor(Color.red).frame(width: self.width, height: self.height).overlay(Text("Back"))
    


struct CardFront: View 
    
    @Binding var width: CGFloat
    @Binding var height: CGFloat
    
    var body: some View 
        Rectangle().foregroundColor(Color.blue).frame(width: self.width, height: self.height).overlay(Text("Front"))
    


这会产生以下视图:

【讨论】:

当你两个视图的大小和位置不同时,这并不能很好地工作,在我的情况下它们是 我必须承认我并没有真正看到你的问题。这是一个模型,应该可以帮助您理解这个概念。不实施成品。只需添加其他属性并在 tapGesture 中更改它们。我添加了卡片宽度和高度的示例。 对我的不清楚的地方表示歉意。具体来说,我想要一个网格/堆栈内的视图,以及该堆栈的 顶部 的另一个视图(即,将网格内的视图移动到整个网格之上),而不仅仅是一个视图来回翻转。因此,动画必须从 Grid 内部移动到其上方作为示例,请参阅 ios 14 Widgets 的编辑动画【参考方案2】:

当卡片在 LazyVGrid 中使用 .matchedGeometryEffect() 时,我从来没有设法让它正常工作。所以这是我在项目中使用的滥用偏移和缩放的相当混乱的解决方案:

struct TestView: View 
   @State var flippedCard: Int?
   @State var frontCard: Int?
   let cards = [1,2,3,4,5,6,7,8,9,10]
   
   var body: some View 
      let columns = [
         GridItem(.flexible(), spacing: 0),
         GridItem(.flexible(), spacing: 0),
         GridItem(.flexible(), spacing: 0)
      ]
      
      GeometryReader  screenGeometry in
         ZStack 
            ScrollView 
               LazyVGrid(columns: columns, alignment: .center, spacing: 0) 
                  ForEach(cards, id: \.self)  card in
                     let isFaceUp = flippedCard == card
                     GeometryReader  cardGeometry in
                        ZStack 
                           CardBackView(card: card)
                              .modifier(FlipOpacity(pct: isFaceUp ? 0 : 1))
                              .rotation3DEffect(Angle.degrees(isFaceUp ? 180 : 360), axis: (0,1,0))
                              .frame(width: cardGeometry.size.width, height: cardGeometry.size.height)
                              .scaleEffect(isFaceUp ? screenGeometry.size.width / cardGeometry.size.width: 1)
                           CardFrontView(card: card)
                              .modifier(FlipOpacity(pct: isFaceUp ? 1 : 0))
                              .rotation3DEffect(Angle.degrees(isFaceUp ? 0 : 180), axis: (0,1,0))
                              .frame(width: screenGeometry.size.width, height: screenGeometry.size.height)
                              .scaleEffect(isFaceUp ? 1 : cardGeometry.size.width / screenGeometry.size.width)
                        
                        .offset(x: isFaceUp ? -cardGeometry.frame(in: .named("mainFrame")).origin.x: -screenGeometry.size.width/2 + cardGeometry.size.width/2,
                                y: isFaceUp ? -cardGeometry.frame(in: .named("mainFrame")).origin.y: -screenGeometry.size.height/2 + cardGeometry.size.height/2)
                        .onTapGesture 
                           withAnimation(.linear(duration: 1.0)) 
                              if flippedCard == nil 
                                 flippedCard = card
                                 frontCard = card
                               else if flippedCard == card 
                                 flippedCard = nil
                              
                           
                        
                     
                     .aspectRatio(1, contentMode: .fit)
                     .zIndex(frontCard == card ? 1 : 0)
                  
               
            
         
         .background(Color.black)
      
      .coordinateSpace(name: "mainFrame")
   


struct FlipOpacity: AnimatableModifier 
   var pct: CGFloat = 0
   
   var animatableData: CGFloat 
      get  pct 
      set  pct = newValue 
   
   
   func body(content: Content) -> some View 
      return content.opacity(Double(pct.rounded()))
   


struct CardBackView: View 
   var card: Int
   
   var body: some View 
      ZStack 
         RoundedRectangle(cornerRadius: 10)
            .fill(Color.red)
            .padding(5)
         Text("Back \(card)")
      
   


struct CardFrontView: View 
   var card: Int
   
   var body: some View 
      ZStack 
         RoundedRectangle(cornerRadius: 10)
            .fill(Color.blue)
            .padding(10)
            .aspectRatio(1.0, contentMode: .fit)
         Text("Front \(card)")
      
   

【讨论】:

非常感谢您分享这个示例和 gif 可视化。该代码允许基于这种工作方式理解和创建我自己的代码。【参考方案3】:

所以为了解释答案,我想解释你需要实现什么。 您希望您的 view/editView 在它出现在前面时进行动画处理。这意味着我们需要使用transition 修饰符。

现在 Apple 的内置转场修饰符使用了许多转场,例如easeIn、out 等,而没有这个转场,所以我们需要创建自定义转场来实现它。让我们先这样做。

extension AnyTransition 
    static var rotate: AnyTransition  get 
        AnyTransition.modifier(active: RotateTransition(percent: 0), identity: RotateTransition(percent: 1))
        
    



struct RotateTransition: GeometryEffect 
    var percent: Double
    
    var animatableData: Double 
        get  percent 
        set  percent = newValue 
    
    
    func effectValue(size: CGSize) -> ProjectionTransform 

        let rotationPercent = percent
        let a = CGFloat(Angle(degrees: 170 * (1-rotationPercent)).radians)
        
        var transform3d = CATransform3DIdentity;
        transform3d.m34 = -1/max(size.width, size.height)
        
        transform3d = CATransform3DRotate(transform3d, a, 0, 1, 0)
        transform3d = CATransform3DTranslate(transform3d, -size.width/2.0, -size.height/2.0, 0)
        
        let affineTransform1 = ProjectionTransform(CGAffineTransform(translationX: size.width/2.0, y: size.height / 2.0))
        let affineTransform2 = ProjectionTransform(CGAffineTransform(scaleX: CGFloat(percent * 2), y: CGFloat(percent * 2)))
        
        if percent <= 0.5 
            return ProjectionTransform(transform3d).concatenating(affineTransform2).concatenating(affineTransform1)
         else 
            return ProjectionTransform(transform3d).concatenating(affineTransform1)
        
    

现在我们有了自定义过渡,我们需要应用到那个视图。

所以这是考虑到你有一个 cardView 的代码。

cardView(card: cardName)
.transition(.rotate)
.matchedGeometryEffect(id: "popup", in: animation)

在您的情况下,父视图就像您单击编辑的视图

添加这个

  ParentView() //your view

  .matchedGeometryEffect(id: "popup", in: animation)

你可以在这里看到输出:

https://imgur.com/pIhBQ72

【讨论】:

以上是关于具有两个视图的 SwiftUI Card Flip 动画,其中一个嵌入在 Stack 中的主要内容,如果未能解决你的问题,请参考以下文章

为啥 SwiftUI 不能正确显示两个视图?

Flip-Card CSS/HTML:翻转时卡片背面不显示

React Native Flip Card 不支持翻转

SwiftUI:具有计算属性的 ObservableObject

SwiftUI - 从数据库中随机选择两个值

制作具有清晰背景的 SwiftUI 视图?