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

2024-06-12 06:18

本文主要是介绍音视频开发之旅(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的初始化 主要是两个里面TransformKeyframeAnimationbuildCompositionLayer();  //触发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.playAnimation2. LottieValueAnimator.playAnimation3. LottieValueAnimator.setFrame4. BaseLottieAnimator.notifyUpdate5.然后触发回调(LottieDrawable.progressUpdateListener)AnimatorUpdateListener.onAnimationUpdate6. CompositionLayer.setProgress --》计算当前的progress,然后倒序设置每个图层进度 BaseLayer.setProgress6.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的值,根据差值器获取到当前的Bitmap10.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大小,倒序生产每个Layerfor (int i = layerModels.size() - 1; i >= 0; i--) {Layer lm = layerModels.get(i);//这个是一个工程方法,根据layerType构造对应的LayerBaseLayer 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->tyswitch (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);//空图层,可以作为其他图层的parentcase NULL:return new NullLayer(drawable, layerModel);//文本图层case TEXT:return new TextLayer(drawable, layerModel);case UNKNOWN:default:// Do nothingLogger.warning("Unknown layer type " + layerModel.getLayerType());return null;}}

我们上面看到layerView.setParentLayer(parentLayer);那么这个ParentLayer有什么用呐?
主要在确定每个图层的边界和绘制时使用

 // BaseLayer#buildParentLayerListIfNeeded//该方法会在确定当前图层边界getBounds以及绘制该图层的时候调用drawprivate void buildParentLayerListIfNeeded() {if (parentLayers != null) {return;}//如果该图层有父图层,则创新if (parentLayer == null) {parentLayers = Collections.emptyList();return;}//该图层的LayerViewTreeparentLayers = 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:parentLayersbuildParentLayerListIfNeeded();//子图层的矩阵变换,以作用再父图层的矩阵变换为基础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的drawcontentGroup.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));//设置colorFilterif (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 drawpathcanvas.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 源码分析之动画与绘制的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1053412

相关文章

这15个Vue指令,让你的项目开发爽到爆

1. V-Hotkey 仓库地址: github.com/Dafrok/v-ho… Demo: 戳这里 https://dafrok.github.io/v-hotkey 安装: npm install --save v-hotkey 这个指令可以给组件绑定一个或多个快捷键。你想要通过按下 Escape 键后隐藏某个组件,按住 Control 和回车键再显示它吗?小菜一碟: <template

Hadoop企业开发案例调优场景

需求 (1)需求:从1G数据中,统计每个单词出现次数。服务器3台,每台配置4G内存,4核CPU,4线程。 (2)需求分析: 1G / 128m = 8个MapTask;1个ReduceTask;1个mrAppMaster 平均每个节点运行10个 / 3台 ≈ 3个任务(4    3    3) HDFS参数调优 (1)修改:hadoop-env.sh export HDFS_NAMENOD

【前端学习】AntV G6-08 深入图形与图形分组、自定义节点、节点动画(下)

【课程链接】 AntV G6:深入图形与图形分组、自定义节点、节点动画(下)_哔哩哔哩_bilibili 本章十吾老师讲解了一个复杂的自定义节点中,应该怎样去计算和绘制图形,如何给一个图形制作不间断的动画,以及在鼠标事件之后产生动画。(有点难,需要好好理解) <!DOCTYPE html><html><head><meta charset="UTF-8"><title>06

性能分析之MySQL索引实战案例

文章目录 一、前言二、准备三、MySQL索引优化四、MySQL 索引知识回顾五、总结 一、前言 在上一讲性能工具之 JProfiler 简单登录案例分析实战中已经发现SQL没有建立索引问题,本文将一起从代码层去分析为什么没有建立索引? 开源ERP项目地址:https://gitee.com/jishenghua/JSH_ERP 二、准备 打开IDEA找到登录请求资源路径位置

嵌入式QT开发:构建高效智能的嵌入式系统

摘要: 本文深入探讨了嵌入式 QT 相关的各个方面。从 QT 框架的基础架构和核心概念出发,详细阐述了其在嵌入式环境中的优势与特点。文中分析了嵌入式 QT 的开发环境搭建过程,包括交叉编译工具链的配置等关键步骤。进一步探讨了嵌入式 QT 的界面设计与开发,涵盖了从基本控件的使用到复杂界面布局的构建。同时也深入研究了信号与槽机制在嵌入式系统中的应用,以及嵌入式 QT 与硬件设备的交互,包括输入输出设

OpenHarmony鸿蒙开发( Beta5.0)无感配网详解

1、简介 无感配网是指在设备联网过程中无需输入热点相关账号信息,即可快速实现设备配网,是一种兼顾高效性、可靠性和安全性的配网方式。 2、配网原理 2.1 通信原理 手机和智能设备之间的信息传递,利用特有的NAN协议实现。利用手机和智能设备之间的WiFi 感知订阅、发布能力,实现了数字管家应用和设备之间的发现。在完成设备间的认证和响应后,即可发送相关配网数据。同时还支持与常规Sof

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听

活用c4d官方开发文档查询代码

当你问AI助手比如豆包,如何用python禁止掉xpresso标签时候,它会提示到 这时候要用到两个东西。https://developers.maxon.net/论坛搜索和开发文档 比如这里我就在官方找到正确的id描述 然后我就把参数标签换过来

Java ArrayList扩容机制 (源码解读)

结论:初始长度为10,若所需长度小于1.5倍原长度,则按照1.5倍扩容。若不够用则按照所需长度扩容。 一. 明确类内部重要变量含义         1:数组默认长度         2:这是一个共享的空数组实例,用于明确创建长度为0时的ArrayList ,比如通过 new ArrayList<>(0),ArrayList 内部的数组 elementData 会指向这个 EMPTY_EL

【WebGPU Unleashed】1.1 绘制三角形

一部2024新的WebGPU教程,作者Shi Yan。内容很好,翻译过来与大家共享,内容上会有改动,加上自己的理解。更多精彩内容尽在 dt.sim3d.cn ,关注公众号【sky的数孪技术】,技术交流、源码下载请添加微信号:digital_twin123 在 3D 渲染领域,三角形是最基本的绘制元素。在这里,我们将学习如何绘制单个三角形。接下来我们将制作一个简单的着色器来定义三角形内的像素