概念
Lottie 是 Airbnb开源的一套跨平台的完整的动画效果解决方案,它可以使用Bodymovin解析以json导出的Adobe After Effects动画,并在移动设备上进行本地渲染。
可以直接运用在 iOS
、Android
、Web
和 React Native
之上。开发者无需关注动画中的实现细节。
特点
传统动画
使用序列帧,开发者需要设计时序、位移、透明度、尺寸、插值器等各类参数信息,再播放出动画。lottie的设计方案
将设计软件中的时间轴完整地导出来,包括里面的各种关键帧信息、矢量路径、层级、样式等等。
即动画的描述文件导出,再将动画元素导出,然后在对应的客户端,解析描述文件,还原出整个动画。
- 大小
导出的json文件比gif文件小很多 - 性能
性能也更好(应用到矢量图,内存占用小,缩放效果好) - 使用
API简单,代码实现简单,开发无需编写动画,降低动画的开发成本 - 灵活度
可动态配置下发,更换替换动画效果,易于调试和维护。 - 适配
不同的手机分辨率无需适配 - 通用
跨平台,设计稿导出一份动画描述文件,android,ios,react native,web多端通用 - 效果
几乎与设计出的动画无差别
使用流程
使用方法
由于API文档是1.0.3未及时更新,建议在源码里看方法
API文档
2.7.0版本
动画来源可以从本地,网络等
public void setAnimation(@RawRes final int rawRes) //src/main/res/raw
public void setAnimation(final String assetName) //src/main/assets
public void setAnimationFromJson(String jsonString)
public void setAnimationFromJson(String jsonString, @Nullable String cacheKey) {
public void setAnimation(JsonReader reader, @Nullable String cacheKey) //JSON文件或zip文件的InputStream
public void setAnimationFromUrl(String url) //json或zip文件的网址
public boolean addLottieOnCompositionLoadedListener(lottieOnCompositionLoadedListener)
public boolean removeLottieOnCompositionLoadedListener(lottieOnCompositionLoadedListener)
public void playAnimation()
public void pauseAnimation()
public void setProgress(float progress)
...
public void setImageBitmap(Bitmap bm)
public void setImageResource(int resId)
public void setImageDrawable(Drawable drawable)
...
AnimatorListener 、AnimatorUpdateListener 接口的支持
使用示例
<....ui.lottie.RecyclableLottieAnimationView
android:id="@+id/title_bar_toolbox_ani"
android:layout_width="@dimen/titlebar_web_action_width"
android:layout_height="@dimen/titlebar_web_action_width"
android:layout_centerVertical="true"
android:layout_alignParentRight="true">
mToolBoxAniView = mTopView.findViewById(R.id.title_bar_toolbox_ani);
mToolBoxAniView.setAnimation("sniffer_animation.json");
mToolBoxAniView.playAnimation();
public class RecyclableLottieAnimationView extends LottieAnimationView {
public RecyclableLottieAnimationView(Context context) {
super(context);
}
public RecyclableLottieAnimationView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public RecyclableLottieAnimationView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
/**
* lottie依赖 onDetachedFromWindow停止动画,回收动画资源
* 但动画的 play可能是异步的,
* 如果有post出去的异步任务,在detach后动画仍会执行
*/
cancelAnimation();
}
}
兼容性
版本支持 sdkVersion: >=16
= 2.8以上需AndroidX
依赖库的版本与动画导出版本有关
效果支持
引入后的影响
包大小新增: 71K
Json文件结构
- 回忆: 帧动画的播放.
核心类
LottieComposition 将json文件解析成数据对象.
LottieDrawable 承载所有的绘制工作, 将LottieComposition 解析的数据对象, 绘制成 drawable。
LottieAnimationView 提供了异步加载, 反序列化,显示, 封装了一些动画的操作,并处理了图片的回收onDetachWindow . 控制动画的实际操作委托给LottieDrawable
备注:
如直接使用LottieDrawable,需在合适的时机 invoke recycleBitmaps,否则内存泄漏.
动画播放原理
#LottieAnimationView.java
@MainThread
public void playAnimation() {
lottieDrawable.playAnimation();
enableOrDisableHardwareLayer();
}
#LottieDrawable.java
@MainThread
public void playAnimation() {
if (compositionLayer == null) {
lazyCompositionTasks.add(new LazyCompositionTask() {
@Override public void run(LottieComposition composition) {
playAnimation();
}
});
return;
}
animator.playAnimation(); // LottieValueAnimator
}
#LottieValueAnimator.java
@MainThread
public void playAnimation() {
running = true;
notifyStart(isReversed());
// update frame and notifyUpdate
setFrame((int) (isReversed() ? getMaxFrame() : getMinFrame()));
lastFrameTimeNs = System.nanoTime();
repeatCount = 0;
postFrameCallback();
}
// 说明
BaseLottieAnimator extends ValueAnimator
LottieValueAnimator extends BaseLottieAnimator
BaseLottieAnimator 提供了 notifyStart 、notifyEnd、notifyCancel 、notifyUpdate、 notifyRepeat 等notifyX方法.
通知 所有listeners : ValueAnimator.AnimatorUpdateListener 与ValueAnimator.AnimatorListener
对应者各自的回调方法 eg: onAnimationStart 、onAnimationEnd、onAnimationX方法
#LottieDrawable.java
public LottieDrawable() {
this.animator.addUpdateListener(new AnimatorUpdateListener() {
public void onAnimationUpdate(ValueAnimator animation) {
if (LottieDrawable.this.compositionLayer != null) {
LottieDrawable.this.compositionLayer.setProgress(LottieDrawable.this.animator.getAnimatedValueAbsolute());
}
}
});
}
#CompositionLayer.java
@Override public void setProgress(@FloatRange(from = 0f, to = 1f) float progress) {
super.setProgress(progress);
//... update the progress
for (int i = layers.size() - 1; i >= 0; i--) {
layers.get(i).setProgress(progress);
}
}
#BaseLayer.java
void setProgress(@FloatRange(from = 0f, to = 1f) float progress) {
//... layout.setProgress
for (int i = 0; i < animations.size(); i++) {
// List> animations
animations.get(i).setProgress(progress);
}
}
#BaseKeyframeAnimation.java
public void setProgress(@FloatRange(from = 0f, to = 1f) float progress) {
//... update the progress
notifyListeners();
}
public void notifyListeners() {
for (int i = 0; i < listeners.size(); i++) {
listeners.get(i).onValueChanged();
}
}
#BaseLayer
@Override public void onValueChanged() {
invalidateSelf();
}
private void invalidateSelf() {
lottieDrawable.invalidateSelf();
}
动画适配原理
① Android开发中, 不同屏幕分辨率适配.
② 为自定义View为其添加缩放属性
LottieAnimationView.java
private void init(@Nullable AttributeSet attrs) {
...
if (ta.hasValue(R.styleable.LottieAnimationView_lottie_scale)) {
lottieDrawable.setScale(ta.getFloat(R.styleable.LottieAnimationView_lottie_scale, 1f));
}
...
}
LottieAnimationView$setScale 委托给LottieDrawable
#LottieDrawable.java
public void setScale(float scale) {
this.scale = scale;
updateBounds();
}
#LottieDrawable.java
private void updateBounds() {
if (composition == null) {
return;
}
float scale = getScale();
// Drawable#setBounds
setBounds(0, 0, (int) (composition.getBounds().width() * scale),
(int) (composition.getBounds().height() * scale));
}
#LottieDrawable.java
private float getMaxScale(@NonNull Canvas canvas) {
float maxScaleX = canvas.getWidth() / (float) composition.getBounds().width();
float maxScaleY = canvas.getHeight() / (float) composition.getBounds().height();
return Math.min(maxScaleX, maxScaleY);
}
@Override public void draw(@NonNull Canvas canvas) {
float scale = this.scale;
float extraScale = 1f;
float maxScale = getMaxScale(canvas);
if (scale > maxScale) {
scale = maxScale;
extraScale = this.scale / scale;
}
if (extraScale > 1) {
// ... translate and scale
}
matrix.reset();
matrix.preScale(scale, scale);
compositionLayer.draw(canvas, matrix, alpha);
}
#LottieCompositionParser.java
public static LottieComposition parse(JsonReader reader) throws IOException {
float scale = Utils.dpScale();
// ...parse
int scaledWidth = (int) (width * scale);
int scaledHeight = (int) (height * scale);
Rect bounds = new Rect(0, 0, scaledWidth, scaledHeight);
composition.init(bounds, startFrame, endFrame, frameRate, layers, layerMap, precomps,
images, characters, fonts);
}
小结:
Lottie 适配原理:
解析json文件,获得取宽高之后, 乘以手机的相对密度。得到初始的Rect边界.
在使用的时候判断适配后的宽高是否超过屏幕的宽高,如果超过则再进行缩放。以此保障 Lottie 在 Android 平台的显示效果。
绘制原理
猜想: 参考动画播放思路,猜想下绘制流程
BaseLayer.java
@Override
public void draw(Canvas canvas, Matrix parentMatrix, int parentAlpha) {
if (!visible) {
return;
}
buildParentLayerListIfNeeded();
if (!hasMatteOnThisLayer() && !hasMasksOnThisLayer()) {
matrix.preConcat(transform.getMatrix());
drawLayer(canvas, matrix, alpha);
recordRenderTime(L.endSection(drawTraceName));
return;
}
if (hasMasksOnThisLayer()) {
//...draw maskLayer
applyMasks(canvas, matrix);
}
if (hasMatteOnThisLayer()) {
//...draw matteLayer
matteLayer.draw(canvas, parentMatrix, alpha);
}
}
CompositionLayer.java
@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) {
BaseLayer layer = layers.get(i);
layer.draw(canvas, parentMatrix, parentAlpha);
}
}
//...
}
遇到的问题与解决方案
①依赖库的版本与导出动画版本
java.lang.IllegalStateException: Missing values for keyframe.
at com.airbnb.lottie.animation.keyframe.FloatKeyframeAnimation.getValue(FloatKeyframeAnimation.java:16)
at com.airbnb.lottie.animation.keyframe.FloatKeyframeAnimation.getValue(FloatKeyframeAnimation.java:8)
at com.airbnb.lottie.animation.keyframe.BaseKeyframeAnimation.getValue(BaseKeyframeAnimation.java:125)
at com.airbnb.lottie.animation.keyframe.TransformKeyframeAnimation.getMatrix(TransformKeyframeAnimation.java:113)
原因:
json格式与解析规则不匹配
Lottie 3.0和Bodymovin 5.5有一些重要的json优化,可以节省json大小和解析速度的1/3。 但是,必须在3.0以上生效,否则就在bodymovin设置中启用“导出为旧格式”
②issues: zip 播放的问题
https://github.com/airbnb/lottie-android/issues/1009
③. 内存泄漏问题
图片在回收时机
@Override protected void onDetachedFromWindow() {
if (isAnimating()) { // 动画的加载play是异步的
cancelAnimation();
wasAnimatingWhenDetached = true;
}
recycleBitmaps();
super.onDetachedFromWindow();
}
以此可能引发的内存抖动的场景
假设在RecyclerView中使用包涵mattes或者mask的动画
④ 内存抖动的风险
bitmap在动画加载到window时被创建,onDetachedFromWindow删除时回收。所以不宜在RecyclerView中使用包涵mattes或者mask的动画,否则会引起bitmap抖动。
⑤ 版本变更比较多, API变化比较大 解决方案: 封装,提供统一接口外观
与SVGA对比
SVGA里面的每一帧都是关键帧,SVGA已经在导出动画的时候,把每一帧的信息都计算好了,如此一来,
播放时无需关心插值计算的过程。
通过帧率去刷每一帧的画面,这个思路跟gif很像,SVGA可以同时支持Flash和After Effects的导出.
且通过配置使得动画过程中图片都可以得到复用。
- 由于拥有所有帧, 不用解析高阶插值(二次线性方程,贝塞尔曲线方程),节省了CPU
- 2.x之后的svga,使用Protocol Buffers 来做序列化,序列化的数据体更小,传递效率比xml,json 更高。
Lottie关键帧Keyframes, 是通过传参的方式,交由cpu去运算. 所以复杂动画实现耗费cpu
与SVGA对比
SVGA动画原理
逐帧渲染,每一帧均为关键帧,只需渲染每个元素无需插值计算
播放前一次性上传纹理到 GPU,并在动画过程中复用纹理
2.x之后的svga,使用Protocol Buffers 来做序列化,序列化的数据体更小,传递效率比xml,json 更高。
Lottie动画原理
逐层渲染,完全按照设计工具的设计思路还原
播放解析多个图层配置并添加相应动画,并在动画过程中复用图层
当需要解析高阶插值,性能相对差一些 (关键帧Keyframes,是通过传参的方式,交由cpu去运算. 所以复杂动画实现耗费cpu)
通过帧率去刷每一帧的画面,与gif很像,所以SVGA可以同时支持Flash和After Effects的导出.
- 由于拥有所有帧, 不用解析高阶插值(二次线性方程,贝塞尔曲线方程),节省了CPU
Lottie关键帧Keyframes, 是通过传参的方式,交由cpu去运算. 所以复杂动画实现耗费cpu
位图与json
SVGA是将图片与描述文件集成在.svga文件当中的,而Lottie则是把二者分离开。
Lottie可以在导出后,再对图片进行文件大小优化;而SVGA最好是在事先就对图片进行大小优化。
SVGA应用场景:
在直播应用场景,礼物播放,游戏炫酷动画.
Lottie应用场景:
高德地图,支付宝,全民K歌
阿里提供的犸良动画
它最底层采用的技术就是Lottie,阿里对其二次封装了许多预设的动画效果,
可以自定义其中的元素与参数,然后试着导出你的第一个json文件~
文献参考
Lottie生态
https://github.com/airbnb/lottie-android 31.1K star
https://github.com/airbnb/lottie-ios
https://github.com/airbnb/lottie-web
Lottie doc
导出工具
https://github.com/bodymovin/bodymovin
动画预览器
https://lottiefiles.com/preview
https://airbnb.design/lottie/
SVGA生态
开源iOS/Android/Web三个平台的源码。
https://github.com/yyued/SVGAPlayer-Android 2.5K star
https://github.com/yyued/SVGAPlayer-iOS
https://github.com/yyued/SVGAPlayer-Web
设计师工具
http://svga.io/designer.html
动画预览器
http://svga.io/svga-preview.html