如果你有一个视图,分别出现在界面中的两个不同的地方,并且想要在它们之间设置动画(例如从列表视图转到缩放的详细视图) ,那么您应该使用 matchedGeometryEffect()
修饰符,这有点像 Keynote 中的 Magic Move 效果。
// 例如有以下代码:在一个视图状态下有一个红色圆圈,然后是一些文本;但在另一种视图状态下,圆圈出现在文本后面并改变了颜色
struct ContentView: View {
@State private var isFlipped = false
var body: some View {
VStack {
if isFlipped {
Circle()
.fill(.red)
.frame(width: 44, height: 44)
Text("Taylor Swift – 1989")
.font(.headline)
} else {
Text("Taylor Swift – 1989")
.font(.headline)
Circle()
.fill(.blue)
.frame(width: 44, height: 44)
}
}
.onTapGesture {
withAnimation {
isFlipped.toggle()
}
}
}
}
以上布局发生了变化,相比于使用传统方式去设置动画,更快的方式是使用 matchedGeometryEffect
修饰符,具体使用方法如下:
@Namespace
命名空间首先,您需要使用 @Namespace
属性包装器,为您的视图创建全局命名空间。@Namespace
实际上只是某种视图上的属性,在背后,它让我们可以将视图附加在一起。您可以像下面这样添加属性:
@Namespace private var animation
接下来,您需要将 .matchedGeometryEffect(id: YourIdentifierHere, in: animation)
添加到 “您想要使用同步效果进行动画处理” 的所有视图。 YourIdentifierHere
部分应替换为双方各部分共享的某个唯一编号。
// 在示例中,可以将其用于 Circle:
.matchedGeometryEffect(id: "Shape",in: animation)
// 并将其用于 Text 文本:
.matchedGeometryEffect(id: "AlbumTitle",in: animation)
// 当您再次运行该示例时,您将看到两个视图能够平滑地移动。
最终代码如下所示:
struct ContentView: View {
// 创建命名空间
@Namespace private var animation
@Stateprivatevar isFlipped = false
var body:some View {
VStack {
if isFlipped {
Circle()
.fill(.red)
.frame(width: 44, height: 44)
// 在创建的命名空间里,id 要和另外一个状态下的一样
.matchedGeometryEffect(id: "Shape",in: animation)
Text("Taylor Swift – 1989")
.matchedGeometryEffect(id: "AlbumTitle",in: animation)
.font(.headline)
}else {
Text("Taylor Swift – 1989")
.matchedGeometryEffect(id: "AlbumTitle",in: animation)
.font(.headline)
Circle()
.fill(.blue)
.frame(width: 44, height: 44)
.matchedGeometryEffect(id: "Shape",in: animation)
}
}
.onTapGesture {
withAnimation {
isFlipped.toggle()
}
}
}
}
以下是更高级的示例,它借用了 Apple Music 的专辑显示样式,点击时将小视图扩展为更大的视图。
在本例中,只有文本是动画的,因为它的位置改变了:
struct ContentView: View {
@Namespaceprivatevar animation
@Stateprivatevar isZoomed = false
var frame: Double {
isZoomed ? 300 : 44
}
var body:some View {
VStack {
Spacer()
VStack {
HStack {
RoundedRectangle(cornerRadius: 10)
.fill(.blue)
.frame(width: frame, height: frame)
.padding(.top, isZoomed ? 20 : 0)
if isZoomed == false {
Text("Taylor Swift – 1989")
.matchedGeometryEffect(id: "AlbumTitle",in: animation)
.font(.headline)
Spacer()
}
}
if isZoomed == true {
Text("Taylor Swift – 1989")
.matchedGeometryEffect(id: "AlbumTitle",in: animation)
.font(.headline)
.padding(.bottom, 60)
Spacer()
}
}
.onTapGesture {
withAnimation(.spring()) {
isZoomed.toggle()
}
}
.padding()
.frame(maxWidth: .infinity)
.frame(height: 400)
.background(Color(white: 0.9))
.foregroundStyle(.black)
}
}
}
SwiftUI 提供了一个 withTransaction()
函数,允许我们在运行时覆盖动画,例如删除隐式动画并将其替换为自定义动画。
// 例如,此代码在小尺寸和大尺寸之间切换文本,并一直进行动画处理,因为它附加了隐式动画:
struct ContentView: View {
@State private var isZoomed = false
var body: some View {
VStack {
Button("Toggle Zoom") {
isZoomed.toggle()
}
Text("Zoom Text")
.font(.title)
.scaleEffect(isZoomed ? 3 : 1)
.animation(.easeInOut(duration: 2), value: isZoomed)
}
}
}
Transactions 允许我们根据具体情况覆盖现有动画。具体做法如下:
Transaction
实例disablesAnimations
值设置为 true
,以便覆盖任何现有的适用动画withTransaction()
方法,并传入前面定义的 Transaction
对象,在方法中需要包含想要更改的动画状态值,它将使用您的事务进行动画处理例如您希望在特定情况下,文本动画以 linear
的方式发生,而不是之前定义的动画。于是使用 Transaction
插入自定义动画:
struct ContentView: View {
@State private var isZoomed = false
var body: some View {
VStack {
Button("Toggle Zoom") {
var transaction = Transaction(animation: .linear)
transaction.disablesAnimations = true
withTransaction(transaction) {
isZoomed.toggle()
}
}
Spacer()
.frame(height: 100)
Text("Zoom Text")
.font(.title)
.scaleEffect(isZoomed ? 3 : 1)
.animation(.easeInOut(duration: 2), value: isZoomed)
}
}
}
为了获得更多控制,您可以将 transaction()
修饰符,附加到您想要的任何视图,从而允许您覆盖应用于该视图的任何 Transaction
。
例如,我们可以在示例中添加第二个缩放文本,仍然使用 transaction 来触发缩放动画,但这次我们在第二个文本视图上使用 transaction()
修饰符,并且禁用该视图上的任何 transactions。该设定将覆盖之前的 Transaction
…
struct ContentView: View {
@State private var isZoomed = false
var body:some View {
VStack {
Button("Toggle Zoom") {
var transaction = Transaction(animation: .linear)
transaction.disablesAnimations = true
withTransaction(transaction) {
isZoomed.toggle()
}
}
Spacer()
.frame(height: 100)
Text("Zoom Text 1")
.font(.title)
.scaleEffect(isZoomed ? 3 : 1)
Spacer()
.frame(height: 100)
Text("Zoom Text 2")
.font(.title)
.scaleEffect(isZoomed ? 3 : 1)
.transaction { t in
t.animation = .none
}
}
}
}