前段时间airbnb开源的动画库Lottie得到了不错的反响,旨在解决Android、IOS、RN 上面开发动画成本高、表现不一致的问题,可以说降低了三端动画的开发成本。
项目地址:https://github.com/airbnb/lottie-android
先上几个git上的效果:
如果需要在3端都分别完成这些动画,可能就需要折磨设计&开发同学了。人肉写出这些效果简直是处女座也无法完成的一件事。
Lottie在这件事上就是来拯救移动开发程序员的。
Lottie借助AE生成动画,再利用AE插件bodymovin来导出可描述的json文件,而Lottie负责在不同端上解析json文件完成动画的绘制。
从设计思路上可以看出,这确实是一个很好的解法,Lottie抹平了各端的差异性,通过统一描述(JSON)来表述动画。这看上去和近年来很火的Weex思路一致。
作为一个Android搬砖程序员,接下来的篇幅主要以Lottie Android SDK(ios也不会呀~~~)来分析一下Lottie的解法。
先来感受一下用法,感觉还是很清爽的:
Dependency:
java
compile 'com.airbnb.android:lottie:1.5.3'
使用lottie_fileName="hello-world.json"
来指定动画路径,也可以使用:setAnimation("hello-world.json")
来指定。
java
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:lottie_fileName="hello-world.json"
app:lottie_loop="true"
app:lottie_autoPlay="true" />
or
java
LottieAnimationView animationView = (LottieAnimationView) findViewById(R.id.animation_view);
animationView.setAnimation("hello-world.json");
animationView.loop(true);
然后你就可以像使用普通动画对待Lottie了。
我们先从能接触到的类com.airbnb.lottie.LottieAnimationView
看起,所有的逻辑处理、json解析、绘制工作都在这里完成。
可以看到这个是AppCompatImageView
,里面有个很重要的变量:
LottieDrawable lottieDrawable;
LottieAnimationView在初始化时会调用setImageDrawable(lottieDrawable);
方法,使用LottieDrawable
作为ImageDrawable。并且同时重写invalidateDrawable
方法,当需要invalidate时通知lottieDrawable。
从类结构看,继承自AnimatableLayer
,里面实现了Drawable
需要实现的方法,主要是public void draw(@NonNull Canvas canvas)
用来实现Drawable的绘制工作。
我们先来看看父类 AnimatableLayer 定义了什么行为。
首先当然是来看最重要的:draw
public void draw(@NonNull Canvas canvas) {
int saveCount = canvas.save();
applyTransformForLayer(canvas, this);
int backgroundAlpha = Color.alpha(backgroundColor);
...
canvas.drawRect(getBounds(), solidBackgroundPaint);
for (int i = 0; i < layers.size(); i++) {
layers.get(i).draw(canvas);
}
canvas.restoreToCount(saveCount);
}
void applyTransformForLayer(@Nullable Canvas canvas, AnimatableLayer layer) {
...
PointF position = layer.transform.getPosition().getValue();
canvas.translate(position.x, position.y);
//省略 rotation scale anchorPoint等变换
}
可以看到,在绘制前,调用applyTransformForLayer
对画布canvas进行了transform
操作。接着绘制background
、layers
。
而layers的定义是:final List
每一层,都是一个AnimatableLayer
对象。
我们来看看AnimatableLayer
的子类。
我们目前大概可以分析出,Lottie 根据JSON 构造了一个树形结构,root节点是LottieDrawable,将绘制的元素添加到了LottieDrawable.layers
中。递归的描述每一个元素。
我们接着看“root”:LottieDrawable
都做了些什么。
由于Drawable本身只是负责绘制工作,并不会像动画一样,有start、end、duration
等属性。因此在LottieDrawable
引入了一个简单的animator = ValueAnimator.ofFloat(0f, 1f)
来解决这个问题。
所有对动画的操作(repeat、start),都是对这个ValueAnimator
元素的操作。
从上面可以看到,调用父类AnimatableLayer
的setProgerss
会通知animations、layers
。之前我们已经了解过layers,那么animations是什么呢?来看定义:
private final List
BaseKeyframeAnimation, ?>
从字面意思大概是用来描述关键帧的动画。
做过动画的朋友大概清楚“关键帧”的概念。BaseKeyframeAnimation
的意义就是存放了一个动画中所有的关键帧,以及每一帧的过度进度。
listeners
可以注册对动画的监听,当外界调用setProgerss
时,会通知给监听者。keyframes
存放了当前动画所有关键帧。最终对外的方法是:
abstract A getValue(Keyframe keyframe, float keyframeProgress);
从入参看很清晰,传入了当前所在的关键帧,以及帧进度。
KeyFrame是对每一帧的描述,字段如下:
LottieComposition
是当前帧的所有数据,我们后面会讲到。并且提供了静态工厂方法来生成一个KeyFrame。将JSON文件解析成一个KeyFrame对象。
前面提到的animations
是在何时注册的呢?
我们在AnimatableLayer.setTransform
方法中看到了注册方法。具体如下:
KeyframeAnimation.AnimationListener pointChangedListener = ()->invalidateSelf();
void setTransform(TransformKeyframeAnimation transform) {
BaseKeyframeAnimation, PointF> anchorPoint = transform.getAnchorPoint();
...
anchorPoint.addUpdateListener(pointChangedListener);
addAnimation(opacity);
invalidateSelf();
}
可以看到,在addAnimation后以及 注册的updatelistener中调用了invalidateSelf()
方法。而invalidateSelf()
方法会触发draw流程。draw 会递归的调用layers
进行绘制。由于每一个layer都拥有当前的 progress,因此就可以正确的绘制出来啦。
解析工作在调用animationView.setAnimation
后,就开始了。
JSON 解析 会用InputStream
异步读取json内容,经过处理后封装成LottieComposition
对象。
当加载完成后,会回调animationView.setComposition
方法。animationView会接着调用LottieDrawable.setComposition
。
void setComposition(LottieComposition composition) {
...
buildLayersForComposition(composition);
...
}
这里完成了前面提到的 layers
的构建:
private void buildLayersForComposition(LottieComposition composition) {
...
LongSparseArray layerMap = new LongSparseArray<>(composition.getLayers().size());
List layers = new ArrayList<>(composition.getLayers().size());
LayerView mattedLayer = null;
for (int i = composition.getLayers().size() - 1; i >= 0; i--) {
Layer layer = composition.getLayers().get(i);
LayerView layerView;
layerView = new LayerView(layer, composition, this, canvasPool);
layerMap.put(layerView.getId(), layerView);
if (mattedLayer != null) {
mattedLayer.setMatteLayer(layerView);
mattedLayer = null;
} else {
layers.add(layerView);
if (layer.getMatteType() == Layer.MatteType.Add) {
mattedLayer = layerView;
} else if (layer.getMatteType() == Layer.MatteType.Invert) {
mattedLayer = layerView;
}
}
}
for (int i = 0; i < layers.size(); i++) {
LayerView layerView = layers.get(i);
addLayer(layerView);
}
for (int i = 0; i < layerMap.size(); i++) {
long key = layerMap.keyAt(i);
LayerView layerView = layerMap.get(key);
LayerView parentLayer = layerMap.get(layerView.getLayerModel().getParentId());
if (parentLayer != null) {
layerView.setParentLayer(parentLayer);
}
}
}
里面做了一次转换,构造了一个LayerView
对象,这个是关键所在。
LayerView也是继承自 AnimatableLayer
。 完成了所有Layer类型的转换。
在初始化方式中会调用:setupForModel();
方法。
private void setupForModel() {
...
switch (layerModel.getLayerType()) {
case Shape:
setupShapeLayer();
break;
case PreComp:
setupPreCompLayer();
break;
}
...
}
private void setupShapeLayer() {
List
摘取了setupShapeLayer
的代码片段,里面会根据不同的数据类型,生成不同的LayerView,这些细节就不再做细究了。
首先给Lottie的工程师一个star。
本文只是对Lottie的主要逻辑进行了梳理,没有梳理动画格式之类,里面比较复杂的的部分是不同的Layer的绘制、KeyFrame的计算。这部分由于对AE以及bodymovin不太了解就没有深究了。旨在掌握Lottie的大体设计。如有错误之处,请指出。
最后吐槽一波Lottie的代码结构,虽然源码不多,但是这个一层结构。。。。