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

有了前兩篇的概念基礎,本篇從以下兩點結合具體代碼探索下CoreAnimation的一些原理。

UIView動畫實現原理
展示層(presentationLayer)和模型層(modelLayer)

1.UIView動畫實現原理

UIView提供了一系列UIViewAnimationWithBlocks,我們只需要把改變可動畫屬性的代碼放在animations的block中即可實現動畫效果,比如:

- (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對象持有一個CALayer,真正來做動畫的是這個layer,UIView只是對它做了一層封裝,可以通過一個簡單的實驗驗證一下:我們寫一個MyTestLayer類繼承CALayer,并重寫它的set方法;再寫一個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

當我們給view設置bounds時,getter、setter的調用順序是這樣的:

也就是說,在view的setBounds方法中,會調用layer的setBounds;同樣view的getBounds也會調用layer的getBounds。其他屬性也會得到相同的結論。那么動畫又是怎么產生的呢?當我們layer的屬性發生變化時,會調用代理方法actionForLayer: forKey: 來獲得這次屬性變化的動畫方案,而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

注釋中說明,該方法返回一個實現了CAAction的對象,通常是一個動畫對象;當返回nil時執行默認的隱式動畫,返回null時不執行動畫。還是上面那個改變bounds的動畫,我們在MyTestView中重寫actionForLayer:方法

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

觀察它的返回值:


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

如果兩個動畫重疊在一起會是什么效果呢?
還是最開始的例子,我們添加兩個相同的UIView動畫,一個時間為3s,一個時間為1s,并打印finished的值和兩個動畫的持續時間。先執行3s的動畫,當它還沒有結束時加上一個1s的動畫,可以先看下實際效果:

很明顯,兩個動畫的finished都為true且時間也是我們設置好的3s和1s。也就是說第二個動畫并不會打斷第一個動畫的執行,而是將動畫進行了疊加。我們先來觀察一下運行效果:

  • 最開始方塊的bounds為(100,100),點擊執行3s動畫,bounds變為(200,200),并開始展示變大的動畫;

  • 動畫過程中(假設到了(120,120)),點擊1s動畫,由于這時真實bounds已經是(200,200)了,所以bounds將變回100,并產生一個fromValue為(200,200)的動畫。


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

  • 1s動畫結束,finished為1,耗時1s。此時屏幕上的方塊是一個比100還要小的狀態,又緩緩變回到100—3s動畫結束,finished為1,耗時3s,方塊最終停在(100,100)的大小。

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

所以剛才那個方塊為什么會有一個比100還小的過程就不難理解了:當第二個動畫加上去的時候,由于它是一個1s由200變為100的動畫,肯定要比3s動畫執行的快,而且是從120的位置開始執行的,所以一定會朝反方向變化到比100還小;1s動畫結束后,又會以適當的速度在3s的時間點回到最終位置(100,100)。當然疊加后的整個過程在內部實現中可能是根據時間函數已經計算好的。

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


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

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

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

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


都已經變成了(200,200),這是肯定的,之前已經驗證過set view的bounds實際上就是set 它的layer的bounds。可動畫不是layer實現的么?layer也已經到達終點了,它是怎么將動畫展示出來的呢?
這里就要提到CALayer的兩個實例方法presentationLayer和modelLayer:

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

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

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


明顯,layer.presentationLayer是動畫當前狀態的值,而layer.modelLayer 和 layer.presentationLayer.modelLayer 都是layer本身。(關于modelLayer注釋中兩句話的區別還請各位指教~)

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

雖然我們可以使用fillMode控制它結束時保持狀態,但這種方法在動畫執行完之后并沒有將動畫從渲染樹中移除(因為我們需要設置animation.removedOnCompletion = NO才能讓fillMode生效)。如果我們想讓動畫停在終點,更合理的辦法是一開始就將layer設置成終點狀態,其實前文提到的UIView的block動畫就是這么做的。

如果我們一開始就將layer設置成終點狀態再加入動畫,會不會造成動畫在終點位置閃一下呢?其實是不會的,因為我們看到的實際上是presentationLayer,而我們修改layer的屬性,presentationLayer是不會立即改變的:

    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是立即改變的因為它就是layer本身。但presentationLayer是沒有變的,我們嘗試延遲一定時間再去取presentationLayer,發現它是在一個很短的時間之后才發生變化的,這個時間跟具體設備的屏幕刷新頻率有關。也就是說我們給layer設置屬性后,當下次屏幕刷新時,presentationLayer才會獲取新值進行繪制。因為我們不可能對每一次屬性修改都進行一次繪制,而是將這些修改保存在model層,當下次屏幕刷新時再統一取model層的值重繪。

如果我們添加了動畫,并將modelLayer設置到終點位置,下次屏幕刷新時,presentationLayer會優先從動畫中取值來繪制,所以并不會造成在終點位置閃一下。


總結
  • UIView持有一個CALayer負責展示,view是這個layer的delegate。改變view的屬性實際上是在改變它持有的layer的屬性,layer屬性發生改變時會調用代理方法actionForLayer: forKey: 來得知此次變化是否需要動畫。對同一個屬性疊加動畫會從當前展示狀態開始疊加并最終停在modelLayer的真實位置。
  • CALayer內部控制兩個屬性presentationLayer和modelLayer,modelLayer為當前layer真實的狀態,presentationLayer為當前layer在屏幕上展示的狀態。presentationLayer會在每次屏幕刷新時更新狀態,如果有動畫則根據動畫獲取當前狀態進行繪制,動畫移除后則取modelLayer的狀態。

Demo代碼地址

參考資料

對UIView動畫和Core Animation的關系的一點理解
iOS CoreAnimation專題 系列
iOS核心動畫高級技巧 系列
iOS動畫開發 系列

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容