解析 iOS 動畫原理與實現

發表於2016-03-15

這篇文章不會教大家如何實現一個具體的動畫效果,我會從動畫的本質出發,來說說 iOS 動畫的原理與實現方式。

什麼是動畫

動畫,顧名思義,就是能“動”的畫。
人的眼睛對影像有短暫的記憶效應,所以當眼睛看到多張圖片連續快速的切換時,就會被認為是一段連續播放的動畫了。

比如,中國古代的“走馬燈”,就是用的這個原理。
有些人還會在一個本子每頁上手繪一些漫畫,當快速翻頁的時候,也會看到動畫的效果,比如:

圖片來自網路

計算機動畫的實現方式

動畫是由一張張圖片組成的,在計算機中,我們稱每一張圖片為 一幀畫面

如果我們想實現這麼一個動畫:一個水杯放在桌子的左邊,移動到右邊,那麼我們實際操作的,只是水杯。
所以動畫的實現,只是對運動變化了的部分的處理。

逐幀 與 關鍵幀

類似於上面提到的手繪翻頁方式,我們可以將這個水杯在每幀畫面中的位置一一找出來,這樣實現動畫的方式就叫作 逐幀動畫,我們需要處理動畫中的每一幀。

我們一般在計算機上用 FPS ( Frames Per Second) ,即 每秒的幀數 來表示動畫的重新整理速度,基於螢幕的重新整理率等其他原因,在計算機上一般採用 60 FPS。
如果運動變化幅度較緩,減半到 30 FPS 時,我們肉眼也是可接受的。
較低的 FPS 會讓我們有“卡頓”的感覺。

逐幀動畫是最直接的,但要處理的幀數太多,所以實現過程是會麻煩。

計算機的工作就是來完成重複單調的工作的,所以,有些工作是可以考慮讓計算機來完成的。


上面的例子,可以變成一個涉及數學和物理的問題:一個杯子初始位置在左邊,n秒後勻速運動到右邊,那麼在每 1/60 秒的時候,這個杯子的位置顯然是可以計算出來的了。
所以,我們其實只需要指定一些 關鍵 資訊就能讓計算機自己計算出每一幀杯子的位置了:

  • 起始位置,比如一個座標 (0,0)
  • 結束位置,再比如一個座標 (100,0)
  • 動畫總時間,比如 0.25 秒
  • 勻速運動

這種方式就稱之為 關鍵幀動畫。即我們只需要給定幾個關鍵幀的畫面資訊,關鍵幀與關鍵幀之間的過渡幀都將由計算機自動生成。

這裡說的 關鍵幀動畫,是指的廣義上的一種動畫製作方式,並不僅指 CAKeyframeAnimationCABasicAnimation的實現方式也屬於 關鍵幀動畫

iOS 動畫

說完廣義上的動畫,就可以來說說 iOS 的動畫了。
先來說說動畫的本質。

動畫的本質

繼續用上面的簡單例子:一個 UIView 從 (0,0) 勻速移動到 (100,0)的動畫,動畫總時間是0.25秒。
假設我們基於 60 FPS 來顯示動畫,那麼在0.25秒內就應該有15幀畫面,在每幀畫面中,這個 UIViewx座標,每次應移動 100/15 的距離。
如果我們每隔 0.25/15 秒重新整理一次UIViewx座標,那麼就能實現這個動畫效果了。
對於 x座標而言,每幀的位置就可以通過一個基於時間變化量的函式來求得:x=f(t)

所以,一個動畫的本質,就是動畫物件(這裡是 UIView)的狀態,基於時間變化的反應了。
簡單說,就是給定任意一個時刻,如果你都能得到這個動畫物件的位置和、形狀等等屬性,你就能實現這個動畫了。
屬性值的變化,既可能是位置、透明度、旋轉角度等的變化,也包括形狀的改變,比如從一條直線變化成一個圓圈,目標就是要得到變化過程中特定時刻的中間態。

動畫的實現

我們也可將 iOS 的動畫分為兩大類:

  • 系統提供的 關鍵幀動畫 實現方式;使用者指定 關鍵 資訊,系統實現動畫過程,對使用者而言操作起來會簡單些。
  • 逐幀動畫 實現方式;使用者自己 出每一幀畫面,系統操作方法簡單,但使用者操作的工作量就會大一些。

逐幀動畫實現方式

簡單的說,要實現逐幀的方式,就是需要 週期性 的呼叫 繪製 方法,繪製每幀的動畫物件。

這裡說的 繪製,不光是指覆寫 UIView- drawRect:的方法來手動重繪檢視,也包括修改 UIView 它的屬性,比如位置、顏色等。

iOS 的動畫都是基於 CALayer 的,iOS 的 UIView 背後都有一個對應的 CALayer 。對 UIView 的修改實際上都是對背後 CALayer 的修改。
但如果在逐幀繪製的方法中修改了一個自建的 CALayer,這個 CALayer 不是對應某個 UIView 的,需注意系統的 隱式動畫 的影響,後面會提到這點。

