CoreAnimation初探(三) —— UIView與CAlayer動(dòng)畫原理

有了前兩篇的概念基礎(chǔ),本篇從以下兩點(diǎn)結(jié)合具體代碼探索下CoreAnimation的一些原理。

UIView動(dòng)畫實(shí)現(xiàn)原理
展示層(presentationLayer)和模型層(modelLayer)

1.UIView動(dòng)畫實(shí)現(xiàn)原理

UIView提供了一系列UIViewAnimationWithBlocks,我們只需要把改變可動(dòng)畫屬性的代碼放在animations的block中即可實(shí)現(xiàn)動(dòng)畫效果,比如:

- (void)btnClick:(id)sender
{
    [UIView animateWithDuration:1 animations:^(void){        
          if (_testView.bounds.size.width > 150)
          {
              _testView.bounds = CGRectMake(0, 0, 100, 100);
          }
          else
          {
              _testView.bounds = CGRectMake(0, 0, 200, 200);
          }
      } completion:^(BOOL finished){
          NSLog(@"%d",finished);
      }];
}

效果如下:


上一篇說過,UIView對象持有一個(gè)CALayer,真正來做動(dòng)畫的是這個(gè)layer,UIView只是對它做了一層封裝,可以通過一個(gè)簡單的實(shí)驗(yàn)驗(yàn)證一下:我們寫一個(gè)MyTestLayer類繼承CALayer,并重寫它的set方法;再寫一個(gè)MyTestView類繼承UIView,重寫它的layerClass方法指定圖層類為MyTestLayer:

@interface MyTestLayer : CALayer
@end
@implementation MyTestLayer
- (void)setBounds:(CGRect)bounds
{
    NSLog(@"----layer setBounds");
    [super setBounds:bounds];
    NSLog(@"----layer setBounds end");
}
...
@end

@interface MyTestView : UIView
- (void)setBounds:(CGRect)bounds
{
    NSLog(@"----view setBounds");
    [super setBounds:bounds];
    NSLog(@"----view setBounds end");
}
...
+(Class)layerClass
{
    return [MyTestLayer class];
}
@end

當(dāng)我們給view設(shè)置bounds時(shí),getter、setter的調(diào)用順序是這樣的:

也就是說,在view的setBounds方法中,會(huì)調(diào)用layer的setBounds;同樣view的getBounds也會(huì)調(diào)用layer的getBounds。其他屬性也會(huì)得到相同的結(jié)論。那么動(dòng)畫又是怎么產(chǎn)生的呢?當(dāng)我們layer的屬性發(fā)生變化時(shí),會(huì)調(diào)用代理方法actionForLayer: forKey: 來獲得這次屬性變化的動(dòng)畫方案,而view就是它所持有的layer的代理:

@interface CALayer : NSObject <NSCoding, CAMediaTiming>
...
@property(nullable, weak) id <CALayerDelegate> delegate;
...
@end

@protocol CALayerDelegate <NSObject>
@optional
...
/* If defined, called by the default implementation of the
 * -actionForKey: method. Should return an object implementating the
 * CAAction protocol. May return 'nil' if the delegate doesn't specify
 * a behavior for the current event. Returning the null object (i.e.
 * '[NSNull null]') explicitly forces no further search. (I.e. the
 * +defaultActionForKey: method will not be called.) */
