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

Andy_Ron發表於2018-12-27

本文是我學習《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上繪製。
  • 不那麼靈活,子類的類更少。

檢視和圖層的選擇技巧: 任何時候都可以選擇檢視動畫; 當需要更高的效能時,就需要使用圖層動畫。

兩者在架構中的位置:

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

預覽:

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

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

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

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

8-圖層動畫入門

圖層動畫的工作方式與檢視動畫非常相似; 只需在定義的時間段內為起始值和結束值之間的屬性設定動畫,然後讓Core Animation處理兩者之間的渲染。

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

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

可動畫屬性

可與檢視動畫的可動畫屬性對照著看。

位置 和 大小

boundspositiontransform

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

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(),則立即啟動動畫), 並在動畫完成時刪除動畫期間所做的更改:

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

    實際效果:

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

    nowbegin 這段時間動畫沒有開始,但Username Field直接顯示了,然後到 begin時動畫才開始,這就是之前遇到的情況。

  • kCAFillModeBackwards

    無論動畫的實際開始時間如何,kCAFillModeBackwards都會立即在螢幕上顯示動畫的第一幀,並在以後啟動動畫:

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

    實際效果:

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

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

  • kCAFillModeForwards

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

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

    實際效果:

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

    除了設定kCAFillModeForwards之外,還需要對圖層進行一些其他更改以使最後一幀“貼上”。 你將在本章後面稍後瞭解這一點。 和第一個有點類似,但還是有區別的。

  • kCAFillModeBoth

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

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

    實際效果:

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

    要解決之前發現的問題,將使用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(此時這段程式碼可以暫時註釋掉),執行後發現問題,動畫結束後兩個文字框消失了,這是怎麼回事呢?

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

繼續在上面的程式碼後新增一個延遲函式:

delay(seconds: 5.0)
  print("where are the fields?")
}
複製程式碼

並打斷點後執行:

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

進入UI hierarchy 視窗:

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

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

UI hierarchy 模式下可以檢視當前執行時的UI層次結構,包括已經隱藏或透明檢視以及在螢幕外的檢視。還可以3D檢視。

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

當然還可以在右側檢測器中檢視實時屬性:

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 真實內容) 章節

本章節的最終效果:

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

9-動畫的Keys和代理

關於檢視動畫和相應的閉包語法的一個棘手問題是,一旦您建立並執行檢視動畫,您就無法暫停,停止或以任何方式訪問它。

但是,使用核心動畫,您可以輕鬆檢查在圖層上執行的動畫,並在需要時停止它們。 此外,您甚至可以在動畫上設定委託物件並對動畫事件做出反應。

本章的開始專案使用上一章完成的專案

動畫代理介紹

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

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)

移動動畫結束後有一個簡單變大的脈動動畫效果:

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

動畫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是透明度漸漸變大的動畫。

此時的動畫效果如下:

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

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立即到達終點,也就是螢幕中央:

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

當然也可以通過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)
        }
    }
}
複製程式碼

本章的效果:

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

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

登入按鈕的效果為:

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

動畫緩動

圖層動畫中的動畫緩動與1-檢視動畫入門中介紹的檢視動畫的動畫選項的,在概念上是相同的, 只是語法有所不同。

圖層動畫中的動畫緩動通過類CAMediaTimingFunction來表示 。用法如下:

groupAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
複製程式碼

name引數有如下幾種,和檢視動畫中的差不多:

kCAMediaTimingFunctionLinear 速度不變化

kCAMediaTimingFunctionEaseIn 開始時慢,結束時快

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

kCAMediaTimingFunctionEaseOut 開始時快,結束時慢

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

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已經省略了前幾次滾動):

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

這也很好理解,最後一個迴圈以標籤離開螢幕結束。解決辦法就是半個動畫週期:

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

本章節的最終效果:

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

11-圖層彈簧動畫

前面檢視動畫中的2-彈簧動畫可以用於建立一些相對簡單的彈簧式動畫,而本章節學習的**圖層彈簧動畫(Layer Springs)**可以呈現一個看起來更自然的物理模擬。

本章的開始專案使用上一章完成的專案,新增一些新的圖層彈簧動畫,並說明兩種彈簧動畫之間的差異。

先說一些理論知識:

阻尼諧振子

阻尼諧振子,Damped harmonic oscillators(直譯就是,逐漸衰弱的振盪器),可以理解為逐漸衰減的振動。

UIKit API簡化了彈簧動畫的製作,不需要了解它們的原理就可以很方便的使用。 但是,由於您現在是核心動畫專家,因此您需要深入研究細節。

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

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

對應的運動軌跡圖就像:

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

但現實中由於能量的損耗,鐘擺的搖擺的幅度會逐漸減小:

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秒內穩定下來; 提供的變數意味著動畫應該在它停止前再執行一段時間。 關於如何切斷彈簧動畫的視覺演示:

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

如果抖動時間太長,可以加大阻尼係數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
複製程式碼

效果:

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

由於開始時的額外推動,文字框彈的更高了。

mass

增加初始速度會使動畫持續時間更長,如果增加質量會怎麼樣?

jump.initialVelocity = 100.0後新增:

jump.mass = 10.0
複製程式碼

