You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI卡片从堆叠中心飞至目标位置的轨迹过渡动画问题

SwiftUI实现卡片从堆叠中心飞至目标位置的过渡动画

要实现卡片从堆叠中心出发、平滑飞至最终位置的动画,内置的transition(.move(edge: .bottom))无法满足需求——它是从边缘而非指定中心位置开始过渡,导致动画生硬。下面是具体的实现方案:

核心思路

  1. 捕获堆叠中心坐标:用GeometryReader获取卡片堆叠视图(CardsDeckView)的全局位置,计算出中心坐标作为动画起点。
  2. 自定义过渡动画:通过AnyTransition.asymmetric定义插入动画,让卡片从堆叠中心的偏移位置移动到目标位置,搭配缩放动画增强自然感。
  3. 优化状态管理:把分散的firstCardShown等布尔值改成数组管理卡片状态,简化逻辑同时支持后续扩展。

完整代码示例

struct CardFlyAnimationView: View {
    @StateObject private var viewModel = CardViewModel()
    @State private var deckCenter: CGPoint = .zero
    private let cardWidth: CGFloat = 120 // 根据实际需求调整

    var body: some View {
        VStack {
            HStack(spacing: 20) {
                ForEach(viewModel.shownCards) { card in
                    RotatableCardView(
                        cardLayout: card.layout,
                        width: cardWidth - 20,
                        isFlipped: binding(for: card.id)
                    )
                    // 应用自定义飞行动画过渡
                    .transition(cardFlyTransition(from: deckCenter))
                }
            }
            .padding(.top, 40)
            .containerRelativeFrame(.vertical) { height, _ in
                height / 2
            }

            // 捕获堆叠视图的中心位置
            GeometryReader { proxy in
                CardsDeckView()
                    .onTapGesture {
                        withAnimation(.easeInOut(duration: 0.6)) {
                            viewModel.showNextCard()
                        }
                    }
                    .onAppear {
                        // 计算堆叠视图的全局中心坐标
                        let frame = proxy.frame(in: .global)
                        deckCenter = CGPoint(x: frame.midX, y: frame.midY)
                    }
            }
            .containerRelativeFrame(.vertical) { height, _ in
                height / 2
            }
        }
    }

    // 自定义飞行动画过渡
    private func cardFlyTransition(from startPoint: CGPoint) -> AnyTransition {
        // 计算当前卡片到堆叠中心的偏移量(反向计算,实现从中心飞至目标位置)
        let cardSize = CGSize(width: cardWidth - 20, height: (cardWidth - 20) * 1.4) // 按实际卡片宽高比调整
        let targetMidX = UIScreen.main.bounds.width / 2 - (cardSize.width + 20) * CGFloat(viewModel.shownCards.count - 1) / 2
        let offsetX = startPoint.x - (targetMidX + cardSize.width / 2)
        let offsetY = startPoint.y - cardSize.height / 2

        return .asymmetric(
            insertion: .offset(x: offsetX, y: offsetY)
                .combined(with: .scale(scale: 0.8))
                .animation(.easeInOut(duration: 0.6)),
            removal: .identity // 移除动画可按需自定义
        )
    }

    // 为每个卡片创建翻转状态绑定
    private func binding(for cardId: UUID) -> Binding<Bool> {
        .init(
            get: { viewModel.shownCards.first(where: { $0.id == cardId })?.isFlipped ?? false },
            set: { newValue in
                if let index = viewModel.shownCards.firstIndex(where: { $0.id == cardId }) {
                    viewModel.shownCards[index].isFlipped = newValue
                }
            }
        )
    }
}

// 优化后的ViewModel,用数组管理卡片状态
class CardViewModel: ObservableObject {
    struct CardItem: Identifiable {
        let id = UUID()
        let layout: CardLayout // 替换为你的实际CardLayout类型
        var isFlipped = false
    }

    @Published var shownCards: [CardItem] = []
    private let allCards: [CardItem] = [
        CardItem(layout: CardLayout()), // 替换为你的实际布局数据
        CardItem(layout: CardLayout()),
        CardItem(layout: CardLayout())
    ]

    func showNextCard() {
        guard shownCards.count < allCards.count else { return }
        shownCards.append(allCards[shownCards.count])
    }
}

// 以下为占位视图,替换为你的实际实现
struct RotatableCardView: View {
    let cardLayout: CardLayout
    let width: CGFloat
    @Binding var isFlipped: Bool

    var body: some View {
        RoundedRectangle(cornerRadius: 10)
            .fill(Color.white)
            .frame(width: width, height: width * 1.4)
            .shadow(radius: 5)
    }
}

struct CardsDeckView: View {
    var body: some View {
        RoundedRectangle(cornerRadius: 10)
            .fill(Color.gray.opacity(0.3))
            .frame(width: 100, height: 140)
    }
}

struct CardLayout {} // 占位类型,替换为你的实际布局模型

关键细节说明

  • 堆叠位置捕获:在CardsDeckView出现时,通过GeometryReader计算其全局中心坐标,确保动画起点准确。
  • 自定义过渡:asymmetric过渡仅定义插入动画,让卡片从堆叠中心偏移位置移动到目标位置,配合scale(0.8)模拟卡片“从堆叠中飞出、逐渐放大”的自然效果。
  • 状态管理优化:用数组替代单独的布尔值,简化视图代码的同时,后续新增卡片无需修改视图结构。

内容的提问来源于stack exchange,提问作者Igor

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 16:43:24