SwiftUI 中創(chuàng)建反彈動畫的實現(xiàn)
SwiftUI 中的動畫
在寫動畫之前呢先簡單回顧一下 SwiftUI 中動畫的幾個要點:
- 動畫是 view 發(fā)生變化時的漸變效果
- SwiftUI 動畫分為隱式動畫( .animation() )與顯式動畫( withAnimation() )兩種
- 隱式動畫是給 view 加動畫,view 所有的能動畫的變化都能被隱式動畫影響
- 顯式動畫是針對某個變化進(jìn)行動畫,能精準(zhǔn)控制。
- view 的插入和移除通過過渡( transition )來做效果,可以組合多個過渡或自定義過渡
- 要構(gòu)建自定義動畫,我們需要實現(xiàn)一個可動畫的 view 修飾器(遵守 AnimatableModier 協(xié)議)或者實現(xiàn)一個 GeometryEffect ,并將可動畫的屬性通過 animatableData 暴露出來
反彈動畫
反彈動畫屬于“起始點和終止點相等”的動畫,所以不能夠通過 SwiftUI 中內(nèi)建的動畫來實現(xiàn)(因為這個 view 從結(jié)果來看沒有發(fā)生變化)
我們先來構(gòu)建反彈動畫修飾器的框架如下:
struct Bounce: AnimatableModifier {
var animCount: CGFloat = 0
var amplitude: CGFloat = 10 // 振幅
var animatableData: CGFloat {
get { animCount }
set { animCount = newValue }
}
func body(content: Content) -> some View {
// change view to animate
}
}
下面一步一步來
實現(xiàn)動畫曲線
實現(xiàn) body 方法,好讓 times 每次增加時 view 能以反彈的方式進(jìn)行動畫。一次反彈就是 view 向上彈起又落下,下面是動畫曲線大致的樣子:

三角函數(shù)是 y = -abs(sin x) ,所以 body 方法這樣實現(xiàn):
struct Bounce: AnimatableModifier {
...
func body(content: Content) -> some View {
let offset: CGFloat = -abs(sin(animCount * .pi * 2) * amplitude)
return content.offset(y: offset)
}
}
測試代碼:
struct BouncingView: View {
@State var taps = 0
var body: some View {
Button(action: {
withAnimation(Animation.easeInOut(duration: 1)) {
taps += 1
}
}, label: {
RoundedRectangle(cornerRadius: 15)
.modifier(Bounce(animCount: CGFloat(taps)))
})
.frame(width: 100, height: 100)
}
}
點擊按鈕,就能彈兩次了~~
這個 @State var taps
其實并沒有什么實際的意義,只是為了觸發(fā)動畫。
因為 SwiftUI 里只有 View 的狀態(tài)發(fā)生變化才會觸發(fā)動畫,而無法通過某個事件來觸發(fā);而我們的動畫是一個初始狀態(tài)和結(jié)束狀態(tài)相等的情況,并沒有狀態(tài)的變化,所以這里強(qiáng)行把點擊的次數(shù)作為 View 狀態(tài)的變化來觸發(fā)動畫。
我找了好久有沒有更優(yōu)雅的方式來解決這個問題,然而并沒有找到 = =b
View 擴(kuò)展
暴露給外面的 animCount 應(yīng)該是一個 Int 才對,那么就增加一個 animValue 來代替它
struct Bounce: AnimatableModifier {
let animCount: Int
var animValue: CGFloat
var amplitude: CGFloat = 10 // 振幅
init(animCount: Int) {
self.animCount = animCount
self.animValue = CGFloat(animCount)
}
var animatableData: CGFloat {
get { animValue }
set { animValue = newValue }
}
func body(content: Content) -> some View {
let offset: CGFloat = -abs(sin(animValue * .pi * 2) * amplitude)
return content.offset(y: offset)
}
}
因為 animValue 被隱藏起來了,所以需要初始化方法
為了方便使用,再添加一個 View 的擴(kuò)展方法:
extension View {
func bounce(animCount: Int) -> some View {
self.modifier(Bounce(animCount: animCount))
}
}
增加彈性
現(xiàn)在雖然能彈了,但是相對還比較生硬,就想在 View “落地“后再給它增加振幅逐步衰減的幾次反彈;一開始嘗試了簡單的減半反彈,實驗證明觀感更差,看起來有點難受。
我們實際生活中的反彈的振幅變化應(yīng)該是符合 阻尼正弦波 的,參考鏈接里的公式,修改如下:
struct Bounce: AnimatableModifier {
let animCount: Int
var animValue: CGFloat
var amplitude: CGFloat // 振幅
var bouncingTimes: Int
init(animCount: Int, amplitude: CGFloat = 10, bouncingTimes: Int = 3) {
self.animCount = animCount
self.animValue = CGFloat(animCount)
self.amplitude = amplitude
self.bouncingTimes = bouncingTimes
}
var animatableData: CGFloat {
get { animValue }
set { animValue = newValue }
}
func body(content: Content) -> some View {
let t = animValue - CGFloat(animCount)
let offset: CGFloat = -abs(pow(CGFloat(M_E), -t) * sin(t * .pi * CGFloat(bouncingTimes)) * amplitude)
return content.offset(y: offset)
}
}
extension View {
func bounce(animCount: Int,
amplitude: CGFloat = 10,
bouncingTimes: Int = 3) -> some View {
self.modifier(Bounce(animCount: animCount,
amplitude: amplitude,
bouncingTimes: bouncingTimes))
}
}
這里我們還增加了 bouncingTimes 作為彈跳次數(shù)的參數(shù),振幅也作為參數(shù)開放給用戶;
由于阻尼正弦波是逐步衰減的,為了每次點擊的彈跳都一樣,所以得用 animValue - CGFloat(animCount) 作為阻尼正弦波函數(shù)的參數(shù)。
測試代碼修改如下:
struct BouncingView: View {
@State var taps = 0
var body: some View {
Button(action: {
withAnimation(Animation.linear(duration: 1)) {
taps += 1
}
}, label: {
RoundedRectangle(cornerRadius: 15)
.bounce(animCount: taps)
})
.frame(width: 100, height: 100)
}
}
實際效果如下:

到此這篇關(guān)于SwiftUI 中創(chuàng)建反彈動畫的實現(xiàn)的文章就介紹到這了,更多相關(guān)SwiftUI 反彈動畫內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
swift中利用runtime交換方法的實現(xiàn)示例
這篇文章主要給大家介紹了關(guān)于swift中利用runtime交換方法的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。2018-05-05
Swift解決UITableView空數(shù)據(jù)視圖問題的簡單方法
這篇文章主要給大家介紹了關(guān)于Swift解決UITableView空數(shù)據(jù)視圖問題的簡單方法,文中通過示例代碼介紹的非常詳細(xì),對大家學(xué)習(xí)或者使用swift具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2018-10-10

