Lottie是一个很成熟的开源动画框架,它支持直接使用从AE导出的动画文件,在不同平台均可快速使用,大大减轻了程序员的工作量,也让复杂的动画成为可能。该动画文件使用Json格式来描述内容,可以大大缩减文件的体积。在Android平台,Lottie的作用就是把Json文件做解析,使用code做控制,并通过Canvas绘制出来。Lottie效果如下:
如果用纯代码去手搓以上的动画效果,估计码农们会原地狗带。而用Lottie作为衔接,这一切就很丝滑,就像显示一张图片一样简单,这就是Lottie的强大,可以说是降维打击。
1.文件结构
1.1最外层文件
Json的最外层结构如下:
{"v": "5.8.0", //bodymovin的版本"fr": 60, //帧率"ip": 0, //起始关键帧"op": 102, //结束关键帧"w": 1350, //动画宽度"h": 800, //动画高度"nm": "recommend_turn page_x0.75_original", //名称"ddd": 0, //是否为3d"assets":[], //资源信息"layers":[], //图层信息"markers": [] //遮罩
}
其中的layers是动画最核心的内容,因为Lottie的动画是由不同的Layer组合而成的。
1.2 Layers层
结构如下:
"layers": [ //图层信息{"ddd": 0, //是否为3d"ind": 1, //图层id 唯一性"ty": 4, //图层类型"nm": "page back 4",//图层名称"refId": "comp_0", // 引用的资源,图片/预合成层"td": 1,"sr": 1,"ks": {...}, // 变换。对应AE中的变换设置"ao": 0,"layer": [], // 该图层包含的子图层"shaps": [], // 形状图层"ip": 12, //该图层起始关键帧"op": 1782, //该图层结束关键帧"st": -18, "bm": 0}
ty : 定义了图层的类型,类型有ImageLayer、ShapeLayer、ScaleLayer、SolidLayer、TextLayer和NullLayer,不同的layer会使用不同的变化策略以及不同的资源。
refId :这是ImageLayer会用到的图片资源。
shaps :ShapeLayer会用到的资源,描述形状。
ks :变换的描述,所有图层都会用到这个资源,它是对动画怎么变化的具体描述,它内部包含了很多维度的变。
1.3 KS层
具体如下:
"ks": { // 变换。对应AE中的变换设置"o": { // 透明度"a": 0,"k": 100,"ix": 11},"r": { // 旋转"a": 0,"k": 0,"ix": 10},"p": { // 位置"a": 0,"k": [-167, 358.125, 0],"ix": 2},"a": { // 锚点"a": 0,"k": [667, 375, 0],"ix": 1},"s": { // 缩放"a": 0,"k": [100, 100, 100],"ix": 6}
}
2.文件解析过程
2.1 LottieTask
在LottieAnimationView的setAnimation方法是加载资源的入口,它内部会把加载资源的任务创建一个LottieTask,并且在执行完文件解析之后,创建相关的Layer,并封装在LottieComposition作为结果返回给LottieAnimationView,如下:
public void setAnimation(@RawRes final int rawRes) {LottieTask<LottieComposition> task = cacheComposition ?LottieCompositionFactory.fromRawRes(getContext(), rawRes) : LottieCompositionFactory.fromRawRes(getContext(), rawRes, null);//添加任务监听,在任务执行成功之后回调setCompositionTask(task); }
LottieCompositionFactory内部会创建一个LottieTask并添加到线程池中去异步执行:
public static LottieTask<LottieComposition> fromRawRes(Context context, @RawRes final int rawRes, @Nullable String cacheKey) {return cache(cacheKey, new Callable<LottieResult<LottieComposition>>() {@Overridepublic LottieResult<LottieComposition> call() {// 这里执行异步解析任务return fromRawResSync(context, rawRes);}});}
在LottieTask内部中添加线程池任务:
LottieTask(Callable<LottieResult<T>> runnable, boolean runNow) {EXECUTOR.execute(new LottieFutureTask(runnable));}
2.2 LayerParser
跟进fromRawResSync方法里面去,会调用到LottieCompositionMoshiParser去做解析最外层的Json数据,我们主要关注是解析layers字段:
LottieCompositionMoshiParser.java
public static LottieComposition parse(JsonReader reader) throws IOException {switch (reader.selectName(NAMES)) {case 6:parseLayers(reader, composition, layers, layerMap);}
}private static void parseLayers(JsonReader reader, LottieComposition composition,List<Layer> layers, LongSparseArray<Layer> layerMap) throws IOException {Layer layer = LayerParser.parse(reader, composition);layers.add(layer);layerMap.put(layer.getId(), layer);}
最后再调用了LayerParser做生成各个Layer数据结构,当然LayerParser内部还要对下一层的数据做解析,最后形成一个树形数据结构返回给LottieAnimationView。
3.创建绘制图层
3.1 CompositionLayer和BaseLayer
LottieTask在解析完之后,会把LottieComposition返回给LottieAnimationView:
LottieAnimationView.java
public void setComposition(@NonNull LottieComposition composition) {boolean isNewComposition = lottieDrawable.setComposition(composition);}
LottieAnimationView又把数据透传给LottieDrawable:
LottieDrawable.java
public boolean setComposition(LottieComposition composition) {buildCompositionLayer();
}compositionLayer = new CompositionLayer(this, LayerParser.parse(composition), composition.getLayers(), composition);
这里会创建一个CompositionLayer,它继承自BaseLayer,而在它内部会创建各个类型的图层,如下:
static BaseLayer forModel(Layer layerModel, LottieDrawable drawable, LottieComposition composition) {switch (layerModel.getLayerType()) {case SHAPE:return new ShapeLayer(drawable, layerModel);case PRE_COMP:return new CompositionLayer(drawable, layerModel,composition.getPrecomps(layerModel.getRefId()), composition);case SOLID:return new SolidLayer(drawable, layerModel);case IMAGE:return new ImageLayer(drawable, layerModel);case NULL:return new NullLayer(drawable, layerModel);case TEXT:return new TextLayer(drawable, layerModel);}}
这些不同类型的Layer也是继承自BaseLayer,所以这里创建图层的过程,就是把之前Json文件解析的树形数据结构,转换成绘制图层的树形结构。CompositionLayer和其他各个BaseLayer的关系,就类似于View和ViewGroup之间的关系一样。
3.2 关键帧动画
同时在创建各个BaseLayer的时候,它们内部还会继续创建对应的关键帧动画类,我们重点看一下TransformKeyframeAnimation,也就是上面Json文件中对应的“ks”数据:
TransformKeyframeAnimation.java
@NonNull private BaseKeyframeAnimation<PointF, PointF> anchorPoint;@NonNull private BaseKeyframeAnimation<?, PointF> position;@NonNull private BaseKeyframeAnimation<ScaleXY, ScaleXY> scale;@NonNull private BaseKeyframeAnimation<Float, Float> rotation;@NonNull private BaseKeyframeAnimation<Integer, Integer> opacity;@Nullable private FloatKeyframeAnimation skew;@Nullable private FloatKeyframeAnimation skewAngle;
可以看到内部定义了很多关键帧动画类,它们决定了该Layer在某个时刻应该绘制怎样的内容。
关系图如下:
4.动画绘制过程
4.1LottieValueAnimator计算progress
准备好了资源,创建好了图层,在LottieAnimationView 调用了playAnimation()方法之后,就开始播放动画了。同样,也会调用到LottieDrawable的playAnimation()方法:
LottieDrawable.java
public void playAnimation() {//调用animator去播放动画animator.playAnimation(); }
LottieDrawable会调用它内部的LottieValueAnimator去计算动画播放的具体帧:
LottieValueAnimator.java
@MainThreadpublic void playAnimation() { //这里会通知更新动画setFrame((int) (isReversed() ? getMaxFrame() : getMinFrame()));//这里注册Choreographer,以便同步vsync信号,并在doframe方法中继续更新动画postFrameCallback();}
setFrame会调用到父类里的notifyUpdate()方法,然后回调通知到LottieDrawable里面的监听接口:
LottieDrawable.java
private final ValueAnimator.AnimatorUpdateListener progressUpdateListener = new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {if (compositionLayer != null) {compositionLayer.setProgress(animator.getAnimatedValueAbsolute());}}};
从这里可以看出,LottieValueAnimator的作用主要是根据屏幕的刷新信号,来计算应该播放的帧序列,也就是播放的具体进度。当存在卡断的情况下,Choreographer的doframe方法会延迟通知,就会导致LottieValueAnimator存在跳帧的情况。
4.2关键帧动画设置progress
接着看绘制流程,播放进度会给到CompositionLayer,而CompositionLayer又会把播放进度再分发给具体的每一个Layer,如下:
CompositionLayer.java
setProgress(@FloatRange(from = 0f, to = 1f) float progress) {for (int i = layers.size() - 1; i >= 0; i--) {layers.get(i).setProgress(progress);}
}
每个Layer都是继承自BaseLayer,在BaseLayer中又会把进度给到内部的各个关键帧动画,最重要的是TransformKeyframeAnimation,它内部包含各个维度的变化序列:
TransformKeyframeAnimation.java
setProgress(float progress) {
if (opacity != null) {opacity.setProgress(progress); //透明度}if (anchorPoint != null) {anchorPoint.setProgress(progress); //锚点}if (position != null) {position.setProgress(progress); //位置}if (scale != null) {scale.setProgress(progress); //缩放}if (rotation != null) {rotation.setProgress(progress); //旋转}if (skewAngle != null) {skewAngle.setProgress(progress); //斜角
}
4.3Layer请求刷新
在设置完progress之后,各个Layer就会触发onValueChanged()方法,并请求刷新自己:
BaseLayer.java
public void onValueChanged() {invalidateSelf();}private void invalidateSelf() {lottieDrawable.invalidateSelf();}
接着会调用到LottieDrawable的绘制:
LottieDrawable.java
public void invalidateSelf() {final Callback callback = getCallback();if (callback != null) {callback.invalidateDrawable(this);}}
4.4Layer绘制
当自定义的Drawable需要重绘的时候,就需要调用invalidateDrawable方法,请求绘制自己,然后会通过ImageView的onDraw方法里调用到Drawable的draw方法:
Drawable.java
public void draw(@NonNull Canvas canvas) {drawInternal(canvas);}
接下来就会调用到CompositionLayer中的draw方法,在CompositionLayer中又会把实际绘制的工作交给各个Layer去完成:
CompositionLayer.java
draw(Canvas canvas, Matrix parentMatrix, int parentAlpha) {drawLayer(canvas, matrix, alpha);
}drawLayer(Canvas canvas, Matrix parentMatrix, int parentAlpha) {for (int i = layers.size() - 1; i >= 0; i--) {BaseLayer layer = layers.get(i);layer.draw(canvas, parentMatrix, childAlpha);}
}
不同的Layer层,对draw的实现也不一样,比如ImageLayer是drawBitmap,绘制前会在父类调关键帧动画计算出来的参数,对Bitmap做位移、旋转、透明度变化等等变化操作,最后才是使用Canvas做绘制:
BaseLayer.java:
public void draw(Canvas canvas, Matrix parentMatrix, int parentAlpha) {matrix.preConcat(transform.getMatrix()); //通过关键帧动画做变化操作drawLayer(canvas, matrix, alpha); //调用子类的绘制
}
ImageLayer.java
@Override public void drawLayer(@NonNull Canvas canvas, Matrix parentMatrix, int parentAlpha) {Bitmap bitmap = getBitmap(); //读取图片内容canvas.drawBitmap(bitmap, src, dst , paint);//绘制图片
}
至此,一帧动画的绘制就完成了,后面会由LottieValueAnimator的doFrame回调不停地重复上面的步骤,实现连续的动画效果。