本文是我學習《iOS Animations by Tutorials》 筆記中的一篇。 文中詳細代碼都放在個人Github上 andyRon/LearniOSAnimations。html
系統學習iOS動畫之一:視圖動畫 學習了建立視圖動畫(View Animations),這一部分學習功能更強大、更偏底層的Core Animation(核心動畫) APIs。核心動畫的這個名字可能使人有點誤解,暫時能夠理解爲本文的標題圖層動畫(Layer Animations)。ios
在本書的這一部分中,將學習動畫層而不是視圖以及如何使用特殊圖層。git
圖層是一個簡單的模型類,它公開了許多屬性來表示一些基於圖像的內容。 每一個UIView
都有一個圖層支持(都有一個layer
屬性)。github
視圖 vs 圖層swift
因爲如下緣由,圖層(Layers)與視圖(Views)(對於動畫)不一樣:數組
單個來講,二者的優勢。緩存
視圖:bash
圖層:閉包
視圖和圖層的選擇技巧: 任什麼時候候均可以選擇視圖動畫; 當須要更高的性能時,就須要使用圖層動畫。架構
二者在架構中的位置:
預覽:
本文比較長,圖片比較多,預警⚠️😀。
8-圖層動畫入門 —— 從最簡單的圖層動畫開始,瞭解調試動畫錯誤的方法。
9-動畫的Keys和代理 —— 怎麼更好地控制當前運行的動畫,並使用代理方法對動畫事件作出響應。
10-動畫組和時間控制 —— 組合許多簡單的動畫,並將它們做爲一個組一塊兒運行。
11-圖層彈簧動畫 —— 學習如何使用CASpringAnimation
建立強大而靈活的彈簧圖層動畫。
12-圖層關鍵幀動畫和結構屬性 —— 學習圖層關鍵幀動畫, 動畫結構屬性的一些特殊處理。
接下來,學習幾個專門的圖層:
13-形狀和蒙版 —— 經過CAShapeLayer
在屏幕上繪製形狀,併爲其特殊路徑屬性設置動畫。
14-漸變更畫 —— 瞭解如何使用CAGradientLayer
來繪製漸變和動畫漸變。
15-Stroke和路徑動畫 —— 以交互方式繪製形狀,並使用關鍵幀動畫的一些強大功能。
16-複製動畫 —— 學習如何建立圖層內容的多個副本,而後利用副本製做動畫。
圖層動畫的工做方式與視圖動畫很是類似; 只需在定義的時間段內爲起始值和結束值之間的屬性設置動畫,而後讓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查看。
固然還能夠在右側檢測器中查看實時屬性:
動畫完成後,代碼更改會致使字段跳回其初始位置。 但爲何?
當你爲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 真實內容) 章節
本章節的最終效果:
關於視圖動畫和相應的閉包語法的一個棘手問題是,一旦您建立並運行視圖動畫,您就沒法暫停,中止或以任何方式訪問它。
可是,使用核心動畫,您能夠輕鬆檢查在圖層上運行的動畫,並在須要時中止它們。 此外,您甚至能夠在動畫上設置委託對象並對動畫事件作出反應。
本章的開始項目使用上一章完成的項目
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:)
把動畫添加給圖層時,都會拷貝一份,這在前面的圖層動畫基礎知識中說明過。
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)
移動動畫結束後有一個簡單變大的脈動動畫效果:
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)
}
}
}
複製代碼
本章的效果:
在上一章中,學習瞭如何向單個圖層添加多個獨立動畫。 可是,若是您但願您的動畫同步工做並保持彼此一致,該怎麼辦? 這就用到動畫組(animation groups)。
本章介紹如何使用CAAnimationGroup
對動畫進行分組,能夠向組中添加多個動畫並同時調整持續時間,委託和timingFunction
等屬性。 對動畫進行分組會產生簡化的代碼,並確保您的全部動畫將做爲一個實體單元同步。
本章的開始項目使用上一章完成的項目
刪除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
複製代碼
下面代碼:
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)
複製代碼
本章節的最終效果:
前面視圖動畫中的2-彈簧動畫能夠用於建立一些相對簡單的彈簧式動畫,而本章節學習的**圖層彈簧動畫(Layer Springs)**能夠呈現一個看起來更天然的物理模擬。
本章的開始項目使用上一章完成的項目,添加一些新的圖層彈簧動畫,並說明兩種彈簧動畫之間的差別。
先說一些理論知識:
阻尼諧振子,Damped harmonic oscillators(直譯就是,逐漸衰弱的振盪器),能夠理解爲逐漸衰減的振動。
UIKit API簡化了彈簧動畫的製做,不須要了解它們的原理就能夠很方便的使用。 可是,因爲您如今是核心動畫專家,所以您須要深刻研究細節。
鐘擺,理想情況下鐘擺是不停的擺動,像下面的同樣:
對應的運動軌跡圖就像:
但現實中因爲能量的損耗,鐘擺的搖擺的幅度會逐漸減少:
對應的運動軌跡:
這就是一個阻尼諧振子 。
鐘擺停下來所需的時間長度,以及最終振盪器圖形的方式取決於振盪系統的如下參數:
阻尼(damping):因爲空氣摩擦、機械摩擦和其餘做用在系統上的外部減速力。
質量(mass):擺錘越重,擺動的時間越長。
剛度(stiffness):振盪器的「彈簧」越硬(鐘擺的「彈簧」是指地球的引力),鐘擺擺動越困難,系統停下來也越快。想象一下,若是在月球或木星上使用這個鐘擺;在低重力和高重力狀況下的運動將是徹底不一樣的。
初始速度(initial velocity):推一下鐘擺。
「這一切都很是有趣,但與彈簧動畫有什麼關係呢?」
阻尼諧振子系統是推進iOS中彈簧動畫的動力。 下一節將更詳細地討論這個問題。
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
}
複製代碼
圖層上的關鍵幀動畫(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的旋轉組件,可是接下來你會發現動態結構值的動畫比 你可能會先考慮一下。
結構體是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)
複製代碼
運行,效果:
本章學習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)
}
複製代碼
這樣的最終效果就是:
本章經過之前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]
複製代碼
效果:
本章的最終效果:
注: stroke 可翻譯成 筆畫,但好像又不當恰當,就乾脆不翻譯😏。
開始項目 PullToRefresh
有一個TableView,下拉新視圖保持可見狀態四秒鐘,而後縮回。本章就是在這個下拉視圖中作一個相似菊花轉的動畫。
構建動畫的第一步是建立一個圓形。 打開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)
複製代碼
在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)
複製代碼
在上面的代碼中,建立一個動畫組並重復動畫五次。 這應該足夠長,以便在刷新視圖可見時保持動畫運行。 而後,將兩個動畫添加到組中,並將組添加到加載層。
運行效果:
在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
複製代碼
最終效果
本章節學習複製動畫(Replicating Animations)。
CAReplicatorLayer
是CALayer
的另外一個子類。它意思很簡單,當建立了一些內容 —— 能夠是一個形狀,一個圖像或任何能夠用圖層繪製的東西 —— 而CAReplicatorLayer
能夠在屏幕上覆制它,以下所示:
爲何須要複製形狀或圖像?
CAReplicatorLayer
的超級強大之處,在於可讓每一個複製體與母體略有不一樣。 例如,能夠逐步更改每一個副本的顏色。 原始圖層多是洋紅色,而在建立每一個副本時,將顏色向青色方向改變:
此外,還能夠在副本之間應用轉換(transform)。 例如,能夠在每一個副本之間應用簡單的旋轉轉換,將它們繪製成圓形,以下所示:
但最好的功能是每一個副本都可以設置動畫延遲。 當原始內容的instanceDelay
設置0.2秒時,第一個副本將延遲0.2秒執行動畫,第二個副本將延遲0.4秒執行動畫,第三個副本將延遲0.6秒執行動畫,依此類推。
可使用這種方式來建立引人入勝且複雜的動畫。
在本章中,將建立一個模仿Siri,聽到聲音後,根據聲音而產生波浪狀的動畫。這個開始項目 命名爲Iris。
這個項目將建立兩個不一樣的複製。 首先,是在Iris會話時播放的視覺反饋動畫,它看起來很像一個迷幻的正弦波:
而後是一個交互式麥克風驅動的音頻波,當用戶說話時,它將提供視覺反饋:
這兩個動畫覆蓋了CAReplicatorLayer
的大部分功能。
打開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
的基本屬性(如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動畫之間是沒有過渡,是直接跳過。這是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動畫之三:圖層動畫