美文网首页
音视频开发之旅(63) -Lottie 源码分析之动画与绘制

音视频开发之旅(63) -Lottie 源码分析之动画与绘制

作者: yabin小站 | 来源:发表于2022-01-16 07:50 被阅读0次

    目录

    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:


    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的确定自己的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

    感谢你的阅读
    欢迎关注公众号“音视频开发之旅”,一起学习成长。
    欢迎交流

    相关文章

      网友评论

          本文标题:音视频开发之旅(63) -Lottie 源码分析之动画与绘制

          本文链接:https://www.haomeiwen.com/subject/pyffhrtx.html