Lottie 是什么
Lottie 是 Airbnb 开源的一个动画渲染库,它可以解析 Adobe After Effects 使用 Bodymovin 插件把做的动画导出的 json 文件,并在 Android/iOS、React Native 和 web 端实现相同的动画效果。
简单来说,设计师可以使用 After Effects 制作动画,经由 Lottie 便可以很简单的在移动端/ web 端渲染,不再需要工程师进行大量的手动实现。俗话说“一图胜千言”,如下动画便是由 Lottie 渲染。
为什么要用 Lottie
1、对设计:做的动画素材想一次设计,各个平台能直接运行,效果可由自己灵活掌控,减少于开发的调整沟通。
2、对开发:针对设计越来越复杂的动画,比如上图所示,可以解决的方案有:
- png 序列帧:它需要大量的图片素材支持,在进行动画播放时会占用很多内存。
- 视频:没有交互,只能等视频播放完毕才能交互。
需要一种更好的解决方案,能够以比较低的成本,来实现这些复杂动画。
3、对运营:动画实现可配置,不同时间想有不同的动画效果,而目前支持的动画都是需要跟版本的。
使用 Lottie ,这一切问题就迎刃而解了。Lottie 只需要解析导出的 JSON 文件及所需要的图片,就能在各个平台上实现相同的动画效果,它实现成本低,上线后只需要动态替换对应的 JSON 文件就能实现可配置、可运营。
怎么使用 Lottie
Lottie 在不同平台上分别实现了类库:
- Android :lottie-android,最低支持 API 14;
- iOS :lottie-ios,最低支持 IOS 7;
- React Native:lottie-react-native;
- Web 端:https://cdnjs.com/libraries/bodymovin。
得益于 Lottie 封装的很好, Android 侧使用也是非常简单的,使用如下所示:
1、加载资源
Adobe After Effects 导出的 JSON 文件,可以放在 /assets/,/raw/,或自定义的目录中,如下方式加载:
1 | //Raw 文件加载 |
2、设置数据对象
创建一个 OnCompositionLoadedListener,放入加载的监听中,等待加载完成后,会返回一个数据对象 LottieComposition,接下来只要为 LottieAnimationView 设置这个数据对象即可。
1 | OnCompositionLoadedListener compositionListener = new OnCompositionLoadedListener() { |
3、播放动画
接下来调用 LottieAnimationView.playAnimation()
就可以播放动画了。
1 | mLottieAnim.playAnimation(); |
4、设置图片/字体代理
图片默认认为是在 /assets/
目录的,如果图片放在这个根目录,其子目录只需要调用mLottieAnim.setImageAssetsFolder();
就可以正确读取到,如果是自定义的图片,则可以设置个图片代理,如下所示:
1 | mLottieAnim.setImageAssetDelegate(new ImageAssetDelegate() { |
asset.getFileName
是 JSON 中所需要图片的名称,这里可以直接拼接一个自定义路径,decode 获取 bitmap 返回。
如果有涉及到字体,可以和图片一样,设置字体的 delegate,自定义获取。
Lottie 动画实现原理
Lottie 先将动画 JSON 文件转换为 LottieComposition 数据对象,继承 ImageView 的 LottieAnimationView 将数据对象 LottieComposition 和渲染能力委托给 LottieDrawable 处理,在 LottieDrawable 中会将数据对象 LottieComposition 组建为具有 draw 能力的 BaseLayer,并在 LottieAnimationView 需要绘制时,调用自己和各个层级 BaseLayer 的渲染,从而达到动画效果。
从 Json 到 LottieCompisition 数据对象
Lottie 的第一步就是去解析 AE 动画文件导出的 Json ,转变为 Java 实体对象。以上便是解析过程中涉及的类图:
同步/异步加载 Json 数据
在 LottieComposition.Factory 工厂类中,利用简单工厂模式,支持多种加载 Json 资源方式,最终都是通过 InputStream 流传入,再用 JsonReader 传入 LottieCompositionParser.parse 进行解析,返回完整的数据对象 LottieComposition。
这里支持同步/异步俩种方式加载,对于异步,会使用 AsyncCompositionLoader 异步调用 fromJsonSync 解析。俩者均使用 JsonReader 从流中解析 json 数据,可以避免了因一次性将全部 json 数据加载到内存之中而带来的 OOM 问题。
Json 解析
对于 AE 通过 bodymovin 插件导出来的 Json,里边包含做动画的一切信息,包括帧率、动画形态、图层、字体等等,接下来介绍一下 Json 动画描述文件的一些属性。
解析外部资源
1 | { |
在最外层中,可以获取插件 bodymovin 的版本号,起始关键帧,动画帧率及动画的宽高等属性。
解析图片资源
1 | { |
图片资源是在 “asset” 里边的,可以有多张图片,每张图片数据解析后会转换为实体类 LottieImageAsset
。
解析图层
1 | "layers": [ |
一张复杂的图片可以使用多个图层来表示,每个图层展示一部分内容,图层中的内容也可以拆分为多个元素。在 Lottie 解析的 Json 中,图层可以分为以下几种:PreComp,Solid,Image,Null,Shape,Text。在数据解析时,先会转换为数据实体类 Layer,根据 type 来区分是哪种图层。
从 LottieCompisition 到 CompositionLayer
使用 Lottie 第二步是设置数据对象 LottieComposition,把数据对象的数据转换为具有绘制能力的 BaseLayer,以下是转换的部分类图:
setComposition
在设置数据对象时,我们使用了 mLottieAnim.setComposition(composition);
可以看到源码这里是直接委托给了 LottieDrawable,把 LottieComposition 传了下去。
1 | public void setComposition(@NonNull LottieComposition composition) { |
在 LottieDrawable 中,可以看到调用 buildCompositionLayer 进行构造一个最外层的 CompositionLayer。
1 | public boolean setComposition(LottieComposition composition) { |
构造 CompositionLayer 时,遍历了一下 LottieComposition 数据对象中包含的所有图层 Layer 数据,将 Layer 数据对象转换为图层对象 BaseLayer。
1 | public CompositionLayer(LottieDrawable lottieDrawable, Layer layerModel, List<Layer> layerModels, |
转换过程中,根据在 Json 中支持的图层类别:PreComp,Solid,Image,Null,Shape,Tex,分别创建继承 BaseLayer 的 CompositionLayer,SolidLayer,ImageLayer,NullLayer,ShpeLayer,TextLayer。
1 | static BaseLayer forModel( |
图层的包含关系
CompositionLayer 与其他 BaseLayer 的关系可以用 ViewGroup 和 View 来比喻。
将数据对象 LottieComposition 转换为 BaseLayer 时,会创建一个顶级的 CompositionLayer,来容纳 Json 动画中描述的所有图层。当然,如果在转换复合图层 PreComp 到 CompositionLayer 时,如果这里边还包含其他图层,也会类似这样,把其子图层放入进来。
播放动画
使用 Lottie 的第三步就是调用 LottieAnimationView.playAnimation()
播放动画了,它的调用时序图如下所示:
playAnimation
播放动画会直接委托给 LottieDrawable 中执行,这里有一个继承 ValueAnimator
的子类 LottieValueAnimator
变量 animator,它用来控制整个动画的进度和更新。在 LottieValueAnimator
中,执行动画如下:
1 | public void playAnimation() { |
这里的代码逻辑是:
- notifyStart 去通知为 LottieAnimationView 设置的监听器,动画开始
- setFrame 设置当前帧数据
- postFrameCallback 抛一个 callback 去计算下一帧的数据,最终回调到
Choreographer.FrameCallback.doFrame()
,在 doFrame() 中又会继续抛出下一帧的计算,并且设置当前帧和通知动画的进度,这样整个动画会持续地动起来。
setProgress
在 setFrame()
或 doFrame()
中,进行设置关键帧的数据,然后调用 notifyUpdate()
通知当前动画进度改变,回调到 LottieDrawable 的 animator 添加的监听器中。
1 | public LottieDrawable() { |
animator.getAnimatedValueAbsolute()
,这个会把当前 frame 和起始帧、结束帧计算 一个0~1 的插值,再通知顶层的 CompositionLayer 进度改变。
传递到各个图层,设置关键帧及属性值的进度
在 CompositionLayer 中,会遍历包含的 BaseLyaer,设置当前进度值,传递到各个图层:
1 | public void setProgress(@FloatRange(from = 0f, to = 1f) float progress) { |
在 BaseLayer 里,setProgress 时,先改变当前动画的属性值,再设置有蒙层的 mask 或 matter 和当前关键帧的进度:
1 | void setProgress(@FloatRange(from = 0f, to = 1f) float progress) { |
这个 animations 是 BaseKeyframeAnimation 的列表,BaseKeyframeAnimation 是 Lottie 中定义的一套动画包括位移、缩放、旋转、透明度变化等。
回调通知动画变化
在关键帧 BaseKeyframeAnimation 中设置进度完后会通知动画数值变化:
1 | public void setProgress(@FloatRange(from = 0f, to = 1f) float progress) { |
这个会回调到 BaseLayer 里,触发重绘:
1 | public void onValueChanged() { |
触发重绘
在 BaseLayer 里重绘会调用 LottieDrawable 进行重绘:
1 | private void invalidateSelf() { |
最终会让 LottieDrawable 调用 draw 进行绘制:
1 | public void draw(@NonNull Canvas canvas) { |
这里是把画布 canvas 和 drawable 的矩阵 matrix 传入到 CompositionLayer,通知所有图层绘制。
各个图层进行绘制
在 BaseLayer 的 draw 绘制过程中,会调用抽象方法 drawLayer,各个继承的子类会具体实现。
1 | "WrongConstant") ( |
其中在 CompositionLayer 里,它可能包含多个子图层的,所以这里会遍历所包含的子图层进行 draw。
1 | void drawLayer(Canvas canvas, Matrix parentMatrix, int parentAlpha) { |
对于基本图层 SolidLayer,ImageLayer,NullLayer,ShpeLayer,TextLayer,会实现基类的 drawLayer 方法,完成该图层的具体绘制工作,比如在 ImageLayer 里,就是需要绘制图片:
1 | public void drawLayer(@NonNull Canvas canvas, Matrix parentMatrix, int parentAlpha) { |
各个图层根据当前改变的属性值和进度,绘制当前图层的效果,最终改变 drawable ,至此完成动画的改变。
Lottie 小结
优势
- 设计师只用导出一份动画 Json,就能运行各个平台上,无需跟开发反复调试,非常灵活
- 运营可配置,一次开发,后边可动态配置,实时替换动画效果
- 可实现复杂动画效果,开发效率高,易于调试和维护
不足
有 mask、matters 时,有很大的性能影响
可以查看源码在 BaseLayer,是如何对有 mask 或 matte 进行计算的:
如果不含 mask 或 matte,这里直接调用 drawLayer 就返回了,但是对于 mask 或 matte 会先进行 saveLayer,再去绘制 drawLayer。
在 saveLayer 中,可以看到这是个非常耗时的操作,需要分配和绘制一个 offscreen 的缓冲区,渲染的成本增加了一倍以上。
所以这里 mask 或 matte 的边界越小,性能损耗也会越小。
解码图片在主线程
查看 Lottie 获取图片的 ImageAssetManager
源码:
1 | public Bitmap bitmapForId(String id) { |
这里如果没有设置图片代理,则直接解码图片。如果有设置图片代理,则在代理监听里获取图片。
1 | mLottieAnim.setImageAssetDelegate(new ImageAssetDelegate() { |
这俩种方式,都将在主线程中调用,第一次都会从文件中解码,之后会直接从缓存中获取。对于一些 Android 低端机可能无法播放动画,甚至引发 ANR;可能 Lottie 更多考虑的是没有图片或图片很少的情况,比如一些矢量动画,这种性能会很好。