日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

SwiftUI之深入解析高级动画的路径Paths

發布時間:2024/5/28 编程问答 35 豆豆
生活随笔 收集整理的這篇文章主要介紹了 SwiftUI之深入解析高级动画的路径Paths 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

一、前言

  • 本文將深入探討一些創建 SwiftUI 動畫的高級技術,討論 Animatable 的協議,它可靠的伙伴 AnimatableData,強大但經常被忽略的 GeometryEffect 以及完全被忽視但全能的 AnimatableModifier 協議。
  • 這些都是被官方文檔完全忽略的主題,在 SwiftUI 相關的帖子和文章中也幾乎沒有提及,不過它們還是提供了創建一些相當不錯的動畫的工具。

二、顯式動畫 VS 隱式動畫

① 動畫實現

  • 在 SwiftUI 中,有兩種類型的動畫:顯式和隱式:
    • 隱式動畫是用 .animation() 修飾符指定的那些動畫,每當視圖上的可動畫參數發生變化時,SwiftUI 就會從舊值到新值制作動畫,一些可動畫的參數包括大小(size)、偏移(offset)、顏色(color)、比例(scale)等;
    • 顯式動畫是使用 withAnimation{ … } 指定的動畫閉包,只有那些依賴于 withAnimation 閉包中改變值的參數才會被動畫化。
  • 如下所示,使用隱式動畫更改圖像的大小和不透明度:
struct Example1: View {@State private var half = false@State private var dim = falsevar body: some View {Image("tower").scaleEffect(half ? 0.5 : 1.0).opacity(dim ? 0.2 : 1.0).animation(.easeInOut(duration: 1.0)).onTapGesture {self.dim.toggle()self.half.toggle()}} }
  • 執行效果如下:

  • 如下所示的示例使用顯式動畫,縮放和不透明度都會更改,但只有不透明度會設置動畫,因為它是 withAnimation 閉包中唯一更改的參數:
struct Example2: View {@State private var half = false@State private var dim = falsevar body: some View {Image("tower").scaleEffect(half ? 0.5 : 1.0).opacity(dim ? 0.5 : 1.0).onTapGesture {self.half.toggle()withAnimation(.easeInOut(duration: 1.0)) {self.dim.toggle()}}} }
  • 執行效果如下:

  • 通過更改修飾符的前后順序,可以使用隱式動畫創建相同的效果:
struct Example2: View {@State private var half = false@State private var dim = falsevar body: some View {Image("tower").opacity(dim ? 0.2 : 1.0).animation(.easeInOut(duration: 1.0)).scaleEffect(half ? 0.5 : 1.0).onTapGesture {self.dim.toggle()self.half.toggle()}} }
  • 如果需要禁用動畫,可以使用 .animation(nil)。

② 動畫是如何工作的

  • 在所有 SwiftUI 動畫的背后,有一個名為 Animatable 的協議,它擁有一個計算屬性,其類型遵守 VectorArithmetic 協議,這使得框架可以隨意地插值。
  • 當給一個視圖制作動畫時,SwiftUI 實際上是多次重新生成該視圖,并且每次都修改動畫參數。這樣,它就會從原點值漸漸走向最終值。
  • 假設我們為一個視圖的不透明度創建一個線性動畫,打算從 0.3 到 0.8,該框架將多次重新生成視圖,以小幅度的增量來改變不透明度。由于不透明度是以 Double 表示的,而且 Double 遵守 VectorArithmetic 協議,SwiftUI 可以插值出所需的不透明度值,在框架代碼的某個地方,可能有一個類似的算法:
let from:Double = 0.3 let to:Double = 0.8for i in 0..<6 {let pct = Double(i) / 5var difference = to - fromdifference.scale(by: pct)let currentOpacity = from + differenceprint("currentOpacity = \(currentOpacity)") }
  • 代碼將創建從起點到終點的漸進式更改:
currentOpacity = 0.3 currentOpacity = 0.4 currentOpacity = 0.5 currentOpacity = 0.6 currentOpacity = 0.7 currentOpacity = 0.8

