你好,我是程序亦非猿,阿里資深無線開發工程師一枚。java
自我在內網發佈了一篇關於 Lottie 的原理分析的文章以後,就不斷有同事來找我詢問關於 Lottie 的各類東西,最近又有同事來問,就想着可能對你們也會有所幫助,就稍做處理後分享出來。android
須要注意的是,這文章寫於兩年前,基本版本 2.0.0-beta3,雖然我看過最新版本,主要的類沒有什麼差異,不過可能仍是會存在一些差別。git
能夠感覺一下我兩年前的實力。:-Dgithub
Render After Effects animations natively on Android and iOSjson
Lottie 是 airbnb 發佈的庫,它能夠將 AE 製做的動畫 在 Android&iOS上以 native 代碼渲染出來,目前還支持了 RN 平臺。canvas
來看幾個官方給出的動畫效果案例:bash
有沒有很炫酷?ide
就拿第一個動畫 Jump-through 舉例,若是讓你來實現它,你能在多少時間內完成?三天?一個禮拜? google 的 Nick Butcher 恰好有一篇文章寫 Jump-through 的動畫實現,講述了整個實現過程,從文章裏能夠看出實現這個動畫並不容易,有興趣的能夠看看 Animation: Jump-through。工具
可是如今有了 Lottie,只要設計師用 AE 設計動畫,利用 bodymovin 導出 ,導入到 assets, 再寫下面那麼點代碼就能夠實現了!oop
LottieAnimationView animationView = (LottieAnimationView) findViewById(R.id.animation_view);
animationView.setAnimation("PinJump.json");
animationView.loop(true);
animationView.playAnimation();
複製代碼
不用寫自定義 View!不用畫 Path!不用去計算這個點那個點!
是否是超級方便?!!!
這麼方便的背後,原理是什麼呢?
bodymovin 將 AE 動畫導出爲 ,該 描述了該動畫,而 lottie-android 的原理就是將 描述的動畫用 native code 翻譯出來, 其核心原理是 canvas 繪製。對,lottie 的動畫是靠純 canvas 畫出來的!!!動起來則是靠的屬性動畫。(ValueAnimator.ofFloat(0f, 1f);
)
說具體點就是 lottie 隨屬性動畫修改 progress,每個 Layer 根據當前的 progress 繪製所對應的幀內容,progress 值變爲1,動畫結束。(有點相似於幀動畫)
固然說說簡單,lottie其實作了很是多的工做,後續將詳細解析 lottie-android 的實現原理。
Lottie 提供了一個 LottieAnimationView 給用戶使用,而實際 Lottie 的核心是 LottieDrawable,它承載了全部的繪製工做,LottieAnimationView則是對LottieDrawable 的封裝,再附加了一些例如 解析 的功能。
它們的關係:
bodymovin 導出的 包含了動畫的一切信息, 動畫的關鍵幀信息,動畫該怎麼作,作什麼都包含在 裏,Lottie 裏全部的 Model 的數據都來自於這個 ( 該 對應的 Model 是LottieComposition),因此要理解 Lottie 的原理,理解 的屬性是第一步。
屬性很是多,並且不一樣的動畫的 也有很大的差異,因此這裏只講解部分重要的屬性。
的最外層長這樣:
{
"v": "4.5.9",
"fr": 15,
"ip": 0,
"op": 75,
"w": 500,
"h": 500,
"ddd": 0,
"assets":[]
"layers":[]
}
複製代碼
屬性的含義:
屬性 | 含義 |
---|---|
v | bodymovin的版本 |
fr | 幀率 |
ip | 起始關鍵幀 |
op | 結束關鍵幀 |
w | 動畫寬度 |
h | 動畫高度 |
assets | 動畫圖片資源信息 |
layers | 動畫圖層信息 |
從這裏能夠獲取 設計的動畫的寬高,幀相關的信息,動畫所須要的圖片資源的信息以及圖層信息。
圖片資源信息, 相關類 LottieImageAsset、 ImageAssetBitmapManager。
"assets": [
{
"id": "image_0",
"w": 500,
"h": 500,
"u": "images/",
"p": "voice_thinking_image_0.png"
}
]
複製代碼
屬性的含義:
屬性 | 含義 |
---|---|
id | 圖片 id |
w | 圖片寬度 |
h | 圖片高度 |
p | 圖片名稱 |
圖層信息,相關類:Layer、BaseLayer以及 BaseLayer 的實現類。
{
"ddd": 0,
"ind": 0,
"ty": 2,
"nm": "btnSlice.png",
"cl": "png",
"refId": "image_0",
"ks": {....},
"ao": 0,
"ip": 0,
"op": 90.0000036657751,
"st": 0,
"bm": 0,
"sr": 1
}
複製代碼
屬性的含義:
屬性 | 含義 |
---|---|
nm | layerName 圖層信息 |
refId | 引用的資源 id,若是是 ImageLayer 那麼就是圖片的id |
ty | layertype 圖層類型 |
ip | inFrame 該圖層起始關鍵幀 |
op | outFrame 該圖層結束關鍵幀 |
st | startFrame 開始 |
ind | layer id 圖層 id |
Layer 能夠理解爲圖層,跟 PS 等工具的概念相同,每一個 Layer 負責繪製本身的內容。
在 Lottie 裏擁有不一樣的 Layer,目前有 PreComp,Solid,Image,Null,Shape,Text ,各個 Layer 擁有的屬性各不相同,這裏只指出共有的屬性。
下圖爲 Layer 相關類圖:
在開始使用 Lottie 的時候,咱們團隊設計動畫走的跟設計圖片同樣的路子,想設計2x,3x 多份資源進行適配。可是,經過閱讀源碼發現其實 Lottie自己在 Android 平臺已經作了適配工做,並且適配原理很簡單,解析 時,從 讀取寬高以後 會再乘以手機的密度。再在使用的時候判斷適配後的寬高是否超過屏幕的寬高,若是超過則再進行縮放。以此保障 Lottie 在 Android 平臺的顯示效果。
核心代碼以下:
//LottieComposition.fromSync
float scale = res.getDisplayMetrics().density;
int width = .optInt("w", -1);
int height = .optInt("h", -1);
if (width != -1 && height != -1) {
int scaledWidth = (int) (width * scale);
int scaledHeight = (int) (height * scale);
bounds = new Rect(0, 0, scaledWidth, scaledHeight);
}
//LottieAnimationView.setComposition
int screenWidth = Utils.getScreenWidth(getContext());
int screenHeight = Utils.getScreenHeight(getContext());
int compWidth = composition.getBounds().width();
int compHeight = composition.getBounds().height();
if (compWidth > screenWidth ||
compHeight > screenHeight) {
float xScale = screenWidth / (float) compWidth;
float yScale = screenHeight / (float) compHeight;
setScale(Math.min(xScale, yScale));
Log.w(L.TAG, String.format(
"Composition larger than the screen %dx%d vs %dx%d. Scaling down.",
compWidth, compHeight, screenWidth, screenHeight));
}
複製代碼
這裏值得一提的是,設計師在設計動畫時要注意,須要設計的是1X 的動畫,而不是2X or 3X or 4X。
目前手淘用的方案是 按4X 來設計(1X看不清元素),而後再縮小爲1X,圖片資源是4X。
LottieAnimationView 自己是個 ImageView,因此它的繪製流程跟 ImageView 同樣,全部的繪製其實在 LottieDrawable 控制的。
接下去看看它的源碼實現:
// LottieDrawable
@Override public void draw(@NonNull Canvas canvas) {
if (compositionLayer == null) {
return;
}
float scale = this.scale;
if (compositionLayer.hasMatte()) {
scale = Math.min(this.scale, getMaxScale(canvas));
}
matrix.reset();
matrix.preScale(scale, scale);
compositionLayer.draw(canvas, matrix, alpha);
}
複製代碼
能夠看到在 draw
方法裏調用了 compositionLayer.draw
方法,因爲 CompositionLayer 繼承了 BaseLayer,因此須要跟進 BaseLayer ,繼續跟蹤:
// BaseLayer.draw
@Override
public void draw(Canvas canvas, Matrix parentMatrix, int parentAlpha) {
if (!visible) {
return;
}
buildParentLayerListIfNeeded();
//矩陣變換處理
//....
if (!hasMatteOnThisLayer() && !hasMasksOnThisLayer()) {
matrix.preConcat(transform.getMatrix());
//繪製 layer
drawLayer(canvas, matrix, alpha);
return;
}
//draw matteLayer& maskLayer
//...
canvas.restore();
}
複製代碼
刪除了多餘代碼,只保留核心代碼,能夠看到 draw 方法裏調用了抽象方法 drawLayer
,在這裏的實如今 CompositionLayer ,一塊兒來看看:
@Override void drawLayer(Canvas canvas, Matrix parentMatrix, int parentAlpha) {
//...
for (int i = layers.size() - 1; i >= 0 ; i--) {
boolean nonEmptyClip = true;
if (!newClipRect.isEmpty()) {
nonEmptyClip = canvas.clipRect(newClipRect);
}
if (nonEmptyClip) {
layers.get(i).draw(canvas, parentMatrix, parentAlpha);
}
}
//...
}
複製代碼
上面的代碼中的 layers 是該動畫所包含的層,在 CompositionLayer 的 drawLayer 方法裏遍歷了動畫全部的層,並調用layers 的 draw 方法,這樣就完成了全部的繪製。
上一小節講了 Lottie 的繪製原理,可是 Lottie 是用來作動畫的,光理解它的繪製原理是不夠的,對於動畫,更重要的是它怎麼動起來的。
接下來就分析一下 Lottie 的動畫原理。
Lottie 動畫起始於 LottieAnimationView.playAnimation
,接着調用 LottieDrawable 的同名方法,與繪製相同,動畫也是 LottieDrawable 控制的,來看看代碼:
// animator 的申明
private final ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
private void playAnimation(boolean setStartTime) {
if (compositionLayer == null) {
playAnimationWhenCompositionAdded = true;
reverseAnimationWhenCompositionAdded = false;
return;
}
if (setStartTime) {
animator.setCurrentPlayTime((long) (progress * animator.getDuration()));
}
animator.start();
}
複製代碼
playAnimation 方法其實只是開啓了一個屬性動畫,那麼後續動畫是怎麼動起來的呢?這就必需要看動畫的監聽了:
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override public void onAnimationUpdate(ValueAnimator animation) {
if (systemAnimationsAreDisabled) {
animator.cancel();
setProgress(1f);
} else {
setProgress((float) animation.getAnimatedValue());
}
}
});
複製代碼
在 animator 進行的過程當中回去調用 setProgress
方法,下面跟蹤一下代碼:
//LottieDrawable
public void setProgress(@FloatRange(from = 0f, to = 1f) float progress) {
this.progress = progress;
if (compositionLayer != null) {
compositionLayer.setProgress(progress);
}
}
//CompositionLayer
@Override public void setProgress(@FloatRange(from = 0f, to = 1f) float progress) {
super.setProgress(progress);
progress -= layerModel.getStartProgress();
for (int i = layers.size() - 1; i >= 0; i--) {
layers.get(i).setProgress(progress);
}
}
//BaseLayer
void setProgress(@FloatRange(from = 0f, to = 1f) float progress) {
//...
for (int i = 0; i < animations.size(); i++) {
animations.get(i).setProgress(progress);
}
}
//BaseKeyframeAnimation
void setProgress(@FloatRange(from = 0f, to = 1f) float progress) {
if (progress < getStartDelayProgress()) {
progress = 0f;
} else if (progress > getEndProgress()) {
progress = 1f;
}
if (progress == this.progress) {
return;
}
this.progress = progress;
for (int i = 0; i < listeners.size(); i++) {
listeners.get(i).onValueChanged();
}
}
//BaseLayer
@Override public void onValueChanged() {
invalidateSelf();
}
//BaseLayer
private void invalidateSelf() {
lottieDrawable.invalidateSelf();
}
複製代碼
上面列出了後續流程的主要代碼,能夠看到,setProgress 的最後觸發了每一個 layer 的 invalidateSelf,這都會讓 lottieDrawable 從新繪製,而後重走一遍繪製流程,這樣隨着 animator 動畫的進行,lottieDrawable 不斷的繪製,就展示出了一個完整的動畫。
PS: 動畫過程當中的一些變量好比 scale,都是由BaseKeyframeAnimation控制,但這個偏於細節,這裏就不講了。
動畫原理流程稍微有點長,也稍微有些複雜,我繪製了一張圖梳理了一下總體的流程,方便理解:
BaseKeyframeAnimation 類圖:
我的以爲 Lottie 是個很是很是棒的項目,甚至能夠說是個偉大的項目。
Lottie 極大的縮減了動畫的開發成本,給 APP 增長很是強力的動畫能力,不須要各個端再本身去實現,並且目前 Lottie 已經支持了很是多的 AE 動畫效果,經過 Lottie 能夠輕鬆實現不少酷炫的效果,因此如今作動效考驗的是設計同窗的設計能力了,哈哈。
本文只針對重點原理進行分析,歡迎留言討論交流。