【源碼分析】Lottie 實(shí)現(xiàn)炫酷動(dòng)畫背后的原理

mat-reding-1525395-unsplash.jpg
mat-reding-1525395-unsplash.jpg

0. 前言

你好,我是程序亦非猿,阿里資深無線開發(fā)工程師一枚。
歡迎關(guān)注我的公眾號(hào):程序亦非猿

自我在內(nèi)網(wǎng)發(fā)布了一篇關(guān)于 Lottie 的原理分析的文章之后,就不斷有同事來找我詢問關(guān)于 Lottie 的各種東西,最近又有同事來問,就想著可能對(duì)大家也會(huì)有所幫助,就稍作處理后分享出來。

需要注意的是,這文章寫于兩年前,基本版本 2.0.0-beta3,雖然我看過最新版本,主要的類沒有什么差別,不過可能還是會(huì)存在一些差異。

可以感受一下我兩年前的實(shí)力。:-D

1. Lottie 是什么?

Render After Effects animations natively on Android and iOS

Lottie 是 airbnb 發(fā)布的庫,它可以將 AE 制作的動(dòng)畫 在 Android&iOS上以 native 代碼渲染出來,目前還支持了 RN 平臺(tái)。

來看幾個(gè)官方給出的動(dòng)畫效果案例:

[圖片上傳失敗...(image-207758-1555986322657)]

[圖片上傳失敗...(image-6336b8-1555986322657)]

有沒有很炫酷?

就拿第一個(gè)動(dòng)畫 Jump-through 舉例,如果讓你來實(shí)現(xiàn)它,你能在多少時(shí)間內(nèi)完成?三天?一個(gè)禮拜? google 的 Nick Butcher 剛好有一篇文章寫 Jump-through 的動(dòng)畫實(shí)現(xiàn),講述了整個(gè)實(shí)現(xiàn)過程,從文章里可以看出實(shí)現(xiàn)這個(gè)動(dòng)畫并不容易,有興趣的可以看看 Animation: Jump-through。

但是現(xiàn)在有了 Lottie,只要設(shè)計(jì)師用 AE 設(shè)計(jì)動(dòng)畫,利用 bodymovin 導(dǎo)出 ,導(dǎo)入到 assets, 再寫下面那么點(diǎn)代碼就可以實(shí)現(xiàn)了!

LottieAnimationView animationView = (LottieAnimationView) findViewById(R.id.animation_view);
animationView.setAnimation("PinJump.json");
animationView.loop(true);
animationView.playAnimation();

不用寫自定義 View!不用畫 Path!不用去計(jì)算這個(gè)點(diǎn)那個(gè)點(diǎn)!

是不是超級(jí)方便??。?!

這么方便的背后,原理是什么呢?

<a name="489878a0"></a>

2. TL;DR

bodymovin 將 AE 動(dòng)畫導(dǎo)出為 ,該 描述了該動(dòng)畫,而 lottie-android 的原理就是將 描述的動(dòng)畫用 native code 翻譯出來, 其核心原理是 canvas 繪制。對(duì),lottie 的動(dòng)畫是靠純 canvas 畫出來的?。?!動(dòng)起來則是靠的屬性動(dòng)畫。(ValueAnimator.ofFloat(0f, 1f); )

說具體點(diǎn)就是 lottie 隨屬性動(dòng)畫修改 progress,每一個(gè) Layer 根據(jù)當(dāng)前的 progress 繪制所對(duì)應(yīng)的幀內(nèi)容,progress 值變?yōu)?,動(dòng)畫結(jié)束。(有點(diǎn)類似于幀動(dòng)畫)

當(dāng)然說說簡(jiǎn)單,lottie其實(shí)做了非常多的工作,后續(xù)將詳細(xì)解析 lottie-android 的實(shí)現(xiàn)原理。

<a name="efc22b8e"></a>

3. Lottie 關(guān)鍵類介紹

Lottie 提供了一個(gè) LottieAnimationView 給用戶使用,而實(shí)際 Lottie 的核心是 LottieDrawable,它承載了所有的繪制工作,LottieAnimationView則是對(duì)LottieDrawable 的封裝,再附加了一些例如 解析 的功能。

  • LottieComposition 是 對(duì)應(yīng)的 Model,承載 的所有信息。
  • CompositionLayer 是 layer 的集合。
  • ImageAssetBitmapManager 負(fù)責(zé)管理動(dòng)畫所需的圖片資源。

