玩轉iOS開發:7.《Core Animation》Implicit Animations

CainLuo發表於2019-02-14

文章分享至我的個人技術部落格: https://cainluo.github.io/14807833712288.html


作者感言

在上一篇文章《Core Animation》CALayer的Specialized Layers中, 我們瞭解了CALayer的許多子類特性, 可以為我們在遇到一些特殊的開發需求中提供一定的幫助, 既然我們這次學的的Core Animation, 那怎麼會和動畫不掛鉤呢? 這次讓我們來初體驗一下.

**最後:**
**如果你有更好的建議或者對這篇文章有不滿的地方, 請聯絡我, 我會參考你們的意見再進行修改, 聯絡我時, 請備註**`Core Animation`**如果覺得好的話, 希望大家也可以打賞一下~嘻嘻~祝大家學習愉快~謝謝~**


簡介

Implicit Animations也稱為隱式動畫, 啥? 什麼叫做隱式動畫? 百度去吧~~哈哈哈(後面會講解的), 這些問題就不在這裡做解釋了, 還是進入主題才比較重要.


Transactions

其實在Core Animation中, 動畫效果並不需要我們去手動開啟, 因為系統預設就是Open狀態, 相反過來, 如果我們不需要動畫的話, 我們需要手動的去關閉.

如果我們用一個CALayer的一個動畫屬性, 並且嘗試去改變它, 這個效果並不會馬上就顯示出來, 因為它要從一個預設值平滑的過度到一個新的值, 而這些所有的內部操作我們都不需要去理會, 因為系統預設就是這麼做的.

我們可以先來看個Demo:

- (void)transactionsColor {
    
    UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0,
                                                            100,
                                                            self.view.frame.size.width,
                                                            self.view.frame.size.width)];
    
    view.backgroundColor = [UIColor grayColor];
    
    [self.view addSubview:view];
    
    UIButton *button = [[UIButton alloc] init];
    
    button.center = CGPointMake(self.view.frame.size.width / 2, 50);
    button.bounds = CGRectMake(0, 0, 100, 50);
    button.backgroundColor = [UIColor blueColor];
    
    [button setTitle:@"改變顏色"
            forState:UIControlStateNormal];
    [button addTarget:self
               action:@selector(changeLayerColor)
     forControlEvents:UIControlEventTouchUpInside];
    
    [view addSubview:button];
    
    self.colorLayer  = [CALayer layer];
    self.colorLayer.position = CGPointMake(view.frame.size.width / 2, view.frame.size.height / 2);
    self.colorLayer.bounds = CGRectMake(0, 0, 150, 150);
    self.colorLayer.backgroundColor = [UIColor redColor].CGColor;
    
    [view.layer addSublayer:self.colorLayer];
}

- (void)changeLayerColor {
    
    CGFloat redColor = arc4random() / (CGFloat)INT_MAX;
    CGFloat greenColor = arc4random() / (CGFloat)INT_MAX;
    CGFloat blueColor = arc4random() / (CGFloat)INT_MAX;
    
    self.colorLayer.backgroundColor = [UIColor colorWithRed:redColor
                                                      green:greenColor
                                                       blue:blueColor
                                                      alpha:1.0f].CGColor;
}
複製程式碼
1
2

看完這個Demo其實就已經知道神馬叫做隱式動畫了, 所謂的隱式動畫就是我們沒有給它指定任何的動畫型別, 僅僅只是改變某個屬性, 當然Core Animation也是支援顯示動畫, 不然我們就沒那麼多的興趣來學習Core Animation了~

那麼當我們去改變一個屬性的時候, Core Animation是如何去判斷動畫型別還有動畫的持續時間呢? 這個問題其實也很簡單, 動畫的執行時間取決於Transactions的設定, 而動畫型別是取決於CALayer的行為.

其實Transactions實際上是Core Animation用來包含一堆屬性動畫集合的機制, 任何用指定Transactions去改變可以做動畫效果的圖層屬性都不會馬上發生變化, 而是需要Transactions在提交的一瞬間, 才會開始用一個動畫效果過渡到新設定的值.

