SwiftUI使用Paths和AnimatableData實現(xiàn)酷炫的顏色切換動畫
老鐵們,是時候燥起來了!本文中我們將學(xué)習(xí)如何使用 SwiftUI 中的 Paths 和 AnimatableData 來制作顏色切換動畫。

這些快速切換的動畫是怎么實現(xiàn)的呢?讓我們來看下文吧!
基礎(chǔ)
要實現(xiàn)動畫的關(guān)鍵是在 SwiftUI 中創(chuàng)建一個實現(xiàn) Shape 協(xié)議的結(jié)構(gòu)體。我們把它命名為 SplashShape 。在 Shape 協(xié)議中,有一個方法叫做 path(in rect: CGRect) -> Path ,這個方法可以用來設(shè)置圖形的外觀。我們就用這個方法來實現(xiàn)本文中的各種動畫。
創(chuàng)建 SplashShape 結(jié)構(gòu)體
下面我們創(chuàng)建一個叫做 SplashStruct 的結(jié)構(gòu)體,它繼承于 Shape 協(xié)議。
import SwiftUI
struct SplashShape: Shape {
func path(in rect: CGRect) -> Path {
return Path()
}
}
我們首先創(chuàng)建兩種動畫類型: leftToRight 和 rightToLeft ,效果如下所示:
Splash 動畫
我們創(chuàng)建一個名為 SplashAnimation 的 枚舉 來定義動畫類型,便于以后更方便地擴展新動畫(文章末尾可以驗證?。?。
import SwiftUI
struct SplashShape: Shape {
public enum SplashAnimation {
case leftToRight
case rightToleft
}
func path(in rect: CGRect) -> Path {
return Path()
}
}
在 path() 方法中,我們可以選擇需要使用的動畫,并且返回動畫的 Path 。但是首先,我們必須創(chuàng)建變量來存儲動畫類型,記錄動畫過程。
import SwiftUI
struct SplashShape: Shape {
public enum SplashAnimation {
case leftToRight
case rightToleft
}
var progress: CGFloat
var animationType: SplashAnimation
func path(in rect: CGRect) -> Path {
return Path()
}
}
progress 的取值范圍在 0 和 1 之間,它代表整個動畫的完成進度。當我們編寫 path() 方法時,它就會派上用場。
編寫 path() 方法
跟之前說的一樣,為了返回正確的 Path ,我們需要明確正在使用哪一種動畫。在 path() 方法中編寫 switch 語句,并且用上我們之前定義的 animationType 。
func path(in rect: CGRect) -> Path {
switch animationType {
case .leftToRight:
return Path()
case .rightToLeft:
return Path()
}
}
現(xiàn)在這個方法只會返回空 paths。我們需要創(chuàng)建產(chǎn)生真實動畫的方法。
實現(xiàn)動畫方法
在 path() 方法的下面,創(chuàng)建兩個新的方法: leftToRight() 和 rightToLeft() ,每個方法表示一種動畫類型。在每個方法體內(nèi),我們會創(chuàng)建一個矩形形狀的 Path ,它會根據(jù) progress 變量的值隨時間發(fā)生變換。
func leftToRight(rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: 0, y: 0)) // Top Left
path.addLine(to: CGPoint(x: rect.width * progress, y: 0)) // Top Right
path.addLine(to: CGPoint(x: rect.width * progress, y: rect.height)) // Bottom Right
path.addLine(to: CGPoint(x: 0, y: rect.height)) // Bottom Left
path.closeSubpath() // Close the Path
return path
}
func rightToLeft(rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: rect.width, y: 0))
path.addLine(to: CGPoint(x: rect.width - (rect.width * progress), y: 0))
path.addLine(to: CGPoint(x: rect.width - (rect.width * progress), y: rect.height))
path.addLine(to: CGPoint(x: rect.width, y: rect.height))
path.closeSubpath()
return path
}
然后在 path() 方法中調(diào)用上面兩個新方法。
func path(in rect: CGRect) -> Path {
switch animationType {
case .leftToRight:
return leftToRight(rect: rect)
case .rightToLeft:
return rightToLeft(rect: rect)
}
}
動畫數(shù)據(jù)
為了確保 Swift 知道在更改 progress 變量時如何對 Shape 進行動畫處理,我們需要指定一個響應(yīng)動畫的變量。在 progress 和 animationType 變量下面,定義 animatableData 。這是一個基于 Animatable 協(xié)議 的變量,它可以通知 SwiftUI 在數(shù)據(jù)改變時,對視圖進行動畫處理。
var progress: CGFloat
var animationType: SplashAnimation
var animatableData: CGFloat {
get { return progress }
set { self.progress = newValue}
}
顏色切換時產(chǎn)生動畫
到目前為止,我們已經(jīng)創(chuàng)建了一個 Shape ,它將隨著時間的變化而變化。接下來,我們需要將它添加到視圖中,并在視圖顏色改變時自動對其進行動畫處理。這時候我們引入 SplashView 。我們將創(chuàng)建一個 SplashView 來自動更新 SplashShape 的 progress 變量。當 SplashView 接收到新的 Color 時,它將觸發(fā)動畫。
首先,我們創(chuàng)建 SplashView 結(jié)構(gòu)體。
import SwiftUI
struct SplashView: View {
var body: some View {
// SplashShape Here
}
}
SplashShape 需要使用 SplashAnimation 枚舉作為參數(shù),所以我們會把它作為參數(shù)傳遞給 SplashView 。另外,我們要在視圖的背景顏色變化時設(shè)置動畫,所以我們也要傳遞 Color 參數(shù)。這些細節(jié)會在我們的初始化方法中詳細說明。
ColorStore 是自定義的 ObservableObject。它用來監(jiān)聽 SplashView 結(jié)構(gòu)體中 Color 值的改變,以便我們可以初始化 SplashShape 動畫,并最終改變背景顏色。我們稍后展示它的工作原理。
struct SplashView: View {
var animationType: SplashShape.SplashAnimation
@State private var prevColor: Color // Stores background color
@ObservedObject var colorStore: ColorStore // Send new color updates
init(animationType: SplashShape.SplashAnimation, color: Color) {
self.animationType = animationType
self._prevColor = State<Color>(initialValue: color)
self.colorStore = ColorStore(color: color)
}
var body: some View {
// SplashShape Here
}
}
class ColorStore: ObservableObject {
@Published var color: Color
init(color: Color) {
self.color = color
}
}
構(gòu)建 SplashView body
在 body 內(nèi)部,我們需要返回一個 Rectangle ,它和 SplashView 當前的顏色保持一致。然后使用之前定義的 ColorStore ,以便于我們接收更新的顏色值來驅(qū)動動畫。
var body: some View {
Rectangle()
.foregroundColor(self.prevColor) // Current Color
.onReceive(self.colorStore.$color) { color in
// Animate Color Update Here
}
}
當顏色改變時,我們需要記錄 SplashView 中正在改變的顏色和進度。為此,我們定義 layers 變量。
@State var layers: [(Color,CGFloat)] = [] // New Color & Progress
現(xiàn)在回到 body 變量內(nèi)部,我們給 layers 變量添加新接收的 Colors 。添加的時候我們把進度設(shè)置為 0 。然后,在半秒之內(nèi)的動畫過程中,我們把進度設(shè)置為 1 。
var body: some View {
Rectangle()
.foregroundColor(self.prevColor) // Current Color
.onReceive(self.colorStore.$color) { color in
// Animate Color Update Here
self.layers.append((color, 0))
withAnimation(.easeInOut(duration: 0.5)) {
self.layers[self.layers.count-1].1 = 1.0
}
}
}
現(xiàn)在在這段代碼中, layers 變量中添加了更新后的顏色,但是顏色并沒有展示出來。為了展示顏色,我們需要在 body 變量內(nèi)部為 Rectangle 的每一個圖層添加一個覆蓋層。
var body: some View {
Rectangle()
.foregroundColor(self.prevColor)
.overlay(
ZStack {
ForEach(layers.indices, id: \.self) { x in
SplashShape(progress: self.layers[x].1, animationType: self.animationType)
.foregroundColor(self.layers[x].0)
}
}
, alignment: .leading)
.onReceive(self.colorStore.$color) { color in
// Animate color update here
self.layers.append((color, 0))
withAnimation(.easeInOut(duration: 0.5)) {
self.layers[self.layers.count-1].1 = 1.0
}
}
}
測試效果
你可以在模擬器中運行下面的代碼。這段代碼的意思是,當你點擊 ContentView 中的按鈕時,它會計算 index 來選擇 SplashView 中的顏色,同時也會觸發(fā) ColorStore 內(nèi)部的更新。所以,當 SplashShape 圖層添加到 SplashView 時,就會觸發(fā)動畫。
import SwiftUI
struct ContentView: View {
var colors: [Color] = [.blue, .red, .green, .orange]
@State var index: Int = 0
@State var progress: CGFloat = 0
var body: some View {
VStack {
SplashView(animationType: .leftToRight, color: self.colors[self.index])
.frame(width: 200, height: 100, alignment: .center)
.cornerRadius(10)
.shadow(color: Color.black.opacity(0.2), radius: 10, x: 0, y: 4)
Button(action: {
self.index = (self.index + 1) % self.colors.count
}) {
Text("Change Color")
}
.padding(.top, 20)
}
}
}
還沒有完成!
我們還有一個功能沒實現(xiàn)。現(xiàn)在我們持續(xù)地把圖層添加到 SplashView 上,但是沒有刪除它們。因此,我們需要在動畫完成時把這些圖層清理掉。
在 SplashView 結(jié)構(gòu)體 body 變量的 onReceive() 方法內(nèi)部,做如下改變:
.onReceive(self.colorStore.$color) { color in
self.layers.append((color, 0))
withAnimation(.easeInOut(duration: 0.5)) {
self.layers[self.layers.count-1].1 = 1.0
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.prevColor = self.layers[0].0 // Finalizes background color of SplashView
self.layers.remove(at: 0) // removes itself from layers array
}
}
}
這行代碼能讓我們刪除 layers 數(shù)組中使用過的值,并確保 SplashView 基于最新更新的值顯示正確的背景色。
展示成果!
GitHub 源碼
您可以在我的 Github 上查看本教程的源代碼 !除了顯示的示例外,還包括 SplashShape 和 SplashView 的完整源代碼。 ....但是等等,還有更多!
彩蛋!
如果你熟悉我之前的教程,你應(yīng)該了解我喜歡彩蛋 :wink:。在本文開頭,我說過會實現(xiàn)更多動畫。此刻終于來了…… 擊鼓 ……。
Splash 動畫
哈哈哈?。∵€記得嗎?我說過會添加更多動畫種類。
enum SplashAnimation {
case leftToRight
case rightToLeft
case topToBottom
case bottomToTop
case angle(Angle)
case circle
}
func path(in rect: CGRect) -> Path {
switch self.animationType {
case .leftToRight:
return leftToRight(rect: rect)
case .rightToLeft:
return rightToLeft(rect: rect)
case .topToBottom:
return topToBottom(rect: rect)
case .bottomToTop:
return bottomToTop(rect: rect)
case .angle(let splashAngle):
return angle(rect: rect, angle: splashAngle)
case .circle:
return circle(rect: rect)
}
}
你肯定會想…… “哇, 彩蛋也太多了……” 。不必苦惱。我們只需要在 SplashShape 的 path() 方法中添加幾個方法,就能搞定。
下面我們逐個動畫來搞定……
topToBottom 和 bottomToTop 動畫
這些方法與 leftToRight 和 rightToLeft 非常相似,它們從 shape 的底部或頂部開始創(chuàng)建 path ,并使用 progress 變量隨時間對其進行變換。
func topToBottom(rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: rect.width, y: 0))
path.addLine(to: CGPoint(x: rect.width, y: rect.height * progress))
path.addLine(to: CGPoint(x: 0, y: rect.height * progress))
path.closeSubpath()
return path
}
func bottomToTop(rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: 0, y: rect.height))
path.addLine(to: CGPoint(x: rect.width, y: rect.height))
path.addLine(to: CGPoint(x: rect.width, y: rect.height - (rect.height * progress)))
path.addLine(to: CGPoint(x: 0, y: rect.height - (rect.height * progress)))
path.closeSubpath()
return path
}
circle 動畫
如果你還記得小學(xué)幾何知識,就應(yīng)該了解勾股定理。 a^2 + b^2 = c^2

