安卓動(dòng)畫(huà)
最近業(yè)務(wù)太多,好久沒(méi)更新。。花了兩個(gè)晚上研究了一些lottie框架的實(shí)現(xiàn),學(xué)到了一些思路,有機(jī)會(huì)可以把view繪制深入學(xué)習(xí)一下,ok開(kāi)始。
https://github.com/airbnb/lottie-android
Lottie,Airbnb開(kāi)源的一個(gè)牛逼的動(dòng)畫(huà)框架,絢麗的動(dòng)畫(huà)效果令人瞠目。
沒(méi)錯(cuò)這在以往的意識(shí)來(lái)看是根本不可能實(shí)現(xiàn)的動(dòng)畫(huà)效果,那么究竟它是如何實(shí)現(xiàn)的呢?
初探
打開(kāi)LottieSample工程,并將它運(yùn)行起來(lái),首頁(yè)就可以看到上圖中間的這個(gè)動(dòng)畫(huà)效果,而代碼實(shí)現(xiàn)更是簡(jiǎn)單到?jīng)]朋友。
xml:
java代碼:
沒(méi)錯(cuò)就是初始化了一個(gè)LottieAnimationView并且調(diào)用playAnimation()方法,就出現(xiàn)了上圖的動(dòng)畫(huà)效果,這里注意到在xml初始化參數(shù)中有個(gè)lottie_fileName參數(shù),傳了一個(gè)貌似是json文件路徑,而在assets的Logo目錄下,確實(shí)有個(gè)LogoSmall.json文件,打開(kāi)一看懵逼了,完全看不懂。
原來(lái)這個(gè)json文件的內(nèi)容不是手寫(xiě)的,而是軟件生成的,設(shè)計(jì)師可以使用Adobe的 After Effects(簡(jiǎn)稱 AE)工具制作這個(gè)動(dòng)畫(huà),在AE中安裝一個(gè)叫做Bodymovin的插件,使用這個(gè)插件可以將動(dòng)畫(huà)效果生成一個(gè)json文件,而這個(gè)json文件通過(guò)LottieAnimationView解析并最終生成絢麗的動(dòng)畫(huà)效果展示在我們面前。
使用方法
Lottie supports API 14 and above,要求4.0以上
依賴
dependencies {
compile 'com.airbnb.android:lottie:1.5.1'
}
使用方法一:初始化一個(gè)LottieAnimationView
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/animation_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:lottie_fileName="hello-world.json"
app:lottie_loop="true"
app:lottie_autoPlay="true" />
只接受這三個(gè)參數(shù),語(yǔ)意清楚就不多解釋了。
也可以通過(guò)java代碼設(shè)置
LottieAnimationView animationView = (LottieAnimationView) findViewById(R.id.animation_view);
animationView.setAnimation("hello-world.json");
animationView.loop(true);
setAnimation有三個(gè)方法
其中String是fileName,是在assets目錄下的文件,CacheStrategy表示緩存策略,
代表使用何種策略進(jìn)行存儲(chǔ),默認(rèn)為None即不存儲(chǔ),而使用時(shí)會(huì)優(yōu)先從內(nèi)存緩存中命中讀取,從而減小IO開(kāi)銷(xiāo)。
JSONObject直接傳入一段json數(shù)據(jù),可以通過(guò)網(wǎng)絡(luò)獲取一段json進(jìn)行解析處理。
使用方法二:使用LottieComposition
在LottieComposition中提供了三種from方法,可以接受assets文件名、json對(duì)象、流對(duì)象三種參數(shù),Sync表示同步,但是卻是包可見(jiàn)方法,并不能被外部調(diào)用。
LottieComposition.fromJson(getResources(), jsonObject, new LottieComposition.OnCompositionLoadedListener() {
@Override
public void onCompositionLoaded(LottieComposition composition) {
animationView.setComposition(composition);
animationView.playAnimation();
}
});
外部調(diào)用時(shí)只提供異步方法,使用AsyncTask進(jìn)行異步調(diào)用,將JsonObject的解析處理過(guò)程放在異步線程處理,并將解析生成的LottieComposition對(duì)象回調(diào)主線程,因?yàn)檫@個(gè)json對(duì)象可能有上百k之大,所以整個(gè)處理過(guò)程的復(fù)雜度和耗時(shí)還是很高的,所以不要在ui線程中解析處理。
一點(diǎn)想法:
我們可以通過(guò)請(qǐng)求的方式獲取json對(duì)象,并將解析的過(guò)程放在網(wǎng)絡(luò)請(qǐng)求的異步線程中處理,使用反射調(diào)用同步方法,將調(diào)用放在異步線程中執(zhí)行,這樣就可以將整個(gè)過(guò)程請(qǐng)求和解析的過(guò)程封裝在一起。
注意點(diǎn):
LottieAnimationView內(nèi)部有個(gè)LottieDrawable對(duì)象,setComposition方法實(shí)質(zhì)上是將LottieComposition應(yīng)用到LottieDrawable上,官方readme上有這樣一段說(shuō)明
但應(yīng)該是后面改過(guò),LottieDrawable是包可見(jiàn)的,外部無(wú)法調(diào)用到,并且在LottieDrawable類(lèi)注釋上有這樣一段描述。
推薦使用LottieAnimationView而不是直接使用LottieDrawable,因?yàn)長(zhǎng)ottieDrawable的回收LottieAnimationView幫你做了,而自己操作LottieDrawable需要考慮的回收調(diào)用。
所以僅推薦以上兩種用法,不推薦直接使用Drawable的方式除非一定需要。
源碼解析
好了,說(shuō)完用法,要來(lái)看看到底這個(gè)過(guò)程發(fā)生了什么。
有兩個(gè)重要的過(guò)程
一、json文件解析成LottieComposition的過(guò)程
所有的文件解析過(guò)程都會(huì)走到LottieComposition下的fromJsonSync方法,返回一個(gè)LottieComposition對(duì)象,中間都是對(duì)jsonObject的解析過(guò)程,將jsonObject中的信息解析到LottieComposition對(duì)象中。
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中的數(shù)據(jù)賦值給LottieComposition對(duì)象變量,看下圖LottieComposition的變量。
bounds代表邊界,start和end代表開(kāi)始和結(jié)束時(shí)間,duration為時(shí)長(zhǎng),scale為為density。Layer就是圖層的概念,里面存放的是圖層的數(shù)據(jù),在循環(huán)遍歷jsonLayers生成Layer對(duì)象時(shí)調(diào)用了fromJson方法,同樣的也是解析和賦值過(guò)程。
static Layer fromJson(JSONObject json, LottieComposition composition) {
Layer layer = new Layer(composition);
····
return layer;
}
以上為L(zhǎng)ayer類(lèi)中的變量,除了基礎(chǔ)變量外,會(huì)看到紅框中的變量,這些變量是跟動(dòng)畫(huà)相關(guān)的參數(shù),都是AnimatableValue的實(shí)現(xiàn)類(lèi)。
AnimatableValue的繼承關(guān)系如圖,看樣子是控制顏色、scale、path等基礎(chǔ)動(dòng)畫(huà)的。
那么生成的LottieComposition對(duì)象可以理解成一個(gè)包含所有圖層動(dòng)畫(huà)信息的對(duì)象,等下看看這些變量是如何被使用的。
二、生成LayerView樹(shù)
生成的LottieComposition是通過(guò)LottieDrawable的setComposition方法將動(dòng)畫(huà)信息進(jìn)行設(shè)置的,核心調(diào)用方法為buildLayersForComposition。
private void buildLayersForComposition(LottieComposition composition) {
···
LongSparseArray<LayerView> layerMap = new LongSparseArray<>(composition.getLayers().size());
List<LayerView> 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);
}
}
}
將之前解析出來(lái)的Layers數(shù)據(jù)倒序遍歷并生成同等數(shù)量的LayerView,將LayerView通過(guò)addLayer方法添加到layers列表里面,這段代碼執(zhí)行完,就生成了一個(gè)LayerView的樹(shù)狀結(jié)構(gòu),以LottieDrawable為根節(jié)點(diǎn)(LottieDrawable也是繼承自AnimatableLayer,跟LayerView相同)。
void addLayer(AnimatableLayer layer) {
layer.parentLayer = this;
layers.add(layer);
layer.setProgress(progress);
invalidateSelf();
}
在LayerView的構(gòu)造器中有個(gè)方法:
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<Object> reversedItems = new ArrayList<>(layerModel.getShapes());
Collections.reverse(reversedItems);
Transform currentTransform = null;
ShapeTrimPath currentTrimPath = null;
ShapeFill currentFill = null;
ShapeStroke currentStroke = null;
for (int i = 0; i < reversedItems.size(); i++) {
Object item = reversedItems.get(i);
if (item instanceof ShapeGroup) {
GroupLayerView groupLayer = new GroupLayerView((ShapeGroup) item, currentFill,
currentStroke, currentTrimPath, currentTransform, getCallback());
addLayer(groupLayer);
} else if (item instanceof ShapeTransform) {
currentTransform = (ShapeTransform) item;
} else if (item instanceof ShapeFill) {
currentFill = (ShapeFill) item;
} else if (item instanceof ShapeTrimPath) {
currentTrimPath = (ShapeTrimPath) item;
} else if (item instanceof ShapeStroke) {
currentStroke = (ShapeStroke) item;
} else if (item instanceof ShapePath) {
ShapePath shapePath = (ShapePath) item;
ShapeLayerView shapeLayer =
new ShapeLayerView(shapePath, currentFill, currentStroke, currentTrimPath,
new ShapeTransform(composition), getCallback());
addLayer(shapeLayer);
} else if (item instanceof RectangleShape) {
RectangleShape shapeRect = (RectangleShape) item;
RectLayer shapeLayer =
new RectLayer(shapeRect, currentFill, currentStroke, new ShapeTransform(composition),
getCallback());
addLayer(shapeLayer);
} else if (item instanceof CircleShape) {
CircleShape shapeCircle = (CircleShape) item;
EllipseShapeLayer shapeLayer =
new EllipseShapeLayer(shapeCircle, currentFill, currentStroke, currentTrimPath,
new ShapeTransform(composition), getCallback());
addLayer(shapeLayer);
}
}
if (maskBitmap != null && layerModel.getMasks() != null && !layerModel.getMasks().isEmpty()) {
setMask(new MaskLayer(layerModel.getMasks(), getCallback()));
maskCanvas = new Canvas(maskBitmap);
}
buildAnimations();
}
這里的layerModel就是剛才解析出來(lái)的Layer,這里用到了剛才紅框圈起來(lái)的那些變量,調(diào)用了AnimatableValue的createAnimation方法,生成了一個(gè)KeyframeAnimation對(duì)象,查看KeyframeAnimation,發(fā)現(xiàn)是抽象類(lèi),可以看到有幾個(gè)關(guān)鍵的變量。
首先有個(gè)AnimationListener的list,通過(guò)觀察者模式修改訂閱者的信息,等下看看誰(shuí)是訂閱者。還有個(gè)progress變量和setProgress方法,應(yīng)為進(jìn)度控制。
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);
}
}
調(diào)用setProgress方法,會(huì)將getValue的結(jié)果傳遞給所有的訂閱者。
拿ColorKeyframeAnimation的getValue的實(shí)現(xiàn)類(lèi)為例
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的具體實(shí)現(xiàn),可以看到是將開(kāi)始顏色和結(jié)束顏色通過(guò)progress計(jì)算一個(gè)當(dāng)前進(jìn)度值,并計(jì)算介于兩個(gè)顏色的中間顏色。
其他類(lèi)似。
最后再看一下AnimatableLayer的變量
每個(gè)圖層會(huì)有自己的parentLayer,會(huì)有平移動(dòng)畫(huà)、透明度動(dòng)畫(huà)、旋轉(zhuǎn)動(dòng)畫(huà)、位置及進(jìn)度信息,這些都放在animations列表里面,同時(shí)還有個(gè)layers列表,表示當(dāng)前層還會(huì)包含的一些圖層信息。
所以第二步可以理解為把第一步的信息生成AnimatableLayer樹(shù)的過(guò)程,包含所有的圖層實(shí)現(xiàn),進(jìn)度控制,動(dòng)畫(huà)信息,都已經(jīng)準(zhǔn)備好等待被調(diào)用了。
三、動(dòng)畫(huà)執(zhí)行
最后來(lái)說(shuō)動(dòng)畫(huà)執(zhí)行,調(diào)用了playAnimation方法,最終是調(diào)用到一個(gè)屬性動(dòng)畫(huà)執(zhí)行,
private final ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override public void onAnimationUpdate(ValueAnimator animation) {
setProgress(animation.getAnimatedFraction());
}
});
屬性動(dòng)畫(huà)的執(zhí)行是通過(guò)調(diào)用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);
}
}
剛才提到這是個(gè)樹(shù)狀結(jié)構(gòu),所以通過(guò)修改progress,整個(gè)樹(shù)就運(yùn)作起來(lái),通過(guò)layers.setProgress設(shè)置所有子圖層的progress,子圖層又包含了animations和layers,每個(gè)圖層的animations存放了很多的AnimatableValue,通過(guò)setProgress,將修改的value值回調(diào)訂閱者,而訂閱者其實(shí)就是LottieDrawable,從根節(jié)點(diǎn)開(kāi)始invalidateSelf,調(diào)用到draw方法中進(jìn)行繪制。
@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值發(fā)生變化,drawable就會(huì)重繪,所有的圖層都會(huì)進(jìn)行繪制,重繪時(shí)使用新的值進(jìn)行繪制,從而完成了動(dòng)畫(huà)的變化。簡(jiǎn)單點(diǎn)說(shuō),就是每個(gè)progress的值,會(huì)對(duì)應(yīng)每個(gè)圖層中的一個(gè)狀態(tài),progress的改變,就是把這些狀態(tài)不斷繪制出來(lái),從而實(shí)現(xiàn)了動(dòng)畫(huà)的效果。
一開(kāi)始以為是屬性動(dòng)畫(huà)相關(guān),沒(méi)想到深入到view的繪制,實(shí)現(xiàn)相當(dāng)復(fù)雜,?膜拜大神。