Transactions是需要通過CATransaction這個類來進行管理的, 奇怪的是, CATransaction這個類並不是管理一個簡單的Transactions, 而是管理了一堆我們不能訪問的Transactions, 由於CATransaction並沒有屬性和例項化方法, 也不能用**+ (instancetype)alloc;– (instancetype)init;方法來建立它, 只有它所提供的+ (void)begin;+ (void)commit;來控制.

雖然我們再上面的Demo裡沒有設定動畫時間, 但Core Animation會在每一個run looop週期中自動開始一次新的Transactions**, 即使我們不手動的去呼叫**[CATransaction begin];, 但在每一次run loop**的迴圈中, 被修改的屬性都會集中起來, 然後統一做一次0.25秒的動畫, 這個是系統預設的.

說了那麼多, 我們實際上來改改修改顏色的那個程式碼塊, 讓它有一個顯示動畫的效果:

- (void)changeLayerColorAgain {
    
    [CATransaction begin];
    [CATransaction setAnimationDuration:2.0f];
    
    CGFloat redColor = arc4random() / (CGFloat)INT_MAX;
    CGFloat greenColor = arc4random() / (CGFloat)INT_MAX;
    CGFloat blueColor = arc4random() / (CGFloat)INT_MAX;
    
    self.colorLayer.backgroundColor = [UIColor colorWithRed:redColor
                                                      green:greenColor
                                                       blue:blueColor
                                                      alpha:1.0f].CGColor;
    
    [CATransaction commit];
}
複製程式碼
3

看起來的效果讓人覺得是真的有動畫效果了, 如果大家在之前就已經用過UIView來做過動畫的話, 那麼大家應該對這個動畫模式不會感覺到陌生, 因為UIView就有兩個類似的方法, + (void)beginAnimations:(nullable NSString *)animationID context:(nullable void *)context;+ (void)commitAnimations;, 其實這兩個這兩個方法也是因為在內部設定了CATransaction的原因.

iOS 4的時候, 蘋果就已經對UIView新增了一種基於Block的動畫方法, + (void)animateWithDuration:(NSTimeInterval)duration animations:(void (^)(void))animations NS_AVAILABLE_IOS(4_0);, 用起來更加的方便, 但實際上是做同樣的事情, 但使用這種方法就可以避免**+ (void)begin;+ (void)commit;**匹配的問題造成一些蛋疼的事情.


Completion Blocks

這裡我們就使用一下基於UIViewBlock動畫方法, 我們可以在動畫結束之後再對這個圖層進行一些操作, 當然這裡還是基於上面的Demo來做演示:

- (void)changeLayerColorWithCompletion {
    
    [CATransaction begin];
    [CATransaction setAnimationDuration:2.0f];
    
    [CATransaction setCompletionBlock:^{
        CGAffineTransform transform = self.colorLayer.affineTransform;
        
        transform = CGAffineTransformRotate(transform, M_PI_4);
        
        self.colorLayer.affineTransform = transform;
    }];
    
    CGFloat redColor = arc4random() / (CGFloat)INT_MAX;
    CGFloat greenColor = arc4random() / (CGFloat)INT_MAX;
    CGFloat blueColor = arc4random() / (CGFloat)INT_MAX;
    
    self.colorLayer.backgroundColor = [UIColor colorWithRed:redColor
                                                      green:greenColor
                                                       blue:blueColor
                                                      alpha:1.0f].CGColor;
    
    [CATransaction commit];
}
複製程式碼
4

Layer Actions

開始的時候我們就用一個Demo來進行演示:

- (void)addLayerView {
    
    self.layerView = [[UIView alloc] init];
    self.layerView.center = CGPointMake(self.view.frame.size.width / 2, self.view.frame.size.height / 2);
    self.layerView.bounds = CGRectMake(0, 0, 150, 150);
    self.layerView.backgroundColor = [UIColor redColor];
    
    [self.view addSubview:self.layerView];
    
    UIButton *button = [[UIButton alloc] init];
    
    button.center = CGPointMake(self.view.frame.size.width / 2, 200);
    button.bounds = CGRectMake(0, 0, 100, 50);
    button.backgroundColor = [UIColor blueColor];
    
    [button setTitle:@"改變顏色"
            forState:UIControlStateNormal];
    
    [button addTarget:self
               action:@selector(changeLayerViewColor)
     forControlEvents:UIControlEventTouchUpInside];
    
    [self.view addSubview:button];
}

