本项目研究的是动画和过渡。先创建项目吧,项目名称 Animations。本节内容因为是介绍和动画相关的语法,所以内容叙述上较为分散。
(1)先随便弄个按钮出来。
Button("点我") {
// 先空着
}
.padding(50) // 数字会决定按钮的大小
.background(Color.red) // 前景色,
.foregroundColor(.white) // 背景色,
.clipShape(Circle()) // 圆形,为了后面的演示
// 这些修饰的具体内容可以随便你高兴去具体设定
(2)增加动画状态属性
@State private var animationAmount: CGFloat = 1
这里应该注意的是,因为涉及与旧的 API 交互,所以要使用一种称为 CGFloat 的数据类型,它和 Double 很像,但是比 Double 存储的数值范围要小一些。这是考虑到较老的硬件的兼容性。使用 CGFloat 可以让程序忽略具体的硬件。不过现在越来越多的硬件都支持 Double 了。
Swift 没有办法自动推断出 CGFloat 类型,所以必须要显式声明。
(3)给按钮增加缩放效果
给按钮增加下面的修饰
.scaleEffect(animationAmount)
scaleEffect 修饰是以给定的数字改变视图的尺寸(水平和垂直同步修改,同时和锚点关联)。数字的含义如下
(4)增加按钮动作
在按钮的动作响应代码中,添加下面的代码
self.animationAmount += 1
这样每次点击按钮时,按钮就变大一些。因为不会以新的尺寸重新渲染,所以会变得有点模糊。
(5)增加动画
给按钮增加 animation 修饰
.animation(.default)
这样,我们能看到变大的过程是平缓的,不是直接切换的。现在是默认效果。
(6)增加模糊效果
给按钮增加模糊修饰 blur,其中的参数表示模糊半径,最小为 0(不模糊),负数无意义。
.blur(radius: (animationAmount - 1) * 3)
现在放大和模糊的效果会在点击按钮后同时出现了。我们没有告诉程序关于动画的任何细节(起止时间,关键帧等),但是已经可以看见动画了。
本段内容,我觉得课程仍然使用那个圆形按钮,效果不明显,所以我改成了方块的水平运动(这里有个 offset 修饰可能是没介绍过,这个修饰使用的数据类型也是 CGFloat)
(1)动画形式
刚才使用的 .animation(.default)
是默认动画,实际上是“缓入缓出”动画,即 easeInOut,类似的还有 easeIn,easeOut。还有弹簧动画:
.animation(.interpolatingSpring(stiffness: 50, damping: 1))
刚度 stiffness 会影响动画的初始速度,阻尼 damping 会影响来回弹跳的时间。具体还需要大家自己测试摸索。
(2)时间控制
持续时间 duration 和延迟 delay,数值都是 Double,单位是秒
(3)重复动画
.repeatCount(3, autoreverses: true) // 有限重复,数字是重复次数,autoreverses是否复位
.repeatForever(autoreverses: true) // 无限重复
下面是 CustomAnimation 的主体部分
struct CustomAnimation: View {
@State private var animationAmount: CGFloat = 1
@State private var offsetAmount: CGFloat = 0
var body: some View {
VStack{
HStack {
RoundedRectangle(cornerRadius: 10)
.frame(width: 60, height: 40)
.foregroundColor(.blue)
.offset(x: offsetAmount)
.animation(
Animation.easeInOut(duration: 2) // 动画形式和持续时间
.delay(1) // 延迟时间
.repeatCount(2, autoreverses: true) // 重复次数,是否复位
.repeatForever(autoreverses: false) // 连续动画
)
.onAppear{
self.offsetAmount = 280
}
}
.padding(.horizontal, 15)
.frame(maxWidth: .infinity, alignment: .leading)
.frame(height: 100)
.background(Color(.gray))
// 后面还有三个 HStack,分别注释了 delay 并修改了动画形式
}
}
}
(4)一个有脉动效果的按钮
使用 overlay 修饰,在按钮上覆盖一个圆形。在圆形上使用stroke修饰进行描边,使用 scaleEffect 和 opacity 修饰缩放比例和透明度并使之随 animationAmount 数值改变而改变,使用 animation 修饰定义动画并在动画上使用 repeatForever 修饰重复动画,就得到了一个有脉动效果的按钮。
Button("点我") {
// self.animationAmount += 1
}
.padding(40)
.background(Color.red)
.foregroundColor(.white)
.clipShape(Circle())
.overlay(
Circle()
.stroke(Color.red)
.scaleEffect(animationAmount)
.opacity(Double(2 - animationAmount))
.animation(
Animation.easeOut(duration: 1)
.repeatForever(autoreverses: false)
)
)
.onAppear {
self.animationAmount = 2
}
对于修饰器参数,感觉除去布尔值,几乎所有值的变化都可以制作动画。先看下面的代码
struct ContentView: View {
@State private var animationAmount: CGFloat = 1
var body: some View {
VStack {
Stepper("缩放系数(1~10):\(animationAmount, specifier:"%g")",
value: $animationAmount.animation(), in: 1...10)
.padding()
Spacer()
Button("点我") {
self.animationAmount += 1
}
.padding(40)
.background(Color.red)
.foregroundColor(.white)
.clipShape(Circle())
.scaleEffect(animationAmount)
}
}
}
其中的 Stepper 就是绑定了 $animationAmount.animation() ,所以点击加减按钮的时候会有动画变化。而按钮只是通过在点击时改变 animationAmount 的值来影响 scaleEffect 修饰去改变按钮的缩放,并未定义值变化的动画,所以没有动画显示。
绑定在 Stepper 上的动画就是隐式动画,绑定动画同样可以使用动画修饰器,如动画形式、时长、延迟、重复什么的。绑定动画不是在视图上设定动画和通过状态变量显式定义如何动画。显式动画中的状态变量不知道触发了动画,绑定动画则是视图不知道自己会动画。二者皆有效,二者皆重要。
大叔的小实验
下面的代码是测试两个按钮通过切换 布尔值 状态,分别利用透明度和缩放比例来控制控件是否显示,状态的切换使用动画。实验中发现 CGFloat 从 0 到 1 程序不执行了,改成 0.0001 或者更小(反正能保证看不见就行)就没事了。
@State private var show1 = false
var title1: String {
return show1 ? "解散" : "集合"
}
var opacity1: Double {
return show1 ? 1 : 0
}
@State private var show2 = false
var title2: String {
return show2 ? "解散" : "集合"
}
var scale2: CGFloat {
return show2 ? 1 : 0.00001 // 设成 0 程序就停住了,我猜是 CGFloat 的事情
}
var body: some View {
VStack {
HStack {
Button(action: {
self.show1.toggle()
}) {
Text(title1)
}
RoundedRectangle(cornerRadius: 10)
.frame(width: 60, height: 40)
.foregroundColor(.blue)
.opacity(opacity1)
.animation(Animation.easeInOut(duration: 1))
}
HStack {
Button(action: {
self.show2.toggle()
}) {
Text(title2)
}
RoundedRectangle(cornerRadius: 10)
.frame(width: 60, height: 40)
.foregroundColor(.green)
.scaleEffect(scale2)
.animation(Animation.easeInOut(duration: 1))
}
}.padding()
}
前面已经了解了 SwiftUI 如何通过将animation()
修饰符附加到视图来创建隐式动画,以及如何通过将animation()
修饰符添加到绑定来创建动画的绑定更改,但是还有第三种方式可以创建动画:在状态发生改变后,显式地让 SwiftUI 产生动画变化。
我们仍然不必手动创建动画的每一帧,那是 SwiftUI 的活儿,它通过状态更改前后查看视图的状态持续地找出动画。
但是,现在,我们明确地希望在状态发生任意改变时发生动画:它没有附加到绑定,也没有附加到视图,只是我们明确要求发生特定的动画,因为状态变化。
为了说明这一点,让我们再次回到简单的按钮示例:
struct ContentView: View {
var body: some View {
Button("点我") {
// 点击后的动作
}
.padding(50)
.background(Color.red)
.foregroundColor(.white)
.clipShape(Circle())
}
}
现在要在点击该按钮后,让按钮带有3D效果旋转。这需要另一个新的修饰器,rotation3DEffect()
可以为其指定以度为单位的旋转量以及确定视图旋转方式的轴。共有三个维度的轴:
首先需要一个可以修改的某些状态,并且旋转度指定为Double
。
@State private var animationAmount = 0.0 // 除去 Int 外,我觉得应该尽量显式声明类型
接下来,让按钮animationAmount
沿其 Y 轴旋转:
.rotation3DEffect(.degrees(animationAmount), axis: (x: 0, y: 1, z: 0))
现在是重要部分:在按钮的动作中添加一些代码,以便在animationAmount
每次点击时将其添加360 。
如果只写self.animationAmount += 360
,那么更改将立即发生,因为按钮上没有附加动画修改器。这是显式动画出现的方式。如果使用withAnimation()
闭包,那么SwiftUI将确保由新状态引起的任何更改都将自动进行动画处理。
因此,现在将其放入按钮的操作中:
withAnimation {
self.animationAmount += 360
}
现在运行,每次点击按钮,它就会在3D空间中旋转。可以自己尝试一下其他轴或者多个轴。
withAnimation()
同样可以使用可以在SwiftUI中其他位置使用的所有相同动画作为动画参数。例如,
withAnimation(.interpolatingSpring(stiffness: 5, damping: 1)) {
self.animationAmount += 360
}
这样就使用上了弹簧动画。