這篇文章不會(huì)教大家如何實(shí)現(xiàn)一個(gè)具體的動(dòng)畫效果,我會(huì)從動(dòng)畫的本質(zhì)出發(fā),來說說 iOS 動(dòng)畫的原理與實(shí)現(xiàn)方式。
什么是動(dòng)畫
動(dòng)畫,顧名思義,就是能“動(dòng)”的畫。
人的眼睛對(duì)圖像有短暫的記憶效應(yīng),所以當(dāng)眼睛看到多張圖片連續(xù)快速的切換時(shí),就會(huì)被認(rèn)為是一段連續(xù)播放的動(dòng)畫了。
比如,中國古代的“走馬燈”,就是用的這個(gè)原理。
有些人還會(huì)在一個(gè)本子每頁上手繪一些漫畫,當(dāng)快速翻頁的時(shí)候,也會(huì)看到動(dòng)畫的效果,比如:

計(jì)算機(jī)動(dòng)畫的實(shí)現(xiàn)方式
動(dòng)畫是由一張張圖片組成的,在計(jì)算機(jī)中,我們稱每一張圖片為 一幀畫面 。
如果我們想實(shí)現(xiàn)這么一個(gè)動(dòng)畫:一個(gè)水杯放在桌子的左邊,移動(dòng)到右邊,那么我們實(shí)際操作的,只是水杯。
所以動(dòng)畫的實(shí)現(xiàn),只是對(duì)運(yùn)動(dòng)變化了的部分的處理。
逐幀 與 關(guān)鍵幀
類似于上面提到的手繪翻頁方式,我們可以將這個(gè)水杯在每幀畫面中的位置一一找出來,這樣實(shí)現(xiàn)動(dòng)畫的方式就叫作 逐幀動(dòng)畫,我們需要處理動(dòng)畫中的每一幀。
我們一般在計(jì)算機(jī)上用 FPS ( Frames Per Second) ,即 每秒的幀數(shù) 來表示動(dòng)畫的刷新速度,基于屏幕的刷新率等其他原因,在計(jì)算機(jī)上一般采用 60 FPS。
如果運(yùn)動(dòng)變化幅度較緩,減半到 30 FPS 時(shí),我們?nèi)庋垡彩强山邮艿摹?br> 較低的 FPS 會(huì)讓我們有“卡頓”的感覺。
逐幀動(dòng)畫是最直接的,但要處理的幀數(shù)太多,所以實(shí)現(xiàn)過程是會(huì)麻煩。
計(jì)算機(jī)的工作就是來完成重復(fù)單調(diào)的工作的,所以,有些工作是可以考慮讓計(jì)算機(jī)來完成的。
上面的例子,可以變成一個(gè)涉及數(shù)學(xué)和物理的問題:一個(gè)杯子初始位置在左邊,n秒后勻速運(yùn)動(dòng)到右邊,那么在每 1/60 秒的時(shí)候,這個(gè)杯子的位置顯然是可以計(jì)算出來的了。
所以,我們其實(shí)只需要指定一些 關(guān)鍵 信息就能讓計(jì)算機(jī)自己計(jì)算出每一幀杯子的位置了:
- 起始位置,比如一個(gè)坐標(biāo) (0,0)
- 結(jié)束位置,再比如一個(gè)坐標(biāo) (100,0)
- 動(dòng)畫總時(shí)間,比如 0.25 秒
- 勻速運(yùn)動(dòng)
這種方式就稱之為 關(guān)鍵幀動(dòng)畫。即我們只需要給定幾個(gè)關(guān)鍵幀的畫面信息,關(guān)鍵幀與關(guān)鍵幀之間的過渡幀都將由計(jì)算機(jī)自動(dòng)生成。
這里說的 關(guān)鍵幀動(dòng)畫,是指的廣義上的一種動(dòng)畫制作方式,并不僅指
CAKeyframeAnimation,CABasicAnimation的實(shí)現(xiàn)方式也屬于 關(guān)鍵幀動(dòng)畫
iOS 動(dòng)畫
說完廣義上的動(dòng)畫,就可以來說說 iOS 的動(dòng)畫了。
先來說說動(dòng)畫的本質(zhì)。
動(dòng)畫的本質(zhì)
繼續(xù)用上面的簡(jiǎn)單例子:一個(gè) UIView 從 (0,0) 勻速移動(dòng)到 (100,0)的動(dòng)畫,動(dòng)畫總時(shí)間是0.25秒。
假設(shè)我們基于 60 FPS 來顯示動(dòng)畫,那么在0.25秒內(nèi)就應(yīng)該有15幀畫面,在每幀畫面中,這個(gè) UIView 的 x坐標(biāo),每次應(yīng)移動(dòng) 100/15 的距離。
如果我們每隔 0.25/15 秒刷新一次UIView 的 x坐標(biāo),那么就能實(shí)現(xiàn)這個(gè)動(dòng)畫效果了。
對(duì)于 x坐標(biāo)而言,每幀的位置就可以通過一個(gè)基于時(shí)間變化量的函數(shù)來求得:x=f(t) 。
所以,一個(gè)動(dòng)畫的本質(zhì),就是動(dòng)畫對(duì)象(這里是 UIView)的狀態(tài),基于時(shí)間變化的反應(yīng)了。
簡(jiǎn)單說,就是給定任意一個(gè)時(shí)刻,如果你都能得到這個(gè)動(dòng)畫對(duì)象的位置和、形狀等等屬性,你就能實(shí)現(xiàn)這個(gè)動(dòng)畫了。
屬性值的變化,既可能是位置、透明度、旋轉(zhuǎn)角度等的變化,也包括形狀的改變,比如從一條直線變化成一個(gè)圓圈,目標(biāo)就是要得到變化過程中特定時(shí)刻的中間態(tài)。
動(dòng)畫的實(shí)現(xiàn)
我們也可將 iOS 的動(dòng)畫分為兩大類:
- 系統(tǒng)提供的 關(guān)鍵幀動(dòng)畫 實(shí)現(xiàn)方式;用戶指定 關(guān)鍵 信息,系統(tǒng)實(shí)現(xiàn)動(dòng)畫過程,對(duì)用戶而言操作起來會(huì)簡(jiǎn)單些。
- 逐幀動(dòng)畫 實(shí)現(xiàn)方式;用戶自己 畫 出每一幀畫面,系統(tǒng)操作方法簡(jiǎn)單,但用戶操作的工作量就會(huì)大一些。
逐幀動(dòng)畫實(shí)現(xiàn)方式
簡(jiǎn)單的說,要實(shí)現(xiàn)逐幀的方式,就是需要 周期性 的調(diào)用 繪制 方法,繪制每幀的動(dòng)畫對(duì)象。
這里說的 繪制,不光是指覆寫 UIView 的 - drawRect:的方法來手動(dòng)重繪視圖,也包括修改 UIView 它的屬性,比如位置、顏色等。
iOS 的動(dòng)畫都是基于
CALayer的,iOS 的UIView背后都有一個(gè)對(duì)應(yīng)的CALayer。對(duì)UIView的修改實(shí)際上都是對(duì)背后CALayer的修改。
但如果在逐幀繪制的方法中修改了一個(gè)自建的CALayer,這個(gè)CALayer不是對(duì)應(yīng)某個(gè)UIView的,需注意系統(tǒng)的 隱式動(dòng)畫 的影響,后面會(huì)提到這點(diǎn)。
而 周期性,就需要一個(gè)定時(shí)器來完成了,即 CADisplayLink。
CADisplayLink 與 NSTimer 比較類似,可以周期性的調(diào)用指定的方法。
之所以用 CADisplayLink,是因?yàn)樗腔谄聊凰⑿侣实?,即屏幕每次刷新時(shí)就會(huì)觸發(fā)調(diào)用。
iPhone 的屏幕刷新率是 60 FPS。
如果繪制過程過于復(fù)雜,不能在屏幕刷新一幀的時(shí)間內(nèi)完成,可以考慮改為每隔一幀繪制,相當(dāng)于是 30 FPS的刷新率。
不然可能會(huì)使動(dòng)畫不連貫,有卡頓感。
用逐幀方法繪制的原理不是很麻煩,麻煩的是繪制過程。
對(duì)于一個(gè)復(fù)雜動(dòng)畫,你可能需要運(yùn)用各種物理、幾何知識(shí)去計(jì)算視圖中間狀態(tài)的信息。
比如要實(shí)現(xiàn)一條直線卷曲變化為一個(gè)圓的動(dòng)畫,你就需要計(jì)算出中間態(tài)的曲線的彎曲程度和位置。
著名的 facebook 的 pop 動(dòng)畫框架,就是使用 CADisplayLink 這種逐幀繪制的方式實(shí)現(xiàn)的。
關(guān)鍵幀動(dòng)畫實(shí)現(xiàn)方式
采用關(guān)鍵幀的方式來實(shí)現(xiàn)動(dòng)畫,要講的內(nèi)容相對(duì)逐幀的方式就多的多了。
還是用 UIView 移動(dòng)的簡(jiǎn)單例子。
這里面有兩個(gè)關(guān)鍵幀,起始幀和結(jié)束幀,除此之外還有2個(gè)關(guān)鍵信息:
- 起始幀,變化信息:坐標(biāo)為 (0,0)
- 結(jié)束幀,變化信息:坐標(biāo)為 (100,0)
- 動(dòng)畫時(shí)間,0.25秒
- 勻速運(yùn)動(dòng)
坐標(biāo) 信息是 UIView 的一個(gè)屬性(實(shí)際是對(duì)應(yīng)到 CALayer 的屬性),在動(dòng)畫實(shí)現(xiàn)里,我們只需要指定起始和結(jié)束的兩個(gè)關(guān)鍵值就夠了,中間的過渡值都有系統(tǒng)自動(dòng)生成。
這里出現(xiàn)了兩種值,一個(gè)是我們?cè)O(shè)定的,一個(gè)是系統(tǒng)生成的,所以要先在這里插入一個(gè) 模型層 和 展現(xiàn)層 的概念了
CALayer 的同一個(gè)屬性值,會(huì)分別保存在模型層 modelLayer ,和展現(xiàn)層 presentationLayer 中。當(dāng)我們修改屬性值時(shí),是修改的模型層的數(shù)值,動(dòng)畫時(shí)系統(tǒng)根據(jù)模型層的變化,生成的過渡值,是保存在展現(xiàn)層中的。
在
CALayer的對(duì)象里能直接訪問到這兩層的信息。
而CALayer的底層實(shí)現(xiàn)實(shí)際不止這兩層,但我們現(xiàn)在討論動(dòng)畫的時(shí)候,可以只關(guān)心這兩層。
在整個(gè)動(dòng)畫過程中,呈現(xiàn)出來的過程是這樣的:
- 動(dòng)畫前,顯示模型層的當(dāng)前值;
- 動(dòng)畫開始,切換顯示展現(xiàn)層的值;
- 動(dòng)畫過程中,展現(xiàn)層的值根據(jù)時(shí)間變化,我們看到的實(shí)際是展現(xiàn)層的值在變化;
- 動(dòng)畫結(jié)束,切換回顯示模型層的值,此時(shí)模型層的值應(yīng)被修改為動(dòng)畫結(jié)束時(shí)的值。
用一段代碼來解釋下動(dòng)畫過程。
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);
你會(huì)發(fā)現(xiàn)動(dòng)畫結(jié)束后,view 又跳回了原來的位置,這是因?yàn)樽詈笠恍写a注釋了,而這行代碼的功能就是實(shí)現(xiàn)第4步,將模型層的值修改為動(dòng)畫結(jié)束時(shí)的值。
動(dòng)畫實(shí)現(xiàn)
代碼中的 CABasicAnimation 就是真正的動(dòng)畫實(shí)現(xiàn)部分,也就是設(shè)定關(guān)鍵幀信息的地方。
將動(dòng)畫加入 CALayer 的代碼定義為:
- (void)addAnimation:(CAAnimation *)anim forKey:(NSString *)key
接受的類型是 CAAnimation 類型,有下面這些子類:
-
CABasicAnimation,可設(shè)定起始結(jié)束兩個(gè)關(guān)鍵幀的信息。 -
CAKeyframeAnimation,除首尾外,還可添加多個(gè)中間關(guān)鍵點(diǎn)。 -
CAAnimationGroup,可組合多個(gè)動(dòng)畫,因?yàn)樯厦鎯煞N動(dòng)畫一次只能設(shè)置一個(gè)屬性值。 -
CATransition,圖層過渡動(dòng)畫,默認(rèn)是淡入。比如修改一個(gè)CALayer的背景色時(shí),是從初始色慢慢淡入過渡到結(jié)束色。
可修改為新顏色把舊顏色頂出去等效果。還可使用CIFilter濾鏡做過渡效果,一些開源UIViewController的過渡動(dòng)畫使用了這種方式。
動(dòng)畫中,除了屬性值外,我們還設(shè)置了兩個(gè)和時(shí)間有關(guān)的信息:動(dòng)畫時(shí)間0.25秒,運(yùn)動(dòng)方式是勻速運(yùn)動(dòng)。
動(dòng)畫持續(xù)時(shí)間很簡(jiǎn)單,是通過 CAAnimation 遵守的 CAMediaTiming 協(xié)議設(shè)定的。
勻速運(yùn)動(dòng)是通過設(shè)置 CAAnimation 的 timingFunction 實(shí)現(xiàn)的,這是一個(gè) CAMediaTimingFunction 類的對(duì)象。
之前已經(jīng)說到,動(dòng)畫過程實(shí)際是一個(gè)時(shí)間的函數(shù),橫坐標(biāo)是時(shí)間的變化值,縱坐標(biāo)是動(dòng)畫屬性的變化量。那么我們就可以在一個(gè)直角坐標(biāo)系中,通過作圖來畫出這個(gè)函數(shù)。比如勻速運(yùn)動(dòng)的圖形,就是一條通過原點(diǎn)的直線。
所以這個(gè)類的功能就是畫出一條曲線,來表示時(shí)間和屬性變化之間的關(guān)系。而畫圖的方法,是使用的是畫貝葉斯曲線的方法。
系統(tǒng)提供了幾個(gè)常用的函數(shù),比如 kCAMediaTimingFunctionLinear 就是勻速運(yùn)動(dòng);kCAMediaTimingFunctionEaseInEaseOut 就是一般系統(tǒng)動(dòng)畫的默認(rèn)值,漸入漸出,即在動(dòng)畫開始和結(jié)束的時(shí)候速度稍慢些。

