雖然CoreAnimation框架的名字和蘋果官方文檔的簡介中都是一個關於動畫的框架,但是它在iOS和OS X系統體系結構中扮演的角色卻是一個繪圖的角色。
官方文檔
系統體系結構:
可以看到,最上面一層是是應用層(UI層),直接和用戶打交道(UIKit框架也就是干這件事的),而真正的繪圖層則在下面一層,綠色的這一層。繪圖層由3個部分組成:最上面是CZ喎?/kf/ware/vc/" target="_blank" class="keylink">vcmVBbmltYXRpb26jrMrHw+bP8rbUz/O1xKGjzfnPwr7Nyse4/LXXsuO1xLarzvfBy6O6T3BlbkdMus1Db3JlR3JhcGhpY3OjrMv8w8fM4bmpwcvNs9K7tcS907/awLS3w87Ku+bNvNOyvP6ho7b4u+bNvNOyvP7U8srHu+bNvNXm1f23osn6tcS12Le9oaPEx87Sw8e+zb/J0tTV4tH5wLTA7b3i1eK49szlz7W94bm5o7rV5tX9uMnKwrXEyse75s2807K8/qOozaizo8rHR1BVo6mjrNKyvs3Kx9fuz8LD5sTH0ru/6aOsy/y4utTwsNHP8cvYu621vcbBxLvJz6GjtvjO0sPHzqrBy8P8we7L/LutzbyjqMjnus675tbGo6nQ6NKq09C3vbeoxNy3w87Ktb3L/KOstbHIu9Xi1tbTsrz+suPD5rXEtqvO97/Ptqiyu8Tc1rG907fDzsq1xKOsstnX98+1zbPSu7aou+HX9s/e1sajqMjnufuyu7zT0tTP3tbGtcS7sL/JxNzSu9CptO3O87XEstnX972rtbzWws+1zbO5ytXPo6mjrNXiwO++zbrNw+bP8rbUz/O1xLfi17C63M/xwcujrLLZ1/fPtc2zt+LXsMHL07K8/rLjo6zWu8zhuam88rWltcTE3Lm708m/qrei1d/Wsb3Tt8POyrXEvdO/2qOstviyu82stcTTsrz+v8nE3NPQsrvNrLXEt+LXsLe9yr2jrNaxvdO3w87KxvDAtMrGsdjP4LWxwum3s6OoztLDx7XEtPrC69Do0qrKysXksrvNrLXE07K8/qOpo6zT2srHvs3T0MHLT3BlbkdMo6zL/M2z0rvBy8v509C75s2807K8/rXEvdO/2qOsztLDx8q508NPcGVuR0zM4bmptcTNrNK7zNdBUEm+zcTcv9jWxsjO0uK1xLvmzbzTsrz+wcuho7b4T3BlbkdMy+TIu7rcx7+086OstavKx7rcydm74dPDtb3L/NK70Km4tNTTtcS5psTco6y2+LzytaW1xLmmxNzSssrHQ9Pv0dSyu8yrusPKudPDo6zL+dLUvt/M5bXY1eu21GlPU7rNT1MgWM+1zbOjrMa7ufvOqs7Sw8e34tewwctPcGVuR0yjrMO7tO3V4r7NysdDb3JlQW5pbWF0aW9uoaM8L3A+DQo8cD7L+dLUtPO80r/J0tTM5bvh0rvPwqOsyrW8ysnPQ29yZUFuaW1hdGlvbsvkyLux7cPmyc+4/LbgtcTKx8zhuanBy7avu621xLmmxNyjrLWryse2r7utyse7+dPau+bNvLXEo6zL+dLUzerIq7/J0tSw0UNvcmVBbmltYXRpb26/8rzctbHX9tK7uPbTw8C0u+bNvLXEv/K83MC0tKbA7aGjy/zWsb3TzOG5qbXEtq+7rb3Tv9rKtbzKyc/Kx8/gtbHJ2bXEo6y2+LTzwb+1xMzhuanBy7io1vq2r7uttcRBUEmjrM7Sw8fV4sDvvavTw7W90ru49rTzybHG96O6Q0FEaXNwbGF5TGlua6GjPC9wPg0KPGgyIGlkPQ=="fps">FPS
首先我們從FPS的概念入手來幫助理解CADisplayLink。這裡的FPS不是第一人稱射擊游戲,而是frame per second,也就是幀率,表示屏幕每秒鐘刷新多少次。如果幀率為60,表示屏幕每秒刷新60次,並不代表每1/60秒刷新一次,只能表示在1秒鐘的時間內屏幕會刷新60次,每次屏幕刷新的間隔並不一定是平均的。
動畫是一系列靜態圖片以極快的速度進行切換形成的,這個速度要快到人眼察覺不出其中的間隙(兩張圖片切換之間的間隔時間),具體地,這個切換頻率必須大於人眼的刷新頻率:每秒鐘60次。也就是說,如果屏幕刷新頻率大於每秒鐘60次,那麼我們人眼就感受不到兩幀圖片切換之間的間隙,所以我們感覺起來這些切換就是“連續”的,這就是動畫的產生。也就是說,動畫實際上就是以盡量大於60fps的速度在多張靜態圖片之間進行快速切換。
我們的屏幕每時每刻都在以>60fps的幀率進行刷新,每次刷新都會根據最新的繪制信息重繪屏幕上顯示的內容,這樣你才能順利的看見各種動畫,比如一個UITableView的滾動效果。CADisplayLink提供了API,每當屏幕刷新的時候,系統會回調我們向CADisplayLink注冊的一個方法,也就是說,我們可以在屏幕每次刷新的時候調用一個我們自己的方法。基於上面對繪制動畫的認識,肯定我們就能夠像系統那樣一幀一幀地畫動畫了。
構建一個CADispalyLink非常的簡單,我們先提供一個回調方法:
- (void)onDisplayLink:(CADisplayLink *)displayLink { NSLog(@"display link callback"); }
接下來我們初始化一個displayLink,只有一個便利構造方法:
CADisplayLink * displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(onDisplayLink:)];
通過target-action的形式來向系統注冊回調,然後向runloop中添加displayLink。這裡要注意一下runloop中mode的概念。
一個runloop只能在某一個mode中跑,runloop可以在多個mode之間進行切換,默認的,系統提供了兩個mode:NSDefaultRunloopMode和UITrackingRunloopMode。正常情況下是default,但是如果一個scrollView滑動的時候(UITableView是scrollView的子類)runloop就會切換到UITrackingRunloopMode,這時候所有往default裡面添加的內容都沒法跑起來了。這也是為什麼,如果使用NSTimer的schedule方法來調度timer,當一個tableView滾動的時候timer會停止,就是因為schedule將把timer添加進default,而tableView滾動的時候runloop切換到了UITrackingRunloopMode,此時default中的timer就跑不起來了。
我們的CADisplayLink應該在這兩種情況都能跑,所以我們可以這樣來添加:
[displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode]; [displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:UITrackingRunLoopMode];
這樣就把displayLink添加進了兩種mode,無論runloop處於哪種mode,我們的displayLink都能被系統調度。這裡其實還有一種寫法:
[displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
NSRunLoopCommonModes後面多了一個s,表示mode的復數形式,意味著多個mode,這裡表示向所有被注冊為common的mode中添加displayLink。實際上,NSDefaultRunloopMode和UITrackingRunloopMode都被系統注冊成了common,所以這樣寫的效果和前一種是一樣的,你在自己使用runloop的時候也可以自定義mode,然後把它注冊成為common。
一旦我們把displayLink添加進了runloop,它就已經准備好進行回調了,每當屏幕刷新的時候,就會調用我們注冊的回調方法。運行我們的程序,就會發現控制台開始瘋狂的進行打印輸出。NSLog是日志打印,所以能提供該次打印的系統時間,看看兩次打印的間隔,是不是差不多在1/60秒左右。
為了實現基於CADisplayLink的動畫,我們首先要弄清一個概念:插值。插值在不同的地方有不同的解釋。大家思考一下,我們現在要自己在每一幀進行重繪來實現動畫,想象這樣一個動畫:讓一個質點從(10,20)點移動到(300,400),持續時間2.78秒。我們要做的是,在每一次屏幕刷新的時候根據當前已經經歷的時間(從動畫開始到當前時間)計算出該質點的坐標點並更新它的坐標,也就是我們要解決的是:對於任意時刻t,質點的坐標是多少?
這裡我們將引入線性插值,我們把問題改一下:你現在距離家f米,學校距離家t米,現在你要從當前的位置勻速走到學校,整個過程將持續d秒,問:當時間經過△t後,你距離家多遠?
這是一道很簡單的勻速直線運動問題,首先根據距離和持續時間來獲得速度:
v = (t-f)/d
然後用速度乘以已經經過的時間來獲得當前移動的距離:
△s = v△t = (t-f)/d * △t
最後再用已經移動的距離加上初始的距離得到當前距離家有多遠:
s = △s + f = (t-f)/d * △t + f
我們把上面的公式稍微變一下形:
s = f + (t-f) * (△t/d)
這裡令p = △t/d就有:
s = f + (t-f) * p
這就是線性插值的公式:
value = from + (to - from) * percent
from表示起始值,to表示目標值,percent表示當前過程占總過程的百分比(上個例子中就是當前已經經歷的時間占總時間的百分比所以是△t/d),這個公式成立的前提是變化是線性的,也就是勻速變化,所以叫做線性插值。
有了這個公式,我們回到代碼上面來,使用CADisplayLink加上線性插值來計算每幀所需的數據以實現一個勻速動畫
基於CADisplayLink的動畫
我們已經構建好了CADisplayLink,剩下的只需要添加一個視圖然後在CADisplayLink的回調方法中改變視圖的坐標就行了,很明顯,這個視圖應該使用成員變量或者屬性來聲明。
@property (nonatomic, strong) UIView * myView;
實現屬性的getter方法
- (UIView *)myView
{
if (!_myView) {
_myView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 80, 80)];
_myView.backgroundColor = [UIColor yellowColor];
}
return _myView;
}
在viewDidLoad中添加:
[self.view addSubview:self.myView];
接下來我們用一個私有方法來實現線性插值的公式:
- (CGFloat)_interpolateFrom:(CGFloat)from to:(CGFloat)to percent:(CGFloat)percent
{
return from + (to - from) * percent;
}
然後在onDisplayLink方法中解決以下問題:
1、 計算當前經歷的時間
2、 當前時間占總時間的百分比
3、 利用線性插值計算當前的坐標
4、 更新視圖的坐標
首先是如何計算當前經歷的時間,由於每次調用onDisplayLink的間隔都不是平均的,我們就不能通過調用次數乘以間隔來得到當前經歷的時間,只能用當前時刻減去動畫開始的時刻,所以我們聲明一個屬性用來記錄動畫開始的時刻:
@property (nonatomic, assign) NSTimeInterval beginTime;
在把CADisplayLink添加進runloop的代碼後面賦值:
self.beginTime = CACurrentMediaTime();
這樣我們就可以在onDisplayLink方法裡面這樣獲取動畫經歷的時間了:
NSTimeInterval currentTime = CACurrentMediaTime() - self.beginTime;
然後計算出百分比,我們先在方法開頭定義出動畫的起始值、終止值、持續時間:
CGPoint fromPoint = CGPointMake(10, 20);
CGPoint toPoint = CGPointMake(300, 400);
NSTimeInterval duration = 2.78;
這樣的話百分比就是:
CGFloat percent = currentTime / duration;
然後使用線性插值來計算視圖的x和y,直接調用公式即可:
CGFloat x = [self _interpolateFrom:fromPoint.x to:toPoint.x percent:percent];
CGFloat y = [self _interpolateFrom:fromPoint.y to:toPoint.y percent:percent];
接下來直接使用計算結果來更新視圖的center:
self.myView.center = CGPointMake(x, y);
然後運行就能看見,視圖如我們所願的以動畫的形式開始移動了(這裡由於我的錄制gif軟件的原因,動畫看起來有點卡幀,實際上動畫是相當平滑的)。
但是有一個問題:動畫根本停不下來!這是由於我們沒有停止CADisplayLink,所以onDisplayLink會不停地調用,所以當percent超過1的時候,視圖會朝著我們既定的方向繼續移動。
正確的停止CADisplayLink的方式是這樣的:在計算出percent之後進行判斷
if (percent > 1) {
percent = 1;
[displayLink invalidate];
}
如果少了percent = 1這一行,就會造成一個很小的誤差,但是千萬不能小看這個誤差,我們應該杜絕任何誤差的產生。
再次運行:
我們的視圖就停在了(300,400)的位置
非線性的插值:
剛才的動畫是基於線性插值來實現的,也就是勻速變化,如果我們要實現類似ease效果的變速運動應該如何來做呢?這裡對大家的數學能力有一定挑戰了。
我們先來看一個easeIn的效果,easeIn的s-t圖像大概是這樣的:
首先要搞清楚x和y分別代表什麼。為了讓我們的函數能在任意一種動畫情況中使用,我們把定義域和值域都設置為[0,1],那麼x代表的就是動畫時間的進程了,y代表的就是動畫值的進程。進程的意思表示當前值占總進度的百分比,比如考慮這樣一個函數y = f(x) = x^2(拋物線函數,擁有easeIn的效果,也就是點的斜率隨著x的增大而增大),其中一個點(0.5, 0.25)代表的就是當動畫時間進行到50%的時候,動畫進程執行了25%。
如果對動畫進程還有不清楚的地方,考慮上面一個動畫的例子,視圖的center.x從10變為300,也就是f=10, to=300,那麼動畫進程s就等於視圖的x已經改變的值(x-f)除以x一共可以改變的值(t-f)也就是s= (x-f)/(t-f)
那麼我們就建立了一個從動畫時間進程p到動畫值進程s的一個映射(函數):
s = f(p),這個映射只要滿足其圖像上面的點的斜率隨著p的增大而增大就能達到easeIn的效果了,因為點的斜率就代表這一時刻動畫的速度,比如s = f(p) = p^2就滿足這一easeIn的條件。
這樣我們就有了兩個方程:
s = (x-f)/(t-f) ①
s = f(p) ②
那我們就解得動畫當前值x和時間進程p的關系
x = f(p) * (t-f) + f
其中f(p)是一個緩沖函數,滿足值域和定義域均為[0,1],你可以任意修改f(p)的表達式來達到各種不同的變速效果。仔細觀察就能發現,當f(p)=p時,就是線性插值,這樣我們就可以通過時間來求出p後,把p作用於緩沖函數f(p),返回的值再帶進線性插值的公式,就能算出我們的動畫值了,而勻速動畫的緩沖函數恰好就是f(p)=p。
如果你想實現勻加速動畫,恰好勻加速s-t映射就是一個二次函數:s = 1/2at^2 + v0t,其中初速度v0 = 0,那麼我們的緩沖函數f(p) = 1/2ap^2。
現在我們可以將代碼修改一下以達到一個easeIn的效果。
首先定義一個easeIn的緩沖函數:
- (CGFloat)easeIn:(CGFloat)p
{
return p*p;
}
然後在回調中作用於percent,將回調方法修改為:
- (void)onDisplayLink:(CADisplayLink *)displayLink
{
CGPoint fromPoint = CGPointMake(10, 20);
CGPoint toPoint = CGPointMake(300, 400);
NSTimeInterval duration = 2.78;
NSTimeInterval currentTime = CACurrentMediaTime() - self.beginTime;
CGFloat percent = currentTime / duration;
if (percent > 1) {
percent = 1;
[displayLink invalidate];
}
percent = [self easeIn:percent];
CGFloat x = [self _interpolateFrom:fromPoint.x to:toPoint.x percent:percent];
CGFloat y = [self _interpolateFrom:fromPoint.y to:toPoint.y percent:percent];
self.myView.center = CGPointMake(x, y);
}
這樣我們就有了一個勻速加速啟動的效果了,運行看看。
以上就是我們這次關於CADisplayLink的全部內容,我們使用它來實現了一個基於幀重繪的動畫,並且我們深入研究了插值和easeIn效果的數學實現。我們將在實踐篇中再用一篇來看看CADisplayLink的另一種用法:利用系統自帶的一些動畫效果實現更多的動畫。