導語:Lottie動畫是Airbnb開源的一個支持 Android、iOS 以及 ReactNative。通過AE導出的JSON文件+Lottie庫可快速實現動畫繪製。本文主要講述從AE的bodymovin插件導出的JSON文件到OC的數據模型,再將數據模型拆解成獨立圖層,併爲圖層添加動畫的過程。

Lottie動畫原理概述

上圖是Lottie動畫庫從AE導出動畫到繪製到客戶端屏幕的過程,第一階段是JSON到Model(OC數據模型)的轉換過程,主要是將JSON轉成OC語言可以識別的數據模型Model, Model實際上是一個Object類型的對象,我們可以通過屬性key快速查找數據內容,第二階段是Model(數據模型)依附到CALayer(圖層)上,就像寫一個CALayer一樣,把Model數據一一賦值給CALayer的屬性上,必要時再做特殊處理,最後在圖層CALayer上添加Animation(動畫)。

Lottie結構圖

上圖爲Lottie的結構圖

  • LOTAnimationView: 承接控制動畫的功能,如播放暫停

  • LOTComposition: 主要解析JSON文件內容

  • LOTCompositionContainer: 承載LOTComposition的內容,繪製圖層和添加動畫

JSON字段解讀

一級屬性

JSON最外一層的數據,包括一個動畫的基礎數據:動畫幀率、起始/結束關鍵幀,動畫的寬高等,還有子圖層的信息和關聯的資源信息,如圖片,矢量圖等。

{
  "v": "5.6.10",   // bodymovin插件版本
  "fr": 25,        // 幀率
  "ip": 0,         // 起始關鍵幀
  "op": 277,       // 結束關鍵幀
  "w": 110,        // 視圖寬度
  "h": 110,        // 視圖高度
  "nm": "cloud",   // 動畫名稱
  "ddd": 0,        // 是否是3D
  "assets": [...]  // 資源集合
  "layers": [...]  // 圖層集合
}

assets 資源集合

assets是一個數組,資源信息包含的是矢量圖信息,如形狀,大小等等,也包含位圖;還可能是預合成層,即對已存在的某些圖層進行分組,把它們放置到新的合成中,作爲新的一個資源對象,這裏layers的對象結構是跟上面一級屬性中的layers圖層集合是一樣的圖層結構。

"assets": [
    {
        "id": "image_0",   // 圖片唯一識別的id,圖層獲取圖片的標識
        "w": 167,          // 圖片的寬度
        "h": 165,          // 圖片的高度
        "u": "images/",    // 圖片的路徑
        "p": "img_0.png",  // 圖片的名稱
        "layers":  []      // 預合成層
    }
]

layers 圖層集合

layers對象也是一個數組,數組中的每個元素對應一個圖層,圖層信息包括的圖層的位置,大小,形狀,起始關鍵幀,結束關鍵幀等,一個個圖層動畫疊加起來構成最終的動畫效果。

"layers": [
    {
        "ddd": 0,             // 是否是3D圖層
        "ind": 1,             // 在AE裏的圖層標序號
        "ty": 4,              // 圖層類型
        "nm": "形狀圖層 1",    // 在AE下的命名
        "ks": {},             // 動畫屬性值,下面有進一步拆解
        "shapes": {},         // 矢量圖形圖層信息,下面有進一步拆解
        "ip": 0,              // 起始關鍵幀
        "op": 750,            // 結束關鍵幀
        "refId: 0,            // 引用資源ID
        "parent": 0,          // 父圖層的id,默認都添加到根圖層上,如果指定了id不爲0會尋找父圖層並添加到上面
        "masksProperties":[], // 蒙版的數組
        "w": 100,             // 預合成層:寬度
        "h": 100,             // 預合成層:圖層高度
        "sw": 0,              // 固態層:寬度
        "sh": 0,              // 固態層:圖層高度
        "sc": 0 ,             // 固態層:顏色
    }
]

