系統學習iOS動畫之三:圖層動畫

本文是我學習《iOS Animations by Tutorials》 筆記中的一篇。 文中詳細代碼都放在個人Github上 andyRon/LearniOSAnimationshtml

系統學習iOS動畫之一:視圖動畫 學習了建立視圖動畫(View Animations),這一部分學習功能更強大、更偏底層的Core Animation(核心動畫) APIs。核心動畫的這個名字可能使人有點誤解,暫時能夠理解爲本文的標題圖層動畫(Layer Animations)ios

在本書的這一部分中,將學習動畫層而不是視圖以及如何使用特殊圖層。git

圖層是一個簡單的模型類,它公開了許多屬性來表示一些基於圖像的內容。 每一個UIView都有一個圖層支持(都有一個layer屬性)。github

視圖 vs 圖層swift

因爲如下緣由,圖層(Layers)與視圖(Views)(對於動畫)不一樣:數組

  • 圖層是一個模型對象 —— 它公開數據屬性而且不實現任何邏輯。 它沒有複雜的自動佈局依賴關係,也不用處理用戶交互。
  • 圖層具備預約義的可見特徵 —— 這些特徵是許多影響內容在屏幕上呈現的數據屬性,例如邊框線,邊框顏色,位置和陰影。
  • 最後,Core Animation優化了圖層內容的緩存並直接在GPU上快速繪圖。

單個來講,二者的優勢。緩存

視圖:bash

  • 複雜視圖層次結構佈局,自動佈局等。
  • 用戶交互。
  • 一般具備在CPU上的主線程上執行的自定義邏輯或自定義繪圖代碼。
  • 很是靈活,功能強大,子類不少類。

圖層:閉包

  • 更簡單的層次結構,更快地解決佈局,繪製速度更快。
  • 沒有響應者鏈開銷。
  • 默認狀況下沒有自定義邏輯 並直接在GPU上繪製。
  • 不那麼靈活,子類的類更少。

視圖和圖層的選擇技巧: 任什麼時候候均可以選擇視圖動畫; 當須要更高的性能時,就須要使用圖層動畫。架構

二者在架構中的位置:

預覽:

本文比較長,圖片比較多,預警⚠️😀。

8-圖層動畫入門 —— 從最簡單的圖層動畫開始,瞭解調試動畫錯誤的方法。
9-動畫的Keys和代理 —— 怎麼更好地控制當前運行的動畫,並使用代理方法對動畫事件作出響應。
10-動畫組和時間控制 —— 組合許多簡單的動畫,並將它們做爲一個組一塊兒運行。
11-圖層彈簧動畫 —— 學習如何使用CASpringAnimation建立強大而靈活的彈簧圖層動畫。
12-圖層關鍵幀動畫和結構屬性 —— 學習圖層關鍵幀動畫, 動畫結構屬性的一些特殊處理。

接下來,學習幾個專門的圖層:

13-形狀和蒙版 —— 經過CAShapeLayer在屏幕上繪製形狀,併爲其特殊路徑屬性設置動畫。
14-漸變更畫 —— 瞭解如何使用CAGradientLayer來繪製漸變和動畫漸變。
15-Stroke和路徑動畫 —— 以交互方式繪製形狀,並使用關鍵幀動畫的一些強大功能。
16-複製動畫 —— 學習如何建立圖層內容的多個副本,而後利用副本製做動畫。

8-圖層動畫入門

圖層動畫的工做方式與視圖動畫很是類似; 只需在定義的時間段內爲起始值和結束值之間的屬性設置動畫,而後讓Core Animation處理二者之間的渲染。

可是,圖層動畫具備比視圖動畫更多的可動畫屬性; 在設計效果時,這會提供了不少選擇和靈活性; 圖層動畫還有許多專門的CALayer子類(如CATextLayerCAShapeLayerCATransformLayerCAGradientLayerCAReplicatorLayerCAScrollLayerCAEmitterLayerAVPlayerLayer等),這些子類有提供了許多其餘屬性。

本章介紹CALayer和Core Animation的基礎知識。

可動畫屬性

可與視圖動畫的可動畫屬性對照着看。

位置 和 大小

