Viaje de desarrollo de audio y video (63) - Animación y dibujo del análisis del código fuente de Lottie

Tabla de contenido

  1. El flujo de la animación y el dibujo.
  2. árbol LayerView
  3. Análisis de ShapeLayer
  4. Las ventajas y desventajas de Lottie y la introducción de rLottie y PAG
  5. material
  6. premio

En el artículo anterior, aprendimos y analizamos la parte de análisis json de Lottie . En este artículo, analizamos la parte de animación y renderizado.

El foco del análisis: cómo organizar la relación de capas multicapa y controlar el dibujo y la animación de diferentes capas.

1. El proceso de animación y dibujo.

Analizamos a través de las funciones de la API de entrada (LottieDrawable#setComposition, LottieDrawable#playAnimation).

1.1 LottieDrawable#setProceso de composición

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

   ......

   }

Se puede ver que setComposition llama principalmente a buildCompositionLayer y animator.setComposition para inicializar CompositionLayer y otras capas (campos de capas correspondientes en json), ContentGroup, TransformKeyframeAnimation, etc.
Las capas más utilizadas en la animación de Lottie son CompositionLayer, ShapeLayer e ImageLayer.

Pensando: Entonces, ¿qué es ContentGroup, TransformKeyframeAnimation y cuál es su relación con la capa? (Trataré de analizar la respuesta más adelante)

1.2 LottieDrawable#playProceso de animación

   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 es una subclase de ValueAnimator e implementa la interfaz Choreographer.FrameCallback. A través de la devolución de llamada de transformación de progreso de la animación de atributo y la devolución de llamada de doframe de la señal VSYNC, se notifica a la capa que realice el progreso y el cálculo del valor, y se notifica a LottieDrawble que vuelva a dibujar, para realizar la animación y el dibujo de capas en json, que es decir, varias capas de capas.

El dibujo específico todavía se realiza mediante Canvas, que se puede lograr a través de drawLayer de 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();
  }

En cuanto a ShapeLayer y CompositionLayer, son un poco complicados y los analizaremos por separado a continuación.

Pensamiento: si hay varias capas, cómo garantizar la correlación entre varias capas (al igual que ViewTree, cómo administrar la relación entre ellas y el orden del dibujo).

Dos, árbol LayerView

Hay varias capas en Lottie:

1.jpg

Entonces, ¿cuál es la relación entre ellos? ¿Cómo llevar a cabo la gestión y el control jerárquico?

Construcción de 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);
      }
    }

}

Método de fábrica: 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;
    }
  }

Vimos layerView.setParentLayer(parentLayer) arriba, entonces, ¿cuál es el uso de este ParentLayer?
Se utiliza principalmente para determinar los límites y dibujar cada capa.

 // 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
es el mismo método de procesamiento de matrices que BaseLayer#getBounds.

Establezca el LayerViewTree de la capa a través del parentid, y luego determine su propio límite y dibuje de acuerdo con LayerView al medir y dibujar.

3. Análisis de ShapeLayer

La razón por la que se destaca ShapeLayer es porque es muy importante en la animación de lottie.ShapeLayer
es una subclase de capa dibujada por gráficos vectoriales en lugar de mapa de bits. Especifique propiedades como el color y el ancho de línea, y use Ruta para definir los gráficos que se dibujarán.

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

¿Qué es ContentGroup?
Puede ver que tanto drawLayer como getBound de ShapeLayer se transmiten a través de contentGroup.
Veamos la implementación del sorteo de ContentGroup.

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

}

Atravesar llama contenido, si es DrawingContent, luego dibuje, ¿qué contenido es DrawingContent?

Tomemos FillContent como ejemplo para ver la implementación de su sorteo.

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

  }

El DrawingContent que puede ser ShapeContent también se dibuja a través de Canvas.

Este es el final de la parte de análisis de animación y renderizado de Lottie. Con respecto a BaseKeyframeAnimation, que realiza principalmente el cálculo de interpolación de animación en Layer y DrawingContent, no hay un análisis detallado, así que leámoslo de nuevo si es necesario.

Pensando: ¿Se puede usar OpenGL ES para renderizar y dibujar?

5. Las ventajas y desventajas de Lottie y una simple comparación con PAG

Pros y contras de Lottie

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

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

Una breve introducción a las ventajas y desventajas de PAG

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


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

Breve introducción de rlotti

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

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

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

6. Información

  1. Ideas de implementación de Lottie y análisis de código fuente
  2. Análisis del principio de animación de Lottie
  3. Desmitificando las ventajas y desventajas de la animación de Lottie y sus principios
  4. Análisis de código fuente y uso del framework Lottie-Android
  5. Biblioteca de animación Lottie Análisis de código fuente de Android
  6. PAG de código abierto de Tencent
  7. Samsung-lottie
  8. Comparación de PAG y lottie desde la perspectiva de la decodificación y la representación

7. Cosecha

A través del estudio de análisis de este artículo

  1. Ordenar el proceso de animación y renderizado de lottie
  2. El concepto y la comprensión del árbol LayerView, descubra cómo Lottie gestiona la relación entre las diferentes capas.
  3. Concéntrese en el análisis de CompositionLayer, BaseLayer, ImageLayer y ShapeLayer, donde ShapeLayer contiene ContentGroup
  4. Comparación simple de lottie, PAG, rlottie

Gracias por leer
Bienvenido a prestar atención a la cuenta oficial "Viaje de desarrollo de audio y video" y aprender y crecer juntos.
Bienvenido al intercambio

Supongo que te gusta

Origin blog.csdn.net/u011570979/article/details/122519211
Recomendado
Clasificación