iOS 動畫原理與實現--幀動畫、逐幀動畫、CALayer

這篇文章不會教大家如何實現一個具體的動畫效果,我會從動畫的本質出發,來說說 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. 動畫結束,切換回顯示模型層的值,此時模型層的值應被修改爲動畫結束時的值。

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

    UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
    view.backgroundColor = [UIColor redColor];
    [self.view addSubview:view];

    CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"position"];
    animation.fromValue = [NSValue valueWithCGPoint:CGPointMake(50, 0)];
    animation.toValue = [NSValue valueWithCGPoint:CGPointMake(150, 0)];
    [view.layer addAnimation:animation forKey:nil];

//    view.frame = CGRectOffset(view.frame, 100, 0);

你會發現動畫結束後,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 添加了一個動畫,所以這種方式叫做 顯式動畫
對應的,還有 隱式動畫,即系統自動添加上的動畫。

    CALayer *layer = [CALayer layer];
    layer.backgroundColor = [UIColor greenColor].CGColor;
    layer.frame = CGRectMake(0, 0, 100, 100);
    [self.view.layer addSublayer:layer];

    layer.frame = CGRectOffset(layer.frame, 100, 0);

這段代碼裏,我們沒有添加 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 begin];
    [CATransaction setDisableActions:YES];
    ///...
    layer.frame = CGRectOffset(layer.frame, 100, 0);
    ///...
    [CATransaction commit];

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

對比 總結

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

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

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

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



文/胖花花(簡書作者)
原文鏈接:http://www.jianshu.com/p/13c231b76594
著作權歸作者所有,轉載請聯繫作者獲得授權,並標註“簡書作者”。
發佈了81 篇原創文章 · 獲贊 21 · 訪問量 18萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章