如何使用MatchedGeometryEffect实现MealTabView的全屏缩放动画?
用MatchedGeometryEffect实现MealTabView全屏缩放
要实现点击单元格后缩放至全屏的效果,核心是利用MatchedGeometryEffect匹配选中前后视图的几何属性,配合动画完成过渡。以下是具体实现步骤和修改后的代码:
核心思路
- 用**命名空间(Namespace)**关联所有需要过渡的视图元素,确保SwiftUI能识别匹配关系;
- 调整选中状态的管理(从数组改为可选ID,更适配单元素全屏场景);
- 给卡片背景、图片、标题、边框这些元素添加
matchedGeometryEffect,并设置唯一ID; - 用
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
相关产品推荐
相关产品推荐

