音視頻開發之旅(63) -Lottie 源碼分析之動畫與繪製

目錄

  1. 動畫和繪製的流程
  2. LayerView樹
  3. ShapeLayer的分析
  4. Lottie優劣以及rLottie、PAG的介紹
  5. 資料
  6. 收穫

上一篇我們學習分析了Lottie的json解析部分. 這篇我們分析的動畫和渲染部分。

分析的重點:如何組織多圖層layer的關係,控制先後處理不同圖層的繪製以及動畫。

一、動畫和繪製的流程

我們通過入口API函數(LottieDrawable#setComposition、LottieDrawable#playAnimation)來進行分析。

1.1 LottieDrawable#setComposition 流程

public boolean setComposition(LottieComposition composition) {

    //......
    clearComposition();
    this.composition = composition;
    //構建圖層layer compositionlayer它的作用有點先andoid View樹中ViewGroup,可以包含其他的View和ViewGroup
    //完成CompositionLayer和ContentGroup的初始化 主要是兩個裏面TransformKeyframeAnimation
    buildCompositionLayer();  
  
    //觸發notifyUpdate,進而觸發個Layer的progress的重新計算以及draw的回調(當然此時進度爲0,各種判斷之後也不會觸發composition的drawlayer)
    animator.setComposition(composition);

    //設置當前動畫的進度
    setProgress(animator.getAnimatedFraction());

   ......

   }

可以看到setComposition主要調用了buildCompositionLayer和 animator.setComposition來進行CompositionLayer和其他各Layer(json中對應的layers字段)以及 ContentGroup、TransformKeyframeAnimation等初始化。
Lottie動畫中使用最多Layer是CompositionLayer、ShapeLayer以及ImageLayer。

思考:那麼什麼是ContentGroup、TransformKeyframeAnimation、他們和layer的關係是什麼吶?(後面會嘗試分析解答)

1.2 LottieDrawable#playAnimation 流程

   1. LottieDrawable.playAnimation
   2. LottieValueAnimator.playAnimation
   3. LottieValueAnimator.setFrame
   4. BaseLottieAnimator.notifyUpdate
   5.然後觸發回調(LottieDrawable.progressUpdateListener)AnimatorUpdateListener.onAnimationUpdate
   6. CompositionLayer.setProgress --》計算當前的progress,然後倒序設置每個圖層進度 BaseLayer.setProgress
       6.1(transform.setProgress(progress))TransformKeyframeAnimation.setProgress 設置矩陣變換的進度(縮放、透明度、位移等)--》需要重點分析
       6.2  animations.get(i).setProgress(progress); 遍歷設置每個animation的進度
   7. BaseKeyframeAnimation.notifyListeners 回調給監聽者
   8. BaseLayer.onValueChanged (invalidateSelf())觸發頁面的重新繪製,--》即LottieDrawable.draw(android.graphics.Canvas, android.graphics.Matrix)
   9. compositionLayer.draw(canvas, matrix, alpha)  即 BaseLayer.draw --》這也是一個關鍵的方法
   10. drawLayer(canvas, matrix, alpha); 即 BaseLayer.drawLayer這個方法是抽象方法,各layer具體實現
         10.1 我們以ImageLayer爲例來來看 (重點分析) ImageLayer.drawLayer 首先通過BaseKeyframeAnimation.getValue() 這個就用到前面動畫改變的progress的值,根據差值器獲取到當前的Bitmap
         10.2 然後使用canvas來進行繪製,完成圖片的變換

LottieValueAnimator是ValueAnimator的子類,並且實現了Choreographer.FrameCallback接口。通過屬性動畫的進度變換回調以及VSYNC信號的doframe回調來通知Layer進行進度以及值計算,並且通知LottieDrawble進行重新繪製,從而實現json中layers也即各種Layer圖層的動畫和繪製。

而具體的繪製還是有Canvas來實現,可以通過ImageLayer的drawLayer

public void drawLayer(@NonNull Canvas canvas, Matrix parentMatrix, int parentAlpha) {
    Bitmap bitmap = getBitmap();
    if (bitmap == null || bitmap.isRecycled()) {
      return;
    }
    float density = Utils.dpScale();

    paint.setAlpha(parentAlpha);
    if (colorFilterAnimation != null) {
      paint.setColorFilter(colorFilterAnimation.getValue());
    }
    //將畫布的當前狀態保存
    canvas.save();
    //對matrix的變換應用到canvas上的所有對象
    canvas.concat(parentMatrix);
    //src用來設定要繪製bitmap的區域,即是否進行裁剪
    src.set(0, 0, bitmap.getWidth(), bitmap.getHeight());
    //dst用來設置在canvas畫布上的顯示區域。這裏可以看到顯示的寬高會根據像素密度進行等縮放
    dst.set(0, 0, (int) (bitmap.getWidth() * density), (int) (bitmap.getHeight() * density));
    //第一個Rect(src) 代表要繪製的bitmap 區域,可以對是對圖片進行裁截,若是空null則顯示整個圖片。第二個 Rect(dst) 是圖片在Canvas畫布中顯示的區域,即要將bitmap 繪製在屏幕的什麼地方
   // 通過動態的改變dst,可以實現 移動、縮放等效果,以及根據屏幕的像素密度進行縮放,通過改變src 對繪製的圖片需求做處理,也能夠實現很多有趣的效果,比如 顯示一部分,或者逐漸展開等
    canvas.drawBitmap(bitmap, src, dst, paint);
    //恢復之前保存的畫布狀態,和sava一一對應
    canvas.restore();
  }

至於ShapeLayer和CompositionLayer有些複雜,下面我們會單獨來分析。

思考: 如果有多個圖層,怎麼保證多個圖層之間的關聯性(就像ViewTree一樣,怎麼管理他們之間的關係和繪製的順序)。

二、LayerView樹

Lottie中有各種Layer:


那麼他們之間是什麼關係吶?如何進行管理和層級控制吶?

CompositionLayer的構造

  public CompositionLayer(LottieDrawable lottieDrawable, Layer layerModel, List<Layer> layerModels,
      LottieComposition composition) {

   //主要是TransformKeyframeAnimation的初始化
    super(lottieDrawable, layerModel);
LongSparseArray<BaseLayer> layerMap =
        new LongSparseArray<>(composition.getLayers().size());

    BaseLayer mattedLayer = null;
    //根據layers大小,倒序生產每個Layer
    for (int i = layerModels.size() - 1; i >= 0; i--) {
      Layer lm = layerModels.get(i);
      //這個是一個工程方法,根據layerType構造對應的Layer
      BaseLayer layer = BaseLayer.forModel(this, lm,   lottieDrawable, composition);
      if (layer == null) {
        continue;
      }
      layerMap.put(layer.getLayerModel().getId(), layer);
      ......
     }

    
    for (int i = 0; i < layerMap.size(); i++) {
      long key = layerMap.keyAt(i);
      BaseLayer layerView = layerMap.get(key);
      if (layerView == null) {
        continue;
      }
     // 確定layer之間的父子關係
      BaseLayer parentLayer =   layerMap.get(layerView.getLayerModel().getParentId());
      if (parentLayer != null) {
        layerView.setParentLayer(parentLayer);
      }
    }

}

工廠方法:BaseLayer#forModel

static BaseLayer forModel(
      CompositionLayer compositionLayer, Layer layerModel, LottieDrawable drawable, LottieComposition composition) {
    //對應json中 object->layers->ty
    switch (layerModel.getLayerType()) {
        //輪廓/形態圖層  這個是再lottie動畫中用的基本上是最多的類型
      case SHAPE:
        return new ShapeLayer(drawable, layerModel, compositionLayer);
        //合成圖層,相當於ViewTree的ViewGroup的角色
      case PRE_COMP:
        return new CompositionLayer(drawable, layerModel,
            composition.getPrecomps(layerModel.getRefId()), composition);
        //填充圖層
      case SOLID:
        return new SolidLayer(drawable, layerModel);
        //圖片圖層  這個也很常用,特別是做一些模版特效時
      case IMAGE:
        return new ImageLayer(drawable, layerModel);
        //空圖層,可以作爲其他圖層的parent
      case NULL:
        return new NullLayer(drawable, layerModel);
        //文本圖層
      case TEXT:
        return new TextLayer(drawable, layerModel);
      case UNKNOWN:
      default:
        // Do nothing
        Logger.warning("Unknown layer type " + layerModel.getLayerType());
        return null;
    }
  }

我們上面看到layerView.setParentLayer(parentLayer);那麼這個ParentLayer有什麼用吶?
主要在確定每個圖層的邊界和繪製時使用

 // BaseLayer#buildParentLayerListIfNeeded
 //該方法會在確定當前圖層邊界getBounds以及繪製該圖層的時候調用draw
  private void buildParentLayerListIfNeeded() {
    if (parentLayers != null) {
      return;
    }
    //如果該圖層有父圖層,則創新
    if (parentLayer == null) {
      parentLayers = Collections.emptyList();
      return;
    }

    //該圖層的LayerViewTree
    parentLayers = new ArrayList<>();
    BaseLayer layer = parentLayer;
    //遞歸找到該圖層的父圖層、祖父圖層、曾祖圖層等等
    while (layer != null) {
      parentLayers.add(layer);
      layer = layer.parentLayer;
    }
  }

BaseLayer#getBounds

 public void getBounds(
      RectF outBounds, Matrix parentMatrix, boolean applyParents) {
    rect.set(0, 0, 0, 0);
    //確定該圖層的LayerViewTree:parentLayers
    buildParentLayerListIfNeeded();
    //子圖層的矩陣變換,以作用再父圖層的矩陣變換爲基礎
    boundsMatrix.set(parentMatrix);

    if (applyParents) {
      //遞歸調用父圖層額矩陣變換,進行矩陣相乘
      if (parentLayers != null) {
        for (int i = parentLayers.size() - 1; i >= 0; i--) {
          boundsMatrix.preConcat(parentLayers.get(i).transform.getMatrix());
        }
      } else if (parentLayer != null) {
        boundsMatrix.preConcat(parentLayer.transform.getMatrix());
      }
    }

    //最後再乘以當前圖層的矩陣變換,以確定最終的邊界矩陣
    boundsMatrix.preConcat(transform.getMatrix());
  }

BaseLayer#draw
和BaseLayer#getBounds一樣的矩陣處理方式。

通過parentid確立該圖層的LayerViewTree,再測量繪製時根據LayerView的確定自己的bound和draw。

三、ShapeLayer 的分析

之所以把ShapeLayer單獨拎出來說,是因爲他在lottie動畫中很重要,通過
ShapeLayer是一個通過矢量圖形而不是bitmap來繪製的圖層子類。指定顏色和線寬等屬性,用Path來定義要繪製的圖形.

public class ShapeLayer extends BaseLayer {
  ......
  
 //這個ContentGroup是什麼吶?可以看到ShapeLayer的drawLayer和getBound都是通過contentGroup代理的。
  private final ContentGroup contentGroup;
  

  ShapeLayer(LottieDrawable lottieDrawable, Layer layerModel, CompositionLayer compositionLayer) {
    ......
    //ContentGroup構造
    contentGroup = new ContentGroup(lottieDrawable, this, shapeGroup);
    contentGroup.setContents(Collections.<Content>emptyList(), Collections.<Content>emptyList());
  }

  @Override void drawLayer(@NonNull Canvas canvas, Matrix parentMatrix, int parentAlpha) {
    //調用了contentGroup的draw
    contentGroup.draw(canvas, parentMatrix, parentAlpha);
  }

  @Override public void getBounds(RectF outBounds, Matrix parentMatrix, boolean applyParents) {
    ......
    contentGroup.getBounds(outBounds, boundsMatrix, applyParents);
  }
  ......
}

ContentGroup是什麼吶?
可以看到ShapeLayer的drawLayer和getBound都是通過contentGroup代理的。
我們看下ContentGroup的draw的實現

public void draw(Canvas canvas, Matrix parentMatrix, int parentAlpha){

    //遍歷調用content,如果是DrawingContent則進行draw,那邊什麼是DrawingContent吶
    for (int i = contents.size() - 1; i >= 0; i--) {
      Object content = contents.get(i);
      if (content instanceof DrawingContent) {
        ((DrawingContent) content).draw(canvas, matrix, childAlpha);
      }
    }

}

遍歷調用content,如果是DrawingContent則進行draw,哪些content是DrawingContent吶?


我們以FillContent爲例,來看下其draw的實現

public void draw(Canvas canvas, Matrix parentMatrix, int parentAlpha) {
    ......
    //獲取顏色 透明度等 設置畫筆paint的顏色
    int color = ((ColorKeyframeAnimation) this.colorAnimation).getIntValue();
    int alpha = (int) ((parentAlpha / 255f * opacityAnimation.getValue() / 100f) * 255);
    paint.setColor((clamp(alpha, 0, 255) << 24) | (color & 0xFFFFFF));

    //設置colorFilter
    if (colorFilterAnimation != null) {
      paint.setColorFilter(colorFilterAnimation.getValue());
    }

    ......
    //設置path路徑
    path.reset();
    for (int i = 0; i < paths.size(); i++) {
      path.addPath(paths.get(i).getPath(), parentMatrix);
    }

    //用cavas drawpath
    canvas.drawPath(path, paint);

  }

可以ShapeContent的DrawingContent也是通過Canvas來進行draw的。

Lottie的動畫和渲染解析部分就到這裏,關於BaseKeyframeAnimation主要實現Layer和DrawingContent中動畫的插值計算,沒有詳細分析,有需要再看吧。

思考:能不能通過OpenGL ES來進行渲染繪製吶?

五、Lottie優劣以及和PAG的簡單對比

Lottie的優劣

優點:
支持跨平臺(雖然每個端各自實現一套)
性能好
可以通過配置下發“json和素材”進行更新。

不足點:
Lottie不支持交互和編輯
Lottie不支持壓縮位圖,如果使用png等位圖,需要自行在tiny等壓縮平臺進行圖片壓縮、降低包體積。
Lottie存在mask、matters 時,需要先saveLayer,再調用drawLayer返回。
saveLayer是一個耗時的操作,需要先分配、繪製一個offscreen的緩衝區,這增加了渲染的時間

PAG的優劣簡單介紹

PAG是騰訊昨天剛開源的動畫組件,除lottie的優點外,
 支持更多AE特效,
 支持文本和序列幀,
 支持模版的編輯,
 採用二級值文件而不是json,文件大小和解析的性能都會更好些
 渲染層面:Lottie渲染層面的實現依賴平臺端接口,不同平臺可能會有所差異。PAG渲染層面使用C++實現,所有平臺共享同一套實現,平臺端只是封裝接口調用,提供渲染環境,渲染效果一致。


PAG的不足,渲染基於google開源的skia 2d來實現。增加了包大小。4.0的版本會有改善,去掉skia 2d。自己實現簡單的渲染封裝(估計也是opengl或者metal 、vulkan)。

rlottie簡單介紹

[Samsung-rlottie](https://github.com/Samsung/rlottie)

rLottie 與 lottie 工作流一致,在 SDK 上實現不一樣,rLottie 沒有使用平臺特定實現,是統一 C++實現,素材支持 lottie 的 json 文件,矢量渲染性能還不錯,但缺少各平臺封裝,支持的 AE 特性不全,也不支持文本、序列幀等

這個還沒有分析它的源碼實現。抽時間可以分析學習下。

六、資料

  1. Lottie實現思路和源碼分析
  2. Lottie 動畫原理剖析
  3. 揭祕Lottie動畫的優劣及原理
  4. lottie-android 框架使用及源碼解析
  5. Lottie動畫庫 Android 端源碼淺析
  6. 騰訊開源的PAG
  7. Samsung-rlottie
  8. 從解碼渲染層面對比 PAG 與 lottie

七、收穫

通過本篇的學習分析

  1. 梳理了lottie動畫和渲染的流程
  2. LayerView樹的概念和理解,搞清楚lottie是如何管理不同layer之間的關係的
  3. 重點分析了CompositionLayer、BaseLayer、ImageLayer和ShapeLayer,其中ShapeLayer又包含ContentGroup
  4. 簡單對比了lottie、PAG、rlottie

感謝你的閱讀
歡迎關注公衆號“音視頻開發之旅”,一起學習成長。
歡迎交流

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章