Android 之 Lottie 实现炫酷动画背后的原理
这是程序亦非猿的第 78 期分享。
作者 l 程序亦非猿
来源 l 程序亦非猿(ID:chengxuyifeiyuan)
转载请联系授权(微信ID:ONE-D-PIECE)
0. 前言
自我在内网发布了一篇关于 Lottie 的重点原理分析的文章之后,就不断有同事来找我询问关于 Lottie 的各种东西,最近又有同事来问,就想着可能对大家也会有所帮助,就稍作处理后分享出来。
注意一下哈,这文章写于两年前,基本版本 2.0.0-beta3,现在已经不止2.0.0啦,不过我看过最新版本,主要的类没有什么差别,依然有阅读价值。
可以感受一下我两年前的实力。:-D
走起!
1. Lottie 是什么?
Render After Effects animations natively on Android and iOS
Lottie 是 airbnb 发布的库,它可以将 AE 制作的动画 在 Android&iOS上以 native 代码渲染出来,目前还支持了 RN 平台。
来看几个官方给出的动画效果案例:
就拿第一个动画 Jump-through 举例,如果让你来实现它,你能在多少时间内完成?三天?一个礼拜?google 的 Nick Butcher 刚好有一篇文章写 Jump-through 的动画实现,讲述了整个实现过程,从文章里可以看出实现这个动画并不容易,有兴趣的可以看看 Animation: Jump-through。
但是现在有了 Lottie,只要设计师用 AE 设计动画,利用 bodymovin 导出 ,导入到 assets, 再写下面那么点代码就可以实现了!
LottieAnimationView animationView = (LottieAnimationView) findViewById(R.id.animation_view);animationView.setAnimation("PinJump.json");animationView.loop(true);animationView.playAnimation();"PinJump.json");animationView.loop(true);animationView.playAnimation();
不用写自定义 View!不用画 Path!不用去计算这个点那个点!
是不是超级方便?!!!
这么方便的背后,原理是什么呢?
2. TL;DR
bodymovin 将 AE 动画导出为 ,该 描述了该动画,而 lottie-android 的原理就是将 描述的动画用 native code 翻译出来, 其核心原理是 canvas 绘制。对,lottie 的动画是靠纯 canvas 画出来的!!!动起来则是靠的属性动画。(ValueAnimator.ofFloat(0f, 1f);
)
说具体点就是 lottie 随属性动画修改 progress,每一个 Layer 根据当前的 progress 绘制所对应的帧内容,progress 值变为1,动画结束。(有点类似于帧动画)
当然说说简单,lottie其实做了非常多的工作,后续将详细解析 lottie-android 的实现原理。
3. Lottie 关键类介绍
Lottie 提供了一个 LottieAnimationView 给用户使用,而实际 Lottie 的核心是 LottieDrawable,它承载了所有的绘制工作,LottieAnimationView则是对LottieDrawable 的封装,再附加了一些例如 解析 的功能。
它们的关系:
4. 文件的属性含义
bodymovin 导出的 包含了动画的一切信息, 动画的关键帧信息,动画该怎么做,做什么都包含在 里,Lottie 里所有的 Model 的数据都来自于这个 ( 该 对应的 Model 是LottieComposition),所以要理解 Lottie 的原理,理解 的属性是第一步。
属性非常多,而且不同的动画的 也有很大的差别,所以这里只讲解部分重要的属性。
4.1 文件最外部的结构
的最外层长这样:
{ "v": "4.5.9", "fr": 15, "ip": 0, "op": 75, "w": 500, "h": 500, "ddd": 0, "assets":[] "layers":[] }"v": "4.5.9", "fr": 15, "ip": 0, "op": 75, "w": 500, "h": 500, "ddd": 0, "assets":[] "layers":[] }
属性的含义:
属性 | 含义 |
---|---|
v | bodymovin的版本 |
fr | 帧率 |
ip | 起始关键帧 |
op | 结束关键帧 |
w | 动画宽度 |
h | 动画高度 |
assets | 动画图片资源信息 |
layers | 动画图层信息 |
从这里可以获取 设计的动画的宽高,帧相关的信息,动画所需要的图片资源的信息以及图层信息。
a) assets
图片资源信息, 相关类 LottieImageAsset、 ImageAssetBitmapManager。
"assets": [ { "id": "image_0", "w": 500, "h": 500, "u": "images/", "p": "voice_thinking_image_0.png" } ] { "id": "image_0", "w": 500, "h": 500, "u": "images/", "p": "voice_thinking_image_0.png" } ]
属性的含义:
属性 | 含义 |
---|---|
id | 图片 id |
w | 图片宽度 |
h | 图片高度 |
p | 图片名称 |
b) layers
图层信息,相关类:Layer、BaseLayer以及 BaseLayer 的实现类。
{ "ddd": 0, "ind": 0, "ty": 2, "nm": "btnSlice.png", "cl": "png", "refId": "image_0", "ks": {....}, "ao": 0, "ip": 0, "op": 90.0000036657751, "st": 0, "bm": 0, "sr": 1}"ddd": 0, "ind": 0, "ty": 2, "nm": "btnSlice.png", "cl": "png", "refId": "image_0", "ks": {....}, "ao": 0, "ip": 0, "op": 90.0000036657751, "st": 0, "bm": 0, "sr": 1}
属性的含义:
属性 | 含义 |
---|---|
nm | layerName 图层信息 |
refId | 引用的资源 id,如果是 ImageLayer 那么就是图片的id |
ty | layertype 图层类型 |
ip | inFrame 该图层起始关键帧 |
op | outFrame 该图层结束关键帧 |
st | startFrame 开始 |
ind | layer id 图层 id |
Layer 可以理解为图层,跟 PS 等工具的概念相同,每个 Layer 负责绘制自己的内容。
在 Lottie 里拥有不同的 Layer,目前有 PreComp,Solid,Image,Null,Shape,Text ,各个 Layer 拥有的属性各不相同,这里只指出共有的属性。
下图为 Layer 相关类图:
5. Lottie 的适配原理
在开始使用 Lottie 的时候,我们团队设计动画走的跟设计图片一样的路子,想设计2x,3x 多份资源进行适配。但是,通过阅读源码发现其实 Lottie本身在 Android 平台已经做了适配工作,而且适配原理很简单,解析 时,从 读取宽高之后 会再乘以手机的密度。再在使用的时候判断适配后的宽高是否超过屏幕的宽高,如果超过则再进行缩放。以此保障 Lottie 在 Android 平台的显示效果。
核心代码如下:
//LottieComposition.fromSync float scale = res.getDisplayMetrics().density; int width = .optInt("w", -1); int height = .optInt("h", -1); if (width != -1 && height != -1) { int scaledWidth = (int) (width * scale); int scaledHeight = (int) (height * scale); bounds = new Rect(0, 0, scaledWidth, scaledHeight); } //LottieAnimationView.setComposition int screenWidth = Utils.getScreenWidth(getContext()); int screenHeight = Utils.getScreenHeight(getContext()); int compWidth = composition.getBounds().width(); int compHeight = composition.getBounds().height(); if (compWidth > screenWidth || compHeight > screenHeight) { float xScale = screenWidth / (float) compWidth; float yScale = screenHeight / (float) compHeight; setScale(Math.min(xScale, yScale)); Log.w(L.TAG, String.format( "Composition larger than the screen %dx%d vs %dx%d. Scaling down.", compWidth, compHeight, screenWidth, screenHeight)); } float scale = res.getDisplayMetrics().density; int width = .optInt("w", -1); int height = .optInt("h", -1);
if (width != -1 && height != -1) { int scaledWidth = (int) (width * scale); int scaledHeight = (int) (height * scale); bounds = new Rect(0, 0, scaledWidth, scaledHeight); }
//LottieAnimationView.setComposition
int screenWidth = Utils.getScreenWidth(getContext()); int screenHeight = Utils.getScreenHeight(getContext()); int compWidth = composition.getBounds().width(); int compHeight = composition.getBounds().height(); if (compWidth > screenWidth || compHeight > screenHeight) { float xScale = screenWidth / (float) compWidth; float yScale = screenHeight / (float) compHeight; setScale(Math.min(xScale, yScale)); Log.w(L.TAG, String.format( "Composition larger than the screen %dx%d vs %dx%d. Scaling down.", compWidth, compHeight, screenWidth, screenHeight)); }
这里值得一提的是,设计师在设计动画时要注意,需要设计的是1X 的动画,而不是2X or 3X or 4X。
目前手淘用的方案是 按4X 来设计(1X看不清元素),然后再缩小为1X,图片资源是4X。
6. Lottie的绘制原理
LottieAnimationView 本身是个 ImageView,所以它的绘制流程跟 ImageView 一样,所有的绘制其实在 LottieDrawable 控制的。
接下去看看它的源码实现:
// LottieDrawable@Override public void draw(@NonNull Canvas canvas) { if (compositionLayer == null) { return; } float scale = this.scale; if (compositionLayer.hasMatte()) { scale = Math.min(this.scale, getMaxScale(canvas)); } matrix.reset(); matrix.preScale(scale, scale); compositionLayer.draw(canvas, matrix, alpha); }@Override public void draw(@NonNull Canvas canvas) { if (compositionLayer == null) { return; } float scale = this.scale; if (compositionLayer.hasMatte()) { scale = Math.min(this.scale, getMaxScale(canvas)); }
matrix.reset(); matrix.preScale(scale, scale); compositionLayer.draw(canvas, matrix, alpha); }
可以看到在 draw
方法里调用了 compositionLayer.draw
方法,由于 CompositionLayer 继承了 BaseLayer,所以需要跟进 BaseLayer ,继续跟踪:
// BaseLayer.draw @Override public void draw(Canvas canvas, Matrix parentMatrix, int parentAlpha) { if (!visible) { return; } buildParentLayerListIfNeeded(); //矩阵变换处理 //.... if (!hasMatteOnThisLayer() && !hasMasksOnThisLayer()) { matrix.preConcat(transform.getMatrix()); //绘制 layer drawLayer(canvas, matrix, alpha); return; } //draw matteLayer& maskLayer //... canvas.restore(); } @Override public void draw(Canvas canvas, Matrix parentMatrix, int parentAlpha) { if (!visible) { return; } buildParentLayerListIfNeeded(); //矩阵变换处理 //.... if (!hasMatteOnThisLayer() && !hasMasksOnThisLayer()) { matrix.preConcat(transform.getMatrix()); //绘制 layer drawLayer(canvas, matrix, alpha); return; } //draw matteLayer& maskLayer //... canvas.restore(); }
删除了多余代码,只保留核心代码,可以看到 draw 方法里调用了抽象方法 drawLayer
,在这里的实现在 CompositionLayer ,一起来看看:
@Override void drawLayer(Canvas canvas, Matrix parentMatrix, int parentAlpha) { //... for (int i = layers.size() - 1; i >= 0 ; i--) { boolean nonEmptyClip = true; if (!newClipRect.isEmpty()) { nonEmptyClip = canvas.clipRect(newClipRect); } if (nonEmptyClip) { layers.get(i).draw(canvas, parentMatrix, parentAlpha); } } //... }void drawLayer(Canvas canvas, Matrix parentMatrix, int parentAlpha) { //... for (int i = layers.size() - 1; i >= 0 ; i--) { boolean nonEmptyClip = true; if (!newClipRect.isEmpty()) { nonEmptyClip = canvas.clipRect(newClipRect); } if (nonEmptyClip) { layers.get(i).draw(canvas, parentMatrix, parentAlpha); } } //... }
上面的代码中的 layers 是该动画所包含的层,在 CompositionLayer 的 drawLayer 方法里遍历了动画所有的层,并调用layers 的 draw 方法,这样就完成了所有的绘制。
7. Lottie的动画原理
上一小节讲了 Lottie 的绘制原理,但是 Lottie 是用来做动画的,光理解它的绘制原理是不够的,对于动画,更重要的是它怎么动起来的。
接下来就分析一下 Lottie 的动画原理。
Lottie 动画起始于 LottieAnimationView.playAnimation
,接着调用 LottieDrawable 的同名方法,与绘制相同,动画也是 LottieDrawable 控制的,来看看代码:
// animator 的申明private final ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f); private void playAnimation(boolean setStartTime) { if (compositionLayer == null) { playAnimationWhenCompositionAdded = true; reverseAnimationWhenCompositionAdded = false; return; } if (setStartTime) { animator.setCurrentPlayTime((long) (progress * animator.getDuration())); } animator.start(); }private final ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
private void playAnimation(boolean setStartTime) { if (compositionLayer == null) { playAnimationWhenCompositionAdded = true; reverseAnimationWhenCompositionAdded = false; return; } if (setStartTime) { animator.setCurrentPlayTime((long) (progress * animator.getDuration())); } animator.start(); }
playAnimation 方法其实只是开启了一个属性动画,那么后续动画是怎么动起来的呢?这就必须要看动画的监听了:
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { if (systemAnimationsAreDisabled) { animator.cancel(); setProgress(1f); } else { setProgress((float) animation.getAnimatedValue()); } } }); @Override public void onAnimationUpdate(ValueAnimator animation) { if (systemAnimationsAreDisabled) { animator.cancel(); setProgress(1f); } else { setProgress((float) animation.getAnimatedValue()); } } });
在 animator 进行的过程中回去调用 setProgress
方法,下面跟踪一下代码:
//LottieDrawable public void setProgress(@FloatRange(from = 0f, to = 1f) float progress) { this.progress = progress; if (compositionLayer != null) { compositionLayer.setProgress(progress); } } //CompositionLayer @Override public void setProgress(@FloatRange(from = 0f, to = 1f) float progress) { super.setProgress(progress); progress -= layerModel.getStartProgress(); for (int i = layers.size() - 1; i >= 0; i--) { layers.get(i).setProgress(progress); } }//BaseLayer void setProgress(@FloatRange(from = 0f, to = 1f) float progress) { //... for (int i = 0; i < animations.size(); i++) { animations.get(i).setProgress(progress); } }//BaseKeyframeAnimation void setProgress(@FloatRange(from = 0f, to = 1f) float progress) { if (progress < getStartDelayProgress()) { progress = 0f; } else if (progress > getEndProgress()) { progress = 1f; } if (progress == this.progress) { return; } this.progress = progress; for (int i = 0; i < listeners.size(); i++) { listeners.get(i).onValueChanged(); } }//BaseLayer @Override public void onValueChanged() { invalidateSelf(); }//BaseLayer private void invalidateSelf() { lottieDrawable.invalidateSelf(); } public void setProgress(@FloatRange(from = 0f, to = 1f) float progress) { this.progress = progress; if (compositionLayer != null) { compositionLayer.setProgress(progress); } }
//CompositionLayer @Override public void setProgress(@FloatRange(from = 0f, to = 1f) float progress) { super.setProgress(progress); progress -= layerModel.getStartProgress(); for (int i = layers.size() - 1; i >= 0; i--) { layers.get(i).setProgress(progress); } }
//BaseLayer void setProgress(@FloatRange(from = 0f, to = 1f) float progress) { //... for (int i = 0; i < animations.size(); i++) { animations.get(i).setProgress(progress); } }
//BaseKeyframeAnimation void setProgress(@FloatRange(from = 0f, to = 1f) float progress) { if (progress < getStartDelayProgress()) { progress = 0f; } else if (progress > getEndProgress()) { progress = 1f; }
if (progress == this.progress) { return; } this.progress = progress;
for (int i = 0; i < listeners.size(); i++) { listeners.get(i).onValueChanged(); } }
//BaseLayer @Override public void onValueChanged() { invalidateSelf(); }
//BaseLayer private void invalidateSelf() { lottieDrawable.invalidateSelf(); }
上面列出了后续流程的主要代码,可以看到,setProgress 的最后触发了每个 layer 的 invalidateSelf,这都会让 lottieDrawable 重新绘制,然后重走一遍绘制流程,这样随着 animator 动画的进行,lottieDrawable 不断的绘制,就展现出了一个完整的动画。
PS: 动画过程中的一些变量比如 scale,都是由BaseKeyframeAnimation控制,但这个偏于细节,这里就不讲了。
动画原理流程稍微有点长,也稍微有些复杂,我绘制了一张图梳理了一下整体的流程,方便理解:
BaseKeyframeAnimation 类图:
8. 总结
个人觉得 Lottie 是个非常非常棒的项目,甚至可以说是个伟大的项目。
Lottie 极大的缩减了动画的开发成本,给 APP 增加非常强力的动画能力,不需要各个端再自己去实现,而且目前 Lottie 已经支持了非常多的 AE 动画效果,通过 Lottie 可以轻松实现很多酷炫的效果,所以现在做动效考验的是设计同学的设计能力了,哈哈。
本文只针对重点原理进行分析,欢迎留言讨论交流。
9. 资料
https://github.com/airbnb/lottie-android
https://medium.com/airbnb-engineering/introducing-lottie-4ff4a0afac0e
tie: https://airbnb.design/lottie/
https://github.com/bodymovin/bodymovin
https://medium.com/google-developers/animation-jump-through-861f4f5b3de4
Android 之 Lottie 实现炫酷动画背后的原理相关推荐
- android lottie字体json,从json文件到炫酷动画-Lottie实现思路和源码分析
从json文件到炫酷动画-Lottie实现思路和源码分析,Lottie是最近Airbnb开源的动画项目,支持Android.iOS.ReactNaitve三个平台,本文分析主要Lottie把json文 ...
- Android 炫酷动画APP,21 款炫酷动画开源框架,照亮你的APP
原标题:21 款炫酷动画开源框架,照亮你的APP 2017年安卓巴士全球开发者论坛-上海站 前言 最近对应用的UI视觉效果突然来了兴致,所以找了一些合适开源控件,这样更加省时,再此分享给大家,希望能对 ...
- html舞动特效,7款纯CSS3实现的炫酷动画应用
原标题:7款纯CSS3实现的炫酷动画应用 HTML5确实非常强大,我们之前也分享过很多基于HTML5 Canvas的动画特效.但是你是否知道我们可以利用纯CSS制作一些很酷的动画效果?对,CSS3可以 ...
- canvas实现阿里云云栖大会炫酷动画效果
效果展示: 源码展示: <!doctype html> <html> <head><meta charset="utf-8">< ...
- 高级UI- 属性动画炫酷动画案例+淘宝动画+源码解析+策略模式使用
文章目录 属性动画源码: 案例1 案例2 最终效果 思路 : 代码 TODU 案例3 加载的炫酷动画. 以及策略模式的使用 效果图 思路 动画分析 先实现小圆的旋转动画, 开始在ondraw里面写动画 ...
- 【Qt炫酷动画】专栏导航目录
历时小半年,经过总结积累,详细剖析了Qt框架如何制作动画. 通过本专栏学习,可以学会如何diy窗体动画.控件动画 Qt动画 [Qt炫酷动画]专栏导航目录 [Qt炫酷动画]0.动画类简介 [Qt炫酷动画 ...
- 炫酷html动画,纯CSS3一个炫酷动画
纯CSS3一个炫酷动画 通过下边的代码可以看到这个例子的html代码还是很简单的,中间类似图标的部分是通过给两个 围绕盒子爬的虫子通过给 HTML代码 CSS代码 body{ margin: 0; b ...
- 从 json 文件到炫酷动画 - Lottie 实现思路和源码分析
Lottie是最近Airbnb开源的动画项目,支持Android.iOS.ReactNaitve三个平台,相关背景介绍可以参考之前的文章Airbnb开源炫酷动画库Lottie(译)-看看Airbnb的 ...
- android炫酷动画代码,Android高级UI特效仿直播点赞动画效果
Android高级UI特效仿直播点赞动画效果 发布时间:2020-10-02 16:06:18 来源:脚本之家 阅读:117 作者:mrr 本文给大家分享高级UI特效仿直播点赞效果-一个优美炫酷的点赞 ...
最新文章
- 独家 | Michael I.Jordan:大数据时代下的安全实时决策堆栈与增强学习(视频+精华笔记)
- [Jobdu] 题目1214:丑数
- 在Matlab中使用mex函数进行C/C++混合编程
- 『计算机视觉』YOLO系列总结
- 数据结构-编程实现一个单链表的测长
- 帝国扩展变量是哪个php,帝国CMS后台系统设置里面的扩展变量是干什么的?
- 【9018:1956】线段树1
- python 系统架构_Python之优化系统架构的方案
- android studio课程管理系统,8 个最优秀的 Android Studio 插件
- Linux系统编程8-18总结项目:完成一个简单的自己的shell
- 手机MODEM开发(27)---VOLTE SIP 信令流程图
- 优秀网页案例教你如何排好内容页
- z17刷miui_努比亚Z17刷机包
- 计算机网络属于什么结构,计算机网络体系结构是一种什么结构
- 关于nginx日志的HTTP 499状态码
- 数学建模(4):动态规划
- Hibernate学习总结(一)——hibernate的简单配置使用
- 修复Mac中Mounty无法显示的文件
- python计算日期间的差值,python 计算时间、日期差值类
- 怎么做硬件产品的需求分析?
热门文章
- VMware如何彻底删除干净?
- WPF 3D MeshGeometry3D类的Positions和TriangleIndices属性研究
- Shell程序编写猜数字的小游戏
- 除了吃鸡还能玩什么?2018值得你期待的手游大排行!必看必看!!
- wordpress主题_55个免费和高质量的WordPress主题
- ContinueWith() (Task类的延续性任务)实例
- 如何使用Java写“脚本”(单个Java文件如何像脚本一样使用运行)
- 阿里宣布重大组织调整,张建锋从阿里CTO卸任
- 巴菲特说的七段话,简短精辟
- C++名字转换为数字问题 代码