你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發綜合 >> iOS CoreAnimation專題——技巧篇(一)CADisplayLink

iOS CoreAnimation專題——技巧篇(一)CADisplayLink

編輯:IOS開發綜合

iOS繪圖系統

雖然CoreAnimation框架的名字和蘋果官方文檔的簡介中都是一個關於動畫的框架,但是它在iOS和OS X系統體系結構中扮演的角色卻是一個繪圖的角色。

官方文檔

系統體系結構:

1

可以看到,最上面一層是是應用層(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的速度在多張靜態圖片之間進行快速切換。

CADisplayLink

我們的屏幕每時每刻都在以>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軟件的原因,動畫看起來有點卡幀,實際上動畫是相當平滑的)。
2
但是有一個問題:動畫根本停不下來!這是由於我們沒有停止CADisplayLink,所以onDisplayLink會不停地調用,所以當percent超過1的時候,視圖會朝著我們既定的方向繼續移動。

正確的停止CADisplayLink的方式是這樣的:在計算出percent之後進行判斷

if (percent > 1) {
        percent = 1;
        [displayLink invalidate];
  }

如果少了percent = 1這一行,就會造成一個很小的誤差,但是千萬不能小看這個誤差,我們應該杜絕任何誤差的產生。
再次運行:
我們的視圖就停在了(300,400)的位置

3

非線性的插值:

剛才的動畫是基於線性插值來實現的,也就是勻速變化,如果我們要實現類似ease效果的變速運動應該如何來做呢?這裡對大家的數學能力有一定挑戰了。

我們先來看一個easeIn的效果,easeIn的s-t圖像大概是這樣的:

4

首先要搞清楚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);
}

這樣我們就有了一個勻速加速啟動的效果了,運行看看。
5

以上就是我們這次關於CADisplayLink的全部內容,我們使用它來實現了一個基於幀重繪的動畫,並且我們深入研究了插值和easeIn效果的數學實現。我們將在實踐篇中再用一篇來看看CADisplayLink的另一種用法:利用系統自帶的一些動畫效果實現更多的動畫。

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved