在 SwiftUI 中,动画是提升应用用户体验的重要手段之一。MatchedGeometryEffect 是 SwiftUI 提供的一个强大的工具,它可以在视图层次结构中不同位置的视图之间创建平滑的动画过渡。这种效果特别适用于列表和详情视图之间的动画,或者在视图布局改变时保持视觉上的连贯性。
MatchedGeometryEffect 允许开发者标记两个视图,使得 SwiftUI 可以在这两个视图之间自动计算和应用动画,从而创建一个连续的用户体验。这是通过在两个视图上使用相同的 id 来实现的。
首先看一下这个修饰符的定义:
func matchedGeometryEffect<ID>(
id: ID,
in namespace: Namespace.ID,
properties: MatchedGeometryProperties = .frame,
anchor: UnitPoint = .center,
isSource: Bool = true
) -> some View where ID : Hashable
参数详解
id: 类型ID,必须遵循 Hashable 协议。这个参数用于唯一标识匹配的视图。只有当两个视图使用相同的 id 时,matchedGeometryEffect 才能正确地在它们之间创建动画过渡。这意味着你可以有多组视图使用不同的 id 进行独立的动画过渡。
namespace: Namespace.ID。命名空间用于将不同视图的动画过渡关联起来。你需要在视图的环境中定义一个 Namespace,并将其传递给参与动画的所有视图。这个命名空间确保只有标记了相同命名空间的视图之间才会应用动画效果。
properties: MatchedGeometryProperties(默认值为 .frame)。这个参数定义了哪些属性将包含在动画过渡中。可选的属性包括:
.frame: 包括视图的位置和大小。
.opacity: 包括视图的不透明度。
.scale: 包括视图的缩放比例。
也可以使用数组语法组合多个属性,例如 [.frame, .opacity]。
anchor: UnitPoint(默认值为 .center)。锚点定义了视图的哪一部分用作动画的参考点。
例如,.center 表示动画将围绕视图的中心进行。其他选项包括 .topLeading, .top, .topTrailing, .leading, .trailing, .bottomLeading, .bottom, .bottomTrailing 等,这些都可以改变动画的行为和视觉效果。
isSource: Bool(默认值为 true)。在使用 matchedGeometryEffect 时用于指定视图是动画的起点(源)还是终点(目标)。在动画过渡中,源视图的布局属性(如位置和尺寸)会被捕获,并在动画过程中转移到目标视图。
大多数情况下也只会用到id和namespace两个参数。
下面先看一个简单的示例:
//
// Animation1.swift
// SwiftBook
//
// Created by song on 2024/7/10.
//
import SwiftUI
struct Animation1: View {
@Namespace private var imageNamespace
@State private var showImage: Bool = false
var body: some View {
ZStack {
if showImage {
Image("xigua")
.resizable()
.matchedGeometryEffect(id: "xi", in: imageNamespace, isSource: true)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.aspectRatio(contentMode: .fit)
.onTapGesture {
withAnimation(.spring) {
showImage.toggle()
}
}
} else {
VStack {
Image("xigua")
.resizable()
.matchedGeometryEffect(id: "xi", in: imageNamespace, isSource: true)
.frame(width: 100, height: 80)
.aspectRatio(contentMode: .fit)
.onTapGesture {
withAnimation(.spring) {
showImage.toggle()
}
}
Spacer()
}
}
}
}
}
#Preview {
Animation1()
}
上面的代码中,我们添加了两个Image,并都给添加了.matchedGeometryEffect修饰符,传入相同的id和namespace,在逻辑上,这两个Image是同一个视图。当点击的时候,由一个视图切换到另一个视图去显示。
效果图如下:
现在将上面的代码稍微加工一下:
//
// Animation1.swift
// SwiftBook
//
// Created by song on 2024/7/10.
//
import SwiftUI
struct Animation1: View {
let columns: [GridItem] = [
GridItem(.flexible(minimum: 100, maximum: 150), spacing: 8),
GridItem(.flexible(minimum: 100, maximum: 150), spacing: 8),
GridItem(.flexible(minimum: 100, maximum: 150), spacing: 8),
]
let images: [String] = ["damangguo", "default", "hongyou", "juzi", "taozi", "xigua2"]
@Namespace private var imageNamespace
@State private var selectedImage: String = ""
var body: some View {
if !selectedImage.isEmpty {
Image(selectedImage)
.resizable()
.matchedGeometryEffect(id: selectedImage, in: imageNamespace)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.aspectRatio(contentMode: .fit)
.onTapGesture {
withAnimation(.easeInOut) {
selectedImage = ""
}
}
} else {
VStack {
LazyVGrid(columns: columns, content: {
ForEach(images, id: \.self) { image in
Image(image)
.resizable()
.matchedGeometryEffect(id: image, in: imageNamespace)
.frame(height: 80)
.aspectRatio(contentMode: .fit)
.onTapGesture {
withAnimation(.easeInOut) {
selectedImage = image
}
}
}
})
.padding()
Spacer()
}
}
}
}
#Preview {
Animation1()
}
实现的相册效果图:
上面是一个图片的列表界面,点击每个图片,就将这个图片全屏幕放大。
有了上面的基础后,再看一个类似Segment的组件:
//
// Animation1.swift
// SwiftBook
//
// Created by song on 2024/7/10.
//
import SwiftUI
struct Animation1: View {
let categories: [String] = ["Home", "Videos", "Article", "Event"]
@State private var selected: String = "Home"
@Namespace private var namespace
var body: some View {
HStack {
ForEach(categories, id: \.self) { category in
ZStack(alignment: .bottom) {
if selected == category {
RoundedRectangle(cornerRadius: 10)
.fill(Color.red)
.matchedGeometryEffect(id: "category", in: namespace)
.frame(height: 2)
.offset(y: 10)
}
Text(category)
.font(.headline)
.foregroundColor(selected == category ? .red : .black)
}
.frame(maxWidth: .infinity)
.frame(height: 65)
.onTapGesture {
withAnimation(.spring()) {
selected = category
}
}
}
}
}
}
#Preview {
Animation1()
}
该组件同样使用了matchedGeometryEffect修饰符,动态的将文字底部的指示器切换位置。效果如下:
MatchedGeometryEffect 是 SwiftUI 中一个非常强大的特性,它可以帮助开发者在不同视图之间创建平滑且吸引人的动画过渡。通过正确使用这一特性,可以显著提升应用的用户体验。然而,也需要注意其对性能的潜在影响,并确保在适当的场景中使用。
文章评论