Audio and video development journey (63) - Animation and drawing of Lottie source code analysis

Table of contents

  1. The flow of animation and drawing
  2. LayerView tree
  3. Analysis of ShapeLayer
  4. The advantages and disadvantages of Lottie and the introduction of rLottie and PAG
  5. material
  6. reward

In the previous article, we learned and analyzed the json parsing part of Lottie . In this article, we analyzed the animation and rendering part.

The focus of the analysis: how to organize the relationship of multi-layer layers, and control the drawing and animation of different layers.

1. The process of animation and drawing

We analyze through the entry API functions (LottieDrawable#setComposition, LottieDrawable#playAnimation).

1.1 LottieDrawable#setComposition process

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());

   ......

   }

You can see that setComposition mainly calls buildCompositionLayer and animator.setComposition to initialize CompositionLayer and other Layers (corresponding layers fields in json), ContentGroup, TransformKeyframeAnimation, etc.
The most used Layers in Lottie animation are CompositionLayer, ShapeLayer and ImageLayer.

Thinking: So what is ContentGroup, TransformKeyframeAnimation, and what is their relationship with layer? (I will try to analyze the answer later)

1.2 LottieDrawable#playAnimation process

   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 is a subclass of ValueAnimator and implements the Choreographer.FrameCallback interface. Through the progress transformation callback of the attribute animation and the doframe callback of the VSYNC signal, the Layer is notified to perform progress and value calculation, and LottieDrawble is notified to redraw, so as to realize the animation and drawing of layers in json, that is, various Layer layers.

The specific drawing is still realized by Canvas, which can be achieved through the drawLayer of ImageLayer

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

As for ShapeLayer and CompositionLayer, they are a bit complicated, and we will analyze them separately below.

Thinking: If there are multiple layers, how to ensure the correlation between multiple layers (just like ViewTree, how to manage the relationship between them and the order of drawing).

Two, LayerView tree

There are various Layers in Lottie:

1.jpg

So what is the relationship between them? How to carry out management and hierarchical control?

Construction of 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);
      }
    }

}

Factory method: 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;
    }
  }

We saw layerView.setParentLayer(parentLayer) above; so what is the use of this ParentLayer?
Mainly used when determining the boundaries and drawing of each layer

 // 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
is the same matrix processing method as BaseLayer#getBounds.

Establish the LayerViewTree of the layer through the parentid, and then determine its own bound and draw according to the LayerView when measuring and drawing.

3. Analysis of ShapeLayer

The reason why ShapeLayer is singled out is because it is very important in lottie animation.
ShapeLayer is a layer subclass drawn by vector graphics instead of bitmap. Specify properties such as color and line width, and use Path to define the graphics to be drawn.

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

What is ContentGroup?
You can see that both drawLayer and getBound of ShapeLayer are proxied through contentGroup.
Let's look at the implementation of ContentGroup's 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);
      }
    }

}

Traversing calls content, if it is DrawingContent, then draw, which content is DrawingContent?

Let's take FillContent as an example to see the implementation of its 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);

  }

The DrawingContent that can be ShapeContent is also drawn through Canvas.

This is the end of Lottie's animation and rendering analysis part. Regarding BaseKeyframeAnimation, which mainly realizes the interpolation calculation of animation in Layer and DrawingContent, there is no detailed analysis, so let's read it again if necessary.

Thinking: Can OpenGL ES be used for rendering and drawing?

5. The advantages and disadvantages of Lottie and a simple comparison with PAG

Pros and cons of Lottie

优点:
支持跨平台(虽然每个端各自实现一套)
性能好
可以通过配置下发“json和素材”进行更新。

不足点:
Lottie不支持交互和编辑
Lottie不支持压缩位图,如果使用png等位图,需要自行在tiny等压缩平台进行图片压缩、降低包体积。
Lottie存在mask、matters 时,需要先saveLayer,再调用drawLayer返回。
saveLayer是一个耗时的操作,需要先分配、绘制一个offscreen的缓冲区,这增加了渲染的时间

A brief introduction to the advantages and disadvantages of PAG

PAG是腾讯昨天刚开源的动画组件,除lottie的优点外,
 支持更多AE特效,
 支持文本和序列帧,
 支持模版的编辑,
 采用二级值文件而不是json,文件大小和解析的性能都会更好些
 渲染层面:Lottie渲染层面的实现依赖平台端接口,不同平台可能会有所差异。PAG渲染层面使用C++实现,所有平台共享同一套实现,平台端只是封装接口调用,提供渲染环境,渲染效果一致。


PAG的不足,渲染基于google开源的skia 2d来实现。增加了包大小。4.0的版本会有改善,去掉skia 2d。自己实现简单的渲染封装(估计也是opengl或者metal 、vulkan)。

Brief introduction of rlottie

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

rLottie 与 lottie 工作流一致,在 SDK 上实现不一样,rLottie 没有使用平台特定实现,是统一 C++实现,素材支持 lottie 的 json 文件,矢量渲染性能还不错,但缺少各平台封装,支持的 AE 特性不全,也不支持文本、序列帧等

这个还没有分析它的源码实现。抽时间可以分析学习下。

6. Information

  1. Lottie implementation ideas and source code analysis
  2. Analysis of Lottie animation principle
  3. Demystifying the advantages and disadvantages of Lottie animation and its principles
  4. Lottie-android framework use and source code analysis
  5. Lottie animation library Android source code analysis
  6. Tencent open source PAG
  7. Samsung-rlottie
  8. Comparing PAG and lottie from the perspective of decoding and rendering

7. Harvest

Through the study analysis of this article

  1. Sort out the process of lottie animation and rendering
  2. The concept and understanding of the LayerView tree, figure out how lottie manages the relationship between different layers
  3. Focus on the analysis of CompositionLayer, BaseLayer, ImageLayer and ShapeLayer, where ShapeLayer contains ContentGroup
  4. Simple comparison of lottie, PAG, rlottie

Thank you for reading.
Welcome to pay attention to the official account "Audio and Video Development Journey" and learn and grow together.
Welcome to exchange

Guess you like

Origin blog.csdn.net/u011570979/article/details/122519211