Lottie動畫原理
導語: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方法重繪響應鏈
-
[layer setNeedDisplay] -> [layer displayIfNeed] -> [layer display] -> [layerDelegate displayLayer:]
-
[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再到繪製圖層動畫的過程,這有助於我們理解一個動畫的內部結構,可方便後續理解整個動畫的運作,也對於我們實踐開發中遇到的缺陷或者調優有極大的幫助。