现在越来越多的APP中添加动画来提升用户体验,下面简单介绍下Airbnb开源的动画框架Lottie的使用
一、基本使用
首先添加依赖
compile ‘com.airbnb.android:lottie:1.0.1’
方法一:
1、xml文件中添加布局文件
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/lottie_anim"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
2、Activity中对动画进行控制,注释写的很清楚了
//初始化控件
LottieAnimationView mLottieAnimView= (LottieAnimationView) findViewById(R.id.lottie_anim);
/**
* 添加json格式文件从assets中导入
* 第一个参数:Context
* 第二个参数:动画文件
* 第三个参数:动画数据监听
*/
LottieComposition.fromAssetFileName(this, "LottieLogo.json", new LottieComposition.OnCompositionLoadedListener() {
@Override
public void onCompositionLoaded(LottieComposition composition) {
//设置动画数据
mLottieAnimView.setComposition(composition);
//播放动画
mLottieAnimView.playAnimation();
//设置循环
mLottieAnimView.loop(true);
}
});
// 设置动画监听
mLottieAnimView.addAnimatorUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
}
});
}
直接运行程序动画就开始播放了
方法二
1、直接通过XML布局文件控制动画,不通过Activity进行操作
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/lottie_anim"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:lottie_fileName="LottieLogo.json"
app:lottie_autoPlay="true"
app:lottie_loop="true"
/>
属性说明:
//指定动画文件名
app:lottie_fileName=”“
//设置自动播放 true/false
app:lottie_autoPlay=”true”
//设置动画是否循环 true/false
app:lottie_loop=”true”
接下来直接运行项目即可
方法三
1、xml文件中添加布局文件
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/lottie_anim"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
<Button
android:id="@+id/btn_start"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="开始"/>
2、通过在Activity中按钮来控制动画播放
mLottieAnim = (LottieAnimationView) findViewById(R.id.lottie_anim);
Button mBtnStart= (Button) findViewById(R.id.btn_start);
mBtnStart.setOnClickListener(this);
//通过直接指定动画名的方式设置动画
mLottieAnim.setAnimation("LottieLogo.json");
//点击后播放
@Override
public void onClick(View v) {
//判断动画是否在播放
if (!mLottieAnim.isAnimating()){
//设置进度
mLottieAnim.setProgress(0);
//开始播放动画
mLottieAnim.playAnimation();
}
}
OK,简单使用介绍完毕
二、常用API
// 判断动画是否正在播放中
mLottieAnim.isAnimating()
//设置动画数据 可以接收json格式数据 String格式 、Animation格式
mLottieAnim.setAnimation();
//播放动画
mLottieAnim.playAnimation();
//暂停动画
mLottieAnim.pauseAnimation();
//取消动画
mLottieAnim.cancelAnimation();
//获取动画时长,一定要在动画开始播放后才能获取否则为0
mLottieAnim.getDuration();
// 设置动画更新监听
mLottieAnim.addAnimatorUpdateListener();
//移除动画更新监听
mLottieAnim.removeUpdateListener();
//动画监听,可以用于监听动画的开始、结束、取消、重复
mLottieAnim.addAnimatorListener();
// 移除动画监听
mLottieAnim.removeAnimatorListener();
// 设置合成物 LottieComposition类型
mLottieAnim.setComposition();
// 设置循环
mLottieAnim.loop();
// 添加assets中动画文件
LottieComposition.fromAssetFileName()
// 使用同步方式接收动画文件
LottieComposition.fromFileSync();
// 添加json格式动画文件
LottieComposition.fromJson()
// 使用同步方式接收json格式文件
LottieComposition.fromJsonSync()
// 接收流的形式动画
LottieComposition.fromInputStream()
三、源码分析
由简到难,一步步分析
1、类LottieComposition中方法
A: 加载assets中文件
/**
* Loads a composition from a file stored in /assets.
*/
public static Cancellable fromAssetFileName(Context context, String fileName, OnCompositionLoadedListener loadedListener) {
InputStream stream;
try {
stream = context.getAssets().open(fileName);
} catch (IOException e) {
throw new IllegalStateException("Unable to find file " + fileName, e);
}
return fromInputStream(context, stream, loadedListener);
}
可以看到通过读取assets中文件返回一个流格式,直接跳到fromInputStream()方法
B:接下来我们查看fromInputStream()方法
/**
* Loads a composition from an arbitrary input stream.
*
* ex: fromInputStream(context, new FileInputStream(filePath), (composition) -> {});
*/
public static Cancellable fromInputStream(Context context, InputStream stream, OnCompositionLoadedListener loadedListener) {
FileCompositionLoader loader = new FileCompositionLoader(context.getResources(), loadedListener);
loader.execute(stream);
return loader;
}
通过上面代码我们发现直接通过FileCompositionLoader来执行流文件 loader.execute(stream);
C:查看FileCompositionLoader
private static final class FileCompositionLoader extends CompositionLoader<InputStream> {
private final Resources res;
private final OnCompositionLoadedListener loadedListener;
FileCompositionLoader(Resources res, OnCompositionLoadedListener loadedListener) {
this.res = res;
this.loadedListener = loadedListener;
}
@Override
protected LottieComposition doInBackground(InputStream... params) {
return fromInputStream(res, params[0]);
}
@Override
protected void onPostExecute(LottieComposition composition) {
loadedListener.onCompositionLoaded(composition);
}
}
发现FileCompositionLoader继承CompositionLoader,并且在doInBackground()方法中返回了方法
fromInputStream(res, params[0]);
D:查看CompositionLoader
private abstract static class CompositionLoader<Params>
extends AsyncTask<Params, Void, LottieComposition>
implements Cancellable {
@Override
public void cancel() {
cancel(true);
}
}
CompositionLoader继承了AsyncTask可见该类是采用异步执行的方式加载流
E:fromInputStream(res, params[0])
C中我们发现在方法doInBackground()中异步执行了该方法,继续分析
@SuppressWarnings("WeakerAccess")
public static LottieComposition fromInputStream(Resources res, InputStream file) {
try {
int size = file.available();
byte[] buffer = new byte[size];
//noinspection ResultOfMethodCallIgnored
file.read(buffer);
file.close();
String json = new String(buffer, "UTF-8");
JSONObject jsonObject = new JSONObject(json);
return LottieComposition.fromJsonSync(res,jsonObject);
} catch (IOException e) {
throw new IllegalStateException("Unable to find file.", e);
} catch (JSONException e) {
throw new IllegalStateException("Unable to load JSON.", e);
}
}
通过查看发现该方法将数据转化成json格式并返回给LottieComposition.fromJsonSync(res,jsonObject)方法执行,可见fromJsonSync对json数据进行了解析操作
F:继续跟进方法
@SuppressWarnings("WeakerAccess")
public 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格式数据进行了解析并且通过 LottieComposition composition对象进行了接收并且调用了Layer.fromJson和addLayer(composition, layer);方法
G:查看Layer.fromJson及addLayer(composition, layer);
通过fromJson()方法将json数据解析并赋值给layer
static Layer fromJson(JSONObject json, LottieComposition composition) {
Layer layer = new Layer(composition);
try {
if (L.DBG) Log.d(TAG, "Parsing new layer.");
layer.layerName = json.getString("nm");
if (L.DBG) Log.d(TAG, "\tName=" + layer.layerName);
layer.layerId = json.getLong("ind");
if (L.DBG) Log.d(TAG, "\tId=" + layer.layerId);
layer.frameRate = composition.getFrameRate();
int layerType = json.getInt("ty");
if (layerType <= LottieLayerType.Shape.ordinal()) {
layer.layerType = LottieLayerType.values()[layerType];
} else {
layer.layerType = LottieLayerType.Unknown;
}
.....省略代码段
return layer;
通过addLayer方法将解析出来的json数据Layer添加到composition.layers中
可以将composition理解为一个包含图层信息的对象
private static void addLayer(LottieComposition composition, Layer layer) {
composition.layers.add(layer);
composition.layerMap.put(layer.getId(), layer);
if (!layer.getMasks().isEmpty()) {
composition.hasMasks = true;
}
if (layer.getMatteType() != null && layer.getMatteType() != Layer.MatteType.None) {
composition.hasMattes = true;
}
}
H:通过以上的分析我们只发现了composition对象存储了解析出来的json信息,下面我们就分析怎样将composition对象转化为动画
接下来查看LottieAnimationView中的setAnimation()方法
/**
* Sets the animation from a file in the assets directory.
* This will load and deserialize the file asynchronously.
*
* Will not cache the composition once loaded.
*/
public void setAnimation(String animationName) {
setAnimation(animationName, CacheStrategy.None);
}
上面的方法返回setAnimation(animationName, CacheStrategy.None);
那么我们接着查看setAnimation方法做了什么
@SuppressWarnings("WeakerAccess")
public void setAnimation(final String animationName, final CacheStrategy cacheStrategy) {
this.animationName = animationName;
if (weakRefCache != null && weakRefCache.containsKey(animationName)) {
WeakReference<LottieComposition> compRef = weakRefCache.get(animationName);
if (compRef.get() != null) {
setComposition(compRef.get());
return;
}
} else if (strongRefCache != null && strongRefCache.containsKey(animationName)) {
setComposition(strongRefCache.get(animationName));
return;
}
isAnimationLoading = true;
setProgressWhenCompositionSet = false;
playAnimationWhenCompositionSet = false;
this.animationName = animationName;
cancelLoaderTask();
compositionLoader = LottieComposition.fromAssetFileName(getContext(), animationName, new LottieComposition.OnCompositionLoadedListener() {
@Override
public void onCompositionLoaded(LottieComposition composition) {
if (cacheStrategy == CacheStrategy.Strong) {
if (strongRefCache == null) {
strongRefCache = new HashMap<>();
}
strongRefCache.put(animationName, composition);
} else if (cacheStrategy == CacheStrategy.Weak) {
if (weakRefCache == null) {
weakRefCache = new HashMap<>();
}
weakRefCache.put(animationName, new WeakReference<>(composition));
}
setComposition(composition);
}
});
}
通过上面的方法我们发现最后调用了 setComposition(composition);
继续分析,查看setComposition(composition);
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);
isAnimationLoading = false;
if (setProgressWhenCompositionSet) {
setProgressWhenCompositionSet = false;
setProgress(progress);
} else {
setProgress(0f);
}
this.composition = composition;
if (playAnimationWhenCompositionSet) {
playAnimationWhenCompositionSet = false;
playAnimation();
}
requestLayout();
}
通过上面的分析发现方法 lottieDrawable.setComposition(composition);处理了接收到的composition对象
继续跟踪该方法
public 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());
buildLayersForComposition(composition);
getCallback().invalidateDrawable(this);
}
private void clearComposition() {
recycleBitmaps();
clearLayers();
}
有木有发现一个很熟悉的单词
animator.setDuration(composition.getDuration());
到这里我们终于将composition和动画建立了联系
最终我们发现了下面这个方法
private final ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
哈哈,原来是通过ValueAnimator来实现的,好了简单分析了下,就到这里,如有不足,请指正。