- (nullable id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event;
...
@end

注釋中說明,該方法返回一個(gè)實(shí)現(xiàn)了CAAction的對象,通常是一個(gè)動(dòng)畫對象;當(dāng)返回nil時(shí)執(zhí)行默認(rèn)的隱式動(dòng)畫,返回null時(shí)不執(zhí)行動(dòng)畫。還是上面那個(gè)改變bounds的動(dòng)畫,我們在MyTestView中重寫actionForLayer:方法

- (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event
{
    id<CAAction> action = [super actionForLayer:layer forKey:event];
    return action;
}

觀察它的返回值:


是一個(gè)內(nèi)部使用的_UIViewAddtiveAnimationAction對象,其中包含一個(gè)CABassicAnimation,默認(rèn)fillMode為both,默認(rèn)時(shí)間函數(shù)為淡入淡出,只包含fromValue(即動(dòng)畫之前的值,會(huì)在這個(gè)值和當(dāng)前值(block中修改過后的值)之間做動(dòng)畫)。我們可以嘗試在重寫的這個(gè)方法中強(qiáng)制返回nil,會(huì)發(fā)現(xiàn)我們不寫任何動(dòng)畫的代碼直接改變屬性也將產(chǎn)生一個(gè)默認(rèn)0.25s的隱式動(dòng)畫,這和上面的注釋描述是一致的。

如果兩個(gè)動(dòng)畫重疊在一起會(huì)是什么效果呢?
還是最開始的例子,我們添加兩個(gè)相同的UIView動(dòng)畫,一個(gè)時(shí)間為3s,一個(gè)時(shí)間為1s,并打印finished的值和兩個(gè)動(dòng)畫的持續(xù)時(shí)間。先執(zhí)行3s的動(dòng)畫,當(dāng)它還沒有結(jié)束時(shí)加上一個(gè)1s的動(dòng)畫,可以先看下實(shí)際效果:

很明顯,兩個(gè)動(dòng)畫的finished都為true且時(shí)間也是我們設(shè)置好的3s和1s。也就是說第二個(gè)動(dòng)畫并不會(huì)打斷第一個(gè)動(dòng)畫的執(zhí)行,而是將動(dòng)畫進(jìn)行了疊加。我們先來觀察一下運(yùn)行效果:

  • 最開始方塊的bounds為(100,100),點(diǎn)擊執(zhí)行3s動(dòng)畫,bounds變?yōu)?200,200),并開始展示變大的動(dòng)畫;

  • 動(dòng)畫過程中(假設(shè)到了(120,120)),點(diǎn)擊1s動(dòng)畫,由于這時(shí)真實(shí)bounds已經(jīng)是(200,200)了,所以bounds將變回100,并產(chǎn)生一個(gè)fromValue為(200,200)的動(dòng)畫。


    但此時(shí)方塊并沒有從200開始,而是馬上開始變小,并明顯變到一個(gè)比100更小的值。

  • 1s動(dòng)畫結(jié)束,finished為1,耗時(shí)1s。此時(shí)屏幕上的方塊是一個(gè)比100還要小的狀態(tài),又緩緩變回到100—3s動(dòng)畫結(jié)束,finished為1,耗時(shí)3s,方塊最終停在(100,100)的大小。

從這個(gè)現(xiàn)象我們可以猜想U(xiǎn)IView動(dòng)畫的疊加方式:當(dāng)我們通過改變View屬性實(shí)現(xiàn)動(dòng)畫時(shí),這個(gè)屬性的值是會(huì)立即改變的,動(dòng)畫只是展示出來的效果。當(dāng)動(dòng)畫還未結(jié)束時(shí)如果對同個(gè)屬性又加上另一個(gè)動(dòng)畫,兩個(gè)動(dòng)畫會(huì)從當(dāng)前展示的狀態(tài)開始進(jìn)行疊加,并最終停在view的真實(shí)位置。
舉個(gè)通俗點(diǎn)的例子,我們8點(diǎn)從家出發(fā),要在9點(diǎn)到達(dá)學(xué)校,我們按照正常的步速行走,這可以理解為一個(gè)動(dòng)畫;假如我們半路突然想到忘記帶書包了,需要回家拿書包(相當(dāng)于又添加了一個(gè)動(dòng)畫),這時(shí)我們肯定需要加快步速,當(dāng)我們拿到書包時(shí)相當(dāng)于第二個(gè)動(dòng)畫結(jié)束了,但我們上學(xué)這個(gè)動(dòng)畫還要繼續(xù)執(zhí)行,我們要以合適的速度繼續(xù)往學(xué)校趕,保證在9點(diǎn)準(zhǔn)時(shí)到達(dá)終點(diǎn)—學(xué)校。

所以剛才那個(gè)方塊為什么會(huì)有一個(gè)比100還小的過程就不難理解了:當(dāng)?shù)诙€(gè)動(dòng)畫加上去的時(shí)候,由于它是一個(gè)1s由200變?yōu)?00的動(dòng)畫,肯定要比3s動(dòng)畫執(zhí)行的快,而且是從120的位置開始執(zhí)行的,所以一定會(huì)朝反方向變化到比100還??;1s動(dòng)畫結(jié)束后,又會(huì)以適當(dāng)?shù)乃俣仍?s的時(shí)間點(diǎn)回到最終位置(100,100)。當(dāng)然疊加后的整個(gè)過程在內(nèi)部實(shí)現(xiàn)中可能是根據(jù)時(shí)間函數(shù)已經(jīng)計(jì)算好的。

這么做或許是為了讓動(dòng)畫顯得更流暢平滑,那么既然我們設(shè)置屬性值是立即生效的,動(dòng)畫只是看上去的效果,那剛才疊加的時(shí)刻屏幕展示上的位置(120,120)又是什么呢?這就是本篇要討論的下一個(gè)話題。


2.展示層(presentationLayer)和模型層(modelLayer)

