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

如何使用MatchedGeometryEffect实现MealTabView的全屏缩放动画?

用MatchedGeometryEffect实现MealTabView全屏缩放

要实现点击单元格后缩放至全屏的效果,核心是利用MatchedGeometryEffect匹配选中前后视图的几何属性,配合动画完成过渡。以下是具体实现步骤和修改后的代码:

核心思路

  1. 用**命名空间(Namespace)**关联所有需要过渡的视图元素,确保SwiftUI能识别匹配关系;
  2. 调整选中状态的管理(从数组改为可选ID,更适配单元素全屏场景);
  3. 给卡片背景、图片、标题、边框这些元素添加matchedGeometryEffect,并设置唯一ID;
  4. 用withAnimation包裹状态切换,触发平滑的缩放过渡。

修改后的代码实现

1. 父视图(管理列表与选中状态)

struct MealListView: View {
    let meals: [Meal]
    // 用可选ID管理选中状态,nil表示未选中
    @State private var selectedMealId: Int?
    // 声明命名空间,用于关联过渡视图
    @Namespace private var animationNamespace
    
    var body: some View {
        ZStack {
            // 网格布局的普通单元格列表
            ScrollView {
                LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 20) {
                    ForEach(meals) { meal in
                        MealTabView(
                            meal: meal,
                            selectedMealId: $selectedMealId,
                            animationNamespace: animationNamespace
                        )
                    }
                }
                .padding()
            }
            
            // 选中时显示的全屏视图
            if let selectedId = selectedMealId, 
               let targetMeal = meals.first(where: { $0.id == selectedId }) {
                FullScreenMealView(
                    meal: targetMeal,
                    selectedMealId: $selectedMealId,
                    animationNamespace: animationNamespace
                )
                .transition(.opacity)
            }
        }
    }
}

2. 普通单元格视图(MealTabView)

struct MealTabView: View {
    let meal: Meal
    @Binding var selectedMealId: Int?
    let animationNamespace: Namespace.ID
    
    var body: some View {
        Button(action: {
            // 用弹簧动画包裹状态切换,让过渡更自然
            withAnimation(.spring(response: 0.5, dampingFraction: 0.8)) {
                selectedMealId = selectedMealId == meal.id ? nil : meal.id
            }
        }) {
            ZStack(alignment: .center) {
                // 卡片背景:选中时圆角为0(全屏),未选中时20
                RoundedRectangle(cornerRadius: selectedMealId == meal.id ? 0 : 20)
                    .foregroundColor(.white)
                    .frame(width: 150, height: 200)
                    .matchedGeometryEffect(id: "card_\(meal.id)", in: animationNamespace)
                
                VStack(alignment: .center) {
                    // 餐品图片:选中时放大尺寸
                    Image("meal")
                        .resizable()
                        .frame(
                            width: selectedMealId == meal.id ? 200 : 100,
                            height: selectedMealId == meal.id ? 200 : 100
                        )
                        .matchedGeometryEffect(id: "image_\(meal.id)", in: animationNamespace)
                    
                    Spacer()
                    
                    // 餐品标题:选中时放大字号
                    Text(meal.title)
                        .padding(.bottom, selectedMealId == meal.id ? 40 : 20)
                        .font(.custom("Wotfard", size: selectedMealId == meal.id ? 32 : 20))
                        .foregroundColor(.black)
                        .matchedGeometryEffect(id: "title_\(meal.id)", in: animationNamespace)
                }
            }
            .shadow(color: .black.opacity(0.1), radius: 5)
            .overlay(
                // 边框:选中时显示黑框,未选中时白框
                RoundedRectangle(cornerRadius: selectedMealId == meal.id ? 0 : 20)
                    .stroke(selectedMealId == meal.id ? .black : .white, lineWidth: 4)
                    .matchedGeometryEffect(id: "stroke_\(meal.id)", in: animationNamespace)
            )
        }
    }
}

3. 全屏视图(FullScreenMealView)

struct FullScreenMealView: View {
    let meal: Meal
    @Binding var selectedMealId: Int?
    let animationNamespace: Namespace.ID
    
    var body: some View {
        Button(action: {
            withAnimation(.spring(response: 0.5, dampingFraction: 0.8)) {
                selectedMealId = nil
            }
        }) {
            ZStack(alignment: .center) {
                // 全屏背景,忽略安全区域
                RoundedRectangle(cornerRadius: 0)
                    .foregroundColor(.white)
                    .ignoresSafeArea()
                    .matchedGeometryEffect(id: "card_\(meal.id)", in: animationNamespace)
                
                VStack(alignment: .center) {
                    Image("meal")
                        .resizable()
                        .frame(width: 200, height: 200)
                        .padding(.top, 80)
                        .matchedGeometryEffect(id: "image_\(meal.id)", in: animationNamespace)
                    
                    Spacer()
                    
                    Text(meal.title)
                        .padding(.bottom, 40)
                        .font(.custom("Wotfard", size: 32))
                        .foregroundColor(.black)
                        .matchedGeometryEffect(id: "title_\(meal.id)", in: animationNamespace)
                }
            }
            .overlay(
                RoundedRectangle(cornerRadius: 0)
                    .stroke(.black, lineWidth: 4)
                    .ignoresSafeArea()
                    .matchedGeometryEffect(id: "stroke_\(meal.id)", in: animationNamespace)
            )
        }
    }
}

关键注意点

  • 唯一ID:每个过渡元素的matchedGeometryEffect必须设置唯一ID(比如加上meal.id),避免不同餐品的视图冲突;
  • 命名空间共享:所有关联的matchedGeometryEffect必须使用同一个Namespace.ID,所以需要在父视图声明后传递给子视图;
  • 动画类型:使用spring动画能让缩放过渡更有弹性,更符合用户预期;
  • 适配多选:如果需要支持多选,可将selectedMealId改为Set<Int>,并在父视图中遍历选中的餐品生成全屏视图,但单选中全屏是更常见的交互模式。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.23 09:03:48