- (void)changeLayerViewColor {
    
    [CATransaction begin];
    [CATransaction setAnimationDuration:2.0f];
    
    CGFloat redColor = arc4random() / (CGFloat)INT_MAX;
    CGFloat greenColor = arc4random() / (CGFloat)INT_MAX;
    CGFloat blueColor = arc4random() / (CGFloat)INT_MAX;
    
    self.layerView.layer.backgroundColor = [UIColor colorWithRed:redColor
                                                           green:greenColor
                                                            blue:blueColor
                                                           alpha:1.0f].CGColor;
    
    [CATransaction commit];
}
複製程式碼
5

看完這個Demo, 有很多人肯定會有疑問, 為啥沒有了之前的那個平滑過渡效果呢? 好像是被幹掉了, 這是啥回事?

其實我們可以仔細想一想, 如果UIView裡的屬性都有動畫特性的話, 那我們去修改這些屬性時, 肯定會注意到的, 可為啥UIKit要把這個隱式動畫給禁止呢?

我們都知道Core Animation通常會對CALayer所有的可做動畫的屬性都賦予了動畫特性, 但在UIView中就不一樣了, 它會預設把所關聯在一起的CALayer的這個特性給關閉掉, 這裡就要了解一下隱式動畫是如何實現的.

當我們改變CALayer屬性時, CALayer自動應用的動畫, 我們可以成為CALayer的行為, 每當CALayer的屬性被修改的時候, 它會去呼叫**- (nullable id)actionForKey:(NSString *)event;**這個方法去傳遞屬性的名稱, 然後就會去執行如下幾步:

  • 首先CALayer會去檢測它是否有Delegate, 並且看看這個Delegate有沒有實現CALayerDelegate協議裡的**- (nullable id)actionForLayer:(CALayer *)layer forKey:(NSString *)event;**方法, 如果有, 就直接呼叫並返回結果.

  • 如果CALayer沒有Delegate的話, 或者Delegate沒有實現**- (nullable id)actionForLayer:(CALayer *)layer forKey:(NSString *)event;方法, 那麼圖層就會接著去檢查包含屬性名稱對應的CALayer行為所對映的Actions**字典.

  • 如果Actions字典沒有包含對應的屬性, 那麼圖層接著會在它的style字典裡接著搜尋屬性名.

  • 最後, 在style裡也找不到對應的行為, 那麼圖層就會直接呼叫**+ (nullable id)defaultActionForKey:(NSString *)event;實現系統所提供的每個屬性的預設行為.

    如果一輪完整的搜尋結束之後, – (nullable id)actionForKey:(NSString *)event;返回為空的話, 那麼肯定不會有動畫效果, 如果返回CAAction協議對應的物件, CALayer會拿這個結果去對比先前和當前的值, 並且做一個動畫效果.

    知道這個原理之後, 我們就知道UIKit**是腫麼把隱式動畫給禁止掉了:

  • 每一個UIView對它所關聯的圖層都是充當一個Delegate物件, 並且提供了**- (nullable id)actionForKey:(NSString *)event;**的實現方法.

  • 當不在一個動畫塊的實現中, 那麼UIView就會對所有CALayer的行為返回nil, 如果在動畫的Block範圍之內, UIView就會返回一個非空的值.

    這裡我們簡單的Log一下結果:

- (void)checkViewAction {
    
    UIView *layerView = [[UIView alloc] init];
    
    layerView.center = CGPointMake(self.view.frame.size.width / 2, self.view.frame.size.height / 2);
    layerView.bounds = CGRectMake(0, 0, 150, 150);
    layerView.backgroundColor = [UIColor redColor];
    
    [self.view addSubview:layerView];
    
    NSLog(@"Before: %@", [layerView actionForLayer:layerView.layer
                                            forKey:@"backgroundColor"]);
    
    [UIView beginAnimations:nil
                    context:nil];
    
    NSLog(@"After: %@", [layerView actionForLayer:layerView.layer
                                           forKey:@"backgroundColor"]);
    
    [UIView commitAnimations];
}
複製程式碼
2016-12-04 12:45:28.178 7.ImplicitAnimations[57079:2126402] Before: <null>
2016-12-04 12:45:28.179 7.ImplicitAnimations[57079:2126402] After: <CABasicAnimation: 0x6000000327c0>
複製程式碼
6