boundspositiontransform

borderColorborderWidthcornerRadius

image-20181015154228090

陰影

image-20181015154548338

shadowOffset: 使陰影看起來更接近或更遠離圖層。 shadowOpacity:使陰影淡入或淡出。 shadowPath: 更改圖層陰影的形狀。 能夠建立不一樣的3D效果,使圖層看起來像浮動在不一樣的陰影形狀和位置上。 shadowRadius: 控制陰影的模糊; 當模擬視圖朝向或遠離投射陰影的表面移動時,這尤爲有用。

內容

contents :修改此項以將原始TIFF或PNG數據指定爲圖層內容。

mask :修改它將用於掩蓋圖層可見內容的形狀或圖像。 這個屬性在13-形狀和蒙版將詳細介紹和使用。

opacity

第一個圖層動畫

開始項目使用 3-過渡動畫完成的項目。

把本來head的視圖動畫替換爲圖層動畫。

分別刪除ViewControllerviewWillAppear()中:

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上指定的屬性設置fromValuetoValue。須要動畫對象(此處我要處理的是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 LabelUsername 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)
複製代碼
  • kCAFillModeRemovedfillMode的默認值

    在定義的beginTime處啓動動畫(若是未設置beginTime,也就是beginTime等於CACurrentMediaTime(),則當即啓動動畫), 並在動畫完成時刪除動畫期間所作的更改:

    實際效果:

    nowbegin 這段時間動畫沒有開始,但Username Field直接顯示了,而後到 begin時動畫纔開始,這就是以前遇到的狀況。

  • kCAFillModeBackwards

    不管動畫的實際開始時間如何,kCAFillModeBackwards都會當即在屏幕上顯示動畫的第一幀,並在之後啓動動畫:

    實際效果:

    第一幀在fromValue處,也就是"position.x"是負的在屏幕外,所以開始時沒有看見Username Field,等待2.3s後動畫開始。

  • kCAFillModeForwards

    kCAFillModeForwards像往常同樣播放動畫,但在屏幕上保留動畫的最後一幀,直到您刪除動畫:

    實際效果:

    除了設置kCAFillModeForwards以外,還須要對圖層進行一些其餘更改以使最後一幀「粘貼」。 你將在本章後面稍後瞭解這一點。 和第一個有點相似,但仍是有區別的。

  • kCAFillModeBoth

    kCAFillModeBothkCAFillModeForwardskCAFillModeBackwards的組合; 這會使動畫的第一幀當即出如今屏幕上,並在動畫結束時在屏幕上保留最終幀:

    實際效果:

    要解決以前發現的問題,將使用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查看。

固然還能夠在右側檢測器中查看實時屬性:

image-20181125124028215

動畫完成後,代碼更改會致使字段跳回其初始位置。 但爲何?

動畫 vs 真實內容

當你爲Text Field設置動畫時,你實際上並無看到Text Field自己是動畫的; 相反,你會看到它的緩存版本,稱爲presentation layer(顯示層)。動畫完成後原始圖層再次到本來位置,則從屏幕上移除presentation layer。 首先,請記住在viewWillAppear(_:)中將Text Field設置在屏幕外:

image-20181125145909389

動畫開始時,Text Field暫時隱藏,預渲染的動畫對象將替代它:

image-20181125145923978

如今沒法點擊動畫對象,輸入任何文本或使用任何其餘特定文本字段功能,由於它不是真正的文本字段,只是可見的「幻像」。 動畫一旦完成,它就會從屏幕上消失,原始Text Field將被取消隱藏。但它此時的位置還在屏幕左側!

image-20181125150009137

要解決這個難題,您須要使用另外一個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)
複製代碼

兩種狀態的變化:

image-20181125155719946

