lottie
项目地址: https://github.com/jerryyh/MvpAndroidmaster
Lottie支持Jellybean (API 16)及以上的系统
什么是lottie?
Airbnb最近开源了一个名叫Lottie的动画库,它能够同时支持iOS,Android与ReactNative的开发,使用流程如下图所示
如图所示,通过安装AE上的bodymovin的插件,能够将AE中的动画工程文件转换为通用的json格式描述文件(bodymovin插件本身是用于网页上呈现各种AE效果的一个开源库),lottie所做的事情就是实现在不同移动端平台上呈现AE动画的方式,从而达到动画文件的一次绘制、一次转换,随处可用的效果,这个跟Java一次编译随处运行效果一样
很酷炫有木有!
使用准备
在使用这么酷炫的项目前,需要做一下准备
以windows为例,使用到的工具在云盘中
http://pan.baidu.com/s/1c19FLdA
- 下载AE 安装(AE2007)
`
安装破解:
Adobe After Effects CC 2017 安装后不要运行,直接使用 adobe.snr.patch.v2.0-painter.exe 选择产品破解即可
中文语言更改:
安装后默认是英文界面,找到安装目录(默认是:C:\Program Files\Adobe\Adobe After Effects CC 2017\Support Files\AMT)
在 AMT 文件夹内找到 application.xml 文件
使用文本编辑器打开并修改底部一行:
<Data key=”installedLanguages”>en_GB</Data> 为 <Data key=”installedLanguages”>zh_CN</Data> 保存。
- 1
- 2
- 3
- 4
- 5
- 6
`
- 安装bodymovin插件
该项目地址在https://github.com/bodymovin/bodymovin
安装这个插件有几种方式,采用安装 zxp installer安装bodymovin.zxp(获取这个文件:下载上面的zip文件,在build/extension目录下,这里已经下载好了)方式,
安装云盘中的aescript+aeplugins zxp installer.exe,然后安装bodymovin即可
此时打开AE 在window/extension 文件夹下可以看到bodymovin插件说明就ok了,
- 开启允许脚本写入文件和访问网络
都需要在AE的编辑->首选项->常规中勾选允许脚本写入文件和访问网络(默认不开启)
制作Json
从这里可以找到一些Lottie中演示过的动画的AE源文件,下载到本地后在AE中打开即可(或者去https://material.uplabs.com选择 Download 选择 -> view all -> 在tools这一栏里面选择 After effects 然后选择一个免费的项目下载下来 用ae打开。 ).这里我们选用EmptyState.aep这个实例工程,稍作修改:
然后使用bodymovin插件导出aep文件对应的数据json(点击Render)
你也可以通过http://svgsprite.com/demo/bm/player.php?render=canvas&bg=fff去浏览你制作的json文件动画
使用Lottie库播放动画
Lottie的引入与使用就如其他库一样,这里以Android平台的使用为例.
在项目的build.gradle文件中加入:
dependencies {
compile 'com.airbnb.android:lottie:1.0.1'
}
- 1
- 2
- 3
- 4
- 5
布局文件中添加
Lottie支持Jellybean (API 16)及以上的系统,最简单的使用方式是直接在布局文件中添加:
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/animation_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:lottie_fileName="hello-world.json"
app:lottie_loop="true"
app:lottie_autoPlay="true" />
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
代码中添加
LottieAnimationView animationView = (LottieAnimationView) findViewById(R.id.animation_view);
animationView.setAnimation("hello-world.json");
animationView.loop(true);
- 1
- 2
- 3
- 4
- 5
这方法将在后台线程异步加载数据文件,并在加载完之后开始渲染显示动画
或者从网络上加载jsonObject
LottieAnimationView animationView = (LottieAnimationView) findViewById(R.id.animation_view);
...
LottieComposition composition = LottieComposition.fromJson(getResources(), jsonObject, (composition) -> {
animationView.setComposition(composition);
animationView.playAnimation();
});
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
你也可以通过API控制动画,并且设置一些监听
animationView.addAnimatorUpdateListener((animation) -> {
// Do something.
});
animationView.playAnimation();
...
if (animationView.isAnimating()) {
// Do something.
}
...
animationView.setProgress(0.5f);
...
// 自定义速度与时长
ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f)
.setDuration(500);
animator.addUpdateListener(animation -> {
animationView.setProgress(animation.getAnimatedValue());
});
animator.start();
...
animationView.cancelAnimation();
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
在使用遮罩的情况下,LottieAnimationView 使用 LottieDrawable来渲染动画.如果需要的话,你可以直接使用drawable形式:
LottieDrawable drawable = new LottieDrawable();
LottieComposition.fromAssetFileName(getContext(), "hello-world.json", (composition) -> {
drawable.setComposition(composition);
});
- 1
- 2
- 3
- 4
- 5
- 6
如果你需要频繁使用某一个动画,可以使用LottieAnimationView内置的一个缓存策略:
LottieAnimationView.setAnimation(String, CacheStrategy)
其中CacheStrategy的值可以是Strong,Weak或者None,它们用来决定LottieAnimationView对已经加载并转换好的动画持有怎样形式的引用(强引用/弱引用).
使用小结
关于方法数
使用ClassShark分析官方demo,lottie库其实只占用了783
个方法数,还是比较少的
关于性能
官方说法
- 如果没有mask和mattes,那么性能和内存非常好,没有bitmap创建,大部分操作都是简单的cavas绘制。
- 如果存在mattes,将会创建2~3个bitmap。bitmap在动画加载到window时被创建,被window删除时回收。所以不宜在RecyclerView中使用包涵mattes或者mask的动画,否则会引起bitmap抖动。除了内存抖动,mattes和mask中必要的bitmap.eraseColor()和canvas.drawBitmap()也会降低动画性能。对于简单的动画,在实际使用时性能不太明显。
- 如果在列表中使用动画,推荐使用缓存LottieAnimationView.setAnimation(String, CacheStrategy) 。
原理
Lottie使用json文件来作为动画数据源,json文件是通过Bodymovin插件导出的,查看sample中给出的json文件,其实就是把图片中的元素进行来拆分,并且描述每个元素的动画执行路径和执行时间。Lottie的功能就是读取这些数据,然后绘制到屏幕上。
首先要解析json,建立数据到对象的映射,然后根据数据对象创建合适的Drawable绘制到view上,动画的实现可以通过操作读取到的元素完成。
具体过程如下所示
json文件——>Component——>Drawable——>View
通过如下3个核心类来来完成整个工作流程,因而使用起来比较简单
- LottieComposition(json->数据对象)
Lottie使用LottieComposition
来作为After Effects的数据对象,即把Json文件映射为到LottieComposition
,该类中提供了解析json的静态方法
- LottieDrawable(数据对象->Drawable)
绘制
- LottieAnimationView(绘制)
操作集合,LottieAnimationView 继承自 AppCompatImageView,封装了一些动画的操作,具体的绘制时委托为 LottieDrawable 完成的
LottieComposition(json->数据对象)
定义两个接口
//定义两个通用接口
public interface OnCompositionLoadedListener {
void onCompositionLoaded(LottieComposition composition);
}
interface Cancellable {
void cancel();
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
看下这个类的关键函数调用情况
简单介绍下,通过提供
- fromAssetFileName(资源file)
- fromFileSync(异步文件,通常是网络数据)
- fromJson(直接的json)
通过这三个入口接收json文件、json流,然后异步都通过AsynTask来异步处理,最终核心处理都是在fromJsonSync
中进行json数据的解析
主要分为以下层数据
width = json.getInt(“w”);
height = json.getInt(“h”);
将根据这两个得到一块矩形区域
composition.bounds = new Rect(0, 0, scaledWidth, scaledHeight);
- 1
- 2
composition.startFrame = json.getLong(“ip”);
composition.endFrame = json.getLong(“op”);
composition.frameRate = json.getInt(“fr”);
将根据这几个得到,动画帧持续的时间
long frameDuration = composition.endFrame - composition.startFrame;
composition.duration = (long) (frameDuration / (float) composition.frameRate * 1000);
- 1
- 2
- 3
- 4
JSONArray jsonLayers = json.getJSONArray(“layers”);
其中Layers是包含了几层的参数,层级非常丰富,具体可以查看Layer类中的json解析(该类中除了解析参数之外还实现了属性动画一些基本类,核心方法是 Layer fromJson)
这些数据解析完后,都被存在如下变量中,用以描述After Effects中的动画
private final LongSparseArray<Layer> layerMap = new LongSparseArray<>();
private final List<Layer> layers = new ArrayList<>();
private Rect bounds;
private long startFrame;
private long endFrame;
private int frameRate;
private long duration;
private boolean hasMasks;
private boolean hasMattes;
private float scale;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
从数据变量来看,startFrame、endFrame、duration、scale等都是动画中常见的参数,List layers为映射拆分后的图层数据
LottieDrawable(数据对象->Drawable)
先看下继承关系
LottieDrawable
extends AnimatableLayer
extends Drawable
AnimatableLayer
首先看下AnimatableLayer
继承了Drawable
主要重写了draw,在代码中可以看出,借用canvas的save
、restoreToCount
来实现像PS那种图层叠加的效果
@Override
public void draw(@NonNull Canvas canvas) {
int saveCount = canvas.save();
applyTransformForLayer(canvas, this);
int backgroundAlpha = Color.alpha(backgroundColor);
if (backgroundAlpha != 0) {
int alpha = backgroundAlpha;
if (this.alpha != null) {
alpha = alpha * this.alpha.getValue() / 255;
}
solidBackgroundPaint.setAlpha(alpha);
if (alpha > 0) {
canvas.drawRect(getBounds(), solidBackgroundPaint);
}
}
for (int i = 0; i < layers.size(); i++) {
layers.get(i).draw(canvas);
}
canvas.restoreToCount(saveCount);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
特别是for循环那段,更是体现了将之前json解析的元素图层一层层的画出来,如同PS一般
好了,AnimatableLayer
如其名一样负责将将之前的Layer层动画显示出来,下面再来看LottieDrawable
LottieDrawable
这个类的核心方法
void setComposition(LottieComposition composition) {
if (getCallback() == null) {
throw new IllegalStateException(
"You or your view must set a Drawable.Callback before setting the composition. This " +
"gets done automatically when added to an ImageView. " +
"Either call ImageView.setImageDrawable() before setComposition() or call " +
"setCallback(yourView.getCallback()) first.");
}
//清除之前的数据
clearComposition();
this.composition = composition;
animator.setDuration(composition.getDuration());
setBounds(0, 0, composition.getBounds().width(), composition.getBounds().height());
//核心函数:即根据lottieComposition建立多个layerView,此时已经创建好了多个Drawable,并通过List建立的为以lottieDrawable为根的一个drawable树。
buildLayersForComposition(composition);
getCallback().invalidateDrawable(this);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
该方法在LottieAnimationView
中调用,该方法中实际调用的核心函数是
void buildLayersForComposition(LottieComposition composition)
- 1
- 2
- 3
这个函数的重点做的是为AnimatableLayer创建关键信息:
-
将得到的
bitmap
(mainBitmap, maskBitmap, matteBitmap)+layer
(通过之前setComposition获得的)信息合成LayerView
-
将LayerView通过super.addLayer(AnimatableLayer#layers),在AnimatableLayer#layers中去draw
详见AnimatableLayer#draw方法
//这里的layer来自于LottieDrawabe调用super.addLayers
for (int i = 0; i < layers.size(); i++) {
layers.get(i).draw(canvas);
}
- 1
- 2
- 3
- 4
- 5
- 6
绘制动画的载体LottieAnimationView
这个view将加载、转换、和显示由AE动画插件bodymovin导出的json文件,并且支持设置动画的进度。,其实LottieAnimationView仅仅是个载体,通过管理
- 数据源(LottieComposition)
- 动画执行者(LottieDrawable实际上是AnimatableLayer!,LottieDrawable继承自AnimatableLayer)
LottieAnimationView
继承了AppCompatImageView
,并且封装了一些动画操作,如入口和进度控制,开启、取消、暂停动画
通过重载 setAnimation函数间接调用之前LottieComposition解析文件的三种方式
- 入口
void setAnimation(final JSONObject json)——>LottieComposition.fromJson(getResources(), json, loadedListener)
void setAnimation(final String animationName, final CacheStrategy cacheStrategy)——LottieComposition.fromAssetFileName(getContext(), animationName,new LottieComposition.OnCompositionLoadedListener()
可以看出动画的设置是通过LottieComposition
来代理的
- 进度控制
setProgress(@FloatRange(from = 0f, to = 1f) float progress)——>lottieDrawable.setProgress(progress)
- 开启动画
playAnimation()——>lottieDrawable.playAnimation();
- 取消动画
cancelAnimation()——>lottieDrawable.cancelAnimation();
- 暂停动画
pauseAnimation()——>lottieDrawable.cancelAnimation(); setProgress(progress);
因此可以看出动画的控制是通过lottieDrawable
来代理进行的。
下面将详细的总结下LottieAnimationView
如何工作的
- 创建
LottieAnimationView lottieAnimationView
、LottieDrawable lottieDrawable
- 提供入口setAnimation,实际中是通过
LottieComposition
中的静态方法解析json文件创建LottieComposition lottieComposition,并且这个过程中已经解析出多个Layer对象。
这里要重点提一下 lottieDrawable.setComposition(lottieComposition)
,因为每个setAnimation都会去调用setComposition(@NonNull LottieComposition composition)
里面会调用前面的方法。前面部分已经详述
- 启动动画、取消动画、暂停动画
直接委托给了lottieDrawable,lottieDrawable中有private final ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
在lottieDrawable中有private final ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);,并且在构造函数中初始化
LottieDrawable() {
super(null);
animator.setRepeatCount(0);
animator.setInterpolator(new LinearInterpolator());
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override public void onAnimationUpdate(ValueAnimator animation) {
if (systemAnimationsAreDisabled) {
animator.cancel();
setProgress(1f);
} else {
setProgress(animation.getAnimatedFraction());
}
}
});
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 进度控制setProgress方法
还是在lottieDrawable中实现,
public void setProgress(@FloatRange(from = 0f, to = 1f) float progress) {
this.progress = progress;
for (int i = 0; i < animations.size(); i++) {
animations.get(i).setProgress(progress);
}
for (int i = 0; i < layers.size(); i++) {
layers.get(i).setProgress(progress);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
最终还是调用了private final List
void setProgress(@FloatRange(from = 0f, to = 1f) float progress) {
if (progress < getStartDelayProgress()) {
progress = 0f;
} else if (progress > getDurationEndProgress()) {
progress = 1f;
} else {
progress = (progress - getStartDelayProgress()) / getDurationRangeProgress();
}
if (progress == this.progress) {
return;
}
this.progress = progress;
T value = getValue();
for (int i = 0; i < listeners.size(); i++) {
//在onValueChanged时,各个创建好的Drawable会根据需求进行重绘,达到动画的效果。
listeners.get(i).onValueChanged(value);
}
}