**大叔注:这个标题有些……直译的误导。视图后面的修饰器排列起来就像一个堆栈。动画堆栈实际上是说在修饰器堆栈里面多次使用动画修饰。
先看下面的代码,这两段代码说明了修饰器的顺序如何重要。
Button("Tap Me") {
// do nothing
}
.background(Color.blue)
.frame(width: 200, height: 200)
.foregroundColor(.white)
Button("Tap Me") {
// do nothing
}
.frame(width: 200, height: 200)
.background(Color.blue)
.foregroundColor(.white)
其中的道理前面有讲,而且我们还反复使用background()
和padding()
创造一个条纹边框效果。
这就是概念一:修饰符顺序很重要,因为SwiftUI用修饰符按应用顺序包裹视图。
概念二是我们可以animation()
对视图应用修饰符,以使其隐含地对更改进行动画处理。
为了演示这一点,我们可以修改按钮代码,以便根据某些状态显示不同的颜色。首先,我们定义状态:
struct CustomViewModifier: View {
@State var show = false // 定义状态
var body: some View {
VStack(spacing: 15) {
Button("点我变色"){
self.show.toggle() // 状态切换
}
.frame(width: 200, height: 100, alignment: .center)
.foregroundColor(.white)
.background(Color(show ? .red : .blue)) // 颜色切换
.animation(.default) // 动画
}
}
}
运行代码,将看到点击按钮会在蓝色和红色之间为其设置动画的颜色。
现在给按钮增加一个圆角修饰器,
Button("点我变色,圆角没有动画"){
self.show.toggle() // 状态切换
}
.frame(width: 200, height: 200)
.background(show ? Color.blue : Color.red)
.animation(.default)
.foregroundColor(.white)
.clipShape(RoundedRectangle(cornerRadius: show ? 60 : 0))
运行后会看到点击按钮会使其在红色和蓝色之间进行动画处理,但是在正方形和圆角矩形之间的切换不会进行动画处理。
如果将clipShape()
修改器移到动画之前,如下所示:
Button("点我变色和圆角都有动画") {
self.enabled.toggle()
}
.frame(width: 200, height: 200)
.background(enabled ? Color.blue : Color.red)
.foregroundColor(.white)
.clipShape(RoundedRectangle(cornerRadius: enabled ? 60 : 0))
.animation(.default)
运行代码时,背景颜色和剪辑形状都将进行动画处理。再次说明顺序很重要:animation()
仅影响在它之前发生的更改。
如果应用多个animation()
修改器,则每个修改器控制着之前动画处理过的所有内容。这样能够以各种不同的方式为状态变化设置动画,而不是为所有属性统一设置。
例如,可以使用默认动画来进行颜色更改,但是对剪辑形状使用插值弹簧:
Button("点我变色和圆角有不同动画") {
self.enabled.toggle()
}
.frame(width: 200, height: 200)
.background(enabled ? Color.blue : Color.red)
.animation(.default)
.foregroundColor(.white)
.clipShape(RoundedRectangle(cornerRadius: enabled ? 60 : 0))
.animation(.interpolatingSpring(stiffness: 10, damping: 1))
为了获得更多控制,可以通过传递nil
到修饰器来完全禁用动画。如果需要立即进行颜色更改,但剪辑形状保留其动画,可以这样编写:
Button("点我变色无动画,圆角有动画") {
self.enabled.toggle()
}
.frame(width: 200, height: 200)
.background(enabled ? Color.blue : Color.red)
.animation(nil)
.foregroundColor(.white)
.clipShape(RoundedRectangle(cornerRadius: enabled ? 60 : 0))
.animation(.interpolatingSpring(stiffness: 10, damping: 1))
SwiftUI 允许将手势附加到任何视图,并且这些手势的效果也可以动画。稍后,我们将更详细地介绍手势,但现在让我们尝试一些相对简单的操作:可以在屏幕上拖动的卡片,但是放开后,它会卡回到其原始位置。
首先,我们的初始布局:
struct ContentView: View {
var body: some View {
LinearGradient(
gradient: Gradient(colors: [.yellow, .red]),
startPoint: .topLeading,
endPoint: .bottomTrailing
)
.frame(width: 300, height: 200)
.clipShape(RoundedRectangle(cornerRadius: 10))
}
}
这样可以在屏幕中央绘制类似卡片的视图。我们想根据手指的位置在屏幕上移动它,这需要三个步骤。
首先,我们需要某种状态来存储其拖动量:
@State private var dragAmount = CGSize.zero
其次,我们要使用该大小来影响卡在屏幕上的位置。SwiftUI为此提供了一个专用的修饰符offset()
,它使我们能够调整视图的 X 和 Y 坐标而无需在其周围移动其他视图。您可以根据需要输入离散的 X 和 Y 坐标,但是-绝非偶然- offset()
也可以CGSize
直接采用。
因此,第二步是将此修改器添加到线性渐变中:
.offset(dragAmount)
现在重要的部分到了:我们可以创建一个DragGesture
并将其附加到卡上。在这里我们对拖动手势有用的两个额外的修饰符:移动时运行的onChanged()
和结束拖动时运行的onEnded()
。
它们都有一个参数,描述了拖动操作——它的开始位置,当前位置,移动距离等等。对于onChanged()
修改器,我们将读取拖动的位移,该位移告诉我们拖动距起点有多远——可以直接将其赋值给dragAmount
以便视图随手势一起移动。对于onEnded()
要完全忽略输入,因为需要将设置dragAmount
复位。
因此,现在将此修饰符添加到线性渐变中:
.gesture(
DragGesture()
.onChanged { self.dragAmount = $0.translation }
.onEnded { _ in self.dragAmount = .zero }
)
如果运行代码,您会看到现在可以拖动渐变卡了,放开拖动时,它将跳回到中心。卡的偏移量由dragAmount
确定,该偏移量又由拖动手势设置。
现在一切正常,我们可以通过一些动画使该动作栩栩如生,我们有两个选择:添加一个隐式动画以使拖动和释放具有动画效果,或者添加一个显式动画以使释放成为动画。
要查看前者的实际效果,请将此修改器添加到线性渐变中:
.animation(.spring())
拖动时,由于弹簧动画的作用,卡会稍有延迟地移到拖动位置,但是如果突然移动,它也会轻轻地过冲。
要看到明确的动画在行动,删除animation()
修改和改变现有的onEnded()
拖拽手势的代码如下:
.onEnded { _ in
withAnimation(.spring()) {
self.dragAmount = .zero
}
}
现在,这张卡将立即跟随您的拖动(因为没有被动画化),但是当您放开它时,它将进行动画处理。
如果我们将偏移动画与拖动手势并稍加延迟相结合,则无需大量代码就可以创建非常有趣的动画。
为了证明这一点,我们可以将文本“ Hello SwiftUI”编写为一系列单独的字母,每个字母的背景颜色和偏移量都由某个状态控制。使用Array("Hello SwiftUI")
可以得到一个字符串数组:每个元素是一个字符。
struct ContentView: View {
let letters = Array("Hello SwiftUI")
@State private var enabled = false
@State private var dragAmount = CGSize.zero
var body: some View {
HStack(spacing: 0) {
ForEach(0..<letters.count) { num in
Text(String(self.letters[num]))
.padding(5)
.font(.title)
.background(self.enabled ? Color.blue : Color.red)
.offset(self.dragAmount)
.animation(Animation.default.delay(Double(num) / 20))
}
}
.gesture(
DragGesture()
.onChanged { self.dragAmount = $0.translation }
.onEnded { _ in
self.dragAmount = .zero
self.enabled.toggle()
}
)
}
}
如果运行该代码,您会发现可以拖动任意字母以使整个字符串都跟随该字符串,只是短暂的延迟会导致类似蛇的效果。当您释放拖动时,SwiftUI还将添加颜色更改,即使字母移回中心也可以在蓝色和红色之间进行动画显示。
SwiftUI最强大的功能之一是能够自定义视图的显示和隐藏方式。之前,您已经了解了如何使用常规if
条件有条件地包含视图,这意味着当条件发生变化时,我们可以从视图层次结构中插入或删除视图。
过渡控制插入和删除的方式,我们可以使用内置过渡,以不同方式组合它们,甚至创建完全自定义的过渡。
为了说明这一点,这里有一个VStack
带有按钮和一个矩形的:
struct ContentView: View {
var body: some View {
VStack {
Button("Tap Me") {
// do nothing
}
Rectangle()
.fill(Color.red)
.frame(width: 200, height: 200)
}
}
}
我们可以使矩形仅在满足特定条件时显示。首先,我们添加一些可以操纵的状态:
@State private var isShowingRed = false
接下来,我们将该状态用作显示矩形的条件:
if isShowingRed {
Rectangle()
.fill(Color.red)
.frame(width: 200, height: 200)
}
最后,我们可以isShowingRed
在按钮的操作中在true和false之间切换:
self.isShowingRed.toggle()
如果运行该程序,则会看到按下按钮会显示并隐藏红色方块。没有动画。它只是出现而突然消失。
我们可以使用来包装状态更改withAnimation()
,从而获得SwiftUI的默认视图过渡,如下所示:
withAnimation {
self.isShowingRed.toggle()
}
有了较小的更改,应用程序现在就可以淡入和淡出红色矩形,同时还可以向上移动按钮以腾出空间。看起来不错,但我们可以使用transition()
修饰符做得更好。
例如,我们可以通过在矩形上添加transition()
修饰符来使矩形放大和缩小:
Rectangle()
.fill(Color.red)
.frame(width: 200, height: 200)
.transition(.scale)
现在点击按钮看起来更好:矩形会随着按钮的腾出而扩大,然后再次点击时会缩小。
如果要尝试,还可以尝试其他几种转换。一个有用的是.asymmetric
,它使我们可以在显示视图时使用一个过渡,而在消失时使用另一个过渡。要进行尝试,请使用以下命令替换矩形的现有过渡:
.transition(.asymmetric(insertion: .scale, removal: .opacity))
为SwiftUI创建全新的过渡是可能的,而且实际上出乎意料的容易,这使我们可以使用完全自定义的动画添加和删除视图。
.modifier
过渡使此功能成为可能,该过渡接受我们想要的任何视图修饰符。要注意的是,我们需要能够实例化修饰符,这意味着它必须是我们自己创建的修饰符。
为了尝试这一点,我们可以编写一个视图修改器,让我们模仿Keynote中的Pivot动画-它使新幻灯片从其左上角旋转入。用SwiftUI讲,这意味着创建一个视图修改器,使我们的视图从一个角旋转,而不会逃脱它应该位于的边界。SwiftUI实际上为我们提供了修改器来做到这一点:rotationEffect()
让我们在2D空间中旋转视图,并clipped()
阻止将视图绘制到其矩形空间的外部。
rotationEffect()
与相似rotation3DEffect()
,但它始终绕Z轴旋转。但是,它也使我们能够控制旋转的锚点 -视图的哪一部分应固定在旋转中心。SwiftUI为我们提供了一个UnitPoint
用于控制锚,它可以让我们指定确切的X / Y点的许多内置选项旋转或使用一个类型- ,.topLeading
,.bottomTrailing
,.center
等等。
让我们通过创建一个CornerRotateModifier
结构来构造所有代码,这些结构具有一个锚点来控制旋转的位置,并控制一个旋转量:
struct CornerRotateModifier: ViewModifier {
let amount: Double
let anchor: UnitPoint
func body(content: Content) -> some View {
content.rotationEffect(.degrees(amount), anchor: anchor).clipped()
}
}
clipped()
那里的添加意味着当视图旋转时,不会绘制位于其自然矩形之外的零件。
我们可以使用.modifier
过渡直接尝试一下,但这有点笨拙。一个更好的主意是将其包装到的扩展中AnyTransition
,使它在其最前端的角从-90旋转到0:
extension AnyTransition {
static var pivot: AnyTransition {
.modifier(
active: CornerRotateModifier(amount: -90, anchor: .topLeading),
identity: CornerRotateModifier(amount: 0, anchor: .topLeading)
)
}
}
有了这个,我们现在可以使用以下方法将透视动画附加到任何视图:
.transition(.pivot)
(大叔注:说实话,3 和 4 的实验效果不理想)