同时匹配GeometryEffect和rotation3DEffect

Simultaneous matchedGeometryEffect and rotation3DEffect

我想制作一张卡片的动画,它从屏幕的上半部分飞到屏幕的下半部分,并在飞行过程中翻转。

我使用自定义 .cardify 修饰符控制翻转逻辑。它似乎 单独工作 ,例如当我按 onTapGesture { withAnimation { ... } }.

翻牌时

我还使用 matchedGeometryEffect 使卡片从屏幕顶部飞到底部,反之亦然。

但是,当我点击卡片时,它没有旋转地飞行。

我尝试为两个 if 分支应用 .transition(.assymetric(...))(请参阅下面的代码),但没有帮助。

所以,代码

import SwiftUI

struct ContentView: View {
    @State var cardOnTop = true
    @State var theCard = Card(isFaceUp: true)

    @Namespace private var animationNameSpace
    
    var body: some View {
        VStack {
            if cardOnTop {
                CardView(card: theCard)
                    .matchedGeometryEffect(id: 1, in: animationNameSpace)
                    .onTapGesture {
                        withAnimation {
                            theCard.toggle()
                            cardOnTop.toggle()  // comment me to test flipping alone
                        }
                    }
                Color.white
            } else {
                Color.white
                CardView(card: theCard)
                    .matchedGeometryEffect(id: 1, in: animationNameSpace)
                    .onTapGesture {
                        withAnimation {
                            theCard.toggle()
                            cardOnTop.toggle()
                        }
                    }
            }
        }
        .padding()
    }

    struct Card {
        var isFaceUp: Bool
        
        mutating func toggle() {
            isFaceUp.toggle()
        }
    }
    
    struct CardView: View {
        var card: Card

        var body: some View {
            Rectangle()
                .frame(width: 100, height: 50)
                .foregroundColor(.red)
                .cardify(isFaceUp: card.isFaceUp)
        }
    }
}

/* Cardify ViewModifier */

struct Cardify: ViewModifier, Animatable {
    init(isFaceUp: Bool){
        rotation = isFaceUp ? 0 : 180
    }
    
    var rotation: Double  // in degrees

    var animatableData: Double {
        get { return rotation }
        set { rotation = newValue }
    }
    
    func body(content: Content) -> some View {
        ZStack {
            let shape = RoundedRectangle(cornerRadius: DrawingConstants.cornerRadius)
            if rotation < 90 {
                shape.fill().foregroundColor(.white)
                shape.strokeBorder(lineWidth: DrawingConstants.lineWidth).foregroundColor(.gray)
            } else {
                shape.fill().foregroundColor(.gray)
            }
            
            content
                .opacity(rotation < 90 ? 1 : 0)
        }
        .rotation3DEffect(Angle.degrees(rotation), axis: (0, 1, 0))
    }
    
    private struct DrawingConstants {
        static let cornerRadius: CGFloat = 15
        static let lineWidth: CGFloat = 2
    }
}

extension View {
    func cardify(isFaceUp: Bool) -> some View {
        return self.modifier(Cardify(isFaceUp: isFaceUp))
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

如何让卡片旋转飞起来?

此外,我发现这行得通(如果单独放在 VStack 的正文中,没有 if-branches)

CardView(card: theCard)
    .offset(x: 0, y: cardOnTop ? 0 : 100)
    .onTapGesture {
        withAnimation {
            theCard.toggle()
            cardOnTop.toggle()  // comment me to test flipping alone
        }
    }

但在我的应用程序中,我有一副向下翻转的卡片,这些卡片被处理到向上翻转的板上。我试图在上面 ContentView 的代码中通过 if-branch 来模拟行为(CardViews 出现和消失)。

没有触发旋转,因为您取出卡片并在视图中插入另一张卡片,因此只剩下 .matchedGeometryEffect

您想要的是一张卡片保持在视野中,这样它可以旋转,但也可以改变它的位置。

你可以做到这一点,例如.offsetGeometryReader只是为了找到正确的偏移值。

var body: some View {
    GeometryReader { geo in
        VStack {
            CardView(card: theCard)
                .offset(x: 0, y: cardOnTop ? 0 : geo.size.height / 2)
                .onTapGesture {
                    withAnimation {
                        cardOnTop.toggle() 
                        theCard.toggle()
                    }
                }
            Color.clear
        }
    }
    .padding()
}

这里是一个纯粹使用自定义转换的 .transition 解决方案:

struct ContentView: View {
    @State var cardOnTop = true
    
    var body: some View {
        VStack(spacing:0) {
            if cardOnTop {
                RoundedRectangle(cornerRadius: 15)
                    .fill(.blue)
                    .transition(.rotate3D(direction: 1).combined(with: .move(edge: .bottom)))
                Color.clear
            } else {
                Color.clear
                RoundedRectangle(cornerRadius: 15)
                    .fill(.green)
                    .transition(.rotate3D(direction: -1).combined(with: .move(edge: .top)))
            }
        }
        .onTapGesture {
            withAnimation(.easeInOut(duration: 2)) {
                cardOnTop.toggle()
            }
        }
        .padding()
    }
}

/* Transition Modifier */

extension AnyTransition {
    static func rotate3D(direction: Double) -> AnyTransition {
        AnyTransition.modifier(
            active: Rotate3DModifier(value: 1, direction: direction),
            identity: Rotate3DModifier(value: 0, direction: direction))
    }
}

struct Rotate3DModifier: ViewModifier {
    let value: Double
    let direction: Double
    
    func body(content: Content) -> some View {
        content
            .rotation3DEffect(Angle(degrees: 180 * value * direction), axis: (x: 0, y: 1, z: 0))
            .opacity(1 - value)
    }
}