這樣子我們就可以知道, 當屬性在Block之外發生改變, UIView會直接通過返回nil來禁用隱式動畫, 但如果在動畫塊的範圍之內, 就會根據動畫的具體型別來返回相應的屬性, 這個後續會講到.

其實除了通過返回nil並不是唯一禁止隱式動畫的方法, 我們也可以通過CATransacition的**+ (void)setDisableActions:(BOOL)flag;方法, 通過flag來對所有屬性開啟或者關閉隱式動畫, 哪怕你是在[CATransaction begin];之後來新增, 也是一樣可以關閉的.

這裡還有一個Demo**, 使用CATransaction來實現的一個叫做推進過渡動畫, 其實說白也就是一個Push動畫:

- (void)pushAnimation {
    
    UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0,
                                                            100,
                                                            self.view.frame.size.width,
                                                            self.view.frame.size.width)];
    
    view.backgroundColor = [UIColor grayColor];
    
    [self.view addSubview:view];
    
    UIButton *button = [[UIButton alloc] init];
    
    button.center = CGPointMake(self.view.frame.size.width / 2, 50);
    button.bounds = CGRectMake(0, 0, 100, 50);
    button.backgroundColor = [UIColor blueColor];
    
    [button setTitle:@"改變顏色"
            forState:UIControlStateNormal];
    
    [button addTarget:self
               action:@selector(pushChangeColor)
     forControlEvents:UIControlEventTouchUpInside];
    
    [view addSubview:button];
    
    self.colorLayer  = [CALayer layer];
    self.colorLayer.position = CGPointMake(view.frame.size.width / 2, view.frame.size.height / 2);
    self.colorLayer.bounds = CGRectMake(0, 0, 150, 150);
    self.colorLayer.backgroundColor = [UIColor redColor].CGColor;
    
    CATransition *transition = [CATransition animation];
    
    transition.type = kCATransitionPush;
    transition.subtype = kCATransitionFromLeft;
    
    self.colorLayer.actions = @{@"backgroundColor": transition};
    
    [view.layer addSublayer:self.colorLayer];
}

- (void)pushChangeColor {
    
    [CATransaction begin];
    [CATransaction setAnimationDuration:2.0f];

    CGFloat redColor = arc4random() / (CGFloat)INT_MAX;
    CGFloat greenColor = arc4random() / (CGFloat)INT_MAX;
    CGFloat blueColor = arc4random() / (CGFloat)INT_MAX;
    
    self.colorLayer.backgroundColor = [UIColor colorWithRed:redColor
                                                      green:greenColor
                                                       blue:blueColor
                                                      alpha:1.0f].CGColor;
    [CATransaction commit];
}
複製程式碼
7

Presentation Versus Model

其實仔細想想, CALayer的屬性行為並不太正常, 為何這麼說呢, 因為當我們去改變一個圖層的屬性時, 我們會發現, 這個值的確是立即發生了改變, 但在螢幕上並沒有馬上生效, 為何呢? 因為我們在設定屬性的時候, 並沒有直接去調整圖層的顯示外觀, 僅僅只是定義了圖層動畫結束之後即將要發生改變的外觀.

Core Animation在這裡充當了一個控制器的角色, 並且根據Layer ActionsTransactions來更新檢視在螢幕上顯示的狀態.

在於使用者互動的介面中, CALayer的行為更像是儲存著檢視如何去顯示和動畫的執行資料模型.

iOS中, 螢幕會以每秒鐘重繪60次, 如果動畫市場比60分之一秒還要長, 那麼在這段時間裡, Core Animation就會對螢幕上的圖層進行重新的組合, 這就意味著CALayer除了我們給予的值之外, 還必須要知道當前顯示在螢幕上的屬性值的記錄.

