lottie簡介
Lottie是Airbnb開源的一個(gè)動畫渲染庫始锚,同時(shí)支持Android刽酱、IOS、React Native和Web平臺瞧捌,Lottie目前只支持渲染播放AE動畫棵里。Lottie使用bobymovin(After Effects插件)導(dǎo)出的json數(shù)據(jù)作為動畫數(shù)據(jù)源。
lottie的優(yōu)缺點(diǎn)
優(yōu)點(diǎn):
- 相對與矢量動畫姐呐,lottie的操作更為簡單殿怜,生成文件的操作不需要程序猿完成,而且AE相對于一些矢量圖制作工具更加強(qiáng)大效果更好
矢量圖在線制作工具 https://shapeshifter.design - 使用GIF曙砂,使用幀動畫占用空間大头谜,Android原生不支持GIF動畫的顯示。
- 組合式動畫鸠澈,通過大量代碼實(shí)現(xiàn)復(fù)雜的動畫效果柱告,代碼復(fù)雜,不好調(diào)試笑陈,也會浪費(fèi)很多時(shí)間成本
- Android, iOS, 和React Native多平臺支持
- 降低動畫設(shè)計(jì)和開發(fā)成本
- 完美解決設(shè)計(jì)提供動畫效果與實(shí)現(xiàn)不一致問題
- 不需要ui適配
缺點(diǎn):依然有局限性际度,對于一些復(fù)雜的動畫特效,如高斯模糊等部分AE特效無法實(shí)現(xiàn)涵妥,可能是由于json文件不好描述
框架原理
使用AE工具生成一段json乖菱,Lottie使用json文件來作為動畫數(shù)據(jù)源,然后解析json數(shù)據(jù),根據(jù)解析后的數(shù)據(jù)建立合適的Drawable繪制到View上面窒所,然后不斷觸發(fā)view的繪制
使用
private void play(String name){
// 取消播放
mAnimationView.cancelAnimation();
// 是否循環(huán)播放
mAnimationView.loop(true);
// 設(shè)置播放速率娜氏,例如:2代表播放速率是不設(shè)置時(shí)的二倍
//mAnimationView.setSpeed(2f);
// 開始播放
mAnimationView.playAnimation();
// 暫停播放
mAnimationView.pauseAnimation();
// 設(shè)置播放進(jìn)度
//mAnimationView.setProgress(0.5f);
// 判斷是否正在播放
// mAnimationView.isAnimating();
mAnimationView.setAnimation(name);
mAnimationView.loop(false);
mAnimationView.playAnimation();
}
/**
* 自定義播放動畫和時(shí)長
*/
private void playValueAnimator(){
ValueAnimator valueAnimator = ValueAnimator
.ofFloat(0f, 1f)
.setDuration(5000);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mAnimationView.setProgress((Float) animation.getAnimatedValue());
}
});
valueAnimator.start();
}
動態(tài)屬性
在動畫播放的過程改變一些屬性,如動畫墩新,positon等
/**
* 設(shè)置顏色
*/
private void setColor(){
//Shirt,Group 5,Fill 1都是layer的名稱
KeyPath shirt = new KeyPath("Shirt", "Group 5", "Fill 1");
KeyPath leftArm = new KeyPath("LeftArmWave", "LeftArm", "Group 6", "Fill 1");
KeyPath rightArm = new KeyPath("RightArm", "Group 6", "Fill 1");
//關(guān)鍵path贸弥,需要改變的屬性
mAnimationView.addValueCallback(shirt, LottieProperty.COLOR,
new LottieValueCallback<Integer>(mColorArray[mIndex]){});
mAnimationView.addValueCallback(leftArm, LottieProperty.COLOR,
new LottieValueCallback<Integer>(mColorArray[mIndex]){});
mAnimationView.addValueCallback(rightArm, LottieProperty.COLOR,
new LottieValueCallback<Integer>(mColorArray[mIndex]){});
}
/**
* 設(shè)置彈跳高度
*/
private void setJumpHeight(){
final PointF pointF = new PointF();
mAnimationView.addValueCallback(new KeyPath("Body"), LottieProperty.TRANSFORM_POSITION, new SimpleLottieValueCallback<PointF>() {
@Override
public PointF getValue(LottieFrameInfo<PointF> frameInfo) {
float startX = frameInfo.getStartValue().x;
float startY = frameInfo.getStartValue().y;
float endY = frameInfo.getEndValue().y;
if (startY > endY) {
startY += mJmupArray[mIndex];
} else if (endY > startY) {
endY += mJmupArray[mIndex];
}
pointF.set(startX, MiscUtils.lerp(startY, endY, frameInfo.getInterpolatedKeyframeProgress()));
return pointF;
}
});
}
事件綁定
與手勢事件綁定,但本質(zhì)上還是對positon做操作
private void initData() {
final LottieRelativePointValueCallback largeValueCallback
= new LottieRelativePointValueCallback(new PointF(0f, 0f));
mAnimationView.addValueCallback(new KeyPath("First"), LottieProperty.TRANSFORM_POSITION,
largeValueCallback);
final LottieRelativePointValueCallback mediumValueCallback
= new LottieRelativePointValueCallback(new PointF(0f, 0f));
mAnimationView.addValueCallback(new KeyPath("Fourth"), LottieProperty.TRANSFORM_POSITION,
mediumValueCallback);
final LottieRelativePointValueCallback smallValueCallback
= new LottieRelativePointValueCallback(new PointF(0f, 0f));
mAnimationView.addValueCallback(new KeyPath("Seventh"), LottieProperty.TRANSFORM_POSITION,
smallValueCallback);
//mContainerView的點(diǎn)擊拖動事件委托給ViewDragHelper海渊,ViewDragHelper中對mTargetView做相應(yīng)處理
ViewDragHelper viewDragHelper = ViewDragHelper.create(mContainerView, new ViewDragHelper.Callback() {
/**
* 捕獲拖動的這個(gè)View
*/
@Override
public boolean tryCaptureView(@NonNull View child, int pointerId) {
return child == mTargetView;
}
/**
* 拖動的這個(gè)View的位置發(fā)生變化
*
* @param changedView 當(dāng)前拖動的這個(gè)View
* @param left 距離左邊的距離
* @param top 距離右邊的距離
* @param dx x軸的變化量
* @param dy y軸的變化量
*/
public void onViewPositionChanged(@NonNull View changedView, int left, int top, @Px int dx,
@Px int dy) {
totalDx += dx;
totalDy += dy;
//控制的是圓心然后觸發(fā)重新繪制,就是位置的距離轉(zhuǎn)換一下設(shè)置給新的圓心
//這個(gè)觸摸綁定交互可能不具有參考意義绵疲,因?yàn)閯赢嫑]有特別復(fù)雜,直接canvas畫三個(gè)圓也能達(dá)到同樣的效果
smallValueCallback.setValue(getPoint(totalDx, totalDy, 1.2f));
mediumValueCallback.setValue(getPoint(totalDx, totalDy, 1f));
largeValueCallback.setValue(getPoint(totalDx, totalDy, 0.75f));
}
public int clampViewPositionHorizontal(@NonNull View child, int left, int dx) {
return left;
}
public int clampViewPositionVertical(@NonNull View child, int top, int dy) {
return top;
}
});
mContainerView.setViewDragHelper(viewDragHelper);
}
private PointF getPoint(float dx, float dy, float factor) {
return new PointF(dx * factor, dy * factor);
}
關(guān)于協(xié)議
{
"v": "4.11.1", //使用bodymovin的版本
"fr": 60, //幀率
"ip": 0, //起始關(guān)鍵幀
"op": 180, //結(jié)束關(guān)鍵幀
"w": 300, //視圖的寬度 寬高會根據(jù)屏幕密度做轉(zhuǎn)換成scaleWidth
"h": 300, //視圖的高度
"nm": "Comp 1", //從源碼中未看到對此字段解析
"ddd": 0,
"assets": [], //圖片集合
"layers": [ //圖層集合臣疑,為圖片的本地路徑(assert等等)
{
"ddd": 0,
"ind": 1, //layer的Id盔憨,唯一
"ty": “sh", //layer的類型
"nm": "Shape Layer 1", //layer的名稱,在ae中生成唯一
"sr": 1,
"ks": {}, //外觀信息
"ao": 0,
"shapes": [], //矢量圖形圖層的數(shù)組
"ip": 0, // 該圖層的起始關(guān)鍵幀
"op": 180, //該圖層的結(jié)束關(guān)鍵幀
"st": 0,
"bm": 0
},
{...},
{...},
{...},
]
}
ks中的字段
- a 位置信息
- p 位移信息
- s 縮放信息
- r 翻轉(zhuǎn)信息
- o 不透明度
- so 開始時(shí)不透明度
- eo 結(jié)束時(shí)不透明度
源碼解析
一個(gè)動畫文件的播放過程大概可以分為三部分
- 解析json文件
- view繪制
- 動畫播放
解析json文件
從setAnimation方法點(diǎn)進(jìn)來讯沈,看到在執(zhí)行解析asset文件夾下文件
public void setAnimation(final String assetName) {
this.animationName = assetName;
animationResId = 0;
setCompositionTask(LottieCompositionFactory.fromAsset
(getContext(), assetName));
}
LottieCompositionFactory這個(gè)類有很多解析方法包括raw郁岩,asset等文件夾下
public static LottieTask<LottieComposition> fromAsset(Context context, final String fileName) {
// Prevent accidentally leaking an Activity.
final Context appContext = context.getApplicationContext();
//如果之前緩存過,取緩存缺狠,線程同步的方法问慎,會阻塞主線程
return cache(fileName, new
Callable<LottieResult<LottieComposition>>() {
@Override public LottieResult<LottieComposition> call() {
//該方法就是拿到了json文件的字節(jié)流
return fromAssetSync(appContext, fileName);
}
});
}
拿到文件的字節(jié)流后對內(nèi)容進(jìn)行解析
LottieComposition composition = LottieCompositionParser.parse(reader);
解析時(shí)會對LottieComposition進(jìn)行賦值,拿到以下很多的字段
float scale = Utils.dpScale();
float startFrame = 0f;
float endFrame = 0f;
float frameRate = 0f;
final LongSparseArray<Layer> layerMap = new LongSparseArray<>();
final List<Layer> layers = new ArrayList<>();
int width = 0;
int height = 0;
Map<String, List<Layer>> precomps = new HashMap<>();
Map<String, LottieImageAsset> images = new HashMap<>();
Map<String, Font> fonts = new HashMap<>();
List<Marker> markers = new ArrayList<>();
SparseArrayCompat<FontCharacter> characters = new SparseArrayCompat<>();
LottieTask是一個(gè)線程池挤茄,LottieResult是LottieComposition的結(jié)果或者exception如叼,監(jiān)聽回調(diào)中得到解析后composition數(shù)據(jù)結(jié)構(gòu)
private final LottieListener<LottieComposition> loadedListener = new LottieListener<LottieComposition>() {
@Override public void onResult(LottieComposition composition) {
//得到解析后composition
setComposition(composition);
}
};
drawable的繪制
比較核心的兩個(gè)類
LottieComposition和LottieDrawable將會在下面專門進(jìn)行分析,他們分別進(jìn)行了兩個(gè)重要的工作:json文件的解析和動畫的繪制穷劈。
LottieAnimationView中的setComposition講數(shù)據(jù)結(jié)構(gòu)交給了lottieDrawable
public void setComposition(@NonNull LottieComposition composition) {
if (L.DBG) {
Log.v(TAG, "Set Composition \n" + composition);
}
lottieDrawable.setCallback(this);
this.composition = composition;
//lottieDrawable對解析后composition數(shù)據(jù)做了加工
boolean isNewComposition =
lottieDrawable.setComposition(composition);
enableOrDisableHardwareLayer();
if (getDrawable() == lottieDrawable && !isNewComposition) {
return;
}
setImageDrawable(null);
setImageDrawable(lottieDrawable);
requestLayout();
for (LottieOnCompositionLoadedListener lottieOnCompositionLoadedListener : lottieOnCompositionLoadedListeners) { lottieOnCompositionLoadedListener.onCompositionLoaded(composition);
}
}
lottieDrawable中的setComposition方法中的buildCompositionLayer開始真正的解析layer和繪制
layer算是lottie原理中一個(gè)比較重要的概念笼恰,就是圖層
layer的類型與 AE中的圖層的對應(yīng)關(guān)系為:
- ShapeLayer:形狀圖層
- CompositionLayer:預(yù)合成圖層
- SolidLayer:純色圖層
- ImageLayer:圖片素材圖層
- NullLayer:空圖層
- TextLayer:文本圖層
在android層面可以理解為圖層就是view,在一個(gè)布局viewGroup中有很多的view歇终,就是不斷的繪制這些view來完成這些動畫的社证,LottieComposition對Layer進(jìn)行數(shù)據(jù)的映射,在CompositionLayer中為每一個(gè)layer生成一個(gè)對應(yīng)的LayerView
簡單說就是解析json->layer對象的映射->layer對象為layerview構(gòu)造出各種path等->數(shù)據(jù)全部準(zhǔn)備好就是不斷的驅(qū)使draw方法完成繪制
CompositionLayer中的構(gòu)造方法
public CompositionLayer(LottieDrawable lottieDrawable, Layer layerModel, List<Layer> layerModels,
LottieComposition composition) {
super(lottieDrawable, layerModel);
AnimatableFloatValue timeRemapping = layerModel.getTimeRemapping();
if (timeRemapping != null) {
this.timeRemapping = timeRemapping.createAnimation();
addAnimation(this.timeRemapping);
//noinspection ConstantConditions
this.timeRemapping.addUpdateListener(this);
} else {
this.timeRemapping = null;
}
//hashmap的優(yōu)化數(shù)據(jù)結(jié)構(gòu)
LongSparseArray<BaseLayer> layerMap =
new LongSparseArray<>(composition.getLayers().size());
BaseLayer mattedLayer = null;
//遍歷layer圖層
for (int i = layerModels.size() - 1; i >= 0; i--) {
Layer lm = layerModels.get(i);
BaseLayer layer = BaseLayer.forModel(lm, lottieDrawable, composition);
if (layer == null) {
continue;
}
layerMap.put(layer.getLayerModel().getId(), layer);
if (mattedLayer != null) {
mattedLayer.setMatteLayer(layer);
mattedLayer = null;
} else {
layers.add(0, layer);
switch (lm.getMatteType()) {
case ADD:
case INVERT:
mattedLayer = layer;
break;
}
}
}
//將layer生成各種layerView完成繪制
for (int i = 0; i < layerMap.size(); i++) {
long key = layerMap.keyAt(i);
BaseLayer layerView = layerMap.get(key);
// This shouldn't happen but it appears as if sometimes on pre-lollipop devices when
// compiled with d8, layerView is null sometimes.
// https://github.com/airbnb/lottie-android/issues/524
if (layerView == null) {
continue;
}
BaseLayer parentLayer = layerMap.get(layerView.getLayerModel().getParentId());
if (parentLayer != null) {
layerView.setParentLayer(parentLayer);
}
}
}
父類中根據(jù)不同類型评凝,繪制不同的圖層
static BaseLayer forModel(
Layer layerModel, LottieDrawable drawable, LottieComposition composition) {
switch (layerModel.getLayerType()) {
//形狀圖層,調(diào)用最頻繁
case SHAPE:
return new ShapeLayer(drawable, layerModel);
//預(yù)合成圖層
case PRE_COMP:
return new CompositionLayer(drawable, layerModel,
composition.getPrecomps(layerModel.getRefId()), composition);
//純色圖層
case SOLID:
return new SolidLayer(drawable, layerModel);
//有些會是zip壓縮包中會有圖片追葡,在這里解析成bitmap
case IMAGE:
return new ImageLayer(drawable, layerModel);
//空圖層
case NULL:
return new NullLayer(drawable, layerModel);
//文本圖層
case TEXT:
return new TextLayer(drawable, layerModel);
case UNKNOWN:
default:
// Do nothing
L.warn("Unknown layer type " + layerModel.getLayerType());
return null;
}
}
然后就是通過setImageDrawable(lottieDrawable)將圖像顯示出來,顯示第一幀動畫肥哎。
動畫播放
LottieDrawable構(gòu)造方法中設(shè)置辽俗,animator的監(jiān)聽疾渣,在animator播放的時(shí)候篡诽,這個(gè)回調(diào)就會開始更新progress
public LottieDrawable() {
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
if (compositionLayer != null) {
//根據(jù)animator的進(jìn)度,不斷調(diào)整compositionLayer的progress
compositionLayer.setProgress(animator.getAnimatedValueAbsolute());
}
}
});
}
通過CompositionLayer將setProgress實(shí)現(xiàn)的顯示具體進(jìn)度動畫
@Override
public void setProgress(@FloatRange(from = 0f, to = 1f) float progress) {
super.setProgress(progress);
if (timeRemapping != null) {
float duration = lottieDrawable.getComposition().getDuration();
long remappedTime = (long) (timeRemapping.getValue() * 1000);
progress = remappedTime / duration;
}
if (layerModel.getTimeStretch() != 0) {
progress /= layerModel.getTimeStretch();
}
progress -= layerModel.getStartProgress();
for (int i = layers.size() - 1; i >= 0; i--) {
layers.get(i).setProgress(progress);
}
}
父類中l(wèi)ayer通知進(jìn)度的改變
void setProgress(@FloatRange(from = 0f, to = 1f) float progress) {
// Time stretch should not be applied to the layer transform.
transform.setProgress(progress);
if (mask != null) {
for (int i = 0; i < mask.getMaskAnimations().size(); i++) {
mask.getMaskAnimations().get(i).setProgress(progress);
}
}
if (layerModel.getTimeStretch() != 0) {
progress /= layerModel.getTimeStretch();
}
if (matteLayer != null) {
// The matte layer's time stretch is pre-calculated.
float matteTimeStretch = matteLayer.layerModel.getTimeStretch();
matteLayer.setProgress(progress * matteTimeStretch);
}
for (int i = 0; i < animations.size(); i++) {
//animations會更新BaseKeyframeAnimation.AnimationListener回調(diào)onValueChanged觸發(fā)LottieDrawable重繪
//會調(diào)用invalidateSelf()方法榴捡,該方法會觸發(fā)LottieAnimationView的invalidateDrawable杈女,然后
animations.get(i).setProgress(progress);
}
}
BaseKeyframeAnimation.AnimationListener會粗發(fā)invalidateDrawable的方法
@Override
public void invalidateDrawable(@NonNull Drawable dr) {
if (getDrawable() == lottieDrawable) {
// We always want to invalidate the root drawable so it redraws the whole drawable.
// Eventually it would be great to be able to invalidate just the changed region.
super.invalidateDrawable(lottieDrawable);
} else {
// Otherwise work as regular ImageView
super.invalidateDrawable(dr);
}
}
在LottieDrawable的setComposition()的方法中會開始執(zhí)行一個(gè)ValueAnimation動畫,這個(gè)動畫會驅(qū)使baseLayer的draw()方法不斷執(zhí)行
@Override void drawLayer(Canvas canvas, Matrix parentMatrix, int parentAlpha) {
L.beginSection("CompositionLayer#draw");
canvas.save();
newClipRect.set(0, 0, layerModel.getPreCompWidth(), layerModel.getPreCompHeight());
parentMatrix.mapRect(newClipRect);
for (int i = layers.size() - 1; i >= 0 ; i--) {
boolean nonEmptyClip = true;
if (!newClipRect.isEmpty()) {
nonEmptyClip = canvas.clipRect(newClipRect);
}
if (nonEmptyClip) {
BaseLayer layer = layers.get(i);
layer.draw(canvas, parentMatrix, parentAlpha);
}
}
canvas.restore();
L.endSection("CompositionLayer#draw");
}
總結(jié)
- 創(chuàng)建 LottieAnimationView
- 在LottieAnimationView中創(chuàng)建LottieDrawable
- 在LottieAnimationView中創(chuàng)建compositionLoader,進(jìn)行json文件解析得到LottieComposition达椰,完成數(shù)據(jù)到對象的映射翰蠢。
- 解析完后通過setComposition方法把LottieComposition給lottieDrawable,lottieDrawable在setComposition方法中轉(zhuǎn)換成各種Layer為繪制做準(zhǔn)備比如path啰劲,maritx梁沧,bitmap等等
- 在LottieAnimationView中把lottieDrawable設(shè)置setImageDrawable
- 然后開始動畫lottieDrawable.playAnimation()。
demo地址
源碼中添加了很多注釋
https://github.com/Johncuiqiang/LottieSource
參考
https://blog.csdn.net/weixin_37618354/article/details/84072783
https://blog.csdn.net/dcsff/article/details/80482841
https://blog.csdn.net/xiexiangyu92/article/details/78525456