同时匹配几何效果和旋转3D效果

发布于 2025-01-16 07:32:04 字数 3704 浏览 2 评论 0原文

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

我使用自定义 .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分支)

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 来模拟行为(CardView 出现和消失)。

I want to animate a card, which flies from top half of screen to the bottom half and flips during the fly.

I control the flipping logic using custom .cardify modifier. It seems working alone, e.g. when I flip a card by onTapGesture { withAnimation { ... } }.

I also made a card fly from top of screen to the bottom and vice versa using matchedGeometryEffect.

But, when I tap card it flies without rotation.

I tried to apply .transition(.assymetric(...)) for both if-branches (see code below) but it did not help.

So, the code

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()
    }
}

How can I make the card flying with rotation?

Also, I found that this works (if put in the body of VStack alone, no if-branches)

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

but in my app I have a deck of flipped down cards, which are dealt to board flipping up. I tried to model the behavior by if-branch in the code of ContentView above (CardViews appear and dissapear).

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

攀登最高峰 2025-01-23 07:32:04

这是一个纯粹使用 .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)
    }
}

Here is a solution purely with .transition using a custom transition:

enter image description here

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)
    }
}
就像说晚安 2025-01-23 07:32:04

由于您移除了该卡并将另一张卡插入到视图中,因此不会触发旋转,因此仅保留 .matchedGeometryEffect

您想要的是一张卡片留在视野中,以便它可以旋转,但也可以改变其位置。

您可以使用 .offset 来实现这一点。 GeometryReader 只是为了找到正确的偏移值。

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()
}

输入图片此处描述

The rotation isn't triggered because you remove the card and insert another one into the view, so only the .matchedGeometryEffect remains.

What you want is one card that stays in view so it can rotate, but also change its position.

You can achieve this e.g. with .offset. The GeometryReader is just to find the right offset value.

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()
}

enter image description here

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文