本文是我學習《iOS Animations by Tutorials》 筆記中的一篇。 文中詳細程式碼都放在我的Github上 andyRon/LearniOSAnimations。
系統學習iOS動畫之一:檢視動畫 學習了建立檢視動畫(View Animations),這一部分學習功能更強大、更偏底層的Core Animation(核心動畫) APIs。核心動畫的這個名字可能令人有點誤解,暫時可以理解為本文的標題圖層動畫(Layer Animations)。
在本書的這一部分中,將學習動畫層而不是檢視以及如何使用特殊圖層。
圖層是一個簡單的模型類,它公開了許多屬性來表示一些基於影像的內容。 每個UIView
都有一個圖層支援(都有一個layer
屬性)。
檢視 vs 圖層
由於以下原因,圖層(Layers)與檢視(Views)(對於動畫)不同:
- 圖層是一個模型物件 —— 它公開資料屬性並且不實現任何邏輯。 它沒有複雜的自動佈局依賴關係,也不用處理使用者互動。
- 圖層具有預定義的可見特徵 —— 這些特徵是許多影響內容在螢幕上呈現的資料屬性,例如邊框線,邊框顏色,位置和陰影。
- 最後,Core Animation優化了圖層內容的快取並直接在GPU上快速繪圖。
單個來說,兩者的優點。
檢視:
- 複雜檢視層次結構佈局,自動佈局等。
- 使用者互動。
- 通常具有在CPU上的主執行緒上執行的自定義邏輯或自定義繪圖程式碼。
- 非常靈活,功能強大,子類很多類。
圖層:
- 更簡單的層次結構,更快地解決佈局,繪製速度更快。
- 沒有響應者鏈開銷。
- 預設情況下沒有自定義邏輯 並直接在GPU上繪製。
- 不那麼靈活,子類的類更少。
檢視和圖層的選擇技巧: 任何時候都可以選擇檢視動畫; 當需要更高的效能時,就需要使用圖層動畫。
兩者在架構中的位置:
預覽:
本文比較長,圖片比較多,預警⚠️?。
8-圖層動畫入門 —— 從最簡單的圖層動畫開始,瞭解除錯動畫錯誤的方法。
9-動畫的Keys和代理 —— 怎麼更好地控制當前執行的動畫,並使用代理方法對動畫事件做出響應。
10-動畫組和時間控制 —— 組合許多簡單的動畫,並將它們作為一個組一起執行。
11-圖層彈簧動畫 —— 學習如何使用CASpringAnimation
建立強大而靈活的彈簧圖層動畫。
12-圖層關鍵幀動畫和結構屬性 —— 學習圖層關鍵幀動畫, 動畫結構屬性的一些特殊處理。
接下來,學習幾個專門的圖層:
13-形狀和蒙版 —— 通過CAShapeLayer
在螢幕上繪製形狀,併為其特殊路徑屬性設定動畫。
14-漸變動畫 —— 瞭解如何使用CAGradientLayer
來繪製漸變和動畫漸變。
15-Stroke和路徑動畫 —— 以互動方式繪製形狀,並使用關鍵幀動畫的一些強大功能。
16-複製動畫 —— 學習如何建立圖層內容的多個副本,然後利用副本製作動畫。
8-圖層動畫入門
圖層動畫的工作方式與檢視動畫非常相似; 只需在定義的時間段內為起始值和結束值之間的屬性設定動畫,然後讓Core Animation處理兩者之間的渲染。
但是,圖層動畫具有比檢視動畫更多的可動畫屬性; 在設計效果時,這會提供了很多選擇和靈活性; 圖層動畫還有許多專門的CALayer子類(如CATextLayer
、 CAShapeLayer
、 CATransformLayer
、CAGradientLayer
、CAReplicatorLayer
、CAScrollLayer
、CAEmitterLayer
、AVPlayerLayer
等),這些子類有提供了許多其他屬性。
本章介紹CALayer和Core Animation的基礎知識。
可動畫屬性
可與檢視動畫的可動畫屬性對照著看。
位置 和 大小
bounds
、position
、transform
邊
borderColor
、 borderWidth
、cornerRadius
陰影
shadowOffset
: 使陰影看起來更接近或更遠離圖層。
shadowOpacity
:使陰影淡入或淡出。
shadowPath
: 更改圖層陰影的形狀。 可以建立不同的3D效果,使圖層看起來像浮動在不同的陰影形狀和位置上。
shadowRadius
: 控制陰影的模糊; 當模擬檢視朝向或遠離投射陰影的表面移動時,這尤其有用。
內容
contents
:修改此項以將原始TIFF或PNG資料指定為圖層內容。
mask
:修改它將用於掩蓋圖層可見內容的形狀或影像。 這個屬性在13-形狀和蒙版將詳細介紹和使用。
opacity
第一個圖層動畫
開始專案使用 3-過渡動畫完成的專案。
把原本head的檢視動畫替換為圖層動畫。
分別刪除ViewController的viewWillAppear()
中:
heading.center.x -= view.bounds.width
複製程式碼
和viewDidAppear()
中:
UIView.animate(withDuration: 0.5) {
self.heading.center.x += self.view.bounds.width
}
複製程式碼
在viewWillAppear()
的開始(super
呼叫後)新增:
let flyRight = CABasicAnimation(keyPath: "position.x")
flyRight.fromValue = -view.bounds.size.width/2
flyRight.toValue = view.bounds.size.width/2
flyRight.duration = 0.5
複製程式碼
核心動畫中的動畫物件只是簡單的資料模型; 上面的程式碼建立了CABasicAnimation
的例項,並設定了一些資料屬性。
這個例項描述了一個潛在的圖層動畫:可以選擇立即執行,稍後執行,或者根本不執行。
由於動畫未繫結到特定圖層,因此可以在其他圖層上重複使用動畫,每個圖層將獨立執行動畫的副本。
在動畫模型中,您可以將要設定為動畫的屬性指定為keypath
引數(比如上面設定是"position.x"
); 這很方便,因為動畫總是在圖層中設定。
接下來,為在keypath
上指定的屬性設定fromValue
和toValue
。需要動畫物件(此處我要處理的是heading)從螢幕左側到螢幕中央。動畫持續時間的概念沒有改變; duration
設定為0.5秒。
動圖已經設定完成,現在需要把它新增需要執行此動畫的圖層上。 在剛新增的程式碼下方新增,將動畫新增到heading的圖層:
heading.layer.add(flyRight, forKey: nil)
複製程式碼
add(_:forKey:)
會把動畫做個一個拷貝給將要新增的圖層。 如果之後需要更改或停止動畫,可以新增forKey
引數用於識別動畫。
此時的動畫看上去和之前檢視動畫沒有什麼區別。
更多圖層動畫知識
同一樣的方法應用在Username Filed上,刪除viewWillAppear()
和viewDidAppear()
中對應程式碼。再把之前的動畫新增的Username Filed的layer上:
username.layer.add(flyRight, forKey: nil)
複製程式碼
此時執行專案,看上去會有點彆扭,因為heading Label,Username Filed的動畫是相同的,Username Filed沒有之前的延遲效果。
在新增動畫到Username Filed的layer上之前,新增:
flyRight.beginTime = CACurrentMediaTime() + 0.3
複製程式碼
動畫的beginTime
屬性設定動畫應該開始的絕對時間; 在這種情況下,可以使用CACurrentMediaTime()
獲取當前時間(系統的一個絕對時間,機器開啟時間,取自機器時間 mach_absolute_time()
),並以秒為單位新增所需的延遲。
此時,如果仔細觀察會發現有個問題,Username Filed在開始動畫之前已經出現了,這就涉及到另外一個圖層動畫屬性 fillMode
了。
關於 fillMode
以Username Field的移動動畫來看看fillMode
不同值的區別,為了方便觀察,我把beginTime
時間變大,程式碼類似於:
let flyRight = CABasicAnimation(keyPath: "position.x")
flyRight.fromValue = -view.bounds.size.width/2
flyRight.toValue = view.bounds.size.width/2
flyRight.duration = 0.5
heading.layer.add(flyRight, forKey: nil)
flyRight.beginTime = CACurrentMediaTime() + 2.3
flyRight.fillMode = kCAFillModeRemoved
username.layer.add(flyRight, forKey: nil)
複製程式碼
-
kCAFillModeRemoved
是fillMode
的預設值在定義的
beginTime
處啟動動畫(如果未設定beginTime
,也就是beginTime
等於CACurrentMediaTime()
,則立即啟動動畫), 並在動畫完成時刪除動畫期間所做的更改:實際效果:
now 到 begin 這段時間動畫沒有開始,但Username Field直接顯示了,然後到 begin時動畫才開始,這就是之前遇到的情況。
-
kCAFillModeBackwards
無論動畫的實際開始時間如何,
kCAFillModeBackwards
都會立即在螢幕上顯示動畫的第一幀,並在以後啟動動畫:實際效果:
第一幀在
fromValue
處,也就是"position.x"
是負的在螢幕外,因此開始時沒有看見Username Field,等待2.3s後動畫開始。 -
kCAFillModeForwards
kCAFillModeForwards
像往常一樣播放動畫,但在螢幕上保留動畫的最後一幀,直到您刪除動畫:實際效果:
除了設定kCAFillModeForwards之外,還需要對圖層進行一些其他更改以使最後一幀“貼上”。 你將在本章後面稍後瞭解這一點。 和第一個有點類似,但還是有區別的。
-
kCAFillModeBoth
kCAFillModeBoth
是kCAFillModeForwards
和kCAFillModeBackwards
的組合; 這會使動畫的第一幀立即出現在螢幕上,並在動畫結束時在螢幕上保留最終幀:實際效果:
要解決之前發現的問題,將使用
kCAFillModeBoth
。同樣對於Password Field,也刪除其檢視動畫的程式碼,改換成類似Username Field的圖層動畫,不過
beginTime
要晚一點,具體程式碼:複製程式碼
flyRight.beginTime = CACurrentMediaTime() + 0.3 flyRight.fillMode = kCAFillModeBoth username.layer.add(flyRight, forKey: nil)
flyRight.beginTime = CACurrentMediaTime() + 0.4 password.layer.add(flyRight, forKey: nil)
到目前為止,您的動畫恰好在表單元素最初位於Interface Builder中的確切位置結束。 但是,很多時候情況並非如此。
### 除錯動畫
在上面的動畫後繼續新增:
```swift
username.layer.position.x -= view.bounds.width
password.layer.position.x -= view.bounds.width
複製程式碼
這就是把兩個文字框的圖層移動到螢幕外,類似於flyRight.fromValue = -view.bounds.size.width/2
(此時這段程式碼可以暫時註釋掉),執行後發現問題,動畫結束後兩個文字框消失了,這是怎麼回事呢?
繼續在上面的程式碼後新增一個延遲函式:
delay(seconds: 5.0)
print("where are the fields?")
}
複製程式碼
並打斷點後執行:
進入UI hierarchy 視窗:
UI hierarchy 模式下可以檢視當前執行時的UI層次結構,包括已經隱藏或透明檢視以及在螢幕外的檢視。還可以3D檢視。
當然還可以在右側檢測器中檢視實時屬性:
動畫完成後,程式碼更改會導致欄位跳回其初始位置。 但為什麼?
動畫 vs 真實內容
當你為Text Field設定動畫時,你實際上並沒有看到Text Field本身是動畫的; 相反,你會看到它的快取版本,稱為presentation layer(顯示層)。動畫完成後原始圖層再次到原本位置,則從螢幕上移除presentation layer。
首先,請記住在viewWillAppear(_:)
中將Text Field設定在螢幕外:
動畫開始時,Text Field暫時隱藏,預渲染的動畫物件將替代它:
現在無法點選動畫物件,輸入任何文字或使用任何其他特定文字欄位功能,因為它不是真正的文字欄位,只是可見的“幻像”。 動畫一旦完成,它就會從螢幕上消失,原始Text Field將被取消隱藏。但它此時的位置還在螢幕左側!
要解決這個難題,您需要使用另一個CABasicAnimation
屬性:isRemovedOnCompletion
。
將fillMode
設定為kCAFillModeBoth
可讓動畫在完成後保留在螢幕上,並在動畫開始之前顯示動畫的第一幀。要完成效果,您需要相應地設定removedOnCompletion
,兩者的組合將使動畫在螢幕上可見。
在設定fillMode
之後,將以下行新增到viewWillAppear()
:
flyRight.isRemovedOnCompletion = false
複製程式碼
isRemovedOnCompletion
預設為true
,因此動畫一完成就會消失。將其設定為false
並將其與正確的fillMode
組合可將動畫保留在螢幕上 。
現在執行專案,應該能看到所有元素都按預期保留在螢幕上。
更新圖層模型
從螢幕上刪除圖層動畫後,圖層將回退到其當前位置和其他屬性值。 這意味著您通常需要更新圖層的屬性以反映動畫的最終值。
雖然前面已經說明過把isRemovedOnCompletion
設定成false
是如何工作的,但儘可能避免使用它。 在螢幕上保留動畫會影響效能,因此需要自動刪除它們並更新原始圖層的位置。
需要把原始圖層設定到螢幕中間,在viewWillAppear
中天假:
username.layer.position.x = view.bounds.size.width/2
password.layer.position.x = view.bounds.size.width/2
複製程式碼
當然此時要注意把之前註釋掉的flyRight.fromValue = -view.bounds.size.width/2
,去掉註釋,也要把除錯動畫時的程式碼去掉。
使用圖層動畫實現☁️的淡入
刪除viewWillAppear()
中把四個☁️透明度設為0.0的程式碼,和viewDidAppear()
的☁️的檢視動畫。
然後在viewDidAppear()
加入:
let cloudFade = CABasicAnimation(keyPath: "alpha")
cloudFade.duration = 0.5
cloudFade.fromValue = 0.0
cloudFade.toValue = 1.0
cloudFade.fillMode = kCAFillModeBackwards
cloudFade.beginTime = CACurrentMediaTime() + 0.5
cloud1.layer.add(cloudFade, forKey: nil)
cloudFade.beginTime = CACurrentMediaTime() + 0.7
cloud2.layer.add(cloudFade, forKey: nil)
cloudFade.beginTime = CACurrentMediaTime() + 0.9
cloud3.layer.add(cloudFade, forKey: nil)
cloudFade.beginTime = CACurrentMediaTime() + 1.1
cloud4.layer.add(cloudFade, forKey: nil)
複製程式碼
登入按鈕背景顏色變化的動畫
把原登入按鈕背景顏色變化的動畫修改成圖層動畫。
刪除logIn()
中的:
self.loginButton.backgroundColor = UIColor(red: 0.85, green: 0.83, blue: 0.45, alpha: 1.0)
複製程式碼
刪除resetForm()
中的:
self.loginButton.backgroundColor = UIColor(red: 0.63, green: 0.84, blue: 0.35, alpha: 1.0)
複製程式碼
在ViewController.swift檔案中建立一個全域性的背景顏色變化動畫函式:
func tintBackgroundColor(layer: CALayer, toColor: UIColor) {
let tint = CABasicAnimation(keyPath: "backgroundColor")
tint.fromValue = layer.backgroundColor
tint.toValue = toColor.cgColor
tint.duration = 0.5
layer.add(tint, forKey: nil)
layer.backgroundColor = toColor.cgColor
}
複製程式碼
在logIn()
中新增:
let tintColor = UIColor(red: 0.85, green: 0.83, blue: 0.45, alpha: 1.0)
tintBackgroundColor(layer: loginButton.layer, toColor: tintColor)
複製程式碼
在resetForm()
中登入按鈕動畫方法的completion
閉包中新增:
completion: { _ in
let tintColor = UIColor(red: 0.63, green: 0.84, blue: 0.35, alpha: 1.0)
tintBackgroundColor(layer: self.loginButton.layer, toColor: tintColor)
})
複製程式碼
登入按鈕的圓角動畫
在ViewController.swift檔案中建立一個全域性的圓角變化動畫函式:
func roundCorners(layer: CALayer, toRadius: CGFloat) {
let round = CABasicAnimation(keyPath: "cornerRadius")
round.fromValue = layer.cornerRadius
round.toValue = toRadius
round.duration = 0.33
layer.add(round, forKey: nil)
layer.cornerRadius = toRadius
}
複製程式碼
在logIn()
中新增:
roundCorners(layer: loginButton.layer, toRadius: 25.0)
複製程式碼
在resetForm()
中登入按鈕動畫方法的completion
閉包中新增:
roundCorners(layer: self.loginButton.layer, toRadius: 10.0)
複製程式碼
兩種狀態的變化:
兩個動畫函式tintBackgroundColor
和roundCorners
最後都需要把動畫最變化最終值賦值給動畫的屬性,這對應於前面的 [動畫 vs 真實內容](#動畫 vs 真實內容) 章節
本章節的最終效果:
9-動畫的Keys和代理
關於檢視動畫和相應的閉包語法的一個棘手問題是,一旦您建立並執行檢視動畫,您就無法暫停,停止或以任何方式訪問它。
但是,使用核心動畫,您可以輕鬆檢查在圖層上執行的動畫,並在需要時停止它們。 此外,您甚至可以在動畫上設定委託物件並對動畫事件做出反應。
本章的開始專案使用上一章完成的專案
動畫代理介紹
CAAnimationDelegate
的兩個代理方法:
func animationDidStart(_ anim: CAAnimation)
func animationDidStop(_ anim: CAAnimation, finished flag: Bool)
複製程式碼
做個小測試,在flyRight
初始化時,新增:
flyRight.delegate = self
複製程式碼
對ViewController
新增擴充套件,並實現一個代理方法:
extension ViewController: CAAnimationDelegate {
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
print(anim.description, "動畫完成")
}
}
複製程式碼
執行,列印結果:
<CABasicAnimation: 0x6000032376e0> 動畫完成
<CABasicAnimation: 0x600003237460> 動畫完成
<CABasicAnimation: 0x600003237480> 動畫完成
複製程式碼
會發現animationDidStop(_:finished:)
方法被呼叫三次,並且每次呼叫的動畫都不同,這因為當每一次呼叫layer.add(_:forKey:)
把動畫新增給圖層時,都會拷貝一份,這在前面的圖層動畫基礎知識中說明過。
KVO
CAAnimation
類及其子類是用Objective-C編寫的,並且符合鍵值編碼(KVO),這意味著您可以將它們視為字典,並在執行時向它們新增新屬性。(關於KVO,可檢視我的小結文章 OC中的鍵/值編碼(KVC))
使用此機制為flyRight
動畫指定名稱,以便之後可以從其他活動動畫中識別它。
在viewWillAppear()
中的flyRight.delegate = self
後新增:
flyRight.setValue("form", forKey: "name")
flyRight.setValue(heading.layer, forKey: "layer")
複製程式碼
在上面的程式碼中,在flyRight
動畫上建立鍵為"name"
,值為"form"
的鍵值對,可以從委託回撥方法呼叫識別;
也建立了一個鍵為"layer"
,值為heading.layer
的鍵值對,以方便之後引用動畫所屬的圖層。
同樣的可以新增(之前已經說過每次動畫都會拷貝一份,所以不會覆蓋):
flyRight.setValue(username.layer, forKey: "layer")
// ...
flyRight.setValue(password.layer, forKey: "layer")
複製程式碼
在代理回撥方法中驗證上面的程式碼,上面的移動動畫結束後再新增一個簡單的脈動動畫:
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
// print(anim.description, "動畫完成")
guard let name = anim.value(forKey: "name") as? String else {
return
}
if name == "form" {
// `value(forKey:)`的結果總是`Any`,因此需要轉換為所需型別
let layer = anim.value(forKey: "layer") as? CALayer
anim.setValue(nil, forKey: "layer")
// 簡單的脈動動畫
let pulse = CABasicAnimation(keyPath: "transform.scale")
pulse.fromValue = 1.25
pulse.toValue = 1.0
pulse.duration = 0.25
layer?.add(pulse, forKey: nil)
}
}
複製程式碼
注意:
layer?.add()
意味著如果動畫中沒有儲存圖層,則會跳過add(_:forKey:)
的呼叫。 這是Swift中的可選鏈式呼叫,可參考以擼程式碼的形式學習Swift-17:可選鏈式呼叫(Optional Chaining)
移動動畫結束後有一個簡單變大的脈動動畫效果:
動畫Keys
add(_:forKey:)
中的引數forKey
(注意不要和setValue(_:forKey:)
中的forKey
混淆),之前一直沒使用。
在這部分中,將建立另一個圖層動畫,學習如何一次執行多個動畫,並瞭解如何使用動畫Keys控制正在執行的動畫。
新增一個新標籤,新標籤將從右到左緩慢動畫,用來提示使用者輸入。 一旦使用者開始輸入他們的使用者名稱或密碼(Text Field獲得焦點),該標籤將停止移動並直接跳到其最終位置(居中位置)。 一旦使用者知道該怎麼做就沒有必要繼續動畫。
在ViewController
中新增屬性 let info = UILabel()
,並在viewDidLoad()
中配置:
info.frame = CGRect(x: 0.0, y: loginButton.center.y + 60.0, width: view.frame.size.width, height: 30)
info.backgroundColor = UIColor.clear
info.font = UIFont(name: "HelveticaNeue", size: 12.0)
info.textAlignment = .center
info.textColor = UIColor.white
info.text = "Tap on a field and enter username and password"
view.insertSubview(info, belowSubview: loginButton)
複製程式碼
為info
新增兩個動畫:
// 提示資訊Label的兩個動畫
let flyLeft = CABasicAnimation(keyPath: "position.x")
flyLeft.fromValue = info.layer.position.x + view.frame.size.width
flyLeft.toValue = info.layer.position.x
flyLeft.duration = 5.0
info.layer.add(flyLeft, forKey: "infoappear")
let fadeLabelIn = CABasicAnimation(keyPath: "opacity")
fadeLabelIn.fromValue = 0.2
fadeLabelIn.toValue = 1.0
fadeLabelIn.duration = 4.5
info.layer.add(fadeLabelIn, forKey: "fadein")
複製程式碼
flyLeft
是從左到右移動的動畫,fadeLabelIn
是透明度漸漸變大的動畫。
此時的動畫效果如下:
為Text Field新增代理。通過擴充套件,讓ViewController
遵循UITextFieldDelegate
協議:
extension ViewController: UITextFieldDelegate {
func textFieldDidBeginEditing(_ textField: UITextField) {
guard let runningAnimations = info.layer.animationKeys() else {
return
}
print(runningAnimations)
}
}
複製程式碼
在viewDidAppear()
中新增:
username.delegate = self
password.delegate = self
複製程式碼
此時執行,info
動畫還在進行時點選文字框,會列印動畫key值:
["infoappear", "fadein"]
複製程式碼
在 textFieldDidBeginEditing(:)
裡新增:
info.layer.removeAnimation(forKey: "infoappear")
複製程式碼
點選文字框後,刪除從左向右移動的動畫,info
立即到達終點,也就是螢幕中央:
當然也可以通過removeAllAnimations()
方法刪除layer
上的所有動畫。
**注意:**動畫進行完了,會預設被從
layer
上刪除,也就是animationKeys()
方法將獲得不到動畫keys了。
修改☁️的動畫
通過本章所學的動畫代理和動畫KVO修改☁️的動畫
先在ViewController
中新增動畫方法:
/// 雲的圖層動畫
func animateCloud(layer: CALayer) {
let cloudSpeed = 60.0 / Double(view.layer.frame.size.width)
let duration: TimeInterval = Double(view.layer.frame.size.width - layer.frame.origin.x) * cloudSpeed
let cloudMove = CABasicAnimation(keyPath: "position.x")
cloudMove.duration = duration
cloudMove.toValue = self.view.bounds.width + layer.bounds.width/2
cloudMove.delegate = self
cloudMove.setValue("cloud", forKey: "name")
cloudMove.setValue(layer, forKey: "layer")
layer.add(cloudMove, forKey: nil)
}
複製程式碼
把viewDidAppear()
中的四個animateCloud
方法呼叫替代為:
animateCloud(layer: cloud1.layer)
animateCloud(layer: cloud2.layer)
animateCloud(layer: cloud3.layer)
animateCloud(layer: cloud4.layer)
複製程式碼
讓☁️不停的移動,在動畫代理方法animationDidStop
中新增:
if name == "cloud" {
if let layer = anim.value(forKey: "layer") as? CALayer {
anim.setValue(nil, forKey: "layer")
layer.position.x = -layer.bounds.width/2
delay(0.5) {
self.animateCloud(layer: layer)
}
}
}
複製程式碼
本章的效果:
10-動畫組和時間控制
在上一章中,學習瞭如何向單個圖層新增多個獨立動畫。 但是,如果您希望您的動畫同步工作並保持彼此一致,該怎麼辦? 這就用到動畫組(animation groups)。
本章介紹如何使用CAAnimationGroup
對動畫進行分組,可以向組中新增多個動畫並同時調整持續時間,委託和timingFunction
等屬性。
對動畫進行分組會產生簡化的程式碼,並確保您的所有動畫將作為一個實體單元同步。
本章的開始專案使用上一章完成的專案
CAAnimationGroup
刪除viewWillAppear()
中的:
loginButton.center.y += 30.0
loginButton.alpha = 0.0
複製程式碼
刪除viewDidAppear()
中登入按鈕的顯示動畫:
UIView.animate(withDuration: 0.5, delay: 0.5, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.0, options: [], animations: {
self.loginButton.center.y -= 30.0
self.loginButton.alpha = 1.0
}, completion: nil)
複製程式碼
在viewDidAppear()
中組動畫新增:
let groupAnimation = CAAnimationGroup()
groupAnimation.beginTime = CACurrentMediaTime() + 0.5
groupAnimation.duration = 0.5
groupAnimation.fillMode = kCAFillModeBackwards
複製程式碼
CAAnimationGroup
繼承於CAAnimation
,也有beginTime
, duration
, fillMode
, delegate
等屬性。
繼續三個動畫,並把它們加入到上面的組動畫中:
let scaleDown = CABasicAnimation(keyPath: "transform.scale")
scaleDown.fromValue = 3.5
scaleDown.toValue = 1.0
let rotate = CABasicAnimation(keyPath: "transform.rotation")
rotate.fromValue = .pi / 4.0
rotate.toValue = 0.0
let fade = CABasicAnimation(keyPath: "opacity")
fade.fromValue = 0.0
fade.toValue = 1.0
groupAnimation.animations = [scaleDown, rotate, fade]
loginButton.layer.add(groupAnimation, forKey: nil)
複製程式碼
登入按鈕的效果為:
動畫緩動
圖層動畫中的動畫緩動與1-檢視動畫入門中介紹的檢視動畫的動畫選項的,在概念上是相同的, 只是語法有所不同。
圖層動畫中的動畫緩動通過類CAMediaTimingFunction
來表示 。用法如下:
groupAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
複製程式碼
name
引數有如下幾種,和檢視動畫中的差不多:
kCAMediaTimingFunctionLinear
速度不變化
kCAMediaTimingFunctionEaseIn
開始時慢,結束時快
kCAMediaTimingFunctionEaseOut
開始時快,結束時慢
kCAMediaTimingFunctionEaseInEaseOut
開始結束都慢,中間快
可以試一下不同的效果。
另外CAMediaTimingFunction
有個初始化方法init(controlPoints c1x: Float, _ c1y: Float, _ c2x: Float, _ c2y: Float)
,可以自定義緩動模式,具體可參考官方文件
更多動畫時間控制的選項
重複動畫
repeatCount
可設定重複動畫指定的次數。
為提示資訊Label的動畫新增重複次數,在viewDidAppear()
中為flyLeft
動畫設定屬性:
flyLeft.repeatCount = 4
複製程式碼
另外一個repeatDuration
可用來設定總重複時間。
和檢視動畫一樣,也要設定autoreverses
,要不然不連貫:
flyLeft.autoreverses = true
複製程式碼
現在效果看著不錯了,但是還有點問題,就是4次重複結束後,會直接跳到螢幕中心,如下(由於太長,gif已經省略了前幾次滾動):
這也很好理解,最後一個迴圈以標籤離開螢幕結束。解決辦法就是半個動畫週期:
flyLeft.repeatCount = 2.5
複製程式碼
改變動畫的速度
可以通過設定速度屬性來獨立於持續時間來控制動畫的速度。
flyLeft.speed = 2.0
複製程式碼
把三個form的動畫修改為動畫組
下面程式碼:
let flyRight = CABasicAnimation(keyPath: "position.x")
flyRight.fromValue = -view.bounds.size.width/2
flyRight.toValue = view.bounds.size.width/2
flyRight.duration = 0.5
flyRight.fillMode = kCAFillModeBoth
flyRight.delegate = self
flyRight.setValue("form", forKey: "name")
flyRight.setValue(heading.layer, forKey: "layer")
heading.layer.add(flyRight, forKey: nil)
flyRight.setValue(username.layer, forKey: "layer")
flyRight.beginTime = CACurrentMediaTime() + 0.3
username.layer.add(flyRight, forKey: nil)
flyRight.setValue(password.layer, forKey: "layer")
flyRight.beginTime = CACurrentMediaTime() + 0.4
password.layer.add(flyRight, forKey: nil)
複製程式碼
修改為:
let formGroup = CAAnimationGroup()
formGroup.duration = 0.5
formGroup.fillMode = kCAFillModeBackwards
let flyRight = CABasicAnimation(keyPath: "position.x")
flyRight.fromValue = -view.bounds.size.width/2
flyRight.toValue = view.bounds.size.width/2
let fadeFieldIn = CABasicAnimation(keyPath: "opacity")
fadeFieldIn.fromValue = 0.25
fadeFieldIn.toValue = 1.0
formGroup.animations = [flyRight, fadeFieldIn]
heading.layer.add(formGroup, forKey: nil)
formGroup.delegate = self
formGroup.setValue("form", forKey: "name")
formGroup.setValue(username.layer, forKey: "layer")
formGroup.beginTime = CACurrentMediaTime() + 0.3
username.layer.add(formGroup, forKey: nil)
formGroup.setValue(password.layer, forKey: "layer")
formGroup.beginTime = CACurrentMediaTime() + 0.4
password.layer.add(formGroup, forKey: nil)
複製程式碼
本章節的最終效果:
11-圖層彈簧動畫
前面檢視動畫中的2-彈簧動畫可以用於建立一些相對簡單的彈簧式動畫,而本章節學習的**圖層彈簧動畫(Layer Springs)**可以呈現一個看起來更自然的物理模擬。
本章的開始專案使用上一章完成的專案,新增一些新的圖層彈簧動畫,並說明兩種彈簧動畫之間的差異。
先說一些理論知識:
阻尼諧振子
阻尼諧振子,Damped harmonic oscillators(直譯就是,逐漸衰弱的振盪器),可以理解為逐漸衰減的振動。
UIKit API簡化了彈簧動畫的製作,不需要了解它們的原理就可以很方便的使用。 但是,由於您現在是核心動畫專家,因此您需要深入研究細節。
鐘擺,理想狀況下鐘擺是不停的擺動,像下面的一樣:
對應的運動軌跡圖就像:
但現實中由於能量的損耗,鐘擺的搖擺的幅度會逐漸減小:
對應的運動軌跡:
這就是一個阻尼諧振子 。
鐘擺停下來所需的時間長度,以及最終振盪器圖形的方式取決於振盪系統的以下引數:
-
阻尼(damping):由於空氣摩擦、機械摩擦和其他作用在系統上的外部減速力。
-
質量(mass):擺錘越重,擺動的時間越長。
-
剛度(stiffness):振盪器的“彈簧”越硬(鐘擺的“彈簧”是指地球的引力),鐘擺擺動越困難,系統停下來也越快。想象一下,如果在月球或木星上使用這個鐘擺;在低重力和高重力情況下的運動將是完全不同的。
-
初始速度(initial velocity):推一下鐘擺。
“這一切都非常有趣,但與彈簧動畫有什麼關係呢?”
阻尼諧振子系統是推動iOS中彈簧動畫的動力。 下一節將更詳細地討論這個問題。
檢視彈簧動畫 vs 圖層彈簧動畫
UIKit以動態方式調整所有其他變數,使系統在給定的持續時間內穩定下來。 這就是為什麼UIKit彈簧動畫有時有點被迫 停下來的感覺。 如果仔細觀察會發現UIKit動畫有點不太自然。
幸運的是,核心允許通過CASpringAnimation
類為圖層屬性建立合適的彈簧動畫。 CASpringAnimation
在幕後為UIKit建立彈簧動畫,但是當我們直接呼叫它時,可以設定系統的各種變數,讓動畫自己穩定下來。 這種方法的缺點是不能設定固定的持續時間(duration);持續時間取決於提供的其它變數,然後系統計算所得。
CASpringAnimation
的一些屬性(對應之前振盪系統的引數):
damping
阻尼係數,阻止彈簧伸縮的係數,阻尼係數越大,停止越快
mass
質量,影響圖層運動時的彈簧慣性,質量越大,彈簧拉伸和壓縮的幅度越大
stiffness
剛度係數(勁度係數/彈性係數),剛度係數越大,形變產生的力就越大,運動越快
initialVelocity
初始速率,動畫檢視的初始速度大小。速率為正數時,速度方向與運動方向一致,速率為負數時,速度方向與運動方向相反
第一個圖層彈簧動畫
BahamaAirLoginScreen專案中兩個文字框移動動畫結束後有個脈動動畫,讓使用者知道該欄位處於活動狀態並可以使用。 然而,動畫結束時有些突然。 通過用CASpringAnimation
來讓脈動動畫更加自然一點。
把animationDidStop(_:finished:)
動畫程式碼:
// 簡單的脈動動畫
let pulse = CABasicAnimation(keyPath: "transform.scale")
pulse.fromValue = 1.25
pulse.toValue = 1.0
pulse.duration = 0.25
layer?.add(pulse, forKey: nil)
複製程式碼
轉變為:
let pulse = CASpringAnimation(keyPath: "transform.scale")
pulse.damping = 2.0
pulse.fromValue = 1.25
pulse.toValue = 1.0
pulse.duration = pulse.settlingDuration
layer?.add(pulse, forKey: nil)
複製程式碼
效果圖前後對比:
這邊要注意duration
。要使用系統根據當前引數估算的彈簧動畫從開始到結束的時間pulse.settlingDuration
。
彈簧系統不能在0.25秒內穩定下來; 提供的變數意味著動畫應該在它停止前再執行一段時間。 關於如何切斷彈簧動畫的視覺演示:
如果抖動時間太長,可以加大阻尼係數damping
,比如:pulse.damping = 7.5
。
彈簧動畫屬性
CASpringAnimation
預定義的彈簧動畫屬性的預設值分別是:
damping: 10.0
mass: 1.0
stiffness: 100.0
initialVelocity: 0.0
複製程式碼
實現文字框的一個代理方法:
func textFieldDidEndEditing(_ textField: UITextField) {
guard let text = textField.text else {
return
}
if text.count < 5 {
let jump = CASpringAnimation(keyPath: "position.y")
jump.fromValue = textField.layer.position.y + 1.0
jump.toValue = textField.layer.position.y
jump.duration = jump.settlingDuration
textField.layer.add(jump, forKey: nil)
}
}
複製程式碼
上面程式碼,表示當使用者在文字中輸入結束後,如果輸入字元數小於5,出現一個小幅度的抖動動畫,提醒使用者太短了。
initialVelocity
起始速度,預設值0。
在設定持續時間前新增,也就是在jump.duration = jump.settlingDuration
前新增:
jump.initialVelocity = 100.0
複製程式碼
效果:
由於開始時的額外推動,文字框彈的更高了。
mass
增加初始速度會使動畫持續時間更長,如果增加質量會怎麼樣?
在jump.initialVelocity = 100.0
後新增:
jump.mass = 10.0
複製程式碼
效果:
額外質量使文字框的跳躍的要高了,並且穩定下來的持續時間更久了。
stiffness
剛度,預設是100。越大彈簧更“硬”。
在jump.mass = 10.0
後新增:
jump.stiffness = 1500.0
複製程式碼
效果:
現在跳躍的不是那麼高了。
damping
動畫看起來很棒,但似乎確實有點太長了。 增加系統阻尼以使動畫更快地穩定下來。
在jump.stiffness = 1500.0
後新增:
jump.damping = 50.0
複製程式碼
效果:
特殊圖層屬性
在文字框抖動時,新增有顏色的邊框。
在textFieldDidEndEditing(_:)
中的textField.layer.add(jump, forKey: nil)
後新增:
textField.layer.borderWidth = 3.0
textField.layer.borderColor = UIColor.clear.cgColor
複製程式碼
此程式碼給文字框周圍新增了透明邊框。 在上面程式碼後新增:
let flash = CASpringAnimation(keyPath: "borderColor")
flash.damping = 7.0
flash.stiffness = 200.0
flash.fromValue = UIColor(red: 1.0, green: 0.27, blue: 0.0, alpha: 1.0).cgColor
flash.toValue = UIColor.white.cgColor
flash.duration = flash.settlingDuration
textField.layer.add(flash, forKey: nil)
複製程式碼
執行,放慢效果:
注意:在某些iOS版本中,圖層動畫會刪除文字欄位的圓角。此情況可在最後一段程式碼之後新增此行:
textField.layer.cornerRadius = 5
.
把登入按鈕的圓角和背景色變化動畫轉化為彈性動畫
這個改變很方便,只要修改ViewController.swift
中兩個函式:
// 背景顏色變化的圖層動畫
func tintBackgroundColor(layer: CALayer, toColor: UIColor) {
// let tint = CABasicAnimation(keyPath: "backgroundColor")
// tint.fromValue = layer.backgroundColor
// tint.toValue = toColor.cgColor
// tint.duration = 0.5
// layer.add(tint, forKey: nil)
// layer.backgroundColor = toColor.cgColor
let tint = CASpringAnimation(keyPath: "backgroundColor")
tint.damping = 5.0
tint.initialVelocity = -10.0
tint.fromValue = layer.backgroundColor
tint.toValue = toColor.cgColor
tint.duration = tint.settlingDuration
layer.add(tint, forKey: nil)
layer.backgroundColor = toColor.cgColor
}
// 圓角動畫
func roundCorners(layer: CALayer, toRadius: CGFloat) {
// let round = CABasicAnimation(keyPath: "cornerRadius")
// round.fromValue = layer.cornerRadius
// round.toValue = toRadius
// round.duration = 0.33
// layer.add(round, forKey: nil)
// layer.cornerRadius = toRadius
let round = CASpringAnimation(keyPath: "cornerRadius")
round.damping = 5.0
round.fromValue = layer.cornerRadius
round.toValue = toRadius
round.duration = round.settlingDuration
layer.add(round, forKey: nil)
layer.cornerRadius = toRadius
}
複製程式碼
12-圖層關鍵幀動畫和結構屬性
圖層上的關鍵幀動畫(Layer Keyframe Animations,CAKeyframeAnimation
)與UIView
上的關鍵幀動畫略有不同。 檢視關鍵幀動畫是將獨立簡單動畫組合在一起,可以為不同的檢視和屬性設定動畫,動畫兩者之間可以重疊或存在間隙。
相比之下,CAKeyframeAnimation
允許我們為給定圖層上的單個屬性設定動畫。可以定義動畫的不同關鍵點,但動畫中不能有任何間隙或重疊。 儘管聽起來有些限制,但可以使用CAKeyframeAnimation
建立一些非常引人注目的效果。
在本章中,將建立許多圖層關鍵幀動畫,從非常基本模擬真實世界碰撞到更高階的動畫。 在15-Stroke和路徑動畫中,您將學習如何進一步獲取圖層動畫,並沿給定路徑為圖層設定動畫。
現在,您將在跑步之前走路,併為您的第一層關鍵幀動畫建立一個時髦的搖擺效果。
介紹圖層關鍵幀動畫
想一想基本動畫是如何運作的? 使用fromValue
和toValue
,核心動畫會在指定的持續時間內逐步修改這些值之間的特定圖層屬性。
例如,當在45°和-45°(或π/ 4和-π/ 4)之間旋轉圖層時,只需要指定這兩個值,然後圖層渲染所有中間值以完成動畫:
CAKeyframeAnimation
使用一組值來完成動畫,而不是fromValue
和toValue
。 另外,還需要提供動畫應達到每個值的關鍵點的時間。
在上面的動畫中,圖層從45°旋轉到-45°,但這次它有兩個獨立的階段:
首先,它在動畫持續時間的前三分之二內從45°旋轉到22°,然後它在剩餘的時間內一直旋轉到-45°。 實質上,使用關鍵幀設定動畫,要求我們為設定動畫的屬性提供關鍵值,以及在0.0和1.0之間進行相應數量的相對關鍵時間。
本章的開始專案使用上一章完成的專案
建立圖層關鍵幀動畫
在resetForm()
中新增:
let wobble = CAKeyframeAnimation(keyPath: "transform.rotation")
wobble.duration = 0.25
wobble.repeatCount = 4
wobble.values = [0.0, -.pi/4.0, 0.0, .pi/4.0, 0.0]
wobble.keyTimes = [0.0, 0.25, 0.5, 0.75, 1.0]
heading.layer.add(wobble, forKey: nil)
複製程式碼
keyTimes
是從0.0
到1.0
的一系列值,並且與values
一一對應。在登入按鈕恢復原狀後,heading有一個搖擺的效果:
眼睛敏銳的讀者可能已經注意到我還沒有介紹過結構屬性的動畫。 大多數情況下,你可以放棄動畫結構的單個元件,例如CGPoint的x元件,或CATransformation3D的旋轉元件,但是接下來你會發現動態結構值的動畫比 你可能會先考慮一下。
Animating struct values
結構體是Swift中的一等公民。 實際上,在使用類和結構之間語法上幾乎沒有區別。(關於類和結構體可查以擼程式碼的形式學習Swift-9:類和結構體(Classes and Structures))
但是,核心動畫是一個基於C構建的Objective-C框架,這意味著結構體的處理方式與Swift的結構體截然不同。 Objective-C API喜歡處理物件,因此結構體需要一些特殊的處理。
這就是為什麼對圖層屬性(如顏色或數字)進行動畫製作相對容易的原因,但是為CGPoint
等結構體屬性設定動畫並不容易。
CALayer
有許多可動畫屬性,它們包含struct值,包括CGPoint
型別的位置,CATransform3D
型別的轉換和CGRect
型別的邊界。
為了解決這個問題,Cocoa使用NSValue
類,它可將一個struct
值“包裝”為一個核心動畫好處理的物件。
NSValue
附帶了許多便利初始化程式:
init(cgPoint: CGPoint)
init(cgSize: CGSize)
init(cgRect rect: CGRect)
init(caTransform3D: CATransform3D)
複製程式碼
使用例子, 以下是使用CGPoint的示例位置動畫:
let move = CABasicAnimation(keyPath: "position")
move.duration = 1.0
move.fromValue = NSValue(cgPoint: CGPoint(x: 100.0, y: 100.0))
move.toValue = NSValue(cgPoint: CGPoint(x: 200.0, y: 200.0))
複製程式碼
在把CGPoint
賦值給fromValue
或toValue
之前,需要把CGPoint
轉化為NSValue
,否則動畫無法工作。關鍵幀動畫同樣如此。
熱氣球的關鍵幀動畫
在logIn()
中新增:
let balloon = CALayer()
balloon.contents = UIImage(named: "balloon")!.cgImage
balloon.frame = CGRect(x: -50.0, y: 0.0, width: 50.0, height: 65.0)
view.layer.insertSublayer(balloon, below: username.layer)
複製程式碼
insertSublayer(_:below)
方法建立了一個圖片圖層作為view.layer
的子圖層。
如果需要在螢幕上顯示影像但不需要使用UIView
的所有好處(例如自動佈局約束,附加手勢識別器等),可以簡單地使用上面的程式碼示例中的CALayer
。
在上面的程式碼後新增動畫程式碼:
let flight = CAKeyframeAnimation(keyPath: "position")
flight.duration = 12.0
flight.values = [
CGPoint(x: -50.0, y: 0.0),
CGPoint(x: view.frame.width + 50.0, y: 160.0),
CGPoint(x: -50.0, y: loginButton.center.y)
].map { NSValue(cgPoint: $0) }
flight.keyTimes = [0.0, 0.5, 1.0]
複製程式碼
values
的三個對應點如下:
最後把動畫新增到氣球圖層上,並且設定氣球圖層最終位置:
balloon.add(flight, forKey: nil)
balloon.position = CGPoint(x: -50.0, y: loginButton.center.y)
複製程式碼
執行,效果:
13-形狀和蒙版
本章學習CALayer
的一個子類CAShapeLayer
,它可以在螢幕上繪製各種形狀,從非常簡單到非常複雜都可以。
本章的開始專案 MultiplayerSearch 模擬了正在搜尋線上對手的戰鬥遊戲的起始螢幕。其中一個檢視控制器顯示一個漂亮的背景影像,一些標籤,一個”Search Again“按鈕(預設是透明的),和兩個頭像影像,其中一個將是空的,直到應用程式”找到“一個對手。
頭像檢視
兩個頭像都是AvatarView
類的一個例項。 下面開始完成一些頭像檢視的效果。
開啟AvatarView.swift
,會發現有幾個已定義的屬性,它們分別表示:
photoLayer
:頭像的圖片圖層。
circleLayer
:用於繪製圓的形狀圖層。
maskLayer
:另一個用於繪製蒙版的形狀圖層。
label
:顯示玩家姓名的標籤。
上面的元件已經存在於專案中,但尚未新增到檢視中,第一個任務就是把它們新增動檢視中。 將以下程式碼新增到didMoveToWindow()
:
photoLayer.mask = maskLayer
複製程式碼
這簡單地用maskLayer
中的圓形掩蓋方形影像。
還可以通過@IBDesignable
(關於@IBDesignable
,可檢視iOS tutorial 8:使用IBInspectable 和 IBDesignable定製UI)在storyboard中看到設定屬性。
執行效果:
現在將圓形邊框圖層新增到頭像檢視圖層,在didMoveToWindow()
中新增程式碼:
layer.addSublayer(circleLayer)
複製程式碼
這時的效果為:
新增名字標籤:
addSubview(label)
複製程式碼
反彈動畫
下面建立類似兩個物體相撞,然後彈開的反彈(bounce-off)動畫。
在ViewController
中建立searchForOpponent()
函式,並在viewDidAppear
中呼叫:
func searchForOpponent() {
let avatarSize = myAvatar.frame.size
let bounceXOffset: CGFloat = avatarSize.width/1.9
let morphSize = CGSize(width: avatarSize.width * 0.85, height: avatarSize.height * 1.1)
}
複製程式碼
bounceXOffset
是相互反彈時應移動的水平距離。
morphSize
是頭像碰撞後的形變大小(寬度變小,長度變大)。
在searchForOpponent()
裡繼續新增:
let rightBouncePoint = CGPoint(x: view.frame.size.width/2.0 + bounceXOffset, y: myAvatar.center.y)
let leftBouncePoint = CGPoint(x: view.frame.size.width/2.0 - bounceXOffset, y: myAvatar.center.y)
myAvatar.bounceOff(point: rightBouncePoint, morphSize: morphSize)
opponentAvatar.bounceOff(point: leftBouncePoint, morphSize: morphSize)
複製程式碼
上面的bounceOff(point:morphSize:)
方法,兩個引數分別代表頭像移動的位置和變形的大小。在AvatarView
中新增:
func bounceOff(point: CGPoint, morphSize: CGSize) {
let originalCenter = center
UIView.animate(withDuration: animationDuration, delay: 0.0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.0, animations: {
self.center = point
}, completion: {_ in
})
UIView.animate(withDuration: animationDuration, delay: animationDuration, usingSpringWithDamping: 0.7, initialSpringVelocity: 1.0, animations: {
self.center = originalCenter
}) { (_) in
delay(seconds: 0.1) {
self.bounceOff(point: point, morphSize: morphSize)
}
}
}
複製程式碼
上面的兩個動畫分別是,使用彈簧動畫將頭像移動到指定位置 和 使用彈簧動畫將頭像移動到原來位置。此時效果如下:
影像變形
實際生活中,兩個物體相撞時,有一個短時間暫停,並且物體變形(”壓扁“的效果)。下面就實現這種效果。
在bounceOff(point:morphSize:)
新增:
let morphedFrame = (originalCenter.x > point.x) ?
CGRect(x: 0.0, y: bounds.height - morphSize.height, width: morphSize.width, height: morphSize.height) :
CGRect(x: bounds.width - bounds.width, y: bounds.height - morphSize.height, width: morphSize.width, height: morphSize.height)
複製程式碼
通過originalCenter.x > point.x
來判斷是左邊頭像還是右邊頭像。
在bounceOff(point:morphSize:)
繼續新增:
let morphAnimation = CABasicAnimation(keyPath: "path")
morphAnimation.duration = animationDuration
morphAnimation.toValue = UIBezierPath(ovalIn: morphedFrame).cgPath
morphAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
circleLayer.add(morphAnimation, forKey: nil)
複製程式碼
通過UIBezierPath
建立橢圓。
執行後,效果有點問題:
只有邊框圖層發生了變形,圖片圖層沒有變化。
把morphAnimation
動畫新增到蒙版圖層:
maskLayer.add(morphAnimation, forKey: nil)
複製程式碼
這樣的效果就好很多:
搜尋對手
在searchForOppoent()
裡最後新增delay(seconds: 4.0, completion: foundOppoent)
,然後在ViewController
中新增:
func foundOpponent() {
status.text = "Connecting..."
opponentAvatar.image = UIImage(named: "avatar-2")
opponentAvatar.name = "Andy"
}
複製程式碼
利用延遲來模擬在尋找對手。
在foundOpponent()
裡新增delay(seconds: 4.0, completion: connectedToOpponent)
,然後然後在ViewController
中新增:
func connectedToOpponent() {
myAvatar.shouldTransitionToFinishedState = true
opponentAvatar.shouldTransitionToFinishedState = true
}
複製程式碼
shouldTransitionToFinishedState
是AvatarView
中自定義的屬性,用於判斷連線是否完成,在下面使用。
在connectedToOpponent()
裡新增delay(seconds: 1.0, completion: completed)
,然後然後在ViewController
中新增:
func completed() {
status.text = "Ready to play"
UIView.animate(withDuration: 0.2) {
self.vs.alpha = 1.0
self.searchAgain.alpha = 1.0
}
}
複製程式碼
對手找到後,修改狀態語,並顯示重新搜尋按鈕。
效果:
連線成功後頭像變成正方形
在AvatarView
中新增一個屬性var isSquare = false
,用於判斷頭像是否需要轉換為正方形。
在bounceOff(point:morphSize:)
的第一個動畫(頭像移動到指定位置)的 completion
閉包中新增:
if self.shouldTransitionToFinishedState {
self.animateToSquare()
}
複製程式碼
其中animateToSquare()
為:
// 變換為正方形動畫
func animateToSquare() {
isSquare = true
let squarePath = UIBezierPath(rect: bounds).cgPath
let morph = CABasicAnimation(keyPath: "path")
morph.duration = 0.25
morph.fromValue = circleLayer.path
morph.toValue = squarePath
circleLayer.add(morph, forKey: nil)
maskLayer.add(morph, forKey: nil)
circleLayer.path = squarePath
maskLayer.path = squarePath
}
複製程式碼
在bounceOff(point:morphSize:)
的第二個動畫(頭像移動到原來位置)的 completion
閉包新增判斷:
if !self.isSquare {
self.bounceOff(point: point, morphSize: morphSize)
}
複製程式碼
這樣的最終效果就是:
14-漸變動畫
本章通過以前iOS的螢幕“滑動解鎖”效果來學習漸變動畫(Gradient Animations)。
開始專案 SlideToReveal是一個簡單的單頁面專案,只有一個顯示時間的UILabel
,和一個之後用於漸變動畫的自定義UIView
子類AnimateMaskLabel
。
第一個漸變圖層
CAGradientLayer
是CALayer
的另一個子類,專門用於漸變的圖層。
配置CAGradientLayer
,在屬性gradientLayer
定義的函式塊中新增:
gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.5)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.5)
複製程式碼
這定義了漸變的方向及其起點和終點。
let gradientLayer: CAGradientLayer = { let gradientLayer = CAGradientLayer() gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.5) gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.5) ... }() 複製程式碼
這種寫法表示定義函式後直接呼叫,返回值直接給屬性。這中寫法在其它語言中也比較常見,比如JS。
繼續新增:
let colors = [
UIColor.black.cgColor,
UIColor.white.cgColor,
UIColor.black.cgColor
]
gradientLayer.colors = colors
let locations: [NSNumber] = [0.25, 0.5, 0.75]
gradientLayer.locations = locations
複製程式碼
上面的定義方式和前面學習的圖層關鍵幀動畫 中的values
和keyTimes
有點類似。
結果就是漸變以黑色開始,中間白色,最後為黑色。通過locations
指定這些顏色應該出現在漸變過程中的確切位置。當然也是可以很多個顏色點,和對應位置點的。
上面的效果就類似:
在layoutSubviews()
中定義漸變圖層的frame
:
gradientLayer.frame = bounds
layer.addSublayer(gradientLayer)
複製程式碼
這就把漸變的圖層定義在AnimateMaskLabel
。
給漸變圖層新增動畫
在didMoveToWindow()
中新增:
let gradientAnimation = CABasicAnimation(keyPath: "locations")
gradientAnimation.fromValue = [0.0, 0.0, 0.25]
gradientAnimation.toValue = [0.75, 1.0, 1.0]
gradientAnimation.duration = 3.0
gradientAnimation.repeatCount = .infinity
gradientLayer.add(gradientAnimation, forKey: nil)
複製程式碼
repeatCount
設定為無窮大,動畫持續3秒並將永遠重複。效果如下:
上面的效果可能一時不好理解,如果把漸變圖層的locations
分別設定成[0.0, 0.0, 0.25]
和[0.75, 1.0, 1.0]
,也就是動畫開始點和結束點,情況分別是:
動畫的效果就是前者的狀態到後者的狀態,這樣就方便理解了。
這看起來很漂亮,但漸變寬度有點小。 只需放大漸變邊界,就會得到更溫和的漸變。
在layoutSubviews()
中找到gradientLayer.frame = bounds
行,替代為:
gradientLayer.frame = CGRect(x: -bounds.size.width, y: bounds.origin.y, width: 3 * bounds.size.width, height: bounds.size.height)
複製程式碼
這會將漸變框設定為可見區域寬度的三倍。 動畫進入檢視,直接穿過它,並從右側退出:
效果:
建立文字蒙版
在AnimateMaskLabel
中創造一個文字屬性:
let textAttributes: [NSAttributedString.Key: Any] = {
let style = NSMutableParagraphStyle()
style.alignment = .center
return [
NSAttributedString.Key.font: UIFont(name: "HelveticaNeue-Thin", size: 28.0)!,
NSAttributedString.Key.paragraphStyle: style
]
}()
複製程式碼
接下來,需要將文字渲染為影像。 在text
屬性的屬性觀察者中的setNeedsDisplay()
之後新增以下程式碼:
let image = UIGraphicsImageRenderer(size: bounds.size).image { (_) in
text.draw(in: bounds, withAttributes: textAttributes)
}
複製程式碼
在這裡,使用影像渲染器來設定上下文。
使用該影像在漸變圖層上建立蒙版,在上面程式碼後繼續新增:
let maskLayer = CALayer()
maskLayer.backgroundColor = UIColor.clear.cgColor
maskLayer.frame = bounds.offsetBy(dx: bounds.size.width, dy: 0)
maskLayer.contents = image.cgImage
gradientLayer.mask = maskLayer
複製程式碼
現在效果:
滑動手勢
在viewDidLoad()
中新增:
let swipe = UISwipeGestureRecognizer(target: self, action: #selector(ViewController.didSlide))
swipe.direction = .right
slideView.addGestureRecognizer(swipe)
複製程式碼
效果:
彩色漸變
修改漸變的圖層的colors
和locations
,然之前的黑白變成彩色:
let colors = [
UIColor.yellow.cgColor,
UIColor.green.cgColor,
UIColor.orange.cgColor,
UIColor.cyan.cgColor,
UIColor.red.cgColor,
UIColor.yellow.cgColor
]
複製程式碼
let locations: [NSNumber] = [0.0, 0.0, 0.0, 0.0, 0.0, 0.25]
複製程式碼
並修改動畫的fromValue
和toValue
:
gradientAnimation.fromValue = [0.0, 0.0, 0.0, 0.0, 0.0, 0.25]
gradientAnimation.toValue = [0.65, 0.8, 0.85, 0.9, 0.95, 1.0]
複製程式碼
效果:
本章的最終效果:
15-Stroke和路徑動畫
注: stroke 可翻譯成 筆畫,但好像又不當恰當,就乾脆不翻譯?。
開始專案 PullToRefresh
有一個TableView,下拉新檢視保持可見狀態四秒鐘,然後縮回。本章就是在這個下拉檢視中做一個類似菊花轉的動畫。
建立互動stroke動畫
構建動畫的第一步是建立一個圓形。 開啟RefreshView.swift
並將以下程式碼新增到init(frame:scrollView:)
中:
// 飛機移動路線圖層
ovalShapeLayer.strokeColor = UIColor.white.cgColor
ovalShapeLayer.fillColor = UIColor.clear.cgColor
ovalShapeLayer.lineWidth = 4.0
ovalShapeLayer.lineDashPattern = [2, 3]
let refreshRadius = frame.size.height/2 * 0.8
ovalShapeLayer.path = UIBezierPath(ovalIn: CGRect(x: frame.size.width/2 - refreshRadius, y: frame.size.height/2 - refreshRadius, width: 2 * refreshRadius, height: 2 * refreshRadius)).cgPath
layer.addSublayer(ovalShapeLayer)
複製程式碼
ovalShapeLayer
是一個型別為CAShapeLayer
的RefreshView
的屬性。CAShapeLayer
之前已經學過了, 在這裡,只需設定筆觸和填充顏色,並將圓直徑設定為檢視高度的80%,這樣可確保形成舒適的邊距。
lineDashPattern
屬性是設定虛線模式,它是一個陣列,其中包含短劃線的長度和間隙的長度(以畫素為單位),當然還可以設定很多種虛線,詳細的可檢視官方文件。
在redrawFromProgress()
中新增:
ovalShapeLayer.strokeEnd = progress
複製程式碼
把飛機圖片新增到飛機圖層中,在init(frame:scrollView:)
中新增:
// 新增飛機
let airplaneImage = UIImage(named: "airplane.png")!
airplaneLayer.contents = airplaneImage.cgImage
airplaneLayer.bounds = CGRect(x: 0.0, y: 0.0, width: airplaneImage.size.width, height: airplaneImage.size.height)
airplaneLayer.position = CGPoint(x: frame.size.width/2 + frame.size.height/2 * 0.8, y: frame.size.height/2)
layer.addSublayer(airplaneLayer)
airplaneLayer.opacity = 0.0
複製程式碼
下拉時逐步更改飛機圖層的不透明度,在redrawFromProgress()
新增:
airplaneLayer.opacity = Float(progress)
複製程式碼
stroke的結尾
在beginRefreshing()
中新增:
let strokeStartAnimation = CABasicAnimation(keyPath: "strokeStart")
strokeStartAnimation.fromValue = -0.5
strokeStartAnimation.toValue = 1.0
let strokeEndAnimation = CABasicAnimation(keyPath: "strokeEnd")
strokeEndAnimation.fromValue = 0.0
strokeEndAnimation.toValue = 1.0
複製程式碼
在beginRefreshing()
的末尾新增以下程式碼以同時執行兩個動畫:
let strokeAnimationGroup = CAAnimationGroup()
strokeAnimationGroup.duration = 1.5
strokeAnimationGroup.repeatDuration = 5.0
strokeAnimationGroup.animations = [strokeEndAnimation, strokeEndAnimation]
ovalShapeLayer.add(strokeAnimationGroup, forKey: nil)
複製程式碼
在上面的程式碼中,建立一個動畫組並重復動畫五次。 這應該足夠長,以便在重新整理檢視可見時保持動畫執行。 然後,將兩個動畫新增到組中,並將組新增到載入層。
執行效果:
建立path關鍵幀動畫
在12-圖層關鍵幀動畫和結構屬性 學習了使用values
屬性來設定關鍵幀動畫。下面學習另一種方式使用關鍵幀動畫。
在beginRefreshing()
的末尾新增飛機動畫:
// 飛機動畫
let flightAnimation = CAKeyframeAnimation(keyPath: "position")
flightAnimation.path = ovalShapeLayer.path
flightAnimation.calculationMode = CAAnimationCalculationMode.paced
let flightAnimationGroup = CAAnimationGroup()
flightAnimationGroup.duration = 1.5
flightAnimationGroup.repeatDuration = 5.0
flightAnimationGroup.animations = [flightAnimation]
airplaneLayer.add(flightAnimationGroup, forKey: nil)
複製程式碼
CAAnimationCalculationMode.paced
是另一種控制動畫時間的方法,這時核心動畫會以恆定的速度設定動畫,忽略設定的任何keyTimes
,這對於在任意路徑上生成平滑動畫非常有用。
CAAnimationCalculationMode
還有其他幾種模式,詳細可檢視官方文件。
執行效果:
這比較奇怪了,✈️移動時,角度也有相應的變化。
在建立flightAnimationGrou
p的行上方插入以下新動畫程式碼,來調整飛機移動時角度
let airplaneOrientationAnimation = CABasicAnimation(keyPath: "transform.rotation")
airplaneOrientationAnimation.fromValue = 0
airplaneOrientationAnimation.toValue = 2.0 * .pi
複製程式碼
最終效果
16-複製動畫
本章節學習複製動畫(Replicating Animations)。
CAReplicatorLayer
是CALayer
的另一個子類。它意思很簡單,當建立了一些內容 —— 可以是一個形狀,一個影像或任何可以用圖層繪製的東西 —— 而CAReplicatorLayer
可以在螢幕上覆制它,如下所示:
為什麼需要複製形狀或影像?
CAReplicatorLayer
的超級強大之處,在於可以讓每個複製體與母體略有不同。
例如,可以逐步更改每個副本的顏色。 原始圖層可能是洋紅色,而在建立每個副本時,將顏色向青色方向改變:
此外,還可以在副本之間應用轉換(transform)。 例如,可以在每個副本之間應用簡單的旋轉轉換,將它們繪製成圓形,如下所示:
但最好的功能是每個副本都能夠設定動畫延遲。 當原始內容的instanceDelay
設定0.2秒時,第一個副本將延遲0.2秒執行動畫,第二個副本將延遲0.4秒執行動畫,第三個副本將延遲0.6秒執行動畫,依此類推。
可以使用這種方式來建立引人入勝且複雜的動畫。
在本章中,將建立一個模仿Siri,聽到聲音後,根據聲音而產生波浪狀的動畫。這個開始專案 命名為Iris。
這個專案將建立兩個不同的複製。 首先,是在Iris會話時播放的視覺反饋動畫,它看起來很像一個迷幻的正弦波:
然後是一個互動式麥克風驅動的音訊波,當使用者說話時,它將提供視覺反饋:
這兩個動畫覆蓋了CAReplicatorLayer
的大部分功能。
Replicating like rabbits
開始專案概述
開啟Main.storyboard
:
只有一個檢視控制器,它具有一個按鈕和一個標籤。 使用者在按下按鈕時詢問問題; 當他們釋放按鈕時,Iris會做出回應。 標籤用來顯示麥克風輸入和Iris的答案。
在ViewController.swift
中,按鈕事件已連線到操作。當使用者觸控按鈕時,actionStartMonitoring()
會觸發;當使用者抬起手指時,actionEndMonitoring()
會觸發。
另外還有兩個超出本章範圍的類:
Assistant
:人工智慧助理。它預定義的有趣答案列表,並根據使用者的問題說出來。
MicMonitor
:監控iPhone麥克風上的輸入,並反覆呼叫您提供的閉包表示式。這是您有機會更新顯示的地方。
下面開始!
設定複製器層
開啟ViewController.swift
並新增以下兩個屬性:
let replicator = CAReplicatorLayer()
let dot = CALayer()
複製程式碼
dot
使用CALayer
,用來繪製基本的簡單形狀。replicator
作為複製器,用來之後複製dot
。
下面新增一些常量 屬性:
let dotLength: CGFloat = 6.0
let dotOffset: CGFloat = 8.0
複製程式碼
doLength
用作點圖層的寬度和高度,dotOffset
是每個點複製體之間的偏移量。
將複製器層新增到檢視控制器的檢視中,在viewDidLoad()
中新增:
replicator.frame = view.bounds
view.layer.addSublayer(replicator)
複製程式碼
下一步是設定點圖層。 在viewDidLoad()
中新增:
dot.frame = CGRect(x: replicator.frame.size.width - dotLength, y: replicator.position.y, width: dotLength, height: dotLength)
dot.backgroundColor = UIColor.lightGray.cgColor
dot.borderColor = UIColor(white: 1.0, alpha: 1.0).cgColor
dot.borderWidth = 0.5
dot.cornerRadius = 1.5
replicator.addSublayer(dot)
複製程式碼
先將點圖層定位到複製器的右邊緣,然後設定圖層的背景顏色並新增邊框等,最後將點圖層加入複製器圖層。執行結果:
在繼續下面之前,先介紹CAReplicatorLayer
的三個屬性:
instanceCount
: 副本數
instanceTransform
: 副本之間的轉換
instanceDelay
: 副本之間的動畫延遲
在viewDidLoad()
中新增:
replicator.instanceCount = Int(view.frame.size.width / dotOffset)
replicator.instanceTransform = CATransform3DMakeTranslation(-dotOffset, 0.0, 0.0)
複製程式碼
螢幕寬度除以偏移量,根據不同螢幕寬度設定副本數。比如5.5英寸(寬度為414)的instanceCount
是51,4.7英寸是46 。。。
每個副本向左(-dotOffset
)移動8 。結果為:
測試複製動畫
新增一個小測試動畫,來了解instanceDelay
的作用。 在viewDidLoad()
的末尾新增:
let move = CABasicAnimation(keyPath: "position.y")
move.fromValue = dot.position.y
move.toValue = dot.position.y - 50.0
move.duration = 1.0
move.repeatCount = 10
dot.add(move, forKey: nil)
複製程式碼
這個動畫很簡單,只是把點向上重複移動10次。
在上面程式碼的的末尾新增:
replicator.instanceDelay = 0.02
複製程式碼
效果:
在繼續之前,需要刪除上面的測試動畫,除了instanceDelay
。
複製多個動畫
在本節中,您將學習在Iris講話時播放的動畫。 為此,您將結合使用具有不同延遲的多個簡單動畫來產生最終效果。
縮放動畫
首先,在startSpeaking()
中新增以下動畫:
let scale = CABasicAnimation(keyPath: "transform")
scale.fromValue = NSValue(caTransform3D: CATransform3DIdentity)
scale.toValue = NSValue(caTransform3D: CATransform3DMakeScale(1.4, 15, 1.0))
scale.duration = 0.33
scale.repeatCount = .infinity
scale.autoreverses = true
scale.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
dot.add(scale, forKey: "dotScale")
複製程式碼
這是一個簡單的層動畫,重點在CATransform3DMakeScale
的幾個引數選擇。此處將點圖層在垂直方向縮放15倍。
執行,並點選灰色按鈕,分別先後呼叫actionStartMonitoring
,actionEndMonitoring()
,最後呼叫startSpeaking()
,效果:
可以嘗試修改CATransform3DMakeScale
的幾個引數和duration
來看看有什麼不同效果。
透明動畫
在startSpeaking()
新增淡出動畫:
let fade = CABasicAnimation(keyPath: "opacity")
fade.fromValue = 1.0
fade.toValue = 0.2
fade.duration = 0.33
fade.beginTime = CACurrentMediaTime() + 0.33
fade.repeatCount = .infinity
fade.autoreverses = true
fade.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
dot.add(fade, forKey: "dotOpacity")
複製程式碼
與縮放動畫的持續時間相同,但延遲0.33秒,透明度從1.0到0.2,當“波浪”充分移動後,開始淡出效果。
當兩個動畫同時執行時,效果會更好一點:
色彩動畫
設定點背景顏色變化動畫,在startSpeaking()
新增:
let tint = CABasicAnimation(keyPath: "backgroundColor")
tint.fromValue = UIColor.magenta.cgColor
tint.toValue = UIColor.cyan.cgColor
tint.duration = 0.66
tint.beginTime = CACurrentMediaTime() + 0.28
tint.fillMode = kCAFillModeBackwards
tint.repeatCount = .infinity
tint.autoreverses = true
tint.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
dot.add(tint, forKey: "dotColor")
複製程式碼
三種動畫的效果:
CAReplicatorLayer的屬性
前面已經通過複製器層製作了很多令人眼花繚亂的效果。 由於CAReplicatorLayer
本身就是一個圖層,因此也可以為其自身的一些屬性設定動畫。
可以為CAReplicatorLayer
的基本屬性(如position
,backgroundColor
或cornerRadius
)設定動畫,也可以通過其特殊的屬性設定非常酷的動畫。
CAReplicatorLayer
特有的可動畫屬性包括(前面已經介紹過三個):
instanceDelay
: 副本之間的動畫延遲
instanceTransform
:副本之間的轉換
instanceColor
: 顏色
instanceRedOffset
,instanceGreenOffset
,instanceBlueOffset
:應用增量以應用於每個例項顏色元件
instanceAlphaOffset
: 透明度增量
在startSpeaking()
的末尾新增一個動畫:
let initialRotation = CABasicAnimation(keyPath: "instanceTransform.rotation")
initialRotation.fromValue = 0.0
initialRotation.toValue = 0.01
initialRotation.duration = 0.33
initialRotation.isRemovedOnCompletion = false
initialRotation.fillMode = kCAFillModeForwards
initialRotation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
replicator.add(initialRotation, forKey: "initialRotation")
複製程式碼
上面只是有一個微小的旋轉,效果:
再需要一個上下扭動的效果,新增下面的動畫以完成效果:
let rotation = CABasicAnimation(keyPath: "instanceTransform.rotation")
rotation.fromValue = 0.01
rotation.toValue = -0.01
rotation.duration = 0.99
rotation.beginTime = CACurrentMediaTime() + 0.33
rotation.repeatCount = .infinity
rotation.autoreverses = true
rotation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
replicator.add(rotation, forKey: "replicatorRotation")
複製程式碼
這是在instanceTransform.rotation
上執行第二個動畫,它在之前第一個動畫完成後啟動。將旋轉從0.01弧度(第一個動畫的最終值)設定到-0.01弧度,這就有了扭到的效果(不同方向的旋轉)。
效果:
下面模擬語音助手,假裝回單。startSpeaking()
的開始處新增:
meterLabel.text = assistant.randomAnswer()
assistant.speak(meterLabel.text!, completion: endSpeaking)
speakButton.isHidden = true
複製程式碼
從Assistant
類中隨機獲得一個答案,然後在meterLabel
上顯示,並且讀處答案,讀完後呼叫endSpeaking
方法。這是過程中按鈕需要隱藏。
之後,需要刪除所有正在執行的動畫,在endSpeaking()
中新增:
replicator.removeAllAnimations()
複製程式碼
接下來,需要將點圖層“優雅”地設定為原始比例的動畫, 在endSpeaking()
繼續中新增:
let scale = CABasicAnimation(keyPath: "transform")
scale.toValue = NSValue(caTransform3D: CATransform3DIdentity)
scale.duration = 0.33
scale.isRemovedOnCompletion = false
scale.fillMode = kCAFillModeForwards
dot.add(scale, forKey: nil)
複製程式碼
上面的動畫,沒有指定fromValue
,會從當前值開始動畫,變換為CATransform3DIdentiy
。
最後,刪除dot
中當前正在執行的其餘動畫,並恢復說話按鈕狀態。 在endSpeaking()
繼續中新增:
dot.removeAnimation(forKey: "dotColor")
dot.removeAnimation(forKey: "dotOpacity")
dot.backgroundColor = UIColor.lightGray.cgColor
speakButton.isHidden = false
複製程式碼
本節的效果:
互動式複製動畫
前面這有Iris回答時,才會有對應波動動畫。這一節要做的是,當使用者按住按鈕說話(問問題)時也就對應波動動畫。
在actionStartMonitoring()
中新增:
dot.backgroundColor = UIColor.green.cgColor
monitor.startMonitoringWithHandler { (level) in
self.meterLabel.text = String(format: "%.2f db", level)
}
複製程式碼
當使用者按下說話按鈕時,觸發actionStartMonitoring
。為了表示“正在收聽”,將點圖層顏色更改為綠色。
然後在監視器例項上呼叫startMonitoringWithHandler()
,它的引數是一個閉包塊,會被重複執行,獲取麥克風分貝數(db)。
這邊的分貝數和我們平常見到分貝數範圍有點不同, 它的值在-160.0 db到0.0 db的範圍內,-160.0 db是最安靜的,0.0 db意味著非常大的聲音。
向上面的閉包中新增一段程式碼,新增完如下:
monitor.startMonitoringWithHandler { (level) in
self.meterLabel.text = String(format: "%.2f db", level)
let scaleFactor = max(0.2, CGFloat(level) + 50) / 2
}
複製程式碼
scaleFactor
將儲存介於0.1和25.0之間的值。
在ViewController
新加一個屬性:
var lastTransformScale: CGFloat = 0.0
複製程式碼
對於縮放動畫,比例不斷變化的,lastTransformScale
儲存最後一個縮放值。
在上面的麥克風處理閉包中新增使用者聲音動畫:
let scale = CABasicAnimation(keyPath: "transform.scale.y")
scale.fromValue = self.lastTransformScale
scale.toValue = scaleFactor
scale.duration = 0.1
scale.isRemovedOnCompletion = false
scale.fillMode = kCAFillModeForwards
self.dot.add(scale, forKey: nil)
複製程式碼
最後,儲存lastTransformScale
,接著上面的程式碼新增:
self.lastTransformScale = scaleFactor
複製程式碼
當使用者手指離開按鈕時,需要重置動畫並停止監聽麥克風。 在actionEndMonitoring()
開始處新增:
monitor.stopMonitoring()
dot.removeAllAnimations()
複製程式碼
這個時候,效果:
平滑麥克風輸入和Iris動畫之間的過渡
仔細之前的效果,我發現使用者麥克風輸入動畫和Iris動畫之間是沒有過渡,是直接跳過。這是actionEndMonitoring()
中的dot.removeAllAnimations()
造成的。
把dot.removeAllAnimations()
替代為:
// 麥克風輸入和Iris動畫之間的過渡
let scale = CABasicAnimation(keyPath: "transform.scale.y")
scale.fromValue = lastTransformScale
scale.toValue = 1.0
scale.duration = 0.2
scale.isRemovedOnCompletion = false
scale.fillMode = kCAFillModeForwards
dot.add(scale, forKey: nil)
dot.backgroundColor = UIColor.magenta.cgColor
let tint = CABasicAnimation(keyPath: "backgroundColor")
tint.fromValue = UIColor.green.cgColor
tint.toValue = UIColor.magenta.cgColor
tint.duration = 1.2
tint.fillMode = kCAFillModeBackwards
dot.add(tint, forKey: nil)
複製程式碼
本章最後的效果:
本文在我的個人部落格中地址:系統學習iOS動畫之三:圖層動畫