生活随笔
收集整理的這篇文章主要介紹了
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
= false var 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
= false var 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
= false var 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.8 for i
in 0 . . < 6 { let pct
= Double ( i
) / 5 var difference
= to
- fromdifference
. scale ( by
: pct
) let currentOpacity
= from
+ difference
print ( "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
: Int func path ( in rect
: CGRect ) - > Path { let h
= Double ( min ( rect
. size
. width
, rect
. size
. height
) ) / 2.0 let 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 let pt
= CGPoint ( x
: c
. x
+ CGFloat ( cos ( angle
) * h
) , y
: c
. y
+ CGFloat ( sin ( angle
) * h
) ) if i
== 0 { path
. move ( to
: pt
) } else { path
. addLine ( to
: pt
) } } 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
: Double var animatableData
: Double { get { return sides
} set { sides
= newValue
} } . . .
}
③ 用小數畫邊
最后,需要教 SwiftUI 如何繪制一個邊數為非整數的多邊形。我們將稍微改變代碼,隨著小數部分的增長,這個新的邊將從零到全長,其他頂點將相應地平穩地重新定位:
func path ( in rect
: CGRect ) - > Path { let h
= Double ( min ( rect
. size
. width
, rect
. size
. height
) ) / 2.0 let 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 : 0 for i
in 0 . . < Int ( sides
) + extra
{ let angle
= ( Double ( i
) * ( 360.0 / Double ( sides
) ) ) * Double . pi
/ 180 let pt
= CGPoint ( x
: c
. x
+ CGFloat ( cos ( angle
) * h
) , y
: c
. y
+ CGFloat ( sin ( angle
) * h
) ) if i
== 0 { path
. move ( to
: pt
) } else { path
. addLine ( to
: pt
) } } path
. closeSubpath ( ) return path
}
如前所述,對于這個形狀的用戶來說,邊的參數是一個 Double,這可能顯得很奇怪,我們期望邊是一個 Int 參數。可以再次改變代碼,把這個事實隱藏在形狀的實現中:
struct PolygonShape : Shape { var sides
: Int private var sidesAsDouble
: Double var animatableData
: Double { get { return sidesAsDouble
} set { sidesAsDouble
= newValue
} } init ( sides
: Int ) { self . sides
= sides
self . sidesAsDouble
= Double ( sides
) } . . .
}
有了這些變化,在內部使用 Double,但在外部則使用 Int,現在它看起來更優雅了。不要忘記了修改繪圖代碼,這樣它就會使用 sidesAsDouble 而不是 sides。
④ 設置多個參數的動畫
很多時候,我們會發現自己需要對多個參數進行動畫處理,單一的 Double 是不夠的,在這個時候,可以使用 AnimatablePair<First, Second>。這 First 和 Second 都是符合 VectorArithmetic 的類型,例如AnimatablePair<CGFloat, Double>:
為了演示 AnimatablePair 的使用,修改示例,現在多邊形形狀將有兩個參數:邊和比例,兩者都將用 Double 來表示:
struct PolygonShape : Shape { var sides
: Double var scale
: Double var animatableData
: AnimatablePair < Double , Double > { get { AnimatablePair ( sides
, scale
) } set { sides
= newValue
. first scale
= 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 var minutes
: Int var seconds
: Double init ( _ h
: Int , _ m
: Int , _ s
: Double ) { self . hours
= h
self . minutes
= m
self . seconds
= s
} init ( _ seconds
: Double ) { let h
= Int ( seconds
) / 3600 let m
= ( Int ( seconds
) - ( h
* 3600 ) ) / 60 let s
= seconds
- Double ( ( h
* 3600 ) + ( m
* 60 ) ) self . hours
= h
self . minutes
= m
self . seconds
= s
} var asSeconds
: Double { return Double ( self . hours
* 3600 + self . minutes
* 60 ) + self . seconds
} func 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
. hours
self . minutes
= ct
. minutes
self . 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 的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔 網站內容還不錯,歡迎將生活随笔 推薦給好友。