我們知道UIView動(dòng)畫其實(shí)是layer層做的,而view是對layer的一層封裝,我們對view的bounds等這些屬性的操作其實(shí)都是對它所持有的layer進(jìn)行操作,我們做一個(gè)簡單的實(shí)驗(yàn)—在UIView動(dòng)畫的block中改變view的bounds后,分別查看下view和layer的bounds的實(shí)際值:

    _testView.bounds = CGRectMake(0, 0, 100, 100);
    [UIView animateWithDuration:1 animations:^(void){
        _testView.bounds = CGRectMake(0, 0, 200, 200);
    } completion:nil];

賦值完成后我們分別打印view,layer的bounds:


都已經(jīng)變成了(200,200),這是肯定的,之前已經(jīng)驗(yàn)證過set view的bounds實(shí)際上就是set 它的layer的bounds??蓜?dòng)畫不是layer實(shí)現(xiàn)的么?layer也已經(jīng)到達(dá)終點(diǎn)了,它是怎么將動(dòng)畫展示出來的呢?
這里就要提到CALayer的兩個(gè)實(shí)例方法presentationLayer和modelLayer:

@interface CALayer : NSObject <NSCoding, CAMediaTiming>
...
/* 以下參考官方api注釋 */
/* presentationLayer
 * 返回一個(gè)layer的拷貝,如果有任何活動(dòng)動(dòng)畫時(shí),包含當(dāng)前狀態(tài)的所有l(wèi)ayer屬性
 * 實(shí)際上是逼近當(dāng)前狀態(tài)的近似值。
 * 嘗試以任何方式修改返回的結(jié)果都是未定義的。
 * 返回值的sublayers 、mask、superlayer是當(dāng)前l(fā)ayer的這些屬性的presentationLayer
 */
- (nullable instancetype)presentationLayer;

/* modelLayer
 * 對presentationLayer調(diào)用,返回當(dāng)前模型值。
 * 對非presentationLayer調(diào)用,返回本身。
 * 在生成表示層的事務(wù)完成后調(diào)用此方法的結(jié)果未定義。
 */
- (instancetype)modelLayer;
...

從注釋不難看出,這個(gè)presentationLayer即是我們看到的屏幕上展示的狀態(tài),而modelLayer就是我們設(shè)置完立即生效的真實(shí)狀態(tài),我們動(dòng)畫開始后延遲0.1s分別打印layer,layer.presentationLayer,layer.modelLayer和layer.presentationLayer.modelLayer :


明顯,layer.presentationLayer是動(dòng)畫當(dāng)前狀態(tài)的值,而layer.modelLayer 和 layer.presentationLayer.modelLayer 都是layer本身。(關(guān)于modelLayer注釋中兩句話的區(qū)別還請各位指教~)

到這里,CALayer動(dòng)畫的原理基本清晰了,當(dāng)有動(dòng)畫加入時(shí),presentationLayer會(huì)不斷的(從按某種插值或逼近得到的動(dòng)畫路徑上)取值來進(jìn)行展示,當(dāng)動(dòng)畫結(jié)束被移除時(shí)則取modelLayer的狀態(tài)展示。這也是為什么我們用CABasicAnimation時(shí),設(shè)定當(dāng)前值為fromValue時(shí)動(dòng)畫執(zhí)行結(jié)束又會(huì)回到起點(diǎn)的原因,實(shí)際上動(dòng)畫結(jié)束并不是回到起點(diǎn)而是到了modelLayer的位置。

雖然我們可以使用fillMode控制它結(jié)束時(shí)保持狀態(tài),但這種方法在動(dòng)畫執(zhí)行完之后并沒有將動(dòng)畫從渲染樹中移除(因?yàn)槲覀冃枰O(shè)置animation.removedOnCompletion = NO才能讓fillMode生效)。如果我們想讓動(dòng)畫停在終點(diǎn),更合理的辦法是一開始就將layer設(shè)置成終點(diǎn)狀態(tài),其實(shí)前文提到的UIView的block動(dòng)畫就是這么做的。