它們的關(guān)系:

[圖片上傳失敗...(image-ae8cb8-1555986176840)]

<a name="4879d65c"></a>

4. 文件的屬性含義

bodymovin 導(dǎo)出的 包含了動(dòng)畫的一切信息, 動(dòng)畫的關(guān)鍵幀信息,動(dòng)畫該怎么做,做什么都包含在 里,Lottie 里所有的 Model 的數(shù)據(jù)都來自于這個(gè) ( 該 對(duì)應(yīng)的 Model 是LottieComposition),所以要理解 Lottie 的原理,理解 的屬性是第一步。

屬性非常多,而且不同的動(dòng)畫的 也有很大的差別,所以這里只講解部分重要的屬性。

<a name="424b1c61"></a>

4.1 文件最外部的結(jié)構(gòu)

的最外層長(zhǎng)這樣:

{
  "v": "4.5.9",
  "fr": 15,
  "ip": 0,
  "op": 75,
  "w": 500,
  "h": 500,
  "ddd": 0,
  "assets":[]
  "layers":[]
 }

屬性的含義:

屬性 含義
v bodymovin的版本
fr 幀率
ip 起始關(guān)鍵幀
op 結(jié)束關(guān)鍵幀
w 動(dòng)畫寬度
h 動(dòng)畫高度
assets 動(dòng)畫圖片資源信息
layers 動(dòng)畫圖層信息

從這里可以獲取 設(shè)計(jì)的動(dòng)畫的寬高,幀相關(guān)的信息,動(dòng)畫所需要的圖片資源的信息以及圖層信息。

<a name="854203e2"></a>

a) assets

圖片資源信息, 相關(guān)類 LottieImageAsset、 ImageAssetBitmapManager。

"assets": [
    {
      "id": "image_0",
      "w": 500,
      "h": 500,
      "u": "images/",
      "p": "voice_thinking_image_0.png"
    }
  ]

屬性的含義:

屬性 含義
id 圖片 id
w 圖片寬度
h 圖片高度
p 圖片名稱

<a name="beb42690"></a>

b) layers

圖層信息,相關(guān)類:Layer、BaseLayer以及 BaseLayer 的實(shí)現(xiàn)類。

{
    "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 該圖層起始關(guān)鍵幀
op outFrame 該圖層結(jié)束關(guān)鍵幀
st startFrame 開始
ind layer id 圖層 id

Layer 可以理解為圖層,跟 PS 等工具的概念相同,每個(gè) Layer 負(fù)責(zé)繪制自己的內(nèi)容。

在 Lottie 里擁有不同的 Layer,目前有 PreComp,Solid,Image,Null,Shape,Text ,各個(gè) Layer 擁有的屬性各不相同,這里只指出共有的屬性。

下圖為 Layer 相關(guān)類圖:

[圖片上傳失敗...(image-ad8301-1555986176840)]

<a name="4afd9264"></a>

5. Lottie 的適配原理

在開始使用 Lottie 的時(shí)候,我們團(tuán)隊(duì)設(shè)計(jì)動(dòng)畫走的跟設(shè)計(jì)圖片一樣的路子,想設(shè)計(jì)2x,3x 多份資源進(jìn)行適配。但是,通過閱讀源碼發(fā)現(xiàn)其實(shí) Lottie本身在 Android 平臺(tái)已經(jīng)做了適配工作,而且適配原理很簡(jiǎn)單,解析 時(shí),從 讀取寬高之后 會(huì)再乘以手機(jī)的密度。再在使用的時(shí)候判斷適配后的寬高是否超過屏幕的寬高,如果超過則再進(jìn)行縮放。以此保障 Lottie 在 Android 平臺(tái)的顯示效果。

核心代碼如下:

//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));
    }

這里值得一提的是,設(shè)計(jì)師在設(shè)計(jì)動(dòng)畫時(shí)要注意,需要設(shè)計(jì)的是1X 的動(dòng)畫,而不是2X or 3X or 4X。

目前手淘用的方案是 按4X 來設(shè)計(jì)(1X看不清元素),然后再縮小為1X,圖片資源是4X。