兩個動畫函數tintBackgroundColorroundCorners最後都須要把動畫最變化最終值賦值給動畫的屬性,這對應於前面的 [動畫 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 開始結束都慢,中間快

image-20181126112903447

能夠試一下不一樣的效果。

另外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簡化了彈簧動畫的製做,不須要了解它們的原理就能夠很方便的使用。 可是,因爲您如今是核心動畫專家,所以您須要深刻研究細節。

鐘擺,理想情況下鐘擺是不停的擺動,像下面的同樣:

對應的運動軌跡圖就像:

但現實中因爲能量的損耗,鐘擺的搖擺的幅度會逐漸減少:

image-20181112222946546

對應的運動軌跡:

image-20181112223028487

這就是一個阻尼諧振子 。

鐘擺停下來所需的時間長度,以及最終振盪器圖形的方式取決於振盪系統的如下參數:

  • 阻尼(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)
複製代碼

效果圖先後對比:

CABasicAnimation

CASpringAnimation

這邊要注意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和路徑動畫中,您將學習如何進一步獲取圖層動畫,並沿給定路徑爲圖層設置動畫。

如今,您將在跑步以前走路,併爲您的第一層關鍵幀動畫建立一個時髦的搖擺效果。

介紹圖層關鍵幀動畫

想想基本動畫是如何運做的? 使用fromValuetoValue,核心動畫會在指定的持續時間內逐步修改這些值之間的特定圖層屬性。 例如,當在45°和-45°(或π/ 4和-π/ 4)之間旋轉圖層時,只須要指定這兩個值,而後圖層渲染全部中間值以完成動畫:

image-20181127104828153

CAKeyframeAnimation使用一組值來完成動畫,而不是fromValuetoValue。 另外,還須要提供動畫應達到每一個值的關鍵點的時間。

在上面的動畫中,圖層從45°旋轉到-45°,但此次它有兩個獨立的階段:

image-20181127104845088

首先,它在動畫持續時間的前三分之二內從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.01.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賦值給fromValuetoValue以前,須要把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「按鈕(默認是透明的),和兩個頭像圖像,其中一個將是空的,直到應用程序」找到「一個對手。

image-20181214163442348

頭像視圖

兩個頭像都是AvatarView類的一個實例。 下面開始完成一些頭像視圖的效果。 打開AvatarView.swift,會發現有幾個已定義的屬性,它們分別表示:

photoLayer:頭像的圖片圖層。 circleLayer:用於繪製圓的形狀圖層。 maskLayer:另外一個用於繪製蒙版的形狀圖層。 label:顯示玩家姓名的標籤。

上面的組件已經存在於項目中,但還沒有添加到視圖中,第一個任務就是把它們添加動視圖中。 將如下代碼添加到didMoveToWindow()

photoLayer.mask = maskLayer
複製代碼

這簡單地用maskLayer中的圓形掩蓋方形圖像。

還能夠經過@IBDesignable(關於@IBDesignable,可查看iOS tutorial 8:使用IBInspectable 和 IBDesignable定製UI)在storyboard中看到設置屬性。

運行效果:

image-20181214164234894

如今將圓形邊框圖層添加到頭像視圖圖層,在didMoveToWindow()中添加代碼:

layer.addSublayer(circleLayer)
複製代碼

這時的效果爲:

image-20181214164408801

添加名字標籤:

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建立橢圓。

運行後,效果有點問題:

image-20181127144558179

只有邊框圖層發生了變形,圖片圖層沒有變化。

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
}
複製代碼

shouldTransitionToFinishedStateAvatarView中自定義的屬性,用於判斷鏈接是否完成,在下面使用。

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)

image-20181214171411641

開始項目 SlideToReveal是一個簡單的單頁面項目,只有一個顯示時間的UILabel,和一個以後用於漸變更畫的自定義UIView子類AnimateMaskLabel

第一個漸變圖層

CAGradientLayerCALayer的另外一個子類,專門用於漸變的圖層。

配置CAGradientLayer,在屬性gradientLayer定義的函數塊中添加:

gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.5)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.5)
複製代碼

這定義了漸變的方向及其起點和終點。

image-20181128090956756

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
複製代碼

上面的定義方式和前面學習的圖層關鍵幀動畫 中的valueskeyTimes有點相似。

結果就是漸變以黑色開始,中間白色,最後爲黑色。經過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],也就是動畫開始點和結束點,狀況分別是:

image-20181128094342790

image-20181128094501134

動畫的效果就是前者的狀態到後者的狀態,這樣就方便理解了。