如果我們一開始就將layer設(shè)置成終點(diǎn)狀態(tài)再加入動(dòng)畫,會(huì)不會(huì)造成動(dòng)畫在終點(diǎn)位置閃一下呢?其實(shí)是不會(huì)的,因?yàn)槲覀兛吹降膶?shí)際上是presentationLayer,而我們修改layer的屬性,presentationLayer是不會(huì)立即改變的:

    MyTestView *view = [[MyTestView alloc]initWithFrame:CGRectMake(200, 200, 100, 100)];
    [self.view addSubview:view];
    
    view.center = CGPointMake(1000, 1000);
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)((1/60) * NSEC_PER_SEC)), dispatchQueue, ^{
        NSLog(@"presentationLayer %@ y %f",view.layer.presentationLayer, view.layer.presentationLayer.position.y);
        NSLog(@"layer.modelLayer %@ y %f",view.layer.modelLayer,view.layer.modelLayer.position.y);
    });
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)((1/20) * NSEC_PER_SEC)), dispatchQueue, ^{
        NSLog(@"presentationLayer %@ y %f",view.layer.presentationLayer, view.layer.presentationLayer.position.y);
        NSLog(@"layer.modelLayer %@ y %f",view.layer.modelLayer,view.layer.modelLayer.position.y);
    });

在上面代碼中我們改變view的center,modelLayer是立即改變的因?yàn)樗褪莑ayer本身。但presentationLayer是沒有變的,我們嘗試延遲一定時(shí)間再去取presentationLayer,發(fā)現(xiàn)它是在一個(gè)很短的時(shí)間之后才發(fā)生變化的,這個(gè)時(shí)間跟具體設(shè)備的屏幕刷新頻率有關(guān)。也就是說我們給layer設(shè)置屬性后,當(dāng)下次屏幕刷新時(shí),presentationLayer才會(huì)獲取新值進(jìn)行繪制。因?yàn)槲覀儾豢赡軐γ恳淮螌傩孕薷亩歼M(jìn)行一次繪制,而是將這些修改保存在model層,當(dāng)下次屏幕刷新時(shí)再統(tǒng)一取model層的值重繪。

如果我們添加了動(dòng)畫,并將modelLayer設(shè)置到終點(diǎn)位置,下次屏幕刷新時(shí),presentationLayer會(huì)優(yōu)先從動(dòng)畫中取值來繪制,所以并不會(huì)造成在終點(diǎn)位置閃一下。


總結(jié)
  • UIView持有一個(gè)CALayer負(fù)責(zé)展示,view是這個(gè)layer的delegate。改變view的屬性實(shí)際上是在改變它持有的layer的屬性,layer屬性發(fā)生改變時(shí)會(huì)調(diào)用代理方法actionForLayer: forKey: 來得知此次變化是否需要?jiǎng)赢?。對同一個(gè)屬性疊加動(dòng)畫會(huì)從當(dāng)前展示狀態(tài)開始疊加并最終停在modelLayer的真實(shí)位置。
  • CALayer內(nèi)部控制兩個(gè)屬性presentationLayer和modelLayer,modelLayer為當(dāng)前l(fā)ayer真實(shí)的狀態(tài),presentationLayer為當(dāng)前l(fā)ayer在屏幕上展示的狀態(tài)。presentationLayer會(huì)在每次屏幕刷新時(shí)更新狀態(tài),如果有動(dòng)畫則根據(jù)動(dòng)畫獲取當(dāng)前狀態(tài)進(jìn)行繪制,動(dòng)畫移除后則取modelLayer的狀態(tài)。

Demo代碼地址

參考資料

對UIView動(dòng)畫和Core Animation的關(guān)系的一點(diǎn)理解
iOS CoreAnimation專題 系列
iOS核心動(dòng)畫高級技巧 系列
iOS動(dòng)畫開發(fā) 系列

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

相關(guān)閱讀更多精彩內(nèi)容

  • 在iOS中隨處都可以看到絢麗的動(dòng)畫效果,實(shí)現(xiàn)這些動(dòng)畫的過程并不復(fù)雜,今天將帶大家一窺ios動(dòng)畫全貌。在這里你可以看...
    每天刷兩次牙閱讀 8,698評論 6 30
  • 在iOS中隨處都可以看到絢麗的動(dòng)畫效果,實(shí)現(xiàn)這些動(dòng)畫的過程并不復(fù)雜,今天將帶大家一窺iOS動(dòng)畫全貌。在這里你可以看...
    F麥子閱讀 5,274評論 5 13
  • 轉(zhuǎn)載:http://m.itdecent.cn/p/32fcadd12108 每個(gè)UIView有一個(gè)伙伴稱為l...
    F麥子閱讀 6,595評論 0 13
  • Core Animation Core Animation,中文翻譯為核心動(dòng)畫,它是一組非常強(qiáng)大的動(dòng)畫處理API,...
    45b645c5912e閱讀 3,165評論 0 21
  • 于 2016-03-27 08:08 出發(fā),歷時(shí) 8 小時(shí), 40 分鐘 浙江 溫州 樂清市-浙江 溫州 樂清市徒...
    六只腳閱讀 2,834評論 0 1

友情鏈接更多精彩內(nèi)容