CALayer的模型層與展示層

前言

本文,我們將深入CALayer內部,通過簡單的CABasicAnimation動畫來探究CALayer的兩個非常重要的屬性:presentationLayer和modelLayer。

從一個改變位置的動畫開始

我們可以使用CABasicAnimation來實現(xiàn)各種動畫,假如我們想讓一個視圖從一個位置動畫地移動到另一個位置,我們的代碼可能會這樣寫:

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    UIView * view = [[UIView alloc] initWithFrame:CGRectMake(80, 80, 100, 100)];
    view.backgroundColor = [UIColor blueColor];
    [self.view addSubview:view];

    // 動畫開始
    CABasicAnimation * animation = [CABasicAnimation animation];
    animation.keyPath = @"position";
    animation.duration = 2;
    animation.fromValue = [NSValue valueWithCGPoint:CGPointMake(80, 80)];
    animation.toValue = [NSValue valueWithCGPoint:CGPointMake(200, 300)];
    [view.layer addAnimation:animation forKey:nil];
}

也就是指定了動畫的四要素:

  • 動什么(keyPath)
  • 動多久(duration)
  • 從什么樣子開始(fromValue)
  • 動到什么樣子(toValue)

這樣CABasicAnimation就能在duration內對keyPath指定的屬性從fromValue到toValue之間進行插值。然后我們將動畫施加于view的layer,這樣它就會按照我們的四要素進行動畫了。

我們運行一下將會有如下效果:

20151223165138882.gif

注意:動畫結束后,視圖又回去了?

瞎子和瘸子

從前有一個瞎子和一個瘸子,瘸子看得見路,但是不能自己走,瞎子能自己走,但是他看不見路。于是他們想到了一個辦法:瞎子背著瘸子,這樣瘸子就指揮瞎子如何走路,瞎子負責走路,每走一步,瞎子都會停下來聽從瘸子的指揮,這樣他們就不緊不慢的走向了目的地。

在CALayer內部也有一個瞎子和一個瘸子:presentationLayer(以下簡稱P)和modelLayer(以下簡稱M)。presentationLayer負責走路(繪制內容),而modelLayer負責看路(如何繪制)。

P有這樣的特點:

1、我們看到的一切,都是P的內容;
2、P只在下次屏幕刷新時才會進行繪制。

M有這樣的特點:

1、我們我們對CALayer的各種繪圖屬性進行賦值和訪問實際上都是訪問的M的屬性,比如bounds、backgroundColor、position等;
2、對這些屬性進行賦值,不會影響P,也就是不會影響繪制內容。
你可以把M理解成一個隱身的家伙,只有P才能感知它的存在。
那么為什么我們對M的屬性進行賦值,“與此同時”視圖的顯示狀態(tài)就會發(fā)生改變呢?

如果我們用t0表示我們對屬性進行賦值的時刻,t1表示下次屏幕刷新的時刻,t0到t1之間的間隔相當短(小于1/60秒),我們的人眼根本察覺不到這樣的間隔時間存在,所以我們的感覺就是:賦值和界面繪制是同時進行的。然而實際上,賦值和界面繪制之間有一個t1-t0的時間間隔。

所以總結以上信息,我們可以知道P和M是這樣進行交互的:當下次屏幕刷新信號到來時(屏幕重繪時),P為了繪制內容到屏幕上,它會去找M要各種它需要的用來繪制的屬性,然后用這些屬性的信息來繪制。

直到屏幕刷新信號到來才進行繪制,這種方式能夠極大提高繪制效率??紤]這樣一種情況,如果連續(xù)寫了以下三行代碼:

view.frame = …; 
view.backgounrdColor = …; 
view.center = …;,

如果每次賦值就會導致屏幕的重繪,這樣就會有三次重繪,也就是GPU會執(zhí)行三次繪制操作。而如果我們先把繪制信息存起來,當需要繪制的時候(屏幕刷新)再用這些信息去重繪,GPU就只會執(zhí)行一次繪制操作。如果不能理解這樣做的優(yōu)勢,我們將會在實戰(zhàn)篇講述如何提高性能的時候再次帶大家了解繪圖機制,也就是你看到的一切究竟是如何被顯示出來的。

上面三行代碼,只是對M的屬性賦值了而已,沒有任何的繪制操作,當下次屏幕刷新信號到來時,P才會去找M要這些屬性,進行一次繪制。執(zhí)行這三次賦值(執(zhí)行三行代碼)所需的時間遠遠小于兩次屏幕刷新的間隔,所以這樣三行代碼是一定能夠在下次繪制開始前執(zhí)行完的。也就是說,當我們寫了這三行代碼后,視圖的顯示內容不會改變三次,而只會改變一次。

如果我們繼續(xù)按照瞎子和瘸子的理論進行下去,P背著M,如果我們在下次屏幕刷新前(P走下一步之前)對M進行了三次操作:把M移動到點1,把M移動到點2,把M移動到點3。那么下次屏幕刷新時,P會直接問M“你在哪啊”,M說我在點3呢,你這樣這樣走過來,于是P就屁顛屁顛的按照M的指揮跑到點3去了,它是不會先跑到點1再跑到點2再跑到點3的。