隱式動(dòng)畫
上面的過程,我們是 顯式 的向一個(gè) CALayer 添加了一個(gè)動(dòng)畫,所以這種方式叫做 顯式動(dòng)畫。
對(duì)應(yīng)的,還有 隱式動(dòng)畫,即系統(tǒng)自動(dòng)添加上的動(dòng)畫。
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 動(dòng)畫,但 layer 不是直接變化到新的位置,而是有一個(gè)動(dòng)畫效果。
這就是 隱式動(dòng)畫 的效果。
當(dāng)我們改變 CALayer 的一個(gè)可動(dòng)畫的屬性值時(shí),就會(huì)觸發(fā)系統(tǒng)的隱式動(dòng)畫。
可動(dòng)畫的屬性值,可以在 CALayer 的文檔中找到,屬性說明中標(biāo)有 ** Animatable** 的,就是可自動(dòng)添加動(dòng)畫的屬性。
但是,有一個(gè)例外,對(duì)于 UIView 背后對(duì)應(yīng)的 CALayer,系統(tǒng)關(guān)閉了隱式動(dòng)畫,所以當(dāng)我們直接修改 UIView 或者是其底層的 CALayer 時(shí),變化是直接生效的,沒有動(dòng)畫效果。
所以當(dāng)我們?cè)谥饚绞缴蓜?dòng)畫時(shí),是可以直接修改
UIView或者是其底層的CALayer的信息。
但是如果修改的是一個(gè)自建的單獨(dú)CALayer時(shí),幀與幀之間的變化還是會(huì)觸發(fā)系統(tǒng)的默認(rèn)隱式動(dòng)畫,這個(gè)時(shí)候就需要我們來手動(dòng)關(guān)閉隱式動(dòng)畫。
當(dāng)快速動(dòng)畫的時(shí)候不會(huì)察覺到這點(diǎn),但這明顯會(huì)帶來性能上的浪費(fèi)。
隱式動(dòng)畫所做的事情和顯示動(dòng)畫是一樣的,我們?cè)O(shè)置的屬性值都是模型層的數(shù)值,而系統(tǒng)會(huì)自動(dòng)添加屬性對(duì)應(yīng)的 CAAnimation 動(dòng)畫到 CALayer 上。
UIView有一系列的animateWithDuration動(dòng)畫方法,在這些方法中UIView會(huì)恢復(fù)隱式動(dòng)畫,所以在動(dòng)畫的 block 中修改屬性時(shí),又會(huì)觸發(fā)隱式動(dòng)畫。
那么系統(tǒng)是如果知道對(duì)一個(gè)屬性應(yīng)該添加哪種動(dòng)畫呢,這就需要讓 CAAction 協(xié)議登場(chǎng)了。
當(dāng)修改一個(gè) CALayer 的屬性時(shí),它會(huì)通過 - actionForKey: 來查詢這個(gè)屬性對(duì)應(yīng)的 action,而 key 就是對(duì)應(yīng)的屬性名稱。
CAAnimation 遵守 CAAction 協(xié)議,返回的 action 其實(shí)是個(gè) CAAnimation 動(dòng)畫。
也就是說, CALayer 通過 - actionForKey: 來查詢某個(gè)屬性被修改時(shí),需要調(diào)用哪個(gè)動(dòng)畫去展現(xiàn)這個(gè)變化。
一般默認(rèn)返回的是 CABasicAnimation ,默認(rèn)動(dòng)畫時(shí)間 0.25秒,時(shí)間函數(shù)為漸入漸出 kCAMediaTimingFunctionEaseInEaseOut。
- actionForKey:查詢 action 的步驟有4步,在這個(gè)方法中有詳細(xì)的說明。
其中一種方式就是通過CALayer的 delegate 返回 action。而對(duì)于UIView背后對(duì)應(yīng)的CALayer,其代理就是它對(duì)應(yīng)的UIView,UIView就是用這種方式關(guān)閉了隱式動(dòng)畫。
動(dòng)畫事務(wù)
創(chuàng)建動(dòng)畫事務(wù)的目的是為了操作的原子性,保證動(dòng)畫的所有修改能同時(shí)生效。
CATransaction 就是動(dòng)畫事務(wù)的操作類。
在創(chuàng)建隱式動(dòng)畫的時(shí)候,系統(tǒng)也會(huì)隱式的創(chuàng)建一個(gè)動(dòng)畫事務(wù),以保證所有的動(dòng)畫能同時(shí)進(jìn)行。
除此之外,還可以顯式的創(chuàng)建一個(gè)事務(wù)。
顯式事務(wù)中可以定義事務(wù)中所有動(dòng)畫的運(yùn)行時(shí)間和時(shí)間函數(shù),此外,還有這個(gè)方法 + (void)setDisableActions:(BOOL)flag 能顯式的關(guān)閉這個(gè)事務(wù)中的 action 查詢操作。
關(guān)閉了查詢也就是關(guān)閉了動(dòng)畫效果,屬性值的變化就會(huì)立即生效,而沒有動(dòng)畫效果了:
[CATransaction begin];
[CATransaction setDisableActions:YES];
///...
layer.frame = CGRectOffset(layer.frame, 100, 0);
///...
[CATransaction commit];
注意別把 CATransaction 和 CATransition 搞混了,一個(gè)單詞是 transaction 事務(wù),另一個(gè)是 transition 轉(zhuǎn)變。
對(duì)比 總結(jié)
關(guān)鍵幀動(dòng)畫的實(shí)現(xiàn)方式,只需要修改某個(gè)屬性值就可以了,簡(jiǎn)單方便,但涉及的深層次內(nèi)容較多,需要更多的理解和練習(xí)。
采用逐幀動(dòng)畫的實(shí)現(xiàn)方式,實(shí)現(xiàn)原理簡(jiǎn)單,但繪制動(dòng)畫的過程要復(fù)雜。如果動(dòng)畫過程處理的事情較多,也會(huì)帶來較大的開銷,就有可能造成動(dòng)畫幀數(shù)的下降,出現(xiàn)卡頓的現(xiàn)象,因此需要較多的測(cè)試和調(diào)試。
動(dòng)畫繪制的過程中,會(huì)要求較多的數(shù)學(xué)、物理等知識(shí)來計(jì)算中間態(tài)的數(shù)據(jù)。
但這兩種方式也不是絕對(duì)分離開的。
關(guān)鍵幀動(dòng)畫實(shí)現(xiàn)方式,一般只能對(duì)系統(tǒng)實(shí)現(xiàn)了可動(dòng)畫的屬性做動(dòng)畫處理,但其實(shí)也是允許實(shí)現(xiàn)自定義屬性的動(dòng)畫處理的。
這就需要自己來實(shí)現(xiàn)系統(tǒng)中自動(dòng)計(jì)算過渡幀的操作了,也就是逐幀實(shí)現(xiàn)動(dòng)畫的方式了。
實(shí)現(xiàn)自定義屬性的動(dòng)畫可以參考這篇文章: Layer 中自定義屬性的動(dòng)畫
對(duì)于 iOS 系統(tǒng)提供的動(dòng)畫方法,上面只是從整體的角度作了一個(gè)全面的整理,還有很多細(xì)節(jié)內(nèi)容沒有寫出來,比如 CALayer 的三維變換、CAKeyframeAnimation 的延路徑動(dòng)畫,CAMediaTiming 的時(shí)間控制,等等。感興趣的話,可以再看看這些內(nèi)容:
- 蘋果官方文檔 Core Animation Programming Guide
- iOS Core Animation: Advanced Techniques -- 中文翻譯
- objc中國 動(dòng)畫專題 中的 動(dòng)畫解釋