在我尝试构建的应用程序中,我有一个带圆圈的滚动视图。当用户点击圆形时,它应该平滑地过渡到全屏矩形。
为了简单起见,我们将只专注于尝试从小圆圈过渡到全屏矩形。
第 1 步:检查是否可动画
使用以下代码,我们可以在圆形和更大的矩形之间设置动画。证明
RoundedRectangle()
视图和 frame
视图修改器都是可动画的。
struct ContentView: View {
@State private var cornerRadius = 60.0
@State private var size = 100.0
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: cornerRadius)
.foregroundStyle(.orange)
.frame(width: size, height: size)
}
.preferredColorScheme(.dark)
.animation(.linear, value: cornerRadius)
.animation(.linear, value: size)
.onTapGesture {
cornerRadius = cornerRadius == 0.0 ? 60.0 : 0.0
size = size == 100.0 ? 300.0 : 100.0
}
}
}
这就是我想要的视图之间转换的效果。
第 2 步:视图之间的转换
这就是事情变得复杂的地方。我们有两种不同的视图:紧凑型和全屏版本。为了清楚起见,我将紧凑版设为橙色,将全屏版设为红色,我这样称呼它们。
我们从前面的示例中删除了可动画属性,并将其替换为在 true 和 false 之间切换的 isFullScreen 属性。使用
matchedGeometryEffect
我们可以匹配尺寸和框架。据我所知,SwiftUI 现在应该拥有在两个视图之间进行插值并平滑过渡的所有构建块。然而事实并非如此。
struct ContentView: View {
@Namespace private var namespace
@State private var isFullScreen = false
var body: some View {
ZStack {
if !isFullScreen {
RoundedRectangle(cornerRadius: 60.0)
.foregroundStyle(.orange)
.matchedGeometryEffect(id: "item", in: namespace)
.frame(width: 100.0, height: 100.0)
} else {
RoundedRectangle(cornerRadius: 0.0)
.foregroundStyle(.red)
.matchedGeometryEffect(id: "item", in: namespace)
.frame(width: 300.0, height: 300.0)
}
}
.preferredColorScheme(.dark)
.animation(.linear, value: isFullScreen)
.onTapGesture {
isFullScreen.toggle()
}
}
}
从淡入淡出动画和重影看来,SwiftUI 不知道该怎么办。因此,当转换橙色视图时,将获得它提供的角半径,并且不会将此属性从原始 60.0 动画到新的 0.0。至于红色视图,它不会将该属性从 60.0 动画到 0.0。它唯一做的就是为尺寸设置动画。
您可以尝试这种方法,如示例代码所示。似乎对我有用。
struct ContentView: View {
@State private var goFullScreen = false
var body: some View {
RoundedRectangle(cornerRadius: goFullScreen ? 0 : 60.0)
.fill(.orange)
.frame(maxWidth: goFullScreen ? .infinity : 300,
maxHeight: goFullScreen ? .infinity : 300)
.clipShape(RoundedRectangle(cornerRadius: goFullScreen ? 0 : 60.0))
.animation(.linear, value: goFullScreen)
.onTapGesture {
goFullScreen.toggle()
}
.preferredColorScheme(.dark)
}
}
根据您的评论,如果您希望在两个单独的视图之间转换时发生动画,那么最好的选择是使用角半径的状态变量。它并不完美,但比你以前的更好:
struct ContentView: View {
@Namespace private var namespace
@State private var goFullScreen = false
private static let roundedCornerRadius = CGFloat(60.0)
@State private var cornerRadius = ContentView.roundedCornerRadius
var body: some View {
ZStack {
if goFullScreen == false {
RoundedRectangle(cornerRadius: cornerRadius)
.fill(.orange)
.matchedGeometryEffect(id: "box", in: namespace, isSource: !goFullScreen)
.frame(width: 300, height: 300)
.onAppear { cornerRadius = ContentView.roundedCornerRadius }
} else {
RoundedRectangle(cornerRadius: cornerRadius)
.fill(.orange)
.matchedGeometryEffect(id: "box", in: namespace, isSource: goFullScreen)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.onAppear { cornerRadius = 0 }
}
}
.animation(.linear, value: goFullScreen)
.animation(.linear, value: cornerRadius)
.onTapGesture {
goFullScreen.toggle()
}
.preferredColorScheme(.dark)
}
}
编辑跟进您的进一步评论:
- 尽管我们将动画设置为线性,但插入的视图不知何故会快速变大。两种视图都应该模仿行为,因此不会出现重影。
我怀疑这是因为
matchedGeometryEffect
正在缩放视图,因此从绝对意义上讲,半径大小不匹配。您可以通过放弃使用 matchesGeometryEffect 并使用大小的状态变量来解决此问题(就像角半径的状态变量一样)。使用此状态变量来控制 maxWidth 和 maxHeight。
- 如果反转动画,插入的视图将不会为角设置动画。
如果您的意思是切换 if-else,那么请确保用正确的值初始化cornerRadius。您还需要更改匹配的几何效果的标志
isSource
。
- 如果您中断动画并且它再次变大,角将保持圆角。
是的,如果在动画发生时双击,则错误的半径可能会生效。这也可以通过添加 onDisappear 回调来解决。
实现该效果的另一种方法是完全避免使用过渡。您可以使用不透明度来控制可见性,然后使用彼此完全模仿的动画。这使得圆角半径和大小的变化能够平滑地进行动画处理。但不确定它是否适用于现实世界。
struct ContentView: View {
@Namespace private var namespace
@State private var goFullScreen = false
var body: some View {
ZStack {
Color.orange
.frame(maxWidth: goFullScreen ? .infinity : 300, maxHeight: goFullScreen ? .infinity : 300)
.cornerRadius(goFullScreen ? 0 : 60)
.opacity(goFullScreen ? 0 : 1)
Color.orange
.frame(maxWidth: goFullScreen ? .infinity : 300, maxHeight: goFullScreen ? .infinity : 300)
.cornerRadius(goFullScreen ? 0 : 60)
.opacity(goFullScreen ? 1 : 0)
}
.animation(.linear, value: goFullScreen)
.onTapGesture {
goFullScreen.toggle()
}
.preferredColorScheme(.dark)
}
}