週期性,就需要一個定時器來完成了,即 CADisplayLink
CADisplayLinkNSTimer 比較類似,可以週期性的呼叫指定的方法。
之所以用 CADisplayLink,是因為它是基於螢幕重新整理率的,即螢幕每次重新整理時就會觸發呼叫。
iPhone 的螢幕重新整理率是 60 FPS。

如果繪製過程過於複雜,不能在螢幕重新整理一幀的時間內完成,可以考慮改為每隔一幀繪製,相當於是 30 FPS的重新整理率。
不然可能會使動畫不連貫,有卡頓感。

用逐幀方法繪製的原理不是很麻煩,麻煩的是繪製過程。
對於一個複雜動畫,你可能需要運用各種物理、幾何知識去計算檢視中間狀態的資訊。
比如要實現一條直線捲曲變化為一個圓的動畫,你就需要計算出中間態的曲線的彎曲程度和位置。

著名的 facebook 的 pop 動畫框架,就是使用 CADisplayLink 這種逐幀繪製的方式實現的。

關鍵幀動畫實現方式

採用關鍵幀的方式來實現動畫,要講的內容相對逐幀的方式就多的多了。

還是用 UIView 移動的簡單例子。
這裡面有兩個關鍵幀,起始幀和結束幀,除此之外還有2個關鍵資訊:

  • 起始幀,變化資訊:座標為 (0,0)
  • 結束幀,變化資訊:座標為 (100,0)
  • 動畫時間,0.25秒
  • 勻速運動

座標 資訊是 UIView 的一個屬性(實際是對應到 CALayer 的屬性),在動畫實現裡,我們只需要指定起始和結束的兩個關鍵值就夠了,中間的過渡值都有系統自動生成。
這裡出現了兩種值,一個是我們設定的,一個是系統生成的,所以要先在這裡插入一個 模型層展現層 的概念了

CALayer 的同一個屬性值,會分別儲存在模型層 modelLayer ,和展現層 presentationLayer 中。當我們修改屬性值時,是修改的模型層的數值,動畫時系統根據模型層的變化,生成的過渡值,是儲存在展現層中的。

CALayer 的物件裡能直接訪問到這兩層的資訊。
CALayer 的底層實現實際不止這兩層,但我們現在討論動畫的時候,可以只關心這兩層。

在整個動畫過程中,呈現出來的過程是這樣的:

  1. 動畫前,顯示模型層的當前值;
  2. 動畫開始,切換顯示展現層的值;
  3. 動畫過程中,展現層的值根據時間變化,我們看到的實際是展現層的值在變化;
  4. 動畫結束,切換回顯示模型層的值,此時模型層的值應被修改為動畫結束時的值。

用一段程式碼來解釋下動畫過程。

你會發現動畫結束後,view 又跳回了原來的位置,這是因為最後一行程式碼註釋了,而這行程式碼的功能就是實現第4步,將模型層的值修改為動畫結束時的值。

動畫實現

程式碼中的 CABasicAnimation 就是真正的動畫實現部分,也就是設定關鍵幀資訊的地方。

將動畫加入 CALayer 的程式碼定義為:
- (void)addAnimation:(CAAnimation *)anim forKey:(NSString *)key
接受的型別是 CAAnimation 型別,有下面這些子類:

  • CABasicAnimation,可設定起始結束兩個關鍵幀的資訊。
  • CAKeyframeAnimation,除首尾外,還可新增多箇中間關鍵點。
  • CAAnimationGroup ,可組合多個動畫,因為上面兩種動畫一次只能設定一個屬性值。
  • CATransition,圖層過渡動畫,預設是淡入。比如修改一個 CALayer的背景色時,是從初始色慢慢淡入過渡到結束色。
    可修改為新顏色把舊顏色頂出去等效果。還可使用 CIFilter 濾鏡做過渡效果,一些開源 UIViewController 的過渡動畫使用了這種方式。

動畫中,除了屬性值外,我們還設定了兩個和時間有關的資訊:動畫時間0.25秒,運動方式是勻速運動。

動畫持續時間很簡單,是通過 CAAnimation 遵守的 CAMediaTiming 協議設定的。

勻速運動是通過設定 CAAnimationtimingFunction 實現的,這是一個 CAMediaTimingFunction 類的物件。

之前已經說到,動畫過程實際是一個時間的函式,橫座標是時間的變化值,縱座標是動畫屬性的變化量。那麼我們就可以在一個直角座標系中,通過作圖來畫出這個函式。比如勻速運動的圖形,就是一條通過原點的直線。

所以這個類的功能就是畫出一條曲線,來表示時間和屬性變化之間的關係。而畫圖的方法,是使用的是畫貝葉斯曲線的方法。