圖層類型ty

圖層有6種類型,不同類型的圖層獲取寬高的方式不同,如圖片層需要從關聯的refId獲取asset,從而獲取到圖片資源的寬高來作爲該圖層的寬高等,具體如下:

  • 0 代表 預合成層:從屬性值w和h獲取

  • 1 代表 固態層:從屬性值w和h獲取

  • 2 代表 圖片層:從圖片資源屬性獲取

  • 3 代表 空層:從根圖層獲取

  • 4 代表 形狀層:從根圖層獲取

  • 5 代表 位置層:從根圖層獲取

圖層動畫ks

  • ks屬性:這是一個比較關鍵的屬性,包含圖層變換transform的信息,包含透明度、位置、錨點、縮放、旋轉等。格式如下

"ks": {
    "o": {          // 透明度
        "k": 100
    },
    "p": {          // 位置
        "k": [
            126.5,
            963,
            0
        ]
    }
}
  • 屬性對應的值主要通過K值獲取, 如上面的例子中透明度o爲 100 , 位置p爲 (126.5,963,0)

  • k對應的值有如下幾種情況:

    • 數字或3個數字組成的數組:不帶動畫。表示對應屬性的值。比如透明度 100 , 位置 (126.5,963,0) 等。

    • 數組類型並且數字第一個對象的t有值:帶幀動畫。第一個對象表示動畫開始的屬性,第二個對象表示動畫結束的屬性。通過以下參數可以拼裝出關鍵幀的屬性值,關鍵幀時間點,關鍵幀之間的時間函數,t表示開始/結束幀,s和e表示開始/結束屬性值,i和o決定動畫的時間函數。

  • 舉個例子:

比如下面的動畫,是有個矩形從上往下的動畫。

從導出的JSON文件截取以下片段:

"ks": {
    ...
    "p": {               // 位置信息
        "a": 1,
        "k": [           // 數組類型並且數字第一個對象的t有值
            {
                "t": 0,  // 起始關鍵幀
                "s": [
                    300,
                    700,
                    0
                ],
            },
            {
                "t": 49, // 結束關鍵幀
                "s": [
                    250,
                    1800,
                    0
                ]
            }
        ],
        "ix": 2
    }
},

從以上片段中我們讀到位置p的k值是一個數組,並且是帶有t的元素, 即爲幀動畫。從內容我們可以讀出關鍵幀幀爲0時,位置信息爲(300,700,0) , 變換到關鍵幀爲49時,位置信息變爲(250,1800,0)。

圖層形狀shapes

shape是一個形狀圖層的數組,對應AE中圖層的內容中的形狀設置,描述形狀的特徵,通過描邊信息、顏色填充等信息的組合形成一個個矢量圖。

"shapes": {
    "ty": "gr",              //  形狀的類型
    "it": [
        {
            "ty": "rc",
            "d": 1,
            "s": {           // 形狀的大小
                "k": [
                    450.094,
                    140.297
                ]
            },
            "p": {
                "k": [
                    0,
                    0
                ]
            },
            "nm": "矩形路徑 1"
        }
    ]
}
  • 形狀類型ty

ty爲形狀的類型,對應的類型值如下:

gr(ShapeGroup): 圖形組合
st(ShapeStroke): 圖形描邊
fl(ShapeFill): 圖形填充
tr(ShapeTransform): 圖形變換
sh(ShapePath): 圖形路徑
rc(RectPath): 矩形路徑
el(EllipsePath): 橢圓路徑
tm(trimPath): 裁剪路徑

生成OC數據模型

LOTComposition類

LOTComposition類是記錄動畫信息的類,繼承 NSObject, 作爲整個json文件內容的映射,用於記錄所有動畫信息的類。在這個類中我們可以看到動畫的基礎信息,包含創建AE文件時的設置:合成名稱、寬高、幀速率(幀/秒),也是JSON文件中一級屬性的映射。以下是一個LOTComposition的實例信息:

LOTLayerGroup 和 LOTLayer

從上圖我們可以看到兩個集合類,LOTLayerGroup記錄圖層信息的數組,對應JSON對象中layers數組,由一個個LOTLayer組成。一個LOTLayer是一個圖層,是一個動畫被拆解的最小單位個體。

例如以下雲朵動畫

可以看出雲朵的運動速度是不一樣的,因此可以判斷他們並不是在一個圖層中,而是由多個圖層的動畫疊加起來的效果,即每個雲朵爲一個圖層, LOTLayer就是記錄一個圖層單位的信息

以下是一個LOTLayer攜帶的信息

LOTAssetsGroup 和 LOTAsset

LOTAssetsGroup是記錄資源信息,對應JSON對象中的assets數組,若圖層需要依賴資源,可以通過自身信息refId關聯到對應的資源ID尋找資源。如以上雲朵動畫,每個雲朵即爲一個資源,LOTAsset爲記錄一個資源的信息。

數據模型轉爲圖層

Lottie底層原理實際是用到了CALayer 和 Core Animation。我們經常可以直觀感受到iOS設備中內容的切換很流暢,就如下圖,彈框不是一閃而出,而是有很平滑從小到大和透明度從0到1的過渡效果。這是因爲在一個圖層中,當我們修改一個圖層屬性時,比如寬度從100px到200px, 它會產生很平滑地從一個值過渡到下一個值這種動畫效果,這個圖層就是CALayer, 執行動畫效果的是Core Animation,我們將這一行爲稱爲隱式動畫。而Lottie使用的正是這種機制。

圖片引用自 https://juejin.im/post/5de481226fb9a0717b5fce84

圖層繪製

lottie繪製圖層過程用到了兩個主要的類:LOTCompositionContainer 和 LOTLayerContainer。

LOTCompositionContainer

  • 顧名思義,LOTCompositionContainer 是 LOTComposition的container(容器),承載LOTComposition的內容。LOTComposition是JSON映射的OC數據模型

  • LOTCompositionContainer 繼承CALayer , 是一個圖層,動畫的根圖層。我們設定的動畫內容,都會放置在這個圖層中

  • 執行子圖層的循環,並且將所有子圖層賦在該根圖層上

// LOTCompositionContainer.m
// ps:  代碼有刪減
 NSArray *reversedItems = [[childGroup.layers reverseObjectEnumerator] allObjects]; // 獲取到子圖層的數據模型

 for (LOTLayer *layer in reversedItems) {
    child = [[LOTLayerContainer alloc] initWithModel:layer inLayerGroup:childGroup]; // 將子圖層數據模型處理的一個個圖層
   }

[self.wrapperLayer addSublayer:child]; // 將子圖層添加到該根圖層上

LOTLayerContainer

LOTLayerContainer是一個很重要的類,它相當於我們上文中講到的LOTLayer,也即是整個動畫拆解成的最小單元的一個層級,不需要依賴其他圖層就可以完整實現自身動畫。這是一個繼承CALayer的類。

我們可以在這裏回顧下CALayer圖層繪製時需要做的事情:

  • 創建一個CALayer實例: CALayer *layer = [CALayer layer];

  • 添加到根圖層: [self.view.layer addSublayer:layer];

  • 創建動畫

    CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"position"];
    animation.fromValue = [NSValue valueWithCGPoint:CGPointMake(50, 0)];
    animation.toValue = [NSValue valueWithCGPoint:CGPointMake(150, 0)];
  • 給圖層添加動畫

在Lottie中也一樣實現了上面四個步驟:

LOTLayerContainer 類繼承CALayer, 在初始化時執行以下步驟:

  • CALayer屬性: LOTComposition中有一個屬性 CALayer *wrapperLayer 寫入當前圖層的信息,從類型可以看出是一個CALayer,因此我們可以在CALayer中使用隱式動畫,也就是文中開頭所講的內容。

  • 添加寬高信息:在LOTComposition初始化時,會先判斷當前的layer是什麼類型, 圖片/立方體/預補償層,如果是圖片,會將圖片的寬高,錨點等信息作爲該圖層 wrapperLayer 的寬高,錨點等。