而每個圖層屬性的顯示值都會被儲存在一個叫做呈現圖層的獨立圖層當中, 我們可以通過**- (nullable instancetype)presentationLayer;方法來訪問, 而這個所謂的呈現圖層**, 實際上就是模型圖層的複製, 但它的好處是它的屬性值代表了在任何指定時間當前所顯示的外觀效果, 通俗點來講, 就是我們可以通過獲取呈現圖層的值來獲取當前螢幕上真正顯示出來的值.

這裡需要注意的一點就是, 如果在呈現圖層僅僅當CALayer首次被提交的時候建立, 那麼去呼叫**- (nullable instancetype)presentationLayer;方法就會返回nil**.

這裡我們或許還會注意到另一個方法**- (instancetype)modelLayer;, 如果我們在呈現圖層上呼叫這個方法, 那麼就會返回一個它正在呈現所以來的CALayer**, 而通常在一個圖層上呼叫這個方法, 就會返回self.

在大多數開發的場景下, 我們都不需要直接訪問呈現圖層, 我們可以通過和模型圖層的互動, 來讓Core Animation更新並且顯示, 但在以下兩種場景下呈現圖層就非常有用了, 一個是在同步動畫裡, 一個是在處理使用者互動的時候:

  • 如果我們在實現一個基於定時器的動畫, 而不僅僅是基於Transactions的動畫, 這個時候我們就要準確的知道在某一時刻圖層顯示在什麼位置, 這就會對正確的佈局起非常大的作用了.
  • 如果我們想讓做動畫的圖層對於使用者有互動, 我們可以使用**- (nullable CALayer *)hitTest:(CGPoint)p;方法來判斷指定的圖層是否被點選了, 這個時候就會顯示更加的友好, 因為呈現圖層代表了使用者當前看到的圖層位置, 而不是當動畫效果結束之後的位置.

    說了那麼多, 還是直接上

    Demo**比較直接:
- (void)presentationVersusModel {
    
    self.colorLayer  = [CALayer layer];
    self.colorLayer.position = CGPointMake(self.view.frame.size.width / 2, self.view.frame.size.height / 2);
    self.colorLayer.bounds = CGRectMake(0, 0, 150, 150);
    self.colorLayer.backgroundColor = [UIColor redColor].CGColor;
    
    [self.view.layer addSublayer:self.colorLayer];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    
    CGPoint point = [[touches anyObject] locationInView:self.view];
    
    if ([self.colorLayer.presentationLayer hitTest:point]) {
        
        CGFloat redColor = arc4random() / (CGFloat)INT_MAX;
        CGFloat greenColor = arc4random() / (CGFloat)INT_MAX;
        CGFloat blueColor = arc4random() / (CGFloat)INT_MAX;
        
        self.colorLayer.backgroundColor = [UIColor colorWithRed:redColor
                                                          green:greenColor
                                                           blue:blueColor
                                                          alpha:1.0f].CGColor;
    } else {
        [CATransaction begin];
        [CATransaction setAnimationDuration:4.0f];
        
        self.colorLayer.position = point;
        
        [CATransaction commit];
    }
}
複製程式碼
8

總結

總結一下:

  • Core Animation預設是開啟動畫效果的, 並且預設的動畫效果是平滑過渡滴.
  • 我們知道了隱式動畫的實現方式.
  • UIView關聯的圖層預設都禁用了隱式動畫, 對這種圖層做動畫的唯一辦法就是使用UIView的動畫函式, 或者是繼承與UIView並且重寫**- (nullable id)actionForLayer:(CALayer *)layer forKey:(NSString *)event;**方法, 最直接的方法就是直接建立一個顯示動畫.
  • 對於一個單獨存在的圖層來講, 我們可以通過實現圖層的**- (nullable id)actionForLayer:(CALayer *)layer forKey:(NSString *)event;方法, 或者是提供一個Actions**的字典來控制隱式動畫.
  • 除此之外, 我們來了解了呈現圖層模型圖層, 知道了這兩個傢伙的一些皮毛.

    好了, 這次就到這裡了, 謝謝大家~


工程地址

專案地址: https://github.com/CainRun/CoreAnimation


最後

碼字很費腦, 看官賞點飯錢可好
微信
支付寶

相關文章