歡迎您光臨本站 註冊首頁

SwiftUI使用Paths和AnimatableData實現酷炫的顏色切換動畫

←手機掃碼閱讀     niceskyabc @ 2020-05-11 , reply:0

本文中我們將學習如何使用 SwiftUI 中的 Paths 和 AnimatableData 來製作顏色切換動畫。
這些快速切換的動畫是怎麼實現的呢?讓我們來看下文吧!
基礎
要實現動畫的關鍵是在 SwiftUI 中建立一個實現 Shape 協議的結構體。我們把它命名為 SplashShape 。在 Shape 協議中,有一個方法叫做 path(in rect: CGRect) -> Path ,這個方法可以用來設定圖形的外觀。我們就用這個方法來實現本文中的各種動畫。
建立 SplashShape 結構體
下面我們建立一個叫做 SplashStruct 的結構體,它繼承於 Shape 協議。
import SwiftUI struct SplashShape: Shape { func path(in rect: CGRect) -> Path { return Path() } }
我們首先建立兩種動畫型別: leftToRight 和 rightToLeft ,效果如下所示:
Splash 動畫
我們建立一個名為 SplashAnimation 的 列舉 來定義動畫型別,便於以後更方便地擴充套件新動畫(文章末尾可以驗證!)。
import SwiftUI struct SplashShape: Shape { public enum SplashAnimation { case leftToRight case rightToleft } func path(in rect: CGRect) -> Path { return Path() } }
在 path() 方法中,我們可以選擇需要使用的動畫,並且返回動畫的 Path 。但是首先,我們必須建立變數來儲存動畫型別,記錄動畫過程。
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() } }
現在這個方法只會返回空 paths。我們需要建立產生真實動畫的方法。
實現動畫方法
在 path() 方法的下面,建立兩個新的方法: leftToRight() 和 rightToLeft() ,每個方法表示一種動畫型別。在每個方法體內,我們會建立一個矩形形狀的 Path ,它會根據 progress 變數的值隨時間發生變換。
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() 方法中呼叫上面兩個新方法。
func path(in rect: CGRect) -> Path { switch animationType { case .leftToRight: return leftToRight(rect: rect) case .rightToLeft: return rightToLeft(rect: rect) } }
動畫資料
為了確保 Swift 知道在更改 progress 變數時如何對 Shape 進行動畫處理,我們需要指定一個響應動畫的變數。在 progress 和 animationType 變數下面,定義 animatableData 。這是一個基於 Animatable 協議 的變數,它可以通知 SwiftUI 在資料改變時,對檢視進行動畫處理。
var progress: CGFloat var animationType: SplashAnimation var animatableData: CGFloat { get { return progress } set { self.progress = newValue} }
顏色切換時產生動畫
到目前為止,我們已經建立了一個 Shape ,它將隨著時間的變化而變化。接下來,我們需要將它新增到檢視中,並在檢視顏色改變時自動對其進行動畫處理。這時候我們引入 SplashView 。我們將建立一個 SplashView 來自動更新 SplashShape 的 progress 變數。當 SplashView 接收到新的 Color 時,它將觸發動畫。
首先,我們建立 SplashView 結構體。
import SwiftUI struct SplashView: View { var body: some View { // SplashShape Here } }
SplashShape 需要使用 SplashAnimation 列舉作為引數,所以我們會把它作為引數傳遞給 SplashView 。另外,我們要在檢視的背景顏色變化時設定動畫,所以我們也要傳遞 Color 引數。這些細節會在我們的初始化方法中詳細說明。
ColorStore 是自定義的 ObservableObject。它用來監聽 SplashView 結構體中 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

(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 } }
構建 SplashView body
在 body 內部,我們需要返回一個 Rectangle ,它和 SplashView 當前的顏色保持一致。然後使用之前定義的 ColorStore ,以便於我們接收更新的顏色值來驅動動畫。
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
現在回到 body 變數內部,我們給 layers 變數新增新接收的 Colors 。新增的時候我們把進度設定為 0 。然後,在半秒之內的動畫過程中,我們把進度設定為 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 } } }
現在在這段程式碼中, layers 變數中添加了更新後的顏色,但是顏色並沒有展示出來。為了展示顏色,我們需要在 body 變數內部為 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 中的顏色,同時也會觸發 ColorStore 內部的更新。所以,當 SplashShape 圖層新增到 SplashView 時,就會觸發動畫。
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) } } }
還沒有完成!
我們還有一個功能沒實現。現在我們持續地把圖層新增到 SplashView 上,但是沒有刪除它們。因此,我們需要在動畫完成時把這些圖層清理掉。
在 SplashView 結構體 body 變數的 onReceive() 方法內部,做如下改變:
.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 陣列中使用過的值,並確保 SplashView 基於最新更新的值顯示正確的背景色。
展示成果!
GitHub 原始碼
您可以在我的 Github 上檢視本教程的原始碼 !除了顯示的示例外,還包括 SplashShape 和 SplashView 的完整原始碼。 ....但是等等,還有更多!
彩蛋!
如果你熟悉我之前的教程,你應該瞭解我喜歡彩蛋 :wink:。在本文開頭,我說過會實現更多動畫。此刻終於來了…… 擊鼓 ……。
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 的底部或頂部開始建立 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 動畫
如果你還記得小學幾何知識,就應該瞭解勾股定理。 a^2 + b^2 = c^2
a 和 b 可以視為矩形的 高度 和 寬度 ,我們能夠根據它們求得 c ,即覆蓋整個矩形所需的圓的半徑。我們以此為基礎構建圓的 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 動畫
這個動畫知識點有點多。你需要使用切線計算角度的斜率,然後根據這個斜率建立一條直線。在矩形上移動這條直線時,根據它來繪製一個直角三角形。參見下圖,各種彩色的線表示該線隨時間移動時,覆蓋整個矩形的狀態。
方法如下:
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 }


[niceskyabc ] SwiftUI使用Paths和AnimatableData實現酷炫的顏色切換動畫已經有115次圍觀

http://coctec.com/docs/program/show-post-233773.html