Lottie动画源码解析

news2025/3/11 9:53:29

Lottie是一个很成熟的开源动画框架,它支持直接使用从AE导出的动画文件,在不同平台均可快速使用,大大减轻了程序员的工作量,也让复杂的动画成为可能。该动画文件使用Json格式来描述内容,可以大大缩减文件的体积。在Android平台,Lottie的作用就是把Json文件做解析,使用code做控制,并通过Canvas绘制出来。Lottie效果如下:
请添加图片描述如果用纯代码去手搓以上的动画效果,估计码农们会原地狗带。而用Lottie作为衔接,这一切就很丝滑,就像显示一张图片一样简单,这就是Lottie的强大,可以说是降维打击。

1.文件结构

1.1最外层文件

Json的最外层结构如下:

{
  "v": "5.8.0",  //bodymovin的版本
  "fr": 60,      //帧率
  "ip": 0,       //起始关键帧
  "op": 102,     //结束关键帧
  "w": 1350,     //动画宽度
  "h": 800,      //动画高度
  "nm": "recommend_turn page_x0.75_original", //名称
  "ddd": 0,       //是否为3d
  "assets":[],   //资源信息
  "layers":[],   //图层信息
  "markers": []  //遮罩
}

其中的layers是动画最核心的内容,因为Lottie的动画是由不同的Layer组合而成的。

1.2 Layers层

结构如下:

"layers": [                            //图层信息
    {
      "ddd": 0,         //是否为3d
      "ind": 1,                     //图层id 唯一性
      "ty": 4,            //图层类型
      "nm": "page back 4",//图层名称
      "refId": "comp_0", // 引用的资源,图片/预合成层
      "td": 1,
      "sr": 1,
      "ks": {...},              // 变换。对应AE中的变换设置
      "ao": 0,
      "layer": [],         // 该图层包含的子图层
      "shaps": [],         // 形状图层
      "ip": 12,                     //该图层起始关键帧
      "op": 1782,         //该图层结束关键帧
      "st": -18,         
      "bm": 0
    }

ty : 定义了图层的类型,类型有ImageLayer、ShapeLayer、ScaleLayer、SolidLayer、TextLayer和NullLayer,不同的layer会使用不同的变化策略以及不同的资源。
refId :这是ImageLayer会用到的图片资源。
shaps :ShapeLayer会用到的资源,描述形状。
ks :变换的描述,所有图层都会用到这个资源,它是对动画怎么变化的具体描述,它内部包含了很多维度的变。

1.3 KS层

具体如下:

"ks": { // 变换。对应AE中的变换设置
    "o": { // 透明度
        "a": 0,
        "k": 100,
        "ix": 11
    },
    "r": { // 旋转
        "a": 0,
        "k": 0,
        "ix": 10
    },
    "p": { // 位置
        "a": 0,
        "k": [-167, 358.125, 0],
        "ix": 2
    },
    "a": { // 锚点
        "a": 0,
        "k": [667, 375, 0],
        "ix": 1
    },
    "s": { // 缩放
        "a": 0,
        "k": [100, 100, 100],
        "ix": 6
    }
}

2.文件解析过程

2.1 LottieTask

在LottieAnimationView的setAnimation方法是加载资源的入口,它内部会把加载资源的任务创建一个LottieTask,并且在执行完文件解析之后,创建相关的Layer,并封装在LottieComposition作为结果返回给LottieAnimationView,如下:

public void setAnimation(@RawRes final int rawRes) {
    LottieTask<LottieComposition> task = cacheComposition ?
        LottieCompositionFactory.fromRawRes(getContext(), rawRes) : LottieCompositionFactory.fromRawRes(getContext(), rawRes, null);
    //添加任务监听,在任务执行成功之后回调
   setCompositionTask(task); 
  }

LottieCompositionFactory内部会创建一个LottieTask并添加到线程池中去异步执行:

public static LottieTask<LottieComposition> fromRawRes(Context context, @RawRes final int rawRes, @Nullable String cacheKey) {
    return cache(cacheKey, new Callable<LottieResult<LottieComposition>>() {
      @Override
      public LottieResult<LottieComposition> call() {
        // 这里执行异步解析任务
        return fromRawResSync(context, rawRes);
      }
    });
  }

在LottieTask内部中添加线程池任务:

LottieTask(Callable<LottieResult<T>> runnable, boolean runNow) {
      EXECUTOR.execute(new LottieFutureTask(runnable));
  }

2.2 LayerParser

跟进fromRawResSync方法里面去,会调用到LottieCompositionMoshiParser去做解析最外层的Json数据,我们主要关注是解析layers字段:
LottieCompositionMoshiParser.java

public static LottieComposition parse(JsonReader reader) throws IOException {
	switch (reader.selectName(NAMES)) {
		case 6:
          parseLayers(reader, composition, layers, layerMap);
	}
}

private static void parseLayers(JsonReader reader, LottieComposition composition,
                                  List<Layer> layers, LongSparseArray<Layer> layerMap) throws IOException {
      Layer layer = LayerParser.parse(reader, composition);
      layers.add(layer);
      layerMap.put(layer.getId(), layer);
  }

最后再调用了LayerParser做生成各个Layer数据结构,当然LayerParser内部还要对下一层的数据做解析,最后形成一个树形数据结构返回给LottieAnimationView。

3.创建绘制图层

3.1 CompositionLayer和BaseLayer

LottieTask在解析完之后,会把LottieComposition返回给LottieAnimationView:
LottieAnimationView.java

public void setComposition(@NonNull LottieComposition composition) {
    boolean isNewComposition = lottieDrawable.setComposition(composition);
    }

LottieAnimationView又把数据透传给LottieDrawable:
LottieDrawable.java

public boolean setComposition(LottieComposition composition) {
	buildCompositionLayer();
}

compositionLayer = new CompositionLayer(
        this, LayerParser.parse(composition), composition.getLayers(), composition);

这里会创建一个CompositionLayer,它继承自BaseLayer,而在它内部会创建各个类型的图层,如下:

 static BaseLayer forModel(
      Layer layerModel, LottieDrawable drawable, LottieComposition composition) {
    switch (layerModel.getLayerType()) {
      case SHAPE:
        return new ShapeLayer(drawable, layerModel);
      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);
      case NULL:
        return new NullLayer(drawable, layerModel);
      case TEXT:
        return new TextLayer(drawable, layerModel);

    }
  }

