解析 iOS 動畫原理與實現

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

什麼是動畫

動畫,顧名思義,就是能「動」的畫。
人的眼睛對圖像有短暫的記憶效應,因此當眼睛看到多張圖片連續快速的切換時,就會被認爲是一段連續播放的動畫了。ios

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


圖片來自網絡

計算機動畫的實現方式

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

若是咱們想實現這麼一個動畫:一個水杯放在桌子的左邊,移動到右邊,那麼咱們實際操做的,只是水杯。
因此動畫的實現,只是對運動變化了的部分的處理。網絡

逐幀 與 關鍵幀

相似於上面提到的手繪翻頁方式,咱們能夠將這個水杯在每幀畫面中的位置一一找出來,這樣實現動畫的方式就叫做 逐幀動畫,咱們須要處理動畫中的每一幀。app

咱們通常在計算機上用 FPS ( Frames Per Second) ,即 每秒的幀數 來表示動畫的刷新速度,基於屏幕的刷新率等其餘緣由,在計算機上通常採用 60 FPS。
若是運動變化幅度較緩,減半到 30 FPS 時,咱們肉眼也是可接受的。
較低的 FPS 會讓咱們有「卡頓」的感受。框架

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

計算機的工做就是來完成重複單調的工做的,因此,有些工做是能夠考慮讓計算機來完成的。函數


上面的例子,能夠變成一個涉及數學和物理的問題:一個杯子初始位置在左邊,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 的時間控制,等等。感興趣的話,能夠再看看這些內容:

相關文章
相關標籤/搜索