安卓动画
最近业务太多,好久没更新。。花了两个晚上研究了一些lottie框架的实现,学到了一些思路,有机会可以把view绘制深入学习一下,ok开始。
https://github.com/airbnb/lottie-android
Lottie,Airbnb开源的一个牛逼的动画框架,绚丽的动画效果令人瞠目。
没错这在以往的意识来看是根本不可能实现的动画效果,那么究竟它是如何实现的呢?
初探
打开LottieSample工程,并将它运行起来,首页就可以看到上图中间的这个动画效果,而代码实现更是简单到没朋友。
xml:
java代码:
没错就是初始化了一个LottieAnimationView并且调用playAnimation()方法,就出现了上图的动画效果,这里注意到在xml初始化参数中有个lottie_fileName参数,传了一个貌似是json文件路径,而在assets的Logo目录下,确实有个LogoSmall.json文件,打开一看懵逼了,完全看不懂。
原来这个json文件的内容不是手写的,而是软件生成的,设计师可以使用Adobe的 After Effects(简称 AE)工具制作这个动画,在AE中安装一个叫做Bodymovin的插件,使用这个插件可以将动画效果生成一个json文件,而这个json文件通过LottieAnimationView解析并最终生成绚丽的动画效果展示在我们面前。
使用方法
Lottie supports API 14 and above,要求4.0以上
依赖
dependencies {
compile 'com.airbnb.android:lottie:1.5.1'
}
使用方法一:初始化一个LottieAnimationView
只接受这三个参数,语意清楚就不多解释了。
也可以通过java代码设置
LottieAnimationView animationView = (LottieAnimationView) findViewById(R.id.animation_view);
animationView.setAnimation("hello-world.json");
animationView.loop(true);
setAnimation有三个方法
其中String是fileName,是在assets目录下的文件,CacheStrategy表示缓存策略,
代表使用何种策略进行存储,默认为None即不存储,而使用时会优先从内存缓存中命中读取,从而减小IO开销。
JSONObject直接传入一段json数据,可以通过网络获取一段json进行解析处理。
使用方法二:使用LottieComposition
在LottieComposition中提供了三种from方法,可以接受assets文件名、json对象、流对象三种参数,Sync表示同步,但是却是包可见方法,并不能被外部调用。
LottieComposition.fromJson(getResources(), jsonObject, new LottieComposition.OnCompositionLoadedListener() {
@Override
public void onCompositionLoaded(LottieComposition composition) {
animationView.setComposition(composition);
animationView.playAnimation();
}
});
外部调用时只提供异步方法,使用AsyncTask进行异步调用,将JsonObject的解析处理过程放在异步线程处理,并将解析生成的LottieComposition对象回调主线程,因为这个json对象可能有上百k之大,所以整个处理过程的复杂度和耗时还是很高的,所以不要在ui线程中解析处理。
一点想法:
我们可以通过请求的方式获取json对象,并将解析的过程放在网络请求的异步线程中处理,使用反射调用同步方法,将调用放在异步线程中执行,这样就可以将整个过程请求和解析的过程封装在一起。
注意点:
LottieAnimationView内部有个LottieDrawable对象,setComposition方法实质上是将LottieComposition应用到LottieDrawable上,官方readme上有这样一段说明
但应该是后面改过,LottieDrawable是包可见的,外部无法调用到,并且在LottieDrawable类注释上有这样一段描述。
推荐使用LottieAnimationView而不是直接使用LottieDrawable,因为LottieDrawable的回收LottieAnimationView帮你做了,而自己操作LottieDrawable需要考虑的回收调用。
所以仅推荐以上两种用法,不推荐直接使用Drawable的方式除非一定需要。
源码解析
好了,说完用法,要来看看到底这个过程发生了什么。
有两个重要的过程
一、json文件解析成LottieComposition的过程
所有的文件解析过程都会走到LottieComposition下的fromJsonSync方法,返回一个LottieComposition对象,中间都是对jsonObject的解析过程,将jsonObject中的信息解析到LottieComposition对象中。
static LottieComposition fromJsonSync(Resources res, JSONObject json) {
LottieComposition composition = new LottieComposition(res);
···
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);
}
····
return composition;
}
这段代码就是把jsonobject中的数据赋值给LottieComposition对象变量,看下图LottieComposition的变量。
bounds代表边界,start和end代表开始和结束时间,duration为时长,scale为为density。Layer就是图层的概念,里面存放的是图层的数据,在循环遍历jsonLayers生成Layer对象时调用了fromJson方法,同样的也是解析和赋值过程。
static Layer fromJson(JSONObject json, LottieComposition composition) {
Layer layer = new Layer(composition);
····
return layer;
}
以上为Layer类中的变量,除了基础变量外,会看到红框中的变量,这些变量是跟动画相关的参数,都是AnimatableValue的实现类。
AnimatableValue的继承关系如图,看样子是控制颜色、scale、path等基础动画的。
那么生成的LottieComposition对象可以理解成一个包含所有图层动画信息的对象,等下看看这些变量是如何被使用的。
二、生成LayerView树
生成的LottieComposition是通过LottieDrawable的setComposition方法将动画信息进行设置的,核心调用方法为buildLayersForComposition。
private void buildLayersForComposition(LottieComposition composition) {
···
LongSparseArray layerMap = new LongSparseArray<>(composition.getLayers().size());
List layers = new ArrayList<>(composition.getLayers().size());
LayerView maskedLayer = null;
for (int i = composition.getLayers().size() - 1; i >= 0; i--) {
Layer layer = composition.getLayers().get(i);
LayerView layerView;
if (maskedLayer == null) {
layerView =
new LayerView(layer, composition, getCallback(), mainBitmap, maskBitmap, matteBitmap);
} else {
···
layerView =
new LayerView(layer, composition, getCallback(), mainBitmapForMatte, maskBitmapForMatte,
null);
}
layerMap.put(layerView.getId(), layerView);
if (maskedLayer != null) {
maskedLayer.setMatteLayer(layerView);
maskedLayer = null;
} else {
layers.add(layerView);
if (layer.getMatteType() == Layer.MatteType.Add) {
maskedLayer = 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);
}
}
}
将之前解析出来的Layers数据倒序遍历并生成同等数量的LayerView,将LayerView通过addLayer方法添加到layers列表里面,这段代码执行完,就生成了一个LayerView的树状结构,以LottieDrawable为根节点(LottieDrawable也是继承自AnimatableLayer,跟LayerView相同)。
void addLayer(AnimatableLayer layer) {
layer.parentLayer = this;
layers.add(layer);
layer.setProgress(progress);
invalidateSelf();
}
在LayerView的构造器中有个方法:
private void setupForModel() {
setBackgroundColor(layerModel.getSolidColor());
setBounds(0, 0, layerModel.getSolidWidth(), layerModel.getSolidHeight());
setPosition(layerModel.getPosition().createAnimation());
setAnchorPoint(layerModel.getAnchor().createAnimation());
setTransform(layerModel.getScale().createAnimation());
setRotation(layerModel.getRotation().createAnimation());
setAlpha(layerModel.getOpacity().createAnimation());
setVisible(layerModel.hasInAnimation(), false);
List
这里的layerModel就是刚才解析出来的Layer,这里用到了刚才红框圈起来的那些变量,调用了AnimatableValue的createAnimation方法,生成了一个KeyframeAnimation对象,查看KeyframeAnimation,发现是抽象类,可以看到有几个关键的变量。
首先有个AnimationListener的list,通过观察者模式修改订阅者的信息,等下看看谁是订阅者。还有个progress变量和setProgress方法,应为进度控制。
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++) {
listeners.get(i).onValueChanged(value);
}
}
调用setProgress方法,会将getValue的结果传递给所有的订阅者。
拿ColorKeyframeAnimation的getValue的实现类为例
float percentageIntoFrame = 0;
if (!isDiscrete) {
percentageIntoFrame = (progress - startKeytime) / (endKeytime - startKeytime);
if (interpolators != null) {
percentageIntoFrame =
interpolators.get(keyframeIndex).getInterpolation(percentageIntoFrame);
}
}
int startColor = values.get(keyframeIndex);
int endColor = values.get(keyframeIndex + 1);
return (Integer) argbEvaluator.evaluate(percentageIntoFrame, startColor, endColor);
以上这段代码是getValue的具体实现,可以看到是将开始颜色和结束颜色通过progress计算一个当前进度值,并计算介于两个颜色的中间颜色。
其他类似。
最后再看一下AnimatableLayer的变量
每个图层会有自己的parentLayer,会有平移动画、透明度动画、旋转动画、位置及进度信息,这些都放在animations列表里面,同时还有个layers列表,表示当前层还会包含的一些图层信息。
所以第二步可以理解为把第一步的信息生成AnimatableLayer树的过程,包含所有的图层实现,进度控制,动画信息,都已经准备好等待被调用了。
三、动画执行
最后来说动画执行,调用了playAnimation方法,最终是调用到一个属性动画执行,
private final ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override public void onAnimationUpdate(ValueAnimator animation) {
setProgress(animation.getAnimatedFraction());
}
});
属性动画的执行是通过调用setProgress。
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);
}
}
刚才提到这是个树状结构,所以通过修改progress,整个树就运作起来,通过layers.setProgress设置所有子图层的progress,子图层又包含了animations和layers,每个图层的animations存放了很多的AnimatableValue,通过setProgress,将修改的value值回调订阅者,而订阅者其实就是LottieDrawable,从根节点开始invalidateSelf,调用到draw方法中进行绘制。
@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);
}
void applyTransformForLayer(@Nullable Canvas canvas, AnimatableLayer layer) {
if (canvas == null) {
return;
}
// TODO: Determine if these null checks are necessary.
if (layer.position != null) {
PointF position = layer.position.getValue();
if (position.x != 0 || position.y != 0) {
canvas.translate(position.x, position.y);
}
}
if (layer.rotation != null) {
float rotation = layer.rotation.getValue();
if (rotation != 0f) {
canvas.rotate(rotation);
}
}
if (layer.transform != null) {
ScaleXY scale = layer.transform.getValue();
if (scale.getScaleX() != 1f || scale.getScaleY() != 1f) {
canvas.scale(scale.getScaleX(), scale.getScaleY());
}
}
if (layer.anchorPoint != null) {
PointF anchorPoint = layer.anchorPoint.getValue();
if (anchorPoint.x != 0 || anchorPoint.y != 0) {
canvas.translate(-anchorPoint.x, -anchorPoint.y);
}
}
}
看到这里,明白了,每次value值发生变化,drawable就会重绘,所有的图层都会进行绘制,重绘时使用新的值进行绘制,从而完成了动画的变化。简单点说,就是每个progress的值,会对应每个图层中的一个状态,progress的改变,就是把这些状态不断绘制出来,从而实现了动画的效果。
一开始以为是属性动画相关,没想到深入到view的绘制,实现相当复杂,�膜拜大神。