這看起來很漂亮,但漸變寬度有點小。 只需放大漸變邊界,就會獲得更溫和的漸變。 在layoutSubviews()中找到gradientLayer.frame = bounds行,替代爲:

gradientLayer.frame = CGRect(x: -bounds.size.width, y: bounds.origin.y, width: 3 * bounds.size.width, height: bounds.size.height)
複製代碼

這會將漸變框設置爲可見區域寬度的三倍。 動畫進入視圖,直接穿過它,並從右側退出:

image-20181128100059850

效果:

建立文本蒙版

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)
複製代碼

效果:

彩色漸變

修改漸變的圖層的colorslocations,然以前的黑白變成彩色:

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]
複製代碼

並修改動畫的fromValuetoValue

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

image-20181214182311997

有一個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是一個類型爲CAShapeLayerRefreshView的屬性。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還有其餘幾種模式,詳細可查看官方文檔

運行效果:

這比較奇怪了,✈️移動時,角度也有相應的變化。

在建立flightAnimationGroup的行上方插入如下新動畫代碼,來調整飛機移動時角度

let airplaneOrientationAnimation = CABasicAnimation(keyPath: "transform.rotation")
airplaneOrientationAnimation.fromValue = 0
airplaneOrientationAnimation.toValue = 2.0 * .pi
複製代碼

最終效果

16-複製動畫

本章節學習複製動畫(Replicating Animations)

CAReplicatorLayerCALayer的另外一個子類。它意思很簡單,當建立了一些內容 —— 能夠是一個形狀,一個圖像或任何能夠用圖層繪製的東西 —— 而CAReplicatorLayer能夠在屏幕上覆制它,以下所示:

爲何須要複製形狀或圖像?

CAReplicatorLayer的超級強大之處,在於可讓每一個複製體與母體略有不一樣。 例如,能夠逐步更改每一個副本的顏色。 原始圖層多是洋紅色,而在建立每一個副本時,將顏色向青色方向改變:

此外,還能夠在副本之間應用轉換(transform)。 例如,能夠在每一個副本之間應用簡單的旋轉轉換,將它們繪製成圓形,以下所示:

image-20181114152157889

但最好的功能是每一個副本都可以設置動畫延遲。 當原始內容的instanceDelay設置0.2秒時,第一個副本將延遲0.2秒執行動畫,第二個副本將延遲0.4秒執行動畫,第三個副本將延遲0.6秒執行動畫,依此類推。

可使用這種方式來建立引人入勝且複雜的動畫。

在本章中,將建立一個模仿Siri,聽到聲音後,根據聲音而產生波浪狀的動畫。這個開始項目 命名爲Iris

這個項目將建立兩個不一樣的複製。 首先,是在Iris會話時播放的視覺反饋動畫,它看起來很像一個迷幻的正弦波:

image-20181214235839247

而後是一個交互式麥克風驅動的音頻波,當用戶說話時,它將提供視覺反饋:

image-20181214235912038

這兩個動畫覆蓋了CAReplicatorLayer的大部分功能。

Replicating like rabbits

開始項目概述

打開Main.storyboard

image-20181215000456402

只有一個視圖控制器,它具備一個按鈕和一個標籤。 用戶在按下按鈕時詢問問題; 當他們釋放按鈕時,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)
複製代碼

先將點圖層定位到複製器的右邊緣,而後設置圖層的背景顏色並添加邊框等,最後將點圖層加入複製器圖層。運行結果:

image-20181215113735985

在繼續下面以前,先介紹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倍。

運行,並點擊灰色按鈕,分別前後調用actionStartMonitoringactionEndMonitoring(),最後調用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的基本屬性(如positionbackgroundColorcornerRadius)設置動畫,也能夠經過其特殊的屬性設置很是酷的動畫。

CAReplicatorLayer特有的可動畫屬性包括(前面已經介紹過三個):

instanceDelay: 副本之間的動畫延遲 instanceTransform:副本之間的轉換 instanceColor: 顏色 instanceRedOffsetinstanceGreenOffsetinstanceBlueOffset:應用增量以應用於每一個實例顏色組件 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動畫之三:圖層動畫

相關文章
相關標籤/搜索