效果:

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

額外質量使文字框的跳躍的要高了,並且穩定下來的持續時間更久了。

stiffness

剛度,預設是100。越大彈簧更“硬”。

jump.mass = 10.0後新增:

jump.stiffness = 1500.0
複製程式碼

效果:

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

現在跳躍的不是那麼高了。

damping

動畫看起來很棒,但似乎確實有點太長了。 增加系統阻尼以使動畫更快地穩定下來。

jump.stiffness = 1500.0後新增:

jump.damping = 50.0
複製程式碼

效果:

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

特殊圖層屬性

在文字框抖動時,新增有顏色的邊框。

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動畫之三:圖層動畫

注意:在某些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和路徑動畫中,您將學習如何進一步獲取圖層動畫,並沿給定路徑為圖層設定動畫。

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

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

介紹圖層關鍵幀動畫

想一想基本動畫是如何運作的? 使用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有一個搖擺的效果:

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

眼睛敏銳的讀者可能已經注意到我還沒有介紹過結構屬性的動畫。 大多數情況下,你可以放棄動畫結構的單個元件,例如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的三個對應點如下:

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

最後把動畫新增到氣球圖層上,並且設定氣球圖層最終位置:

balloon.add(flight, forKey: nil)
balloon.position = CGPoint(x: -50.0, y: loginButton.center.y)
複製程式碼

執行,效果:

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

13-形狀和蒙版

本章學習CALayer的一個子類CAShapeLayer,它可以在螢幕上繪製各種形狀,從非常簡單到非常複雜都可以。

本章的開始專案 MultiplayerSearch 模擬了正在搜尋線上對手的戰鬥遊戲的起始螢幕。其中一個檢視控制器顯示一個漂亮的背景影象,一些標籤,一個”Search Again“按鈕(預設是透明的),和兩個頭像影象,其中一個將是空的,直到應用程式”找到“一個對手。

image-20181214163442348

頭像檢視

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

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

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

上面的元件已經存在於專案中,但尚未新增到檢視中,第一個任務就是把它們新增動檢視中。 將以下程式碼新增到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)
        }
       }
}
複製程式碼

上面的兩個動畫分別是,使用彈簧動畫將頭像移動到指定位置使用彈簧動畫將頭像移動到原來位置。此時效果如下:

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

影象變形

實際生活中,兩個物體相撞時,有一個短時間暫停,並且物體變形(”壓扁“的效果)。下面就實現這種效果。

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

這樣的效果就好很多:

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

搜尋對手

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

對手找到後,修改狀態語,並顯示重新搜尋按鈕。

效果:

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

連線成功後頭像變成正方形

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動畫之三:圖層動畫

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指定這些顏色應該出現在漸變過程中的確切位置。當然也是可以很多個顏色點,和對應位置點的。

上面的效果就類似:

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

layoutSubviews()中定義漸變圖層的frame

gradientLayer.frame = bounds
layer.addSublayer(gradientLayer)
複製程式碼

這就把漸變的圖層定義在AnimateMaskLabel

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

給漸變圖層新增動畫

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秒並將永遠重複。效果如下:

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

上面的效果可能一時不好理解,如果把漸變圖層的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

效果:

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

建立文字蒙版

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
複製程式碼

現在效果:

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

滑動手勢

viewDidLoad()中新增:

let swipe = UISwipeGestureRecognizer(target: self, action: #selector(ViewController.didSlide))
        swipe.direction = .right
        slideView.addGestureRecognizer(swipe)
複製程式碼

效果:

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

彩色漸變

修改漸變的圖層的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]
複製程式碼

效果:

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

本章的最終效果:

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

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

在上面的程式碼中,建立一個動畫組並重復動畫五次。 這應該足夠長,以便在重新整理檢視可見時保持動畫執行。 然後,將兩個動畫新增到組中,並將組新增到載入層。

執行效果:

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

建立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還有其他幾種模式,詳細可檢視官方文件

執行效果:

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

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

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

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

最終效果

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

16-複製動畫

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

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

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

為什麼需要複製形狀或影象?

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

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

此外,還可以在副本之間應用轉換(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 。結果為:

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

測試複製動畫

新增一個小測試動畫,來了解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次。

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

在上面程式碼的的末尾新增:

replicator.instanceDelay = 0.02
複製程式碼

效果:

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

在繼續之前,需要刪除上面的測試動畫,除了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(),效果:

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

可以嘗試修改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,當“波浪”充分移動後,開始淡出效果。

當兩個動畫同時執行時,效果會更好一點:

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

色彩動畫

設定點背景顏色變化動畫,在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")
複製程式碼

三種動畫的效果:

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

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")     
複製程式碼

上面只是有一個微小的旋轉,效果:

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

再需要一個上下扭動的效果,新增下面的動畫以完成效果:

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弧度,這就有了扭到的效果(不同方向的旋轉)。 效果:

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

下面模擬語音助手,假裝回單。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
複製程式碼

本節的效果:

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

互動式複製動畫

前面這有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()
複製程式碼

這個時候,效果:

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

平滑麥克風輸入和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動畫之三:圖層動畫

本文在我的個人部落格中地址:系統學習iOS動畫之三:圖層動畫

相關文章