目前支持android上gif动画主要存在两种方式:第一种为将动画做成一张张图片然后进行快速切换,从而形成动画效果,第二种将动画变成json字符串,利用开源库进行解析,然后进行显示从而达到动画效果。其中两种方式的典型代表库为:android-gif-drawable和Lottie。
android-gif-drawable开源库地址:https://github.com/koral--/android-gif-drawable
Lottie开源库地址:https://github.com/airbnb/lottie-android
Lottie是Airbnb最近开源的一个动画库,它能够同时支持iOS,Android与ReactNative的开发.它的原理是利用Adobe After Effects软件做出动画,然后再用bodyMovin插件将动画导出成为一个json字符串,最后用Airbnb的开源库Lottie进行json文件解析最后绘制到设备上面,显而易见这个动画库的好处就是将动画变成了json字符串,极大地减少了文件大小,从而对apk进行了“瘦身”,所以下面总结了一下使用Lottie动画库进行动画显示的好处:
1、由于将动画变成了json字符串,极大地减少了应用大小。
2、因为Lottie是利用json文件生成动画,从而避免了不同分辨率、不同设备尺寸上面动画效果存在差异的问题。
3、只需要进行一动画绘制,生成一次json文件,从而可以在所有端进行使用(android,ios,web)
上面介绍了Lottie动画库的好处,那么现在我们来具体了解Lottie动画库的工作原理和使用流程:
首先我们需要有一个用AE生成的json动画文件,这个文件由UI美工进行提供,不需要我们程序员进行生成(感兴趣的可以自己试试,反正我是不知道怎么弄),我们尝试打开json文件,得到如下的结果:
发现和普通的json文件没有任何区别,它是由layers数组和其他关键数组组成,学过ps的知道一张图片是有多个图层叠加而成,这里的动画也是如此,通过AE做成动画,然后由插件将图层、图片大小、动画时间、关键帧等信息输出到json字符串中,所以我们只需要解析json文件,将关键信息解析出来然后把图层一层一层绘制上去,然后一帧一帧的切换就形成了动画。
我们将Lottie动画下载下来阅读源码,得出Lottie的大致框架图如下:
从图中可以看出Lottie库主要有三个重要的类型:AnimatableValue、KeyframeAnimation、AnimatableLayer,其中AnimatableLayer是继承Drawable类,所以真正进行动画绘制是依靠Drawable机制进行处理的,所有的关键数组最后都会封装到对应的AnimatableLayer中进行处理,其中流程图大致如下:
接下来我们从源码角度来了解Lottie的工作过程,首先我们知道Lottie显示动画主要有两种方式:
第一种方式是在xml中配置:
第二种方式是在代码中进行动态设置:
LottieAnimationView animationView = (LottieAnimationView) findViewById(R.id.animation_view);
animationView.setAnimation("hello-world.json");
animationView.loop(true);
animationView.playAnimation();
首先我们看一下LottieAnimationView类的初始化,发现在构造方法中调用了init方法,下面就是init方法的实现:
private void init(@Nullable AttributeSet attrs) {
TypedArray ta = getContext().obtainStyledAttributes(attrs,
R.styleable.LottieAnimationView);
String fileName = ta
.getString(R.styleable.LottieAnimationView_lottie_fileName);
if (!isInEditMode() && fileName != null) {
setAnimation(fileName);
}
if (ta.getBoolean(R.styleable.LottieAnimationView_lottie_autoPlay,
false)) {
lottieDrawable.playAnimation();
}
lottieDrawable.loop(ta.getBoolean(
R.styleable.LottieAnimationView_lottie_loop, false));
ta.recycle();
setLayerType(LAYER_TYPE_SOFTWARE, null);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
float systemAnimationScale = Settings.Global.getFloat(
getContext().getContentResolver(),
Settings.Global.ANIMATOR_DURATION_SCALE, 1.0f);
if (systemAnimationScale == 0f) {
lottieDrawable.systemAnimationsAreDisabled();
}
}
}
我们发现在init方法中读取了xml中的配置信息,然后调用setAnimation()方法,最后调用LottieDrawable的playAnimation()方法开始播放动画,其中setAnimation()有多个重载方法,我们就看最核心的方法:
public void setAnimation(final String animationName,
final CacheStrategy cacheStrategy) {
this.animationName = animationName;
if (weakRefCache.containsKey(animationName)) {
WeakReference compRef = weakRefCache
.get(animationName);
if (compRef.get() != null) {
setComposition(compRef.get());
return;
}
} else if (strongRefCache.containsKey(animationName)) {
setComposition(strongRefCache.get(animationName));
return;
}
this.animationName = animationName;
lottieDrawable.cancelAnimation();
cancelLoaderTask();
compositionLoader = LottieComposition.fromAssetFileName(getContext(),
animationName,
new LottieComposition.OnCompositionLoadedListener() {
@Override
public void onCompositionLoaded(
LottieComposition composition) {
if (cacheStrategy == CacheStrategy.Strong) {
strongRefCache.put(animationName, composition);
} else if (cacheStrategy == CacheStrategy.Weak) {
weakRefCache.put(animationName,
new WeakReference<>(composition));
}
setComposition(composition);
}
});
}
在这个方法中有两个参数一个是AnimationName,一个是CacheStrategy,毫无疑问AnimationName就是json文件的路径,而CacheStragy是什么呢?我们跟踪代码发现这个是缓存工具类,它是一个枚举类型,它提供了三种缓存技巧:Weak(弱引用缓存)、Strong(直接缓存在内存中)、None(不缓存),他们分别缓存在两个map中:
private static final Map strongRefCache = new HashMap<>();
private static final Map> weakRefCache = new HashMap<>();
在检查缓存之后利用LottieComposition的静态方法fromAssetFileName()去加载json文件,最后调用了fromInputStream()方法。
public static Cancellable fromInputStream(Context context,
InputStream stream, OnCompositionLoadedListener loadedListener) {
FileCompositionLoader loader = new FileCompositionLoader(
context.getResources(), loadedListener);
loader.execute(stream);
return loader;
}
其中FileCompositionLoader是AyscTask的子类,所以利用它进行了数据的异步加载(因为读取文件是耗时操作),当数据读取完成之后剩下来就是进行json字符串的解析了。
static LottieComposition fromJsonSync(Resources res, JSONObject json) {
LottieComposition composition = new LottieComposition(res);
int width = -1;
int height = -1;
try {
width = json.getInt("w");
height = json.getInt("h");
} catch (JSONException e) {
// ignore.
}
if (width != -1 && height != -1) {
int scaledWidth = (int) (width * composition.scale);
int scaledHeight = (int) (height * composition.scale);
if (Math.max(scaledWidth, scaledHeight) > MAX_PIXELS) {
float factor = (float) MAX_PIXELS
/ (float) Math.max(scaledWidth, scaledHeight);
scaledWidth *= factor;
scaledHeight *= factor;
composition.scale *= factor;
}
composition.bounds = new Rect(0, 0, scaledWidth, scaledHeight);
}
try {
composition.startFrame = json.getLong("ip");
composition.endFrame = json.getLong("op");
composition.frameRate = json.getInt("fr");
} catch (JSONException e) {
//
}
if (composition.endFrame != 0 && composition.frameRate != 0) {
long frameDuration = composition.endFrame - composition.startFrame;
composition.duration = (long) (frameDuration
/ (float) composition.frameRate * 1000);
}
try {
JSONArray jsonLayers = json.getJSONArray("layers");
for (int i = 0; i < jsonLayers.length(); i++) {
Layer layer = Layer.fromJson(jsonLayers.getJSONObject(i),
composition);
addLayer(composition, layer);
}
} catch (JSONException e) {
throw new IllegalStateException("Unable to find layers.", e);
}
// These are precomps. This naively adds the precomp layers to the main
// composition.
// TODO: Significant work will have to be done to properly support them.
try {
JSONArray assets = json.getJSONArray("assets");
for (int i = 0; i < assets.length(); i++) {
JSONObject asset = assets.getJSONObject(i);
JSONArray layers = asset.getJSONArray("layers");
for (int j = 0; j < layers.length(); j++) {
Layer layer = Layer.fromJson(layers.getJSONObject(j),
composition);
addLayer(composition, layer);
}
}
} catch (JSONException e) {
// Do nothing.
}
return composition;
}
从上面方法的返回值可以发现将json字符串中的所有信息都封装在LottieCompositon类中,在这个类中保存了动画时长、帧率等关键信息,其中将json文件中的图层layer封装成了Layer对象并保存在图层list列表layers中,而Layer类负责对图层JsonObject对象进行解析,阅读源码发现Layer类中解析出了scale、rotation、opacity、shape等动作,并将这些动作用对应的AnimatableValue类进行封装,至此json文件的解析过程就已经完成了,接下里就是如何实现动画的绘制了。
至此我们来总结一下json文件的解析过程,首先在LottieAnimationView的init方法中读取xml属性中动画文件,然后解析成LottieComposition对象,其中的解析过程是利用LottieCompostion类的静态方法进行json字符串的第一次基本解析,然后利用Layer对象将每个图层字符串解析变成一个Layer对象并保存在LottieComposition的成员变量layer集合中。在前面提到过我是利用AysncTask子类进行异步解析json字符串的,所以解析完成之后就会调用onPostExecute()方法
protected void onPostExecute(LottieComposition composition) {
loadedListener.onCompositionLoaded(composition);
}
//这个类是在LottieAnimationView中进行定义的
private final LottieComposition.OnCompositionLoadedListener loadedListener = new LottieComposition.OnCompositionLoadedListener() {
@Override
public void onCompositionLoaded(LottieComposition composition) {
setComposition(composition);
compositionLoader = null;
}
};
所以解析完成之后开始调用LottieAnimationView的setComposition()方法,将LottieComposition对象设置到LottieDrawable对象
@Override
public void setComposition(@NonNull LottieComposition composition) {
if (L.DBG) {
Log.v(TAG, "Set Composition \n" + composition);
}
lottieDrawable.setCallback(this);
lottieDrawable.setComposition(composition);
// If you set a different composition on the view, the bounds will not
// update unless
// the drawable is different than the original.
setImageDrawable(null);
setImageDrawable(lottieDrawable);
this.composition = composition;
requestLayout();
}
将LottieComposition设置个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;
setSpeed(speed);
setBounds(0, 0, composition.getBounds().width(), composition.getBounds().height());
buildLayersForComposition(composition);
setProgress(getProgress());
}
void playAnimation() {
if (layers.isEmpty()) {
playAnimationWhenLayerAdded = true;
reverseAnimationWhenLayerAdded = false;
return;
}
animator.setCurrentPlayTime((long) (getProgress() * animator.getDuration()));
animator.start();
}
我们发现在LottieDrawable中实际上调用的是ValueAnimator属性动画,但是这个与我们说讲述的动画有什么关联呢?我们接下来发现了ValueAnimator中添加了一个动画的监听器,在监听器中我们的动画随着属性动画的值改变而发生改变。
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((float) animation.getAnimatedValue());
}
}
});
}
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);
}
}
通过以上分析,我们来总结一下Lottie动画库显示动画的整个流程:首先将json文件解析成为LottieComposition对象并将这个对象设置个LottieDrawable,在LottieDrawable对象将将LottieComposion中的图层Layer数据实体变成LayerView对象(这个对象里面处理了每个图层的控制逻辑),然后在LottieDrawable对象中利用属性动画将这个Lottie动画绘制起来,
将Lottie动画库中的每个图层一层一层的绘制,这个属性动画相当于整个动画的一个引子,将所有的图层联系起来。
下面我们来比较一下展示动画的两种常用方式:
(1)android-gif-drawable开源库,利用.gif文件进行动画展示
(2)Lottie动画库,利用json文件进行动画展示
我们将从三个方面来进行比较:
(1)原理方面:
android-gif-drawable使用c层进行gif图片解析,然后按照关键帧一张一张的显示,所以这个过程中存在gift文件解析、图片解码、图片渲染展示这些过程。
Lottie则是根据Json文件利用canvas进行绘制,所以它的过程为:解析json文件,canvas绘制动画。
(2)实现效果方面
android-gif-drawable由于是使用了图片,所以图片尺寸大小就限定了动画的展示效果,所以他们在不同尺寸的设备上得展现效果存在差异.
Lottie动画库由于使用json文件,所以不存在这个问题,它在所有设备上得展现效果都一样
(2)系统资源占用方面
Lottie方式
对于上面的比较,我们发现这两方式各有各的特点,各有各的优势,至于如何选择得看具体的业务场景,Lottie个人觉得适合做宣介、引导等动画,而gif方式则适合做局部复杂UI动画。