// LOTLayerContainer.m
  if (layer.layerType == LOTLayerTypeImage ||
      layer.layerType == LOTLayerTypeSolid ||
      layer.layerType == LOTLayerTypePrecomp) {
    _wrapperLayer.bounds = CGRectMake(0, 0, layer.layerWidth.floatValue, layer.layerHeight.floatValue);
    _wrapperLayer.anchorPoint = CGPointMake(0, 0);
    _wrapperLayer.masksToBounds = YES;
    DEBUG_Center.position = LOT_RectGetCenterPoint(self.bounds);
  }
  • 添加Transform信息:接下來尋找Transform(位置/旋轉/錨點/縮放/透明度)信息,添加在該圖層 wrapperLayer

  • 填充資源:當圖層類型爲圖片時,需要爲 wrapperLayer 添加 content 屬性內容,即圖片的內容。 _setImageForAsset 方法實現了判斷圖片類型,並賦值在 content 屬性上

// LOTLayerContainer.m
  if (layer.layerType == LOTLayerTypeImage) {
    [self _setImageForAsset:layer.imageAsset];
  }
  • 填充圖形:當圖層類型爲形狀shape時,shape是對矢量圖的信息攜帶,這在lottie動畫中被大量使用。因爲矢量圖要比位圖加載更快,並且也會大大減少對設備內存的使用。這裏的 buildContents 方法實現了對矢量圖進行描邊、填充顏色等操作。

// LOTLayerContainer.m
if (layer.layerType == LOTLayerTypeShape &&
  layer.shapes.count) {
   [self buildContents:layer.shapes];
}
  • 如何繪製矢量圖

    • 初始化LOTRenderGroup,LOTRenderGroup作爲一個矢量圖形的類,包含了LOTRenderNode 和 LOTAnimatorNode 擁有的屬性和方法。

    • 渲染節點:LOTRenderNode 類中有屬性 CAShapeLayer * _Nonnull outputLayer ,它負責計算線條顏色,線寬,填充色等

    • 動畫節點:LOTAnimatorNode 計算構成形狀的線條

  • 遮罩層:判斷是否有遮罩層並賦給 wrapperLayer

  • 添加到父圖層:在上面過程中已經準備好一個CALayer的繪製屬性:寬高、轉換信息、資源內容、圖形繪製內容、遮罩層等。這兒的self.wrapperLayer並非上述幾個過程的wrapperLayer,而是根圖層中的屬性

// LOTCompositionContainer.m
[self.wrapperLayer addSublayer:child];

動畫合成

CALayer添加動畫

在上面講述到繪製圖層,但如何將這些圖層變成動畫呢,在瞭解之前我們得先知道CALayer方法重繪響應鏈與runloop機制,如何讓圖層重新繪製呈現出新的畫面,從而形成動畫。

  • layer首次加載時會調用 +(BOOL)needsDisplayForKey:(NSString *)key方法來判斷當前指定的屬性key改變是否需要重新繪製,默認返回NO

  • 當Core Animartion中的key或者keypath等於+(BOOL)needsDisplayForKey:(NSString *)key 方法中指定的key,便會自動調用 setNeedsDisplay 方法

  • 當指定key發生更改時,會觸發 actionForKey

  • runloop是一個循環處理事件和消息的方法,CATransaction begin和 CATransaction commit 進行修改和提交新事務。

  • 每個RunLoop週期中會自動開始一次新的事務,即使你不顯式的使用[CATranscation begin]開始一次事務,任何在一次RunLoop運行時循環中屬性的改變都會被集中起來,執行默認0.25秒的動畫,在runloop快結束時,它會調用下一個事務 display

  • CALayer方法重繪響應鏈

  1. [layer setNeedDisplay] -> [layer displayIfNeed] -> [layer display] -> [layerDelegate displayLayer:]

  2. [layer setNeedDisplay] -> [layer displayIfNeed] -> [layer display] -> [layer drawInContext:] -> [layerDelegate drawLayer: inContext:]