三、為什么關心 Animatable?

  • 你可能會問,為什么需要關心所有這些小細節?SwiftUI 已經為不透明度制作了動畫,不需要我們擔心這一切?當然是,只需要 SwiftUI 知道如何將數值從原點插值到終點。對于不透明度,這是一個直接的過程,SwiftUI 知道該怎么做。然而,正如接下來要看到的,情況并非總是如此。
  • 例如一些大的例外情況:路徑(paths)、變換矩陣(matrices)和任意的視圖變化(例如,文本視圖中的文本、漸變視圖中的漸變顏色或停頓等),在這種情況下,框架不知道該怎么做。

① 形狀路徑的動畫化

  • 想象一下,有一個形狀,使用路徑來繪制一個規則的多邊形,實現當然會需要指出這個多邊形將有多少條邊:
PolygonShape(sides: 3).stroke(Color.blue, lineWidth: 3) PolygonShape(sides: 4).stroke(Color.purple, lineWidth: 4)

  • 如下所示,是 PolygonShape 的實現,代碼中使用了一點三角學的知識:
struct PolygonShape: Shape {var sides: Intfunc path(in rect: CGRect) -> Path { // hypotenuselet h = Double(min(rect.size.width, rect.size.height)) / 2.0// centerlet c = CGPoint(x: rect.size.width / 2.0, y: rect.size.height / 2.0)var path = Path()for i in 0..<sides {let angle = (Double(i) * (360.0 / Double(sides))) * Double.pi / 180// Calculate vertex positionlet pt = CGPoint(x: c.x + CGFloat(cos(angle) * h), y: c.y + CGFloat(sin(angle) * h))if i == 0 {path.move(to: pt) // move to first vertex} else {path.addLine(to: pt) // draw line to next vertex}}path.closeSubpath()return path} }
  • 可以更進一步,嘗試使用與不透明度相同的方法對形狀邊數(sides)參數進行動畫處理:
PolygonShape(sides: isSquare ? 4 : 3).stroke(Color.blue, lineWidth: 3).animation(.easeInOut(duration: duration))
  • 那么是不是 SwiftUI 知道如何把三角形轉化為正方形呢?很遺憾,它不并知道。當然,框架也不知道如何給它做動畫。我們可以隨心所欲地使用 .animation(),但這個形狀會從三角形跳到正方形,而且沒有任何動畫,原因很簡單:我們只教了 SwiftUI 如何畫一個 3 邊的多邊形,或 4 邊的多邊形,但代碼卻不知道如何畫一個 3.379 邊這樣的多邊形。
  • 因此,為了使動畫發生,需要兩件事:
    • 需要改變形狀的代碼,使其知道如何繪制邊數為非整數的多邊形;
    • 讓框架多次生成這個形狀,并讓可動畫參數一點點變化,也就是說,希望這個形狀被要求繪制多次,每次都有一個不同的邊數數值:3、3.1、3.15、3.2、3.25,一直到 4。
  • 一旦把這兩點做到位,就能夠在任何數量的邊數之間制作動畫:

② 創建可動畫數據(animatableData)

  • 為了使形狀可動畫化,需要 SwiftUI 多次渲染視圖,使用從原點到目標數之間的所有邊值。幸運的是,Shape 已經符合了 Animatable 協議的要求,這意味著,有一個計算的屬性(animatableData),可以用它來處理這個任務。然而,它的默認實現被設置為 EmptyAnimatableData,所以它什么都不做。
  • 為了解決我們的問題,首先改變邊的屬性的類型,從 Int 到 Double,這樣就可以有小數的數字,這里為了使事情簡單,只使用 Double:
struct PolygonShape: Shape {var sides: Double... }
  • 然后,需要創建計算屬性 animatableData:
struct PolygonShape: Shape {var sides: Doublevar animatableData: Double {get { return sides }set { sides = newValue }}... }

③ 用小數畫邊