系統提供了幾個常用的函式,比如 kCAMediaTimingFunctionLinear 就是勻速運動;kCAMediaTimingFunctionEaseInEaseOut 就是一般系統動畫的預設值,漸入漸出,即在動畫開始和結束的時候速度稍慢些。

圖片來源自網路

隱式動畫

上面的過程,我們是 顯式 的向一個 CALayer 新增了一個動畫,所以這種方式叫做 顯式動畫
對應的,還有 隱式動畫,即系統自動新增上的動畫。

這段程式碼裡,我們沒有新增 CAAnimation 動畫,但 layer 不是直接變化到新的位置,而是有一個動畫效果。
這就是 隱式動畫 的效果。

當我們改變 CALayer 的一個可動畫的屬性值時,就會觸發系統的隱式動畫。
可動畫的屬性值,可以在 CALayer 的文件中找到,屬性說明中標有 Animatable 的,就是可自動新增動畫的屬性。

但是,有一個例外,對於 UIView 背後對應的 CALayer,系統關閉了隱式動畫,所以當我們直接修改 UIView 或者是其底層的 CALayer 時,變化是直接生效的,沒有動畫效果。

所以當我們在逐幀方式生成動畫時,是可以直接修改 UIView 或者是其底層的 CALayer 的資訊。
但是如果修改的是一個自建的單獨 CALayer 時,幀與幀之間的變化還是會觸發系統的預設隱式動畫,這個時候就需要我們來手動關閉隱式動畫。
當快速動畫的時候不會察覺到這點,但這明顯會帶來效能上的浪費。

隱式動畫所做的事情和顯示動畫是一樣的,我們設定的屬性值都是模型層的數值,而系統會自動新增屬性對應的 CAAnimation 動畫到 CALayer 上。

UIView 有一系列的 animateWithDuration 動畫方法,在這些方法中 UIView 會恢復隱式動畫,所以在動畫的 block 中修改屬性時,又會觸發隱式動畫。


那麼系統是如果知道對一個屬性應該新增哪種動畫呢,這就需要讓 CAAction 協議登場了。

當修改一個 CALayer 的屬性時,它會通過 - actionForKey: 來查詢這個屬性對應的 action,而 key 就是對應的屬性名稱。
CAAnimation 遵守 CAAction 協議,返回的 action 其實是個 CAAnimation 動畫。
也就是說, CALayer 通過 - actionForKey: 來查詢某個屬性被修改時,需要呼叫哪個動畫去展現這個變化。
一般預設返回的是 CABasicAnimation ,預設動畫時間 0.25秒,時間函式為漸入漸出 kCAMediaTimingFunctionEaseInEaseOut。

- actionForKey: 查詢 action 的步驟有4步,在這個方法中有詳細的說明。
其中一種方式就是通過 CALayer 的 delegate 返回 action。而對於 UIView 背後對應的 CALayer,其代理就是它對應的 UIViewUIView 就是用這種方式關閉了隱式動畫。

動畫事務

建立動畫事務的目的是為了操作的原子性,保證動畫的所有修改能同時生效。
CATransaction 就是動畫事務的操作類。

在建立隱式動畫的時候,系統也會隱式的建立一個動畫事務,以保證所有的動畫能同時進行。

除此之外,還可以顯式的建立一個事務。
顯式事務中可以定義事務中所有動畫的執行時間和時間函式,此外,還有這個方法 + (void)setDisableActions:(BOOL)flag 能顯式的關閉這個事務中的 action 查詢操作。
關閉了查詢也就是關閉了動畫效果,屬性值的變化就會立即生效,而沒有動畫效果了:

注意別把 CATransaction 和 CATransition 搞混了,一個單詞是 transaction 事務,另一個是 transition 轉變。

對比 總結

關鍵幀動畫的實現方式,只需要修改某個屬性值就可以了,簡單方便,但涉及的深層次內容較多,需要更多的理解和練習。

採用逐幀動畫的實現方式,實現原理簡單,但繪製動畫的過程要複雜。如果動畫過程處理的事情較多,也會帶來較大的開銷,就有可能造成動畫幀數的下降,出現卡頓的現象,因此需要較多的測試和除錯。
動畫繪製的過程中,會要求較多的數學、物理等知識來計算中間態的資料。

但這兩種方式也不是絕對分離開的。
關鍵幀動畫實現方式,一般只能對系統實現了可動畫的屬性做動畫處理,但其實也是允許實現自定義屬性的動畫處理的。
這就需要自己來實現系統中自動計算過渡幀的操作了,也就是逐幀實現動畫的方式了。
實現自定義屬性的動畫可以參考這篇文章: Layer 中自定義屬性的動畫

對於 iOS 系統提供的動畫方法,上面只是從整體的角度作了一個全面的整理,還有很多細節內容沒有寫出來,比如 CALayer 的三維變換、CAKeyframeAnimation 的延路徑動畫,CAMediaTiming 的時間控制,等等。感興趣的話,可以再看看這些內容:

相關文章