1. 程式人生 > >iOS CoreAnimation專題——原理篇(二) UIView block動畫實現原理

iOS CoreAnimation專題——原理篇(二) UIView block動畫實現原理

前言

上一章中我們深入研究了UIView和它持有的那個CALayer之間的關係,知道了我們對UIView的各種屬性的操作實際上都是間接的操作了CALayer對應的屬性。
這一章中我們將進一步探究iOS動畫,看看UIView是如何將CoreAnimation封裝成block動畫的。

CALayer的可動畫屬性

CALayer擁有大量的屬性,如果大家按住cmd點進CALayer的標頭檔案中看的話,會發現很多的屬性的註釋中,最後會有一個詞叫做Animatable,直譯過來是可動畫的。下面的截圖只是CALayer眾多可動畫屬性中的一部分(注意frame並不是可動畫的屬性):

如果一個屬性被標記為Animatable,那麼它具有以下兩個特點:

1、直接對它賦值可能產生隱式動畫;
2、我們的CAAnimation的keyPath可以設定為這個屬性的名字。

當我們直接對可動畫屬性賦值的時候,由於有隱式動畫存在的可能,CALayer首先會判斷此時有沒有隱式動畫被觸發。它會讓它的delegate(沒錯CALayer擁有一個屬性叫做delegate)呼叫actionForLayer:forKey:來獲取一個返回值,這個返回值在宣告的時候是一個id物件,當然在執行時它可能是任何物件。這時CALayer拿到返回值,將進行判斷:如果返回的物件是一個nil,則進行預設的隱式動畫;如果返回的物件是一個[NSNull null] ,則CALayer不會做任何動畫;如果是一個正確的實現了CAAction協議的物件,則CALayer用這個物件來生成一個CAAnimation,並加到自己身上進行動畫。

根據上面的描述,我們可以進一步完善我們上一章中重寫的CALayer的屬性的setter方法,拿position作例子:

