這一章雖然叫做動畫時間控制,然而我們並不會去深入到一般的動畫時間中,我們將討論的是CoreAnimation框架是如何來控制時間的。
這一章的大部分內容來自http://ronnqvi.st/controlling-animation-timing/,大家可以看看英文原版來加深理解,畢竟翻譯能力有限。
動畫所有跟時間相關的屬性(duration, beginTime, repeatCount等)都來自於CAMediaTiming協議,它由CABasicAnimation和CAKeyframeAnimation的父類CAAnimation實現。協議一共定義了8個屬性,通過這8個屬性就能完全地控制動畫時間。每個屬性的文檔只有短短幾句話,當然你也可以通過閱讀這些文檔並且手動進行試驗來進行學習,不過我認為更容易讓人理解的方式是將時間可視化。
為了向你們展示不同的時間相關屬性,包括這個屬性自己單獨使用的效果以及和其他屬性混合使用的效果,我將執行一個從橘黃色到藍色轉換的動畫。下圖展示了從動畫開始到動畫結束的進程(橘色到藍色),每一格代表一秒,時間線上任意一點對應到圖上的顏色就是視圖在這一瞬間的顏色。比如,duration這個屬性將被如下進行可視化展示:
duration設置為1.5秒,所以動畫將耗費1秒加上1秒的一半來從橘色完全變為藍色。
圖一. 將duration設為1.5秒
默認地,CAAnimation將會在動畫完成後被移除,這在上面同樣被可視化出來了,一旦動畫到達了結束值,它就會被從layer上移除,所以layer的背景色將會返回到modelLayer的狀態(見上一章:CALayer的模型層與展示層)。在這個可視化例子中,layer本身的背景色是白色,所以你看到的上圖的可視化效果中,在1.5秒後的額外的2.5秒鐘的時間裡layer的背景色回到了白色。
如果我們將動畫的beginTime加入到可視化效果中就能看到更多的情形。
圖二. 將duration設為1.5秒,將開始時間設為1.0秒
將動畫持續時間設為1.5秒,開始時間設為當前時間(CACurrentMediaTime())加上1秒所以動畫將在2.5秒後結束。在動畫被加到layer上之後它將等待1秒然後再開始(相當於動畫延遲時間為1秒)。
如果要讓動畫在開始之前(延遲的這段時間內)顯示fromValue的狀態,你可以設置動畫向後填充:設置fillMode為kCAFillModeBackwards。
圖三、填充模式可以用來在動畫開始之前顯示fromValue。
將使動畫先正常走,完了以後反著從結束值回到起始值(所有動畫屬性都會反過來,比如動畫速度,如果正常的是先快後慢,則反過來後變成先慢後快)。
圖四. Autoreverse將使動畫在結束後又回到動畫開始的狀態。
相比之下,repeatCount可以讓動畫重復執行兩次(首次動畫結束後再執行一次,正如你下面將看到的)或者任意多次(你甚至可以將重復次數設置為小數,比如設置為1.5,這樣第二次動畫只執行到一半)。一旦動畫到達結束值,它將立即返回到起始值並且重新開始。
圖五、repeatCount讓動畫在結束後再次執行
類似於repeatCount,但是極少會使用。它簡單地在給定的持續時間內重復執行動畫(下面設置為2秒)。如果repeatDuration比動畫持續時間小,那麼動畫將提前結束(repeatDuration到達後就結束)
圖六. repeatDuration使動畫在給定時間內不停重復播放
這些屬性可以結合到一起使用來實現一些動畫多次重復反向(也可以在制定時間內重復反向)的效果。如下圖所示:
圖七. 把幾個屬性結合起來用
這是一個非常有意思的時間相關的屬性。如果把動畫的duration設置為3秒,而speed設置為2,動畫將會在1.5秒結束,因為它以兩倍速在執行。
圖八. Speed設置為2將會使動畫的速度變為2倍所以3秒的動畫將只用1.5秒就能執行完
speed屬性的強大之處來自以下兩個特點:
1、 動畫速度是有層級關系的
2、 CAAnimation並不是唯一的實現了CAMediaTiming協議的類。
一個動畫的speed為1.5,它同時是一個speed為2的動畫組的一個動畫成員,則它將以3倍速度被執行。
CAAnimation實現了CAMediaTiming協議,然而CoreAnimation最基本的類:CALayer也實現了CAMediaTiming協議。這意味著你可以給一個CALayer設置speed為2,那麼所有加到它上面的動畫都將以兩倍速執行。這同樣符合動畫時間層級,比如你把一個speed為3的動畫加到一個speed為0.5的layer上,則這個動畫將以1.5倍速度執行。
控制動畫和layer的速度同樣可以用來暫停動畫,你只需要把speed設為0就行了。結合timeOffset屬性,就可以通過一個外部的控制器(比如一個UISlider)來控制動畫了,我們將在這一章較後的內容中進行講解。
timeOffset這個屬性啊,一開始看起來挺奇怪的,光看名字的話,看起來應該是一個用來控制動畫時間進程(計算動畫當前狀態)的屬性。下面這個可視化展示了一個持續時間為3秒,動畫時間偏移量為1秒的動畫。
圖9.你可以偏移整個動畫但是動畫還是會走完全部過程。
這個動畫將從正常動畫(timeOffset為0的狀態)的第一秒開始執行,直到兩秒後它完全變藍,然後它一下子跳回最開始的狀態(橙色)再執行一秒。就像是我們把正常動畫的第一秒給剪下來粘貼到動畫最後一樣。
這個屬性實際上並不會自己單獨使用,而會結合一個暫停動畫(speed=0)一起使用來控制動畫的“當前時間”。暫停的動畫將會在第一幀卡住,然後通過改變timeOffset來隨意控制動畫進程,因為如上圖所示,動畫的第一幀就是timeOffset指定的那一幀。
舉個例子:比如一個改變位置的動畫,讓一個視圖從(0,0)移動到(100,100),持續時間為1秒。如果先暫停動畫,然後設置timeOffset為0.5,那麼首先動畫會卡在“第一幀”,而第一幀由timeOffset決定,也就是動畫正常運作時間進行到0.5秒的那一幀就是“第一幀”,這時候動畫就會停在(50,50)的地方。注意timeOffset是具體的秒數而不是百分比。
結合使用speed和timeOffset屬性可以輕松控制一個動畫當前顯示的內容。為了方便起見,我將把動畫持續時間設為1秒。timeOffset/duration這個分數的值表示動畫進行的百分比,把duration設為1的話,在數值上timeOffset就等於動畫進程百分比了。
我們首先創建一個CABasicAnimation來創建一個改變layer背景顏色的動畫並把它添加到layer上,然後把layer的speed屬性設為0來暫停動畫。
CABasicAnimation *changeColor =
[CABasicAnimation animationWithKeyPath:@"backgroundColor"];
changeColor.fromValue = (id)[UIColor orangeColor].CGColor;
changeColor.toValue = (id)[UIColor blueColor].CGColor;
changeColor.duration = 1.0; // For convenience
[self.myLayer addAnimation:changeColor
forKey:@"Change color"];
self.myLayer.speed = 0.0; // Pause the animation
然後在slider被拖動的action方法中,我們把slider的當前值(默認0到1,剛好也是動畫timeOffset的范圍)設為layer的timeOffset的值。
- (IBAction)sliderChanged:(UISlider *)sender {
self.myLayer.timeOffset = sender.value; // Update "current time"
}
這樣的效果就像我們通過拖動一個slider來改變一個layer的背景顏色。
以上對於fillMode的說明比較簡單,而且在動畫的時間控制中還有一個比較重要的類:CAMediaTimingFunction,將在這裡比較詳細為大家進行講解
這個概念如果用文字來描述是比較難理解的,所以我將啟用我的靈魂畫板來講解這個概念。
為了更好的理解,我們首先定義四個時間點:t0表示動畫被加到layer上的一刻;t1表示動畫開始的一刻;t2表示動畫結束的一刻;t3表示動畫從layer上移除的一刻(這四個時間點也可以叫做動畫的生命周期)。
這裡有少年可能會問,吶,動畫加到layer上不就是動畫開始的時候麼?你忘記考慮延遲了!如果沒有延遲,那麼t0和t1確實是同一個時刻,但是一旦動畫有了延遲,那麼t0和t1就相差一個延遲了。同樣的道理,默認情況下,動畫一旦結束就會從layer上自動移除,也就是默認情況下t2和t3也是同一時刻,但是如果我們設置了removedOnCompletion = false,那麼t3就會無限向前延伸直到我們手動調用layer的removeAnimation方法。
我們來寫一個簡單的動畫,比如修改透明度的動畫。modelLayer的屬性一開始是0.5,然後我們寫一個動畫把透明度從0修改為1,持續時間1秒並設置一個1秒的延遲(t0-t1、t1到t2都相差1秒),動畫結束後不立即移除動畫。
如果你想在視圖一出現就開始動畫,請把動畫寫到viewDidAppear裡面,不要寫到viewDidLoad裡面。
- (void)viewDidAppear:(BOOL)animated
{
CALayer * layer = [CALayer layer];
layer.frame = CGRectMake(100, 100, 200, 200);
layer.opacity = 0.5;
layer.backgroundColor = [UIColor yellowColor].CGColor;
[self.view.layer addSublayer:layer];
CABasicAnimation * animation = [CABasicAnimation animation];
animation.keyPath = @"opacity";
animation.fromValue = @0;
animation.toValue = @1;
animation.duration = 1;
animation.beginTime = CACurrentMediaTime() + 1;
animation.removedOnCompletion = false;
[layer addAnimation:animation forKey:@"opacity"];
}
好了,那麼我們啟用靈魂畫板來表示動畫過程中P(presentationLayer)和M(modelLayer)的屬性的值。在靈魂畫板中,因為0.5這個數字畫起來太麻煩了,我就同時擴大兩倍,在靈魂畫板中的1就是0.5,2就是1。根據P和M的規則,M肯定在整個過程中的值都是0.5(在靈魂畫板中表現為1),而P在t0到t1由於動畫並沒有被告知如何影響P,所以會保持M的狀態也就是0.5,然後在t1到t2(動畫開始到動畫結束)從0到1進行插值,到了t2動畫結束,此時動畫不知道如何影響P,所以P保持M的狀態也就是回到0.5:
vc24w/e2yM6qMbXE17TMrKGjtq+7rb3hyvi686OodDItdDOjqaOsULvYtb1NtcTXtMyso6yx5M6qMC41tcTNuMP3tsjXtMysoaM8YnIgLz4NCs/W1NrO0sPHvNPJz2ZpbGxNb2RlPGJyIC8+DQrO0sPHyejWw2ZpbGxNb2RlzqrP8sewzO6z5KO6PC9wPg0KPHByZSBjbGFzcz0="brush:java;">
animation.fillMode = kCAFillModeForwards;
這裡有兩個概念:向前、填充。
什麼是向前,向前就是朝著時間的正方向。
那麼填充又是什麼呢?因為t2到t3這段時間動畫並不知道如何影響P,所以對於這段時間來講,P的狀態應該是“空”的,如果是空,那麼P就會保持M的狀態。而填充,就是把P的這些“空”的狀態用具體的值填起來。由於我們的動畫的keypath是opacity,所以就會對P的opacity在t2-t3這段時間進行填充,而填充的規則是“向前”,也就是“t2向t3填充”,說直白一點,就是t2到t3這個時間段P的opacity的值就一直保持t2的時候P的opacity的值,實際上就是動畫的toValue的值。
在靈魂畫板中體現為:
這樣的話,直到動畫被移除,P都會保持toValue也就是透明度為1的狀態,效果就是動畫結束後不會閃回動畫開始之前的那個狀態而保持結束值的狀態。
這樣一來,向後填充就比較好理解了:
向後填充是t0到t1,由於向後是時間的負方向,所以就是P的狀態在t0到t1這段時間由t1向t0填充,也就是t0到t1的時間段P保持t1時刻的狀態也就是fromValue的狀態。這樣設置的效果就是在延遲的時間裡面P保持fromValue的狀態,就避免了動畫一開始P從M的狀態就閃到fromValue的狀態:
animation.fillMode = kCAFillModeBackwards;
在靈魂畫板中表現為:
如果既想向前填充又想向後填充,那麼久把fillMode設置為both:
animation.fillMode = kCAFillModeBoth;
這樣設置後在靈魂畫板中表現為:
關於ease效果,在CAAnimation中表現為timingFunction這個屬性,它需要設置一個CAMediaTimingFunction對象,實際上是指定了一個曲線,作為s-t函數圖像,s是豎軸,代表動畫的進程,0表示動畫開始,1表示動畫結束;t是橫軸,代表動畫當前的時間,0表示開始的時候,1表示結束的時候。曲線上一點的切線斜率表示這一時刻的動畫速度。可以高中物理的直線運動的位移-時間圖像。
系統自帶了幾種ease效果,可以通過代碼來實現,使用functionWithName:這個類方法來通過函數名字來指定函數曲線:
CA_EXTERN NSString * const kCAMediaTimingFunctionLinear
__OSX_AVAILABLE_STARTING (__MAC_10_5, __IPHONE_2_0);
CA_EXTERN NSString * const kCAMediaTimingFunctionEaseIn
__OSX_AVAILABLE_STARTING (__MAC_10_5, __IPHONE_2_0);
CA_EXTERN NSString * const kCAMediaTimingFunctionEaseOut
__OSX_AVAILABLE_STARTING (__MAC_10_5, __IPHONE_2_0);
CA_EXTERN NSString * const kCAMediaTimingFunctionEaseInEaseOut
__OSX_AVAILABLE_STARTING (__MAC_10_5, __IPHONE_2_0);
CA_EXTERN NSString * const kCAMediaTimingFunctionDefault
__OSX_AVAILABLE_STARTING (__MAC_10_6, __IPHONE_3_0);
每個名字的函數圖像在網上隨處可見,如果簡單的來理解,那麼Linear就表示線性的,也就是s-t圖像是一條直線,明顯就是勻速運動了;EaseIn表示淡入,也就是勻加速啟動,或者理解為先慢後快;EaseOut表示淡出,也就是勻減速停止,或者理解為先快後慢。EaseInEaseOut就是既有淡入效果也有淡出效果。Default是一種平滑啟動平滑結束的過程,類似EaseInEaseOut,但是效果沒那麼顯著。
除了functionWithName,系統允許我們使用一條貝塞爾曲線作為函數圖像:
+(instancetype)functionWithControlPoints:(float)c1x :(float)c1y :(float)c2x :(float)c2y;
這個方法有四個參數,前兩個參數表示貝塞爾曲線的第一個控制點,後兩個參數表示貝塞爾曲線的第二個控制點。起點是(0,0)而終點是(1,1),所以是一條三階貝塞爾曲線。注意到函數的定義域和值域都是[0,1],所以控制點的x和y的值要計算好。關於貝塞爾曲線如果不太了解,可以在維基百科這裡獲得公式和表現形式:http://en.wikipedia.org/wiki/Bezier_curve
一般來講系統自帶的函數圖像已經夠用了,如果想要一些奇怪的效果,比如很慢很慢的啟動,然後一瞬間加速到很快,就要自己去用貝塞爾曲線來控制了(大概是這樣一種圖像:╯)。
好了,動畫原理部分的內容就到這裡為止,希望大家能熟練掌握這些知識,它們能幫助大家理解很多一些效果是怎樣產生的,以及一些問題出現的原因,更重要的是能幫助大家在接下來的章節中了解一些高級技巧是如何使用的。
這一章的實踐內容比較少,所以需要自己花實踐消化並且做充分的實驗。
下一章就將進入高級動畫技巧,如何使用CoreAnimation提供的簡單API來組合實現各種酷炫的效果,大家除了能學到技術以外,還能學到一些思想,掌握了這些思想,以後遇到各種效果的動畫都能有思路去實現了,這才是我寫這篇專題的目的。