<a name="2dc96526"></a>

6. Lottie的繪制原理

LottieAnimationView 本身是個(gè) ImageView,所以它的繪制流程跟 ImageView 一樣,所有的繪制其實(shí)在 LottieDrawable 控制的。

接下去看看它的源碼實(shí)現(xiàn):

// 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方法里調(diào)用了 compositionLayer.draw方法,由于 CompositionLayer 繼承了 BaseLayer,所以需要跟進(jìn) BaseLayer ,繼續(xù)跟蹤:

// 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 方法里調(diào)用了抽象方法 drawLayer,在這里的實(shí)現(xiàn)在 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 是該動(dòng)畫所包含的層,在 CompositionLayer 的 drawLayer 方法里遍歷了動(dòng)畫所有的層,并調(diào)用layers 的 draw 方法,這樣就完成了所有的繪制。

<a name="df5cc73c"></a>

7. Lottie的動(dòng)畫原理

上一小節(jié)講了 Lottie 的繪制原理,但是 Lottie 是用來做動(dòng)畫的,光理解它的繪制原理是不夠的,對(duì)于動(dòng)畫,更重要的是它怎么動(dòng)起來的。

接下來就分析一下 Lottie 的動(dòng)畫原理。

Lottie 動(dòng)畫起始于 LottieAnimationView.playAnimation,接著調(diào)用 LottieDrawable 的同名方法,與繪制相同,動(dòng)畫也是 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 方法其實(shí)只是開啟了一個(gè)屬性動(dòng)畫,那么后續(xù)動(dòng)畫是怎么動(dòng)起來的呢?這就必須要看動(dòng)畫的監(jiān)聽了:

animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
      @Override public void onAnimationUpdate(ValueAnimator animation) {
        if (systemAnimationsAreDisabled) {
          animator.cancel();
          setProgress(1f);
        } else {
          setProgress((float) animation.getAnimatedValue());
        }
      }
    });

在 animator 進(jìn)行的過程中回去調(diào)用 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();
  }

上面列出了后續(xù)流程的主要代碼,可以看到,setProgress 的最后觸發(fā)了每個(gè) layer 的 invalidateSelf,這都會(huì)讓 lottieDrawable 重新繪制,然后重走一遍繪制流程,這樣隨著 animator 動(dòng)畫的進(jìn)行,lottieDrawable 不斷的繪制,就展現(xiàn)出了一個(gè)完整的動(dòng)畫。

PS: 動(dòng)畫過程中的一些變量比如 scale,都是由BaseKeyframeAnimation控制,但這個(gè)偏于細(xì)節(jié),這里就不講了。

動(dòng)畫原理流程稍微有點(diǎn)長(zhǎng),也稍微有些復(fù)雜,我繪制了一張圖梳理了一下整體的流程,方便理解:

這里的圖片死活上傳不上去,誒,簡(jiǎn)書藥丸啊。

麻煩各位移步:https://juejin.im/post/5cbe760cf265da03a85ac0a8

8. 總結(jié)

個(gè)人覺得 Lottie 是個(gè)非常非常棒的項(xiàng)目,甚至可以說是個(gè)偉大的項(xiàng)目。

Lottie 極大的縮減了動(dòng)畫的開發(fā)成本,給 APP 增加非常強(qiáng)力的動(dòng)畫能力,不需要各個(gè)端再自己去實(shí)現(xiàn),而且目前 Lottie 已經(jīng)支持了非常多的 AE 動(dòng)畫效果,通過 Lottie 可以輕松實(shí)現(xiàn)很多酷炫的效果,所以現(xiàn)在做動(dòng)效考驗(yàn)的是設(shè)計(jì)同學(xué)的設(shè)計(jì)能力了,哈哈。

本文只針對(duì)重點(diǎn)原理進(jìn)行分析,歡迎留言討論交流。

9. 資料

  1. lottie-android : https://github.com/airbnb/lottie-android
  2. Introducing Lottie: https://medium.com/airbnb-engineering/introducing-lottie-4ff4a0afac0e
  3. design-lottie: https://airbnb.design/lottie/
  4. bodymovin: https://github.com/bodymovin/bodymovin
  5. Animation: Jump-through: https://medium.com/google-developers/animation-jump-through-861f4f5b3de4
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容