  • 最后,需要教 SwiftUI 如何繪制一個邊數為非整數的多邊形。我們將稍微改變代碼,隨著小數部分的增長,這個新的邊將從零到全長,其他頂點將相應地平穩地重新定位:
func path(in rect: CGRect) -> Path {// hypotenuselet h = Double(min(rect.size.width, rect.size.height)) / 2.0// centerlet c = CGPoint(x: rect.size.width / 2.0, y: rect.size.height / 2.0)var path = Path()let extra: Int = Double(sides) != Double(Int(sides)) ? 1 : 0for i in 0..<Int(sides) + extra {let angle = (Double(i) * (360.0 / Double(sides))) * Double.pi / 180// Calculate vertexlet pt = CGPoint(x: c.x + CGFloat(cos(angle) * h), y: c.y + CGFloat(sin(angle) * h))if i == 0 {path.move(to: pt) // move to first vertex} else {path.addLine(to: pt) // draw line to next vertex}}path.closeSubpath()return path}
  • 如前所述,對于這個形狀的用戶來說,邊的參數是一個 Double,這可能顯得很奇怪,我們期望邊是一個 Int 參數。可以再次改變代碼,把這個事實隱藏在形狀的實現中:
struct PolygonShape: Shape {var sides: Intprivate var sidesAsDouble: Doublevar animatableData: Double {get { return sidesAsDouble }set { sidesAsDouble = newValue }}init(sides: Int) {self.sides = sidesself.sidesAsDouble = Double(sides)}... }
  • 有了這些變化,在內部使用 Double,但在外部則使用 Int,現在它看起來更優雅了。不要忘記了修改繪圖代碼,這樣它就會使用 sidesAsDouble 而不是 sides。

④ 設置多個參數的動畫

  • 很多時候,我們會發現自己需要對多個參數進行動畫處理,單一的 Double 是不夠的,在這個時候,可以使用 AnimatablePair<First, Second>。這 First 和 Second 都是符合 VectorArithmetic 的類型,例如AnimatablePair<CGFloat, Double>:

  • 為了演示 AnimatablePair 的使用,修改示例,現在多邊形形狀將有兩個參數:邊和比例,兩者都將用 Double 來表示:
struct PolygonShape: Shape {var sides: Doublevar scale: Doublevar animatableData: AnimatablePair<Double, Double> {get { AnimatablePair(sides, scale) }set {sides = newValue.firstscale = newValue.second}}... }
  • 有一個更復雜的路徑,它基本上是相同的形狀,但增加了一條連接每個頂點的線:

⑤ 超過兩個動畫參數

  • 如果瀏覽一下 SwiftUI 的聲明文件,會發現該框架相當廣泛地使用 AnimatablePair,比如說 CGSize、CGPoint、CGRect,盡管這些類型不符合 VectorArithmetic,但它們可以被動畫化,因為它們確實符合 Animatable,它們以這樣或那樣的方式使用 AnimatablePair:
extension CGPoint : Animatable {public typealias AnimatableData = AnimatablePair<CGFloat, CGFloat>public var animatableData: CGPoint.AnimatableData }extension CGSize : Animatable {public typealias AnimatableData = AnimatablePair<CGFloat, CGFloat>public var animatableData: CGSize.AnimatableData }extension CGRect : Animatable {public typealias AnimatableData = AnimatablePair<CGPoint.AnimatableData, CGSize.AnimatableData>public var animatableData: CGRect.AnimatableData }
  • 如果仔細注意一下 CGRect,會發現它實際上是在使用:
AnimatablePair<AnimatablePair<CGFloat, CGFloat>, AnimatablePair<CGFloat, CGFloat>>
  • 這意味著矩形的 x、y、寬度和高度值可以通過 first.first、first.second、second.first 和 second.second 訪問。

⑥ 使自己的類型動畫化(通過VectorArithmetic)

  • Angle、CGPoint、CGRect、CGSize、EdgeInsets、StrokeStyle 和 UnitPoint 等類型都默認實現了 Animatable,AnimatablePair、CGFloat、Double、EmptyAnimatableData 和 Float 符合 VectorArithmetic,我們可以使用它們中的任何一種來為形狀制作動畫。
  • 現有的類型提供了足夠的靈活性來實現任何東西的動畫,如果有一個想做動畫的復雜類型,沒有什么能阻止添加自己的 VectorArithmetic 協議的實現。可以創建一個模擬時鐘形狀,它將根據一個自定義的可動畫的參數類型移動它的指針 ClockTime:

  • 用法如下:
ClockShape(clockTime: show ? ClockTime(9, 51, 15) : ClockTime(9, 55, 00)).stroke(Color.blue, lineWidth: 3).animation(.easeInOut(duration: duration))
  • 首先開始創建自定義類型 ClockTime,它包含三個屬性(小時、分鐘和秒),幾個有用的初始化器,以及一些輔助計算的屬性和方法:
struct ClockTime {var hours: Int // Hour needle should jump by integer numbersvar minutes: Int // Minute needle should jump by integer numbersvar seconds: Double // Second needle should move smoothly// Initializer with hour, minute and secondsinit(_ h: Int, _ m: Int, _ s: Double) {self.hours = hself.minutes = mself.seconds = s}// Initializer with total of secondsinit(_ seconds: Double) {let h = Int(seconds) / 3600let m = (Int(seconds) - (h * 3600)) / 60let s = seconds - Double((h * 3600) + (m * 60))self.hours = hself.minutes = mself.seconds = s}// compute number of secondsvar asSeconds: Double {return Double(self.hours * 3600 + self.minutes * 60) + self.seconds}// show as stringfunc asString() -> String {return String(format: "%2i", self.hours) + ":" + String(format: "%02i", self.minutes) + ":" + String(format: "%02f", self.seconds)} }
  • 為了符合 VectorArithmetic 協議,需要編寫以下方法和計算屬性:
extension ClockTime: VectorArithmetic {static var zero: ClockTime {return ClockTime(0, 0, 0)}var magnitudeSquared: Double { return asSeconds * asSeconds }static func -= (lhs: inout ClockTime, rhs: ClockTime) {lhs = lhs - rhs}static func - (lhs: ClockTime, rhs: ClockTime) -> ClockTime {return ClockTime(lhs.asSeconds - rhs.asSeconds)}static func += (lhs: inout ClockTime, rhs: ClockTime) {lhs = lhs + rhs}static func + (lhs: ClockTime, rhs: ClockTime) -> ClockTime {return ClockTime(lhs.asSeconds + rhs.asSeconds)}mutating func scale(by rhs: Double) {var s = Double(self.asSeconds)s.scale(by: rhs)let ct = ClockTime(s)self.hours = ct.hoursself.minutes = ct.minutesself.seconds = ct.seconds} }
  • 唯一要做的,就是寫出形狀來適當地定位針頭,時鐘形狀的完整代碼,可在本文最后的完整示例的 Example5 中找到。

四、SwiftUI + Metal

  • 如果正在編寫復雜的動畫,可能我們的設備會受到影響,試圖跟上所有的繪圖。如下所示,啟用 Metal 后,一切都會變得不同:

  • 在模擬器上運行時,可能感覺不到有什么不同,然而,在真機設備上感受會更加直觀。幸運的是,啟用 Metal,是非常容易的,只需要添加 .drawingGroup() 修飾符:
FlowerView().drawingGroup()
  • 根據 WWDC 2019(用 SwiftUI 構建自定義視圖):繪圖組是一種特殊的渲染方式,但只適用于圖形等東西,它基本上會將 SwiftUI 視圖平鋪到一個單一的 NSView/UIView 中,并用 Metal 進行渲染。如果你想嘗試一下,但形狀還沒有復雜到讓設備掙扎的地步,添加一些漸變和陰影,就會立即看到不同。

六、完整示例

  • SwiftUI高級動畫之路徑Paths、幾何效果GeometryEffect與AnimatableModifier的效果實現。
與50位技術專家面對面20年技術見證,附贈技術全景圖

總結

以上是生活随笔為你收集整理的SwiftUI之深入解析高级动画的路径Paths的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。