SwiftUI 中創(chuàng)建反彈動(dòng)畫(huà)的實(shí)現(xiàn)
SwiftUI 中的動(dòng)畫(huà)
在寫(xiě)動(dòng)畫(huà)之前呢先簡(jiǎn)單回顧一下 SwiftUI 中動(dòng)畫(huà)的幾個(gè)要點(diǎn):
- 動(dòng)畫(huà)是 view 發(fā)生變化時(shí)的漸變效果
- SwiftUI 動(dòng)畫(huà)分為隱式動(dòng)畫(huà)( .animation() )與顯式動(dòng)畫(huà)( withAnimation() )兩種
- 隱式動(dòng)畫(huà)是給 view 加動(dòng)畫(huà),view 所有的能動(dòng)畫(huà)的變化都能被隱式動(dòng)畫(huà)影響
- 顯式動(dòng)畫(huà)是針對(duì)某個(gè)變化進(jìn)行動(dòng)畫(huà),能精準(zhǔn)控制。
- view 的插入和移除通過(guò)過(guò)渡( transition )來(lái)做效果,可以組合多個(gè)過(guò)渡或自定義過(guò)渡
- 要構(gòu)建自定義動(dòng)畫(huà),我們需要實(shí)現(xiàn)一個(gè)可動(dòng)畫(huà)的 view 修飾器(遵守 AnimatableModier 協(xié)議)或者實(shí)現(xiàn)一個(gè) GeometryEffect ,并將可動(dòng)畫(huà)的屬性通過(guò) animatableData 暴露出來(lái)
反彈動(dòng)畫(huà)
反彈動(dòng)畫(huà)屬于“起始點(diǎn)和終止點(diǎn)相等”的動(dòng)畫(huà),所以不能夠通過(guò) SwiftUI 中內(nèi)建的動(dòng)畫(huà)來(lái)實(shí)現(xiàn)(因?yàn)檫@個(gè) view 從結(jié)果來(lái)看沒(méi)有發(fā)生變化)
我們先來(lái)構(gòu)建反彈動(dòng)畫(huà)修飾器的框架如下:
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 } }
下面一步一步來(lái)
實(shí)現(xiàn)動(dòng)畫(huà)曲線
實(shí)現(xiàn) body 方法,好讓 times 每次增加時(shí) view 能以反彈的方式進(jìn)行動(dòng)畫(huà)。一次反彈就是 view 向上彈起又落下,下面是動(dòng)畫(huà)曲線大致的樣子:
三角函數(shù)是 y = -abs(sin x)
,所以 body 方法這樣實(shí)現(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) } }
測(cè)試代碼:
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) } }
點(diǎn)擊按鈕,就能彈兩次了~~
這個(gè) @State var taps
其實(shí)并沒(méi)有什么實(shí)際的意義,只是為了觸發(fā)動(dòng)畫(huà)。
因?yàn)?SwiftUI 里只有 View 的狀態(tài)發(fā)生變化才會(huì)觸發(fā)動(dòng)畫(huà),而無(wú)法通過(guò)某個(gè)事件來(lái)觸發(fā);而我們的動(dòng)畫(huà)是一個(gè)初始狀態(tài)和結(jié)束狀態(tài)相等的情況,并沒(méi)有狀態(tài)的變化,所以這里強(qiáng)行把點(diǎn)擊的次數(shù)作為 View 狀態(tài)的變化來(lái)觸發(fā)動(dòng)畫(huà)。
我找了好久有沒(méi)有更優(yōu)雅的方式來(lái)解決這個(gè)問(wèn)題,然而并沒(méi)有找到 = =b
View 擴(kuò)展
暴露給外面的 animCount
應(yīng)該是一個(gè) Int 才對(duì),那么就增加一個(gè) animValue
來(lái)代替它
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) } }
因?yàn)? animValue
被隱藏起來(lái)了,所以需要初始化方法
為了方便使用,再添加一個(gè) View 的擴(kuò)展方法:
extension View { func bounce(animCount: Int) -> some View { self.modifier(Bounce(animCount: animCount)) } }
增加彈性
現(xiàn)在雖然能彈了,但是相對(duì)還比較生硬,就想在 View “落地“后再給它增加振幅逐步衰減的幾次反彈;一開(kāi)始嘗試了簡(jiǎn)單的減半反彈,實(shí)驗(yàn)證明觀感更差,看起來(lái)有點(diǎn)難受。
我們實(shí)際生活中的反彈的振幅變化應(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ù)開(kāi)放給用戶;
由于阻尼正弦波是逐步衰減的,為了每次點(diǎn)擊的彈跳都一樣,所以得用 animValue - CGFloat(animCount)
作為阻尼正弦波函數(shù)的參數(shù)。
測(cè)試代碼修改如下:
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) } }
實(shí)際效果如下:
到此這篇關(guān)于SwiftUI 中創(chuàng)建反彈動(dòng)畫(huà)的實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)SwiftUI 反彈動(dòng)畫(huà)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
swift中利用runtime交換方法的實(shí)現(xiàn)示例
這篇文章主要給大家介紹了關(guān)于swift中利用runtime交換方法的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧。2018-05-05iOS Swift讀取本地json文件報(bào)錯(cuò)的解決方法
只要是app開(kāi)發(fā)者都知道,從服務(wù)器端獲得的數(shù)據(jù)要不就是json格式的數(shù)據(jù),要么就是xml格式的數(shù)據(jù),而這篇文章主要給大家介紹了關(guān)于iOS Swift讀取本地json文件報(bào)錯(cuò)的解決方法,需要的朋友可以參考借鑒,下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧。2017-11-11Swift流程控制之循環(huán)語(yǔ)句和判斷語(yǔ)句詳解
這篇文章主要介紹了Swift流程控制之循環(huán)語(yǔ)句和判斷語(yǔ)句詳解,文中還使用了一個(gè)游戲例子來(lái)講解循環(huán)語(yǔ)句和判斷語(yǔ)句的使用,需要的朋友可以參考下2014-07-07倉(cāng)庫(kù)模式及其在Swift?項(xiàng)目中的應(yīng)用詳解
這篇文章主要為大家介紹了倉(cāng)庫(kù)模式及其在Swift?項(xiàng)目中的應(yīng)用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01Swift解決UITableView空數(shù)據(jù)視圖問(wèn)題的簡(jiǎn)單方法
這篇文章主要給大家介紹了關(guān)于Swift解決UITableView空數(shù)據(jù)視圖問(wèn)題的簡(jiǎn)單方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用swift具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2018-10-10詳解Swift編程中的for循環(huán)的編寫(xiě)方法
這篇文章主要介紹了Swift編程中的for循環(huán)的編寫(xiě)方法,包括相關(guān)的for...in循環(huán),需要的朋友可以參考下2015-11-11