CAAnimation對presentationLayer的控制

P這個瞎子背著M這個瘸子,他們互幫互助,完成各種顯示的邏輯。每當屏幕刷新的時候,如果沒有其他的信息告訴P它應該如何繪制,那么P就只能去問M,然后回到M的狀態(tài)。

這樣一個平衡會在一個CAAnimation被加到CALayer上后會被打破。當我們調用了layer的addAnimation方法后,一個CAAnimation(以下簡稱A)就控制了P,就相當于A一腳把M從P身上踹了下來然后自己爬了上去(不要想歪 )。

現(xiàn)在P要如何顯示就完全由A來控制了,因為A被指定了在duration中從fromValue變到toValue。所以在動畫的持續(xù)時間內,M被丟在了原地,P則背著A到處跑(畢竟P是個瞎子,根本不知道他腦袋頂上的家伙是誰),每一步該如何走都是A通過插值計算出來的(通過duration、from、to就能計算任意時刻P的狀態(tài)了)。

動畫結束后,默認情況A就被移除了,這時候P身上啥也沒有了。那么在下一次屏幕刷新的時候,沒有其他的信息告訴P它應該如何繪制,所以它就又開口了“M你在哪里啊”,M說“勞資在這里”(在動畫過程當中它就沒被動過,除非你手動設置了M的值),“你怎么跑那里去了”,“要你管,快給我滾過來”(真是一對好CP)。于是P就屁顛屁顛的滾到M那里去了,所以動畫結束后,你會看到視圖又回去了。

如果想要P在動畫結束后就停在當前狀態(tài)而不回到M的狀態(tài),我們就需要給A設置兩個屬性,一個是A.removedOnCompletion = NO;表示動畫結束后A依然影響著P,另一個是A.fillMode = kCAFillModeForwards,(關于fillMode到底干了什么我們將在下一章詳細進行講解),這兩行代碼將會讓A控制住P在動畫結束后保持不變(不會回到M的狀態(tài)),我們一開始的代碼就會寫成這個樣子:

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    UIView * view = [[UIView alloc] initWithFrame:CGRectMake(80, 80, 100, 100)];
    view.backgroundColor = [UIColor blueColor];
    [self.view addSubview:view];
    CABasicAnimation * animation = [CABasicAnimation animation];
    animation.keyPath = @"position";
    animation.duration = 2;
    animation.fromValue = [NSValue valueWithCGPoint:CGPointMake(80, 80)];
    animation.toValue = [NSValue valueWithCGPoint:CGPointMake(200, 300)];
    animation.removedOnCompletion = NO;
    animation.fillMode = kCAFillModeForwards;
    [view.layer addAnimation:animation forKey:nil];
}
20151223165652502.gif

模型與顯示的同步

我們可以通過設置removedOnCompletion = NO以及fillMode來讓動畫結束后保持狀態(tài),但是此時我們的P和M不同步,我們看到的P是toValue的狀態(tài),而M則還是自己原來的狀態(tài)。舉個栗子,我們初始化一個view,它的狀態(tài)為1,我們給它的layer加個動畫,from是0,to是2,設置fillMode為kCAFillModeForewards,則動畫結束后P的狀態(tài)是2,M的狀態(tài)是1,這可能會導致一些問題出現(xiàn)。比如你點P所在的位置點不動,因為響應點擊的是M。所以我們應該讓P和M同步。

在CABasicAnimation的文檔中寫了這樣一句話:如果不設置toValue,則CABasicAnimation會從fromValue到M的值之間進行插值。也就是說,如果不設置toValue,則CABasicAnimation會把M的值作為toValue,所以我們就可以在加動畫的時候只設置fromValue,再手動修改M的值到你想要動畫停止的那個狀態(tài)就保持同步了。

我們可以擴展到任意的CAAnimation對象,比如CAKeyFrameAnimation,都可以通過設置M的值到動畫結束的狀態(tài)來保持P和M的同步。

所以我們的代碼可能就會寫成這樣:

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    UIView * view = [[UIView alloc] initWithFrame:CGRectMake(80, 80, 100, 100)];
    view.center = CGPointMake(200, 300);
    view.backgroundColor = [UIColor blueColor];
    [self.view addSubview:view];

    CABasicAnimation * animation = [CABasicAnimation animation];
    animation.keyPath = @"position";
    animation.duration = 2;
    animation.fromValue = [NSValue valueWithCGPoint:CGPointMake(80, 80)];
//    animation.toValue = [NSValue valueWithCGPoint:CGPointMake(200, 300)];
//    animation.removedOnCompletion = NO;
//    animation.fillMode = kCAFillModeForwards; 
    [view.layer addAnimation:animation forKey:nil];
}

當我們寫view.center = CGPointMake(200, 300);的時候,只是對M賦值,再次強調,它不會影響P的顯示,而當P想要顯示的時候,它已經被A控制了,所以P會從一開始就在80,80那里然后動畫地移動到200,300,不會出現(xiàn)先在200,300那里閃一下的情況。
運行一下,效果就是這樣:

20151223165812366.gif
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容