오디오 및 비디오 개발 여정(63) - Lottie 소스 코드 분석의 애니메이션 및 그림

목차

  1. 애니메이션과 드로잉의 흐름
  2. LayerView 트리
  3. ShapeLayer 분석
  4. 로티의 장단점과 rLottie와 PAG의 도입
  5. 재료
  6. 보상

이전 글에서는 Lottie 의 json 파싱 부분을 학습하고 분석하였고 , 이번 글에서는 애니메이션과 렌더링 부분을 분석하였다.

분석의 초점: 다중 레이어의 관계를 구성하고 다른 레이어의 그리기 및 애니메이션을 제어하는 ​​방법.

1. 애니메이션과 드로잉 과정

진입 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 및 기타 레이어(json의 해당 레이어 필드), ContentGroup, TransformKeyframeAnimation 등을 초기화하는 것을 볼 수 있습니다.
Lottie 애니메이션에서 가장 많이 사용되는 레이어는 CompositionLayer, ShapeLayer 및 ImageLayer입니다.

생각: ContentGroup, TransformKeyframeAnimation은 무엇이며 레이어와의 관계는 무엇입니까? (나중에 답을 분석하려고 노력할 것입니다)

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에서 레이어의 애니메이션 및 그리기를 실현합니다. 즉, 다양한 레이어 레이어입니다.

특정 드로잉은 여전히 ​​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에는 다양한 레이어가 있습니다.

1.jpg

그래서 그들 사이의 관계는 무엇입니까? 관리 및 계층 제어를 수행하는 방법은 무엇입니까?

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에 따라 자체 바인딩 및 그리기를 결정합니다.

3. ShapeLayer 분석

ShapeLayer를 꼽은 이유는 로티 애니메이션에서 매우 중요하기 때문인데,
ShapeLayer는 비트맵이 아닌 벡터 그래픽으로 그리는 레이어 하위 클래스입니다. 색상 및 선 너비와 같은 속성을 지정하고 경로를 사용하여 그릴 그래픽을 정의합니다.

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

콘텐츠그룹이란?
drawLayer와 ShapeLayer의 getBound가 모두 contentGroup을 통해 프록시되는 것을 볼 수 있습니다.
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);
      }
    }

}

Traversing은 콘텐츠를 호출하고, DrawingContent인 경우 그립니다. 어떤 콘텐츠가 DrawingContent입니까?

FillContent를 예로 들어 그리기 구현을 살펴보겠습니다.

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를 통해 그려집니다.

Lottie의 애니메이션 및 렌더링 분석 부분은 여기까지입니다.Layer 및 DrawingContent에서 애니메이션의 보간 계산을 주로 구현하는 BaseKeyframeAnimation에 대해서는 자세한 분석이 없으므로 필요하면 다시 읽어보도록 하겠습니다.

생각: OpenGL ES를 렌더링 및 그리기에 사용할 수 있습니까?

5. 로티의 장단점과 PAG와의 단순 비교

롯데의 장점과 단점

优点:
支持跨平台(虽然每个端各自实现一套)
性能好
可以通过配置下发“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 特性不全,也不支持文本、序列帧等

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

6. 정보

  1. 롯데 구현 아이디어 및 소스코드 분석
  2. 롯데 애니메이션 원리 분석
  3. 로티 애니메이션의 장단점과 그 원리를 이해하기
  4. Lottie-안드로이드 프레임워크 사용 및 소스코드 분석
  5. 롯데 애니메이션 라이브러리 안드로이드 소스코드 분석
  6. Tencent 오픈 소스 PAG
  7. 삼성-rlottie
  8. 디코딩과 렌더링의 관점에서 PAG와 lottie 비교

7. 수확

이 기사의 연구 분석을 통해

  1. 로또 애니메이션 및 렌더링 과정 정리
  2. LayerView 트리의 개념과 이해, lottie가 서로 다른 레이어 간의 관계를 관리하는 방법 파악
  3. ShapeLayer에 ContentGroup이 포함된 CompositionLayer, BaseLayer, ImageLayer 및 ShapeLayer의 분석에 중점을 둡니다.
  4. lottie, PAG, rlottie의 간단한 비교

읽어 주셔서 감사합니다
공식 계정 "오디오 및 비디오 개발 여정"에 관심을 갖고 함께 배우고 성장하는 것을 환영합니다.
교류에 오신 것을 환영합니다

추천

출처blog.csdn.net/u011570979/article/details/122519211