这些不同类型的Layer也是继承自BaseLayer,所以这里创建图层的过程,就是把之前Json文件解析的树形数据结构,转换成绘制图层的树形结构。CompositionLayer和其他各个BaseLayer的关系,就类似于View和ViewGroup之间的关系一样。

3.2 关键帧动画

同时在创建各个BaseLayer的时候,它们内部还会继续创建对应的关键帧动画类,我们重点看一下TransformKeyframeAnimation,也就是上面Json文件中对应的“ks”数据:
TransformKeyframeAnimation.java

  @NonNull private BaseKeyframeAnimation<PointF, PointF> anchorPoint;
  @NonNull private BaseKeyframeAnimation<?, PointF> position;
  @NonNull private BaseKeyframeAnimation<ScaleXY, ScaleXY> scale;
  @NonNull private BaseKeyframeAnimation<Float, Float> rotation;
  @NonNull private BaseKeyframeAnimation<Integer, Integer> opacity;
  @Nullable private FloatKeyframeAnimation skew;
  @Nullable private FloatKeyframeAnimation skewAngle;

可以看到内部定义了很多关键帧动画类,它们决定了该Layer在某个时刻应该绘制怎样的内容。
关系图如下:
在这里插入图片描述

4.动画绘制过程

4.1LottieValueAnimator计算progress

准备好了资源,创建好了图层,在LottieAnimationView 调用了playAnimation()方法之后,就开始播放动画了。同样,也会调用到LottieDrawable的playAnimation()方法:
LottieDrawable.java

public void playAnimation() {
	//调用animator去播放动画
     animator.playAnimation();   
  }

LottieDrawable会调用它内部的LottieValueAnimator去计算动画播放的具体帧:
LottieValueAnimator.java

 @MainThread
  public void playAnimation() {  
  	//这里会通知更新动画
    setFrame((int) (isReversed() ? getMaxFrame() : getMinFrame()));
    //这里注册Choreographer,以便同步vsync信号,并在doframe方法中继续更新动画
    postFrameCallback();
  }

