SwiftUI 有趣的 animation 動畫
透過 SwiftUI,我們可以快速實現 App 的動畫功能,接下來就讓我們以奔跑吧,Peter
為例,說明各種有趣的動畫效果。
設計 App 畫面
struct ContentView: View {
@State private var moveDistance: Double = 0
@State private var opacity: Double = 1
var body: some View {
VStack {
Button("奔跑吧,Peter") {
moveDistance += 100
opacity -= 0.3
}
.font(.title)
HStack {
Image(.peter)
.offset(x: moveDistance)
.opacity(opacity)
Spacer()
}
}
}
}
當我們點擊 奔跑吧,Peter
時,peter 的圖片將向右移動 100 points,而且因為跑步很累,他將會愈來愈透明。
不過剛剛的 peter 是瞬間移動,這樣太不真實了,讓我們為他加個移動的動畫吧。
從元件呼叫 function animation 加入動畫
我們想要展現動畫的目標是 Image,所以很簡單,我們只要從 Image 呼叫動畫 function animation 即可。
struct ContentView: View {
@State private var moveDistance: Double = 0
@State private var opacity: Double = 1
var body: some View {
VStack {
Button("奔跑吧,Peter") {
moveDistance += 100
opacity -= 0.3
}
.font(.title)
HStack {
Image(.peter)
.offset(x: moveDistance)
.opacity(opacity)
.animation(.default, value: opacity)
.animation(.default, value: moveDistance)
Spacer()
}
}
}
}
function animation 的宣告如下
func animation<V>(_ animation: Animation?, value: V) -> some View where V : Equatable
參數 animation 控制動畫的效果,在此我們傳入 .default 產生預設的動畫效果。value 則代表觸發動畫的內容,當 value 改變時將觸發動畫。在剛剛的程式我們呼叫兩次 animation,value 分別傳入 opacity & moveDistance,因此 opacity & moveDistance 改變時都會觸發動畫,造成點選 button 時圖片以動畫移動和改變透明度。
接著讓我們看一個有問題的例子。以下程式將不會產生動畫,因為 animation 的參數 value 傳入 opacity,但是點選 button 時只有改變 moveDistance,並沒有改變 opacity,因此不會觸發動畫。
struct ContentView: View {
@State private var moveDistance: Double = 0
@State private var opacity: Double = 1
var body: some View {
VStack {
Button("奔跑吧,Peter") {
moveDistance += 100
}
.font(.title)
HStack {
Image("peter")
.offset(x: moveDistance)
.opacity(opacity)
.animation(.default, value: opacity)
Spacer()
}
}
}
}
指定不同的動畫效果
剛剛的 .default 採用預設的動畫效果,身為創意無限的 iOS App 魔法師,我們常想做一些客製的動畫效果。
想做客製的動畫當然 ok 呀,剛剛從 Image 呼叫 function animation 時,我們可傳入 Animation 型別的東西指定動畫效果。從下圖的自動完成選單,我們看到可選擇的 Animation 超多的。
以下是幾種常見的 Animation 介紹
- easeOut
動畫的速度一開始快,然後愈來愈慢。
Image(.peter)
.offset(x: moveDistance)
.opacity(opacity)
.animation(.easeOut, value: moveDistance)
.animation(.easeOut, value: opacity)
- easeIn
動畫速度一開始慢,然後愈來愈快。
.animation(.easeIn, value: moveDistance)
.animation(.easeIn, value: opacity)
- easeInOut
動畫速度一開始慢,然後愈來愈快,最後再愈來愈慢。
.animation(.easeInOut, value: moveDistance)
.animation(.easeInOut, value: opacity)
- linear
動畫維持固定的速度。
.animation(.linear, value: moveDistance)
.animation(.linear, value: opacity)
- spring,smooth,snappy,bouncy
類似彈簧的動畫效果 ,它將像彈簧來回振動般變成最後的畫面,比方從座標 10 移動到 100,它將先移動到超過 100,然後再振動回小於 100,然後不斷來回振動直到抵達 100 的位置。
新版寫法。
參數 bounce 控制振動的次數,它的範圍是 0 ~ 1,數字愈大將振動愈多次。
.animation(.spring(bounce: 0.9), value: moveDistance)
.animation(.spring(bounce: 0.9), value: opacity)
SwiftUI 也提供一些預先定義好的 spring 動畫,振盪的次數由小到大依序為 smooth、snappy、bouncy。
.animation(.bouncy, value: moveDistance)
.animation(.bouncy, value: opacity)
舊版寫法。
spring 的參數 dampingFraction 控制振動的次數,它的範圍是 0 ~ 1,數字愈小將振動愈多次。
.animation(.spring(dampingFraction: 0.1), value: moveDistance)
.animation(.spring(dampingFraction: 0.1), value: opacity)
- default
從 iOS 17 開始,default 的效果是 spring,在 iOS 17 之前則是 easeInOut。
.animation(.default, value: moveDistance)
.animation(.default, value: opacity)
調整動畫的時間
我們可透過兩種方法調整動畫的時間。
- 產生 Animation 時傳入參數 duration
spring、smooth、snappy、bouncy、easeIn、easeOut、easeInOut、linear 都可以搭配參數 duration,單位為秒數,比方指定動畫的時間為 10 秒鐘。
Image(.peter)
.offset(x: moveDistance)
.opacity(opacity)
.animation(.easeOut(duration: 10), value: moveDistance)
.animation(.easeOut(duration: 10), value: opacity)
- 呼叫 Animation 的 function speed
speed(0.5) 將讓速度變慢,變成 0.5 倍,因此時間變成原來的 2 倍。相反的,speed(2) 將讓速度加快,變成 2 倍,因此時間變成原本的 0.5 倍。
.animation(.linear.speed(0.5), value: moveDistance)
.animation(.linear.speed(0.5), value: opacity)
設定動畫重覆次數的 repeatCount
透過呼叫 Animation 的 function repeatCount。
struct ContentView: View {
@State private var rotateDegree: Double = 0
var body: some View {
VStack {
Button("轉吧,Peter") {
rotateDegree = 180
}
.font(.title)
Image(.peter)
.rotationEffect(.degrees(rotateDegree))
.animation(
.linear(duration: 5)
.repeatCount(3, autoreverses: false),
value: rotateDegree
)
}
}
}
function repeatCount 的宣告以下,參數 repeatCount 控制動畫重覆的次數,autoreverses 控制是否會以動畫反轉回原來狀態再繼續下一次的動畫。
public func repeatCount(_ repeatCount: Int, autoreverses: Bool = true) -> Animation
如下圖所示,當我們呼叫 animation(.linear(duration: 5).repeatCount(3, autoreverses: false))
時,圖片 Peter 將花 5 秒旋轉 180 度,然後再瞬間轉回 0 度,接著繼續 5 秒轉 180 度,然後再瞬間轉回 0 度,最後再做一次 5 秒轉 180 度,完成我們要求的 3 次動畫。
當參數 autoreverses 傳入 true 時,將有完全不同的效果。此時圖片 Peter 將花 5 秒旋轉 180 度,接著再花 5 秒轉回一開始的 0 度,最後再花 5 秒旋轉 180 度。由於回到原來狀態也算一次動畫,所以這樣已經算完成 3 次的動畫。
Image(.peter)
.rotationEffect(.degrees(rotateDegree))
.animation(
.linear(duration: 5)
.repeatCount(3, autoreverses: true),
value: rotateDegree
)
了解 repeatCount 的原理後,我們將可輕易實現 App 常見的轉圈動畫,以下程式在按下按鈕時將 rotateDegree 設為 360,讓 peter 轉了華麗的 3 圈。
struct ContentView: View {
@State private var rotateDegree: Double = 0
var body: some View {
VStack {
Button("轉吧,Peter") {
rotateDegree = 360
}
.font(.title)
Image(.peter)
.rotationEffect(.degrees(rotateDegree))
.animation(
.linear(duration: 5)
.repeatCount(3, autoreverses: false),
value: rotateDegree
)
}
}
}
記得動畫的速度要設為 linear,旋轉才會順暢。若是設定其它動畫,比方 easeInOut,將發現圖片似乎體力不太好,轉完一圈後會停頓一下下再轉下一圈。
無限重覆動畫的 repeatForever,以無止盡的旋轉為例
有些特別的動畫我們會希望它永遠不要停止,就像我們希望好看的影集 the big bang theory 永遠不要完結篇一樣。
可惜現實世界的影集總有結束的一天,但在 SwiftUI 的世界,我們可以呼叫 repeatForever 讓動畫永不停止。
以下程式將讓 peter 無止盡地旋轉。
struct ContentView: View {
@State private var rotateDegree: Double = 0
var body: some View {
VStack {
Button("轉吧,Peter") {
rotateDegree = 360
}
.font(.title)
Image(.peter)
.rotationEffect(.degrees(rotateDegree))
.animation(
.linear(duration: 5)
.repeatForever(autoreverses: false),
value: rotateDegree
)
}
}
}
畫面出現時開始動畫的 onAppear
我們可以在 onAppear 裡觸發動畫,讓動畫在畫面出現時自動開始表演。以下例子我們將 rotateDegree 設為 360 度,圖片將在畫面出現時自動開始旋轉。
struct ContentView: View {
@State private var rotateDegree: Double = 0
var body: some View {
Image(.peter)
.rotationEffect(.degrees(rotateDegree))
.animation(
.linear(duration: 5)
.repeatForever(autoreverses: false),
value: rotateDegree
)
.onAppear {
rotateDegree = 360
}
}
}
延遲動畫的 delay
呼叫 Animation 的 delay function,讓動畫幾秒後再開始。
struct ContentView: View {
@State private var rotateDegree: Double = 0
var body: some View {
Image(.peter)
.rotationEffect(.degrees(rotateDegree))
.animation(
.linear(duration: 5)
.repeatForever(autoreverses: false)
.delay(5),
value: rotateDegree
)
.onAppear {
rotateDegree = 360
}
}
}
實現動畫的三種方法
SwiftUI 實現動畫的方法不只一種,以下三種方法都可以實現動畫。接下來我們再來認識一下其它兩種方法。
- 從元件呼叫 function animation 加入動畫。
- 利用 function withAnimation 加入動畫。
- 將 animation 加在 Binding 變數上。
方法2: 利用 function withAnimation 加入動畫
在 withAnimation 的參數 body 裡傳入 closure,描述我們想要的改變, SwifUI 將聰明地以動畫呈現畫面的改變。
struct ContentView: View {
@State private var moveDistance: Double = 0
@State private var opacity: Double = 1
var body: some View {
VStack {
Button("奔跑吧,Peter") {
withAnimation {
moveDistance += 100
opacity -= 0.3
}
}
.font(.title)
HStack {
Image(.peter)
.offset(x: moveDistance)
.opacity(opacity)
Spacer()
}
}
}
}
function withAnimation 的宣告以下,剛剛的程式我們省略第一個參數 animation,因此它將採用預設的動畫效果。
public func withAnimation<Result>(_ animation: Animation? = .default, _ body: () throws -> Result) rethrows -> Result
呼叫 withAnimation 時,我們也可以在參數 animation 傳入想要的 Animation。
Button("奔跑吧,Peter") {
withAnimation(.easeInOut(duration: 10)) {
moveDistance += 100
opacity -= 0.3
}
}
方法3: 將 animation 加在 Binding 變數上
我們可以從 Binding 型別的變數呼叫 animation,當它的內容改變時,畫面的更新將以動畫呈現。
比方我們將 Button 包裝成 RunButton,在裡面以 @Binding 宣告 moveDistance & opacity。
struct RunButton: View {
@Binding var moveDistance: Double
@Binding var opacity: Double
var body: some View {
Button("奔跑吧,Peter") {
moveDistance += 100
opacity -= 0.3
}
.font(.title)
}
}
在傳入加了 $ 的 Binding 變數時,我們額外呼叫 animation(),之後當變數內容改變觸發畫面更新時將產生動畫。
struct ContentView: View {
@State private var moveDistance: Double = 0
@State private var opacity: Double = 1
var body: some View {
VStack {
RunButton(moveDistance: $moveDistance.animation(), opacity: $opacity.animation())
HStack {
Image("peter")
.offset(x: moveDistance)
.opacity(opacity)
Spacer()
}
}
}
}
如下圖所示,從 $moveDistance 呼叫 animation() 將回傳 Binding<Double>,雖然它的型別跟原來的 $moveDistance 一模一樣,但它已經有很大變化,它是有動畫功能的升級版。
同樣的,我們也可以客製動畫,我們可以在呼叫 animation() 時傳入指定的 Animation。
RunButton(moveDistance: $moveDistance.animation(.easeInOut(duration: 10)), opacity: $opacity.animation(.easeInOut(duration: 10)))