a 和 b 可以視為矩形的 高度 和 寬度 ,我們能夠根據(jù)它們求得 c ,即覆蓋整個矩形所需的圓的半徑。我們以此為基礎(chǔ)構(gòu)建圓的 path,并使用 progress 變量隨時間對它進行變換。
func circle(rect: CGRect) -> Path {
let a: CGFloat = rect.height / 2.0
let b: CGFloat = rect.width / 2.0
let c = pow(pow(a, 2) + pow(b, 2), 0.5) // a^2 + b^2 = c^2 --> Solved for 'c'
// c = radius of final circle
let radius = c * progress
// Build Circle Path
var path = Path()
path.addArc(center: CGPoint(x: rect.midX, y: rect.midY), radius: radius, startAngle: Angle(degrees: 0), endAngle: Angle(degrees: 360), clockwise: true)
return path
}
angle 動畫
這個動畫知識點有點多。你需要使用切線計算角度的斜率,然后根據(jù)這個斜率創(chuàng)建一條直線。在矩形上移動這條直線時,根據(jù)它來繪制一個直角三角形。參見下圖,各種彩色的線表示該線隨時間移動時,覆蓋整個矩形的狀態(tài)。

方法如下:
func angle(rect: CGRect, angle: Angle) -> Path {
var cAngle = Angle(degrees: angle.degrees.truncatingRemainder(dividingBy: 90))
// Return Path Using Other Animations (topToBottom, leftToRight, etc) if angle is 0, 90, 180, 270
if angle.degrees == 0 || cAngle.degrees == 0 { return leftToRight(rect: rect)}
else if angle.degrees == 90 || cAngle.degrees == 90 { return topToBottom(rect: rect)}
else if angle.degrees == 180 || cAngle.degrees == 180 { return rightToLeft(rect: rect)}
else if angle.degrees == 270 || cAngle.degrees == 270 { return bottomToTop(rect: rect)}
// Calculate Slope of Line and inverse slope
let m = CGFloat(tan(cAngle.radians))
let m_1 = pow(m, -1) * -1
let h = rect.height
let w = rect.width
// tan (angle) = slope of line
// y = mx + b ---> b = y - mx ~ 'b' = y intercept
let b = h - (m_1 * w) // b = y - (m * x)
// X and Y coordinate calculation
var x = b * m * progress
var y = b * progress
// Triangle Offset Calculation
let xOffset = (angle.degrees > 90 && angle.degrees < 270) ? rect.width : 0
let yOffset = (angle.degrees > 180 && angle.degrees < 360) ? rect.height : 0
// Modify which side the triangle is drawn from depending on the angle
if angle.degrees > 90 && angle.degrees < 180 { x *= -1 }
else if angle.degrees > 180 && angle.degrees < 270 { x *= -1; y *= -1 }
else if angle.degrees > 270 && angle.degrees < 360 { y *= -1 }
// Build Triangle Path
var path = Path()
path.move(to: CGPoint(x: xOffset, y: yOffset))
path.addLine(to: CGPoint(x: xOffset + x, y: yOffset))
path.addLine(to: CGPoint(x: xOffset, y: yOffset + y))
path.closeSubpath()
return path
}
總結(jié)
到此這篇關(guān)于SwiftUI使用Paths和AnimatableData實現(xiàn)酷炫的顏色切換動畫的文章就介紹到這了,更多相關(guān)SwiftUI 顏色切換動畫內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
用Swift構(gòu)建一個簡單的iOS郵件應(yīng)用的方法
這篇文章主要介紹了用Swift構(gòu)建一個簡單的iOS郵件應(yīng)用的方法,包括查看和標記已讀等基本的郵件應(yīng)用功能,需要的朋友可以參考下2015-07-07
Swift 使用 Observe 監(jiān)測頁面滾動的實現(xiàn)方法
這篇文章主要介紹了Swift 使用 Observe 監(jiān)測頁面滾動的實現(xiàn)方法,本文通過實例代碼給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-05-05
swift中AnyObject和Any的介紹與區(qū)別詳解
雖然使用swift開發(fā)了一段時間,但是感覺對一些基礎(chǔ)的東西了解不是比較透徹,在查詢了許多資料以后還是打算自己動手記錄一下,下面這篇文章主要給大家介紹了關(guān)于swift中AnyObject和Any的介紹與區(qū)別的相關(guān)資料,需要的朋友可以參考下。2017-12-12