Lottie動畫繪製

  • 根圖層LOTCompositionContainer繼承CALayer ,添加Currentframe 屬性,給這個屬性添加一個CABaseAnimation 動畫

  • 所有的子Layer根據CurrentFrame 屬性的變化

  • 子圖層layer首次加載時會調用 +(BOOL)needsDisplayForKey:(NSString *)key方法來判斷當前指定的屬性key改變是否需要重新繪製。在LOTLayerContainer可以看到 needsDisplayForKey 指定了key爲 currentFrame 時觸發重繪

// LOTLayerContainer.m
+ (BOOL)needsDisplayForKey:(NSString *)key {
  if ([key isEqualToString:@"currentFrame"]) {
    return YES;
  }
  return [super needsDisplayForKey:key];
}
  • actionForKey 是接收指定key被修改時觸發的行爲操作,在下面代碼中看到當key爲 currentFrame 時添加一個CABasicAnimation動畫

- (id<CAAction>)actionForKey:(NSString *)event {
  if ([event isEqualToString:@"currentFrame"]) {
    CABasicAnimation *theAnimation = [CABasicAnimation
                                      animationWithKeyPath:event];
    theAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
    theAnimation.fromValue = [[self presentationLayer] valueForKey:event];
    return theAnimation;
  }
  return [super actionForKey:event];
}
  • display 方法是在一個runloop即將結束時調用,主要實現重繪的內容。下面是 display 調用的方法,它會根據當前幀是否在該子圖層的顯示幀範圍內,如果不在,則隱藏,否則賦予圖層新的動畫屬性。如下圖,當currentFrame在inFrame和outFrame之間時,動畫顯示,否則隱藏。下圖列舉了多個Layer的情況,每一個Layer在初始化時已經準備好,時間跟根圖層一樣從 startFrame 到  endFrame , 在這個時間線中會根據 inFrame 和  outFrame 來判斷是否顯示

- (void)displayWithFrame:(NSNumber *)frame forceUpdate:(BOOL)forceUpdate {
  NSNumber *newFrame = @(frame.floatValue / self.timeStretchFactor.floatValue);
//  if (ENABLE_DEBUG_LOGGING)
      NSLog(@"View %@ Displaying Frame %@, with local time %@", self, frame, newFrame); 
  BOOL hidden = NO;
  if (_inFrame && _outFrame) {
    hidden = (frame.floatValue < _inFrame.floatValue ||
              frame.floatValue > _outFrame.floatValue);
  }
     
  self.hidden = hidden;
  if (hidden) {
    return;
  }
  if (_opacityInterpolator && [_opacityInterpolator hasUpdateForFrame:newFrame]) {
    self.opacity = [_opacityInterpolator floatValueForFrame:newFrame];
  }
  if (_transformInterpolator && [_transformInterpolator hasUpdateForFrame:newFrame]) {
    _wrapperLayer.transform = [_transformInterpolator transformForFrame:newFrame];
  }
  [_contentsGroup updateWithFrame:newFrame withModifierBlock:nil forceLocalUpdate:forceUpdate];
  _maskLayer.currentFrame = newFrame;
}

至此,每個圖層的繪製和動畫的添加均準備完畢,Lottie提供了 play 播放動畫的方式,實際上就是將根節點的動畫添加到根圖層上,使其可以開始播放動畫。

以上講述的是從AE導出JSON文件到OC讀取後轉成Model再到繪製圖層動畫的過程,這有助於我們理解一個動畫的內部結構,可方便後續理解整個動畫的運作,也對於我們實踐開發中遇到的缺陷或者調優有極大的幫助。

相關文章