setFrame会调用到父类里的notifyUpdate()方法,然后回调通知到LottieDrawable里面的监听接口:
LottieDrawable.java

private final ValueAnimator.AnimatorUpdateListener  progressUpdateListener = new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
      if (compositionLayer != null) {
        compositionLayer.setProgress(animator.getAnimatedValueAbsolute());
      }
    }
  };

从这里可以看出,LottieValueAnimator的作用主要是根据屏幕的刷新信号,来计算应该播放的帧序列,也就是播放的具体进度。当存在卡断的情况下,Choreographer的doframe方法会延迟通知,就会导致LottieValueAnimator存在跳帧的情况。

4.2关键帧动画设置progress

接着看绘制流程,播放进度会给到CompositionLayer,而CompositionLayer又会把播放进度再分发给具体的每一个Layer,如下:
CompositionLayer.java

 setProgress(@FloatRange(from = 0f, to = 1f) float progress) {
	for (int i = layers.size() - 1; i >= 0; i--) {
      layers.get(i).setProgress(progress);
    }
}

每个Layer都是继承自BaseLayer,在BaseLayer中又会把进度给到内部的各个关键帧动画,最重要的是TransformKeyframeAnimation,它内部包含各个维度的变化序列:
TransformKeyframeAnimation.java

setProgress(float progress) {
if (opacity != null) {
      opacity.setProgress(progress);  //透明度
    }
    if (anchorPoint != null) {
      anchorPoint.setProgress(progress); //锚点
    }
    if (position != null) {
      position.setProgress(progress);  //位置
    }
    if (scale != null) {
      scale.setProgress(progress);  //缩放
    }
    if (rotation != null) {
      rotation.setProgress(progress); //旋转
    }
    if (skewAngle != null) {
      skewAngle.setProgress(progress); //斜角
}

4.3Layer请求刷新

在设置完progress之后,各个Layer就会触发onValueChanged()方法,并请求刷新自己:
BaseLayer.java

public void onValueChanged() {
    invalidateSelf();
  }

private void invalidateSelf() {
    lottieDrawable.invalidateSelf();
  }

接着会调用到LottieDrawable的绘制:
LottieDrawable.java

public void invalidateSelf() {
    final Callback callback = getCallback();
    if (callback != null) {
      callback.invalidateDrawable(this);
    }
  }

4.4Layer绘制

当自定义的Drawable需要重绘的时候,就需要调用invalidateDrawable方法,请求绘制自己,然后会通过ImageView的onDraw方法里调用到Drawable的draw方法:
Drawable.java

 public void draw(@NonNull Canvas canvas) {
        drawInternal(canvas)}

接下来就会调用到CompositionLayer中的draw方法,在CompositionLayer中又会把实际绘制的工作交给各个Layer去完成:
CompositionLayer.java

 draw(Canvas canvas, Matrix parentMatrix, int parentAlpha) {
	drawLayer(canvas, matrix, alpha);
}

drawLayer(Canvas canvas, Matrix parentMatrix, int parentAlpha) {
	for (int i = layers.size() - 1; i >= 0; i--) {
		  BaseLayer layer = layers.get(i);
          layer.draw(canvas, parentMatrix, childAlpha);
    	}
}

不同的Layer层,对draw的实现也不一样,比如ImageLayer是drawBitmap,绘制前会在父类调关键帧动画计算出来的参数,对Bitmap做位移、旋转、透明度变化等等变化操作,最后才是使用Canvas做绘制:
BaseLayer.java:

public void draw(Canvas canvas, Matrix parentMatrix, int parentAlpha) {
	 matrix.preConcat(transform.getMatrix()); //通过关键帧动画做变化操作
	 drawLayer(canvas, matrix, alpha); //调用子类的绘制
}

ImageLayer.java

 @Override public void drawLayer(@NonNull Canvas canvas, Matrix parentMatrix, int parentAlpha) {
	Bitmap bitmap = getBitmap(); //读取图片内容
	canvas.drawBitmap(bitmap, src, dst , paint);//绘制图片
}

至此,一帧动画的绘制就完成了,后面会由LottieValueAnimator的doFrame回调不停地重复上面的步骤,实现连续的动画效果。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2268238.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

手机发烫怎么解决?

在当今这个智能手机不离手的时代&#xff0c;手机发烫成了不少人头疼的问题。手机发烫不仅影响使用手感&#xff0c;长期过热还可能损害手机硬件、缩短电池寿命&#xff0c;甚至引发安全隐患。不过别担心&#xff0c;下面这些方法能帮你有效给手机 “降温”。 一、使用习惯方面…

win10、win11-鼠标右键还原、暂停更新

系统优化 win 10jihuo win 11jihuo鼠标右键还原暂停更新 update 2024.12.28win 10 jihuo winx&#xff0c;打开powershell管理员&#xff0c;输入以下命令,选择1并等待 irm https://get.activated.win | iex参考&#xff1a;https://www.bilibili.com/video/BV1TN411M72J/?sp…

Android 学习小记1

目录 先介绍一下Android Studio 看看常见的模板 1. No Activity 2. Empty Activity 3. Gemini API Starter 4. Basic View Activity 5. Bottom Navigation Activity 6. Empty Views Activity 7. Navigation Drawer Views Activity 8. Responsive Views Activity 9. G…

uniapp使用ucharts组件

1.ucharts准备 有两种使用方式&#xff1a;一种是在uni的插件市场下载&#xff08;组件化开发&#xff09;。一种是手动引入ucharts包。官方都封装好组件了&#xff0c;我们不用岂不是浪费。 直接去dcloud插件市场&#xff08;DCloud 插件市场&#xff09;找&#xff0c;第一…

mybatis-plus自动填充时间的配置类实现

mybatis-plus自动填充时间的配置类实现 在实际操作过程中&#xff0c;我们并不希望创建时间、修改时间这些来手动进行&#xff0c;而是希望通过自动化来完成&#xff0c;而mybatis-plus则也提供了自动填充功能来实现这一操作&#xff0c;接下来&#xff0c;就来了解一下mybatis…

一、Hadoop概述

文章目录 一、Hadoop是什么二、Hadoop发展历史三、Hadoop三大发行版本1. Apache Hadoop2. Cloudera Hadoop3. Hortonworks Hadoop 四、Hadoop优势1. 高可靠性2. 高扩展性3. 高效性4. 高容错性 五、Hadoop 组成1. Hadoop1.x、2.x、3.x区别2. HDFS 架构概述3. YARN 架构概述4. Ma…

深度学习笔记(9)——神经网络和反向传播

神经网络和反向传播 神经网络架构&#xff1a; 更多的神经元,更大的模型容量,使用更强的正则化进行约束。 神经网络的分层计算 f W 2 m a x ( 0 , W 1 x b 1 ) b 2 fW_2max(0,W_1xb_1)b_2 fW2​max(0,W1​xb1​)b2​,其中max函数体现了非线性,如果想要加深网络的层次,必须…

大模型辅助测试的正确打开方式?

测试的基本目的之一&#xff0c;是对被测对象进行质量评估。换言之&#xff0c;是要提供关于被测对象质量的“确定性”。因此&#xff0c;我们很忌讳在测试设计中引入“不确定性”&#xff0c;比如采用不可靠的测试工具、自动化测试代码逻辑复杂易错、测试选择假设过于主观等等…

ipad如何直连主机(Moonlight Sunshine)

Windows 被连接主机&#xff08;Windows&#xff09; 要使用的话需要固定ip&#xff0c;不然ip会换来换去&#xff0c;固定ip方法本人博客有记载Github下载Sunshine Sunshine下载地址除了安装路径需要改一下&#xff0c;其他一路点安装完成后会打开Sunshine的Web UI&#xff…

sentinel集成nacos启动报[check-update] get changed dataId error, code: 403错误排查及解决

整合nacos报403错误 因为平台写的一个限流代码逻辑有问题&#xff0c;所以准备使用sentinel来限流。平台依赖里面已经引入了&#xff0c;之前也测试过&#xff0c;把sentinel关于nacos的配置加上后&#xff0c;启动一直输出403错误 [fixed-10.0.20.188_8848-test] [check-upda…

紫光同创-盘古200pro+开发板

本原创文章由深圳市小眼睛科技有限公司创作&#xff0c;版权归本公司所有&#xff0c;如需转载&#xff0c;需授权并注明出处&#xff08;www.meyesemi.com) 一、开发系统介绍 开发系统概述 MES2L676-200HP 开发板采用紫光同创 logos2 系列 FPGA&#xff0c;型号&#xff1a;…

Vue开发环境搭建上篇:安装NVM和NPM(cpnm、pnpm)

文章目录 引言I 安装NVM1.1 Windows系统安装NVM,实现Node.js多版本管理1.2 配置下载镜像1.3 NVM常用操作命令II NPM永久使用淘宝源安装 cnpm安装pnpm【推荐】see also: vscode常用插件引言 淘宝镜像:http://npm.taobao.org 和 http://registry.npm.taobao.org 已在 2022.06.3…

【AI大模型】探索GPT模型的奥秘:引领自然语言处理的新纪元

目录 &#x1f354; GPT介绍 &#x1f354; GPT的架构 &#x1f354; GPT训练过程 3.1 无监督的预训练语言模型 3.2 有监督的下游任务fine-tunning &#x1f354; 小结 学习目标 了解什么是GPT.掌握GPT的架构.掌握GPT的预训练任务. &#x1f354; GPT介绍 GPT是OpenAI公…

正则表达式(三剑客之sed)

1.sed工具的使用 1.1 sed工具 1&#xff09;命令格式&#xff1a;sed -n ‘n’ p filename 1.2 打印某行 1&#xff09;打印第二行 [rootlocalhost ~]# sed -n 2p /etc/passwd 2&#xff09;第二行重复打印 [rootlocalhost ~]# sed 2p /etc/passwd 3&#xff09;所有行全部…

细说STM32F407单片机IIC总线基础知识

目录 一、 I2C总线结构 1、I2C总线的特点 2、I2C总线通信协议 3、 STM32F407的I2C接口 二、 I2C的HAL驱动程序 1、 I2C接口的初始化 2、阻塞式数据传输 &#xff08;1&#xff09;函数HAL_I2C_IsDeviceReady() &#xff08;2&#xff09;主设备发送和接收数据 &#…

Android笔试面试题AI答之Android基础(7)

Android入门请看《Android应用开发项目式教程》&#xff0c;视频、源码、答疑&#xff0c;手把手教 文章目录 1.Android开发如何提高App的兼容性&#xff1f;**1. 支持多版本 Android 系统****2. 适配不同屏幕尺寸和分辨率****3. 处理不同硬件配置****4. 适配不同语言和地区**…

《机器学习》线性回归模型实现

目录 一、一元线性回归模型 1、数据 2、代码 3、结果 二、多元线性回归模型 1、数据 2、代码 3、结果 一、一元线性回归模型 1、数据 2、代码 # 导入所需的库 import pandas as pd # 用于数据处理和分析 from matplotlib import pyplot as plt # 用于数据可视化 fr…

基于DIODES AP43781+PI3USB31531+PI3DPX1207C的USB-C PD Video 之全功能显示器连接端口方案

随着USB-C连接器和PD功能的出现&#xff0c;新一代USB-C PD PC显示器可以用作个人和专业PC工作环境的电源和数据集线器。 虽然USB-C PD显示器是唯一插入墙壁插座的交流电源输入设备&#xff0c;但它可以作为数据UFP&#xff08;上游接口&#xff09;连接到连接到TCD&#xff0…

QWidget应用封装为qt插件,供其他qt应用调用

在之前的文章中,有介绍通过QProcess的方式启动QWidget应用,然后将其窗口嵌入到其他的qt应用中,作为子窗口使用.这篇文章主要介绍qt插件的方式将QWidget应用的窗口封装为插件,然后作为其他Qt应用中的子窗口使用. 插件优点: 与主程序为同一个进程,免去了进程间繁琐的通信方式,…

关于 覆铜与导线之间间距较小需要增加间距 的解决方法

若该文为原创文章&#xff0c;转载请注明原文出处 本文章博客地址&#xff1a;https://hpzwl.blog.csdn.net/article/details/144776995 长沙红胖子Qt&#xff08;长沙创微智科&#xff09;博文大全&#xff1a;开发技术集合&#xff08;包含Qt实用技术、树莓派、三维、OpenCV…