- (void)setPosition:(CGPoint)position
{
//    [super setPosition:position];
    if ([self.delegate respondsToSelector:@selector(actionForLayer:forKey:)]) {
        id obj = [self.delegate actionForLayer:self forKey:@"position"];
        if
(!obj) { // 隱式動畫 } else if ([obj isKindOfClass:[NSNull class]]) { // 直接重繪(無動畫) } else { // 使用obj生成CAAnimation CAAnimation * animation; [self addAnimation:animation forKey:nil]; } } // 隱式動畫 }

UIView的block動畫

Amazing things happen when they are in a block.

有趣的是,如果這個CALayer被一個UIView所持有,那麼這個CALayer的delegate就是持有它的那個UIView,結合上一章講的CALayer的各個屬性是如何與UIView互動的,大家應該可以思考出這樣的問題:為什麼同樣的一行程式碼在block裡面就有動畫在block外面就沒動畫,就像下面這樣:

    // 這樣寫沒有動畫
    view.center = CGPointMake(80, 80);


    [UIView animateWithDuration:1.25 animations:^{
        // 寫在block裡面就有動畫
        view.center = CGPointMake(80, 80);
    }];

既然UIView就是CALayer的delegate,那麼actionForLayer:forKey:方法就是由UIView來實現的。所以UIView可以相當靈活的控制動畫的產生。

當我們對UIView的一個屬性賦值的時候,它只是簡單的呼叫了它持有的那個CALayer的對應的屬性的setter方法而已,根據上面的可動畫屬性的特點,CALayer會讓它的delegate(也就是這個UIView)呼叫actionForLayer:forKey:方法。實際上結果大家都應該能想得到:在UIView的動畫block外面,UIView的這個方法將返回NSNull,而在block裡面,UIView將返回一個正確的CAAction物件(這裡將不深究UIView是如何判斷此時setter的呼叫是在動畫block外面還是裡面的)。

為了證明這個結論,我們將繼續進行實驗:

    NSLog(@"%@",[view.layer.delegate actionForLayer:view.layer forKey:@"position"]);

    [UIView animateWithDuration:1.25 animations:^{
        NSLog(@"%@",[view.layer.delegate actionForLayer:view.layer forKey:@"position"]);
}];

我們分別在block外面和block裡面列印actionForLayer:forKey:方法的返回值,看看它究竟是什麼玩意。

打印發現,我們的結論是正確的:在block外面,這個方法將返回一個NSNull(是尖括號的null,nil打印出來是圓括號的null),而在block裡面返回了一個叫做UIViewAdditiveAnimationAction類的物件,這個類是一個私有類,遵循了蘋果一罐的命名規範: xxAction,一定就是一個實現了CAAction協議的物件了。

這也就說明了為什麼我們對一個view的center賦值,如果這行程式碼在動畫block裡面,就會有動畫,在block外面則沒有動畫。

注意:

1、 如果你的程式碼大概是這樣的:

    view.center = CGPointMake(80, 80);
    [UIView animateWithDuration:1.25 animations:^{
        view.center = CGPointMake(80, 80);
    } completion:^(BOOL finished) {
        NSLog(@"aaa");
}];

那麼completion裡面的block將會瞬間被呼叫而不是1.25秒之後呼叫,因為你這樣寫的動畫是沒有意義的(從一個地方移動到這個地方),所以就沒有動畫產生,也就是動畫一開始就結束了。
2、 如果你的程式碼大概是這樣的:

    [UIView animateWithDuration:1.25 animations:^{
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            view.center = CGPointMake(80, 80);
        });
        } completion:^(BOOL finished) {
        NSLog(@"aaa");
    }];

也就是在動畫block裡面延遲呼叫一段程式碼,同樣是沒有卵用的,completionBlock將會直接被呼叫,因為當animateWithDuration…這個類方法被呼叫的時候animationBlock裡面沒有任何與動畫相關的程式碼(view.center = CGPointMake(80, 80);這行程式碼被延遲呼叫了),UIView就認為你沒有在裡面寫東西,那麼肯定就沒有動畫了。接著兩秒後,檢視就瞬移了,因為當view.center = CGPointMake(80, 80);真正被呼叫的時候,animateWithDuration…這個方法早就返回了,這次呼叫肯定發生在動畫block之外。

再次深入

我們繼續深入探究一下,看看動畫是怎樣被加到CALayer上的。

還記得我們上一章中用來做實驗的一個UIView一個私有的CALayer麼,這裡我們繼續使用它們來完成我們的實驗。

找到我們的TestAnimationView和它的私有CALayer:TestAnimationLayer。在TestAnimationView中重寫方法:

- (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event
{
    id<CAAction> obj = [super actionForLayer:layer forKey:event];
    NSLog(@"%@",obj);
    return obj;
}

打好斷點,然後在TestAnimationLayer中重寫方法:

- (void)addAnimation:(CAAnimation *)anim forKey:(NSString *)key
{
    [super addAnimation:anim forKey:key];
    NSLog(@"%@",[anim debugDescription]);
}

同樣打好斷點。接下來在ViewController中呼叫一下:

- (void)viewDidLoad {
    [super viewDidLoad];
    TestAnimationView * view = [[TestAnimationView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
    [self.view addSubview:view];

    [UIView animateWithDuration:1.25 animations:^{
        view.center = CGPointMake(80, 80);
    } completion:^(BOOL finished) {
        NSLog(@"aaa");
    }];
}

在view.center = CGPointMake(80, 80);這裡打斷點,然後執行。

一開始斷點會瘋狂的進入actionForLayer方法,因為我們呼叫initWithFrame的時候view會呼叫createLayerWithFrame:(見上一章),如果大家列印event引數,會發現在createLayerWithFrame:方法中,系統會依次給layer的以下幾個屬性賦值:bounds, opaque, contentsScale, rasterizationScale, position,所以actionForLayer:forKey:方法會被呼叫5次,這5次是在動畫block外面呼叫的,所以我們會發現obj打印出來都是NSNull。

接下來斷點會進入到動畫block裡面,繼續執行,肯定就又會進入actionForLayer:forKey:方法,事實也是如此,並且這次列印obj,出來的就是那個實現了CAAction協議的私有類了。我們再次繼續執行,這時斷點會進入到TestAnimationLayer中的addAnimation方法中。呼叫NSLog(@”%@”,[anim debugDescription]);後我們會打印出這樣的內容:

沒錯,UIView將CAAction返回給layer後,layer使用這個物件生成了一個CABasicAnimation並且呼叫了[self addAnimation..]。通過這個CABasicAnimation物件的資訊我們可以知道很多東西:

首先是additive這個屬性為true(它預設為false),這就導致了fromValue和toValue的值顯得似乎很奇怪。關於additive幹了什麼事情,我們在後面的章節將會提到。

接下來是delegate,CAAnimation將在動畫結束後回撥它delegate的animationDidStop方法,我們發現這個delegate又是一個私有類,因為我們在呼叫UIView動畫的時候設定了completionBlock,也就是動畫結束後要呼叫的block,所以UIView會將這個私有delegate的資訊放進CAAction物件中告知CALayer動畫結束後我要幹事情(呼叫這個block)。

關於fillMode和timingFunction我們將在探究動畫時間的時候來詳細對它進行講解。這裡你會發現timingFunction預設的是easeInEaseOut,也就是淡入淡出效果。

keyPath被設定為了position,那是因為我們在動畫block中是對center賦值,對應到layer中就是position了。

總結

CALayer中有大量屬性在註釋的時候標記了Animatable,表示這個屬性是可動畫的。可動畫的屬性具有兩個特點:1、直接對它賦值可能產生隱式動畫;2、我們的CAAnimation的keyPath可以設定為這個屬性的名字。

當我們對這些屬性賦值的時候,layer會讓它的delegate呼叫actionForLayer:forKey:方法獲取一個返回值,這個返回值可能是這樣幾種情況:1、是一個nil,則layer會走自己的隱式動畫;2、是一個NSNull,則layer不會做任何動畫;3、是一個實現了CAAction協議的物件,則layer會用這個物件生成一個CABasicAnimation加到自己身上執行動畫。

有趣的是,如果一個UIView持有一個CALayer,那麼這個layer的delegate就是這個view。當我們對view的一個屬性,比如center賦值的時候,view同時會去對layer的position賦值。這時layer會讓它的delegate(就是這個view)呼叫actionForLayer:forKey:方法,UIView在這個方法中是這樣實現的:如果這次呼叫發生在[UIView animateWithDuration:animations:]的動畫block裡面,則UIView生成一個CAAction物件,返回給layer。如果沒有發生在這個block外面,則返回NSNull。這也就說明了為什麼我們對一個view的center賦值,如果這行程式碼在動畫block裡面,就會有動畫,在block外面則沒有動畫。

在下一章中,我們將深入到CALayer內部,結合CABasicAnimation看看兩個非常重要的概念:modelLayer和presentationLayer。