目錄
- 動畫和繪制的流程
- LayerView樹
- ShapeLayer的分析
- Lottie優劣以及rLottie、PAG的介紹
- 資料
- 收獲
上一篇我們學習分析了Lottie的json解析部分. 這篇我們分析的動畫和渲染部分。
分析的重點:如何組織多圖層layer的關系,控制先后處理不同圖層的繪制以及動畫。
一、動畫和繪制的流程
我們通過入口API函數(LottieDrawable#setComposition、LottieDrawable#playAnimation)來進行分析。
1.1 LottieDrawable#setComposition 流程
public boolean setComposition(LottieComposition composition) {
//......
clearComposition();
this.composition = composition;
//構建圖層layer compositionlayer它的作用有點先andoid View樹中ViewGroup,可以包含其他的View和ViewGroup
//完成CompositionLayer和ContentGroup的初始化 主要是兩個里面TransformKeyframeAnimation
buildCompositionLayer();
//觸發notifyUpdate,進而觸發個Layer的progress的重新計算以及draw的回調(當然此時進度為0,各種判斷之后也不會觸發composition的drawlayer)
animator.setComposition(composition);
//設置當前動畫的進度
setProgress(animator.getAnimatedFraction());
......
}
可以看到setComposition主要調用了buildCompositionLayer和 animator.setComposition來進行CompositionLayer和其他各Layer(json中對應的layers字段)以及 ContentGroup、TransformKeyframeAnimation等初始化。
Lottie動畫中使用最多Layer是CompositionLayer、ShapeLayer以及ImageLayer。
思考:那么什么是ContentGroup、TransformKeyframeAnimation、他們和layer的關系是什么吶?(后面會嘗試分析解答)
1.2 LottieDrawable#playAnimation 流程
1. LottieDrawable.playAnimation
2. LottieValueAnimator.playAnimation
3. LottieValueAnimator.setFrame
4. BaseLottieAnimator.notifyUpdate
5.然后觸發回調(LottieDrawable.progressUpdateListener)AnimatorUpdateListener.onAnimationUpdate
6. CompositionLayer.setProgress --》計算當前的progress,然后倒序設置每個圖層進度 BaseLayer.setProgress
6.1(transform.setProgress(progress))TransformKeyframeAnimation.setProgress 設置矩陣變換的進度(縮放、透明度、位移等)--》需要重點分析
6.2 animations.get(i).setProgress(progress); 遍歷設置每個animation的進度
7. BaseKeyframeAnimation.notifyListeners 回調給監聽者
8. BaseLayer.onValueChanged (invalidateSelf())觸發頁面的重新繪制,--》即LottieDrawable.draw(android.graphics.Canvas, android.graphics.Matrix)
9. compositionLayer.draw(canvas, matrix, alpha) 即 BaseLayer.draw --》這也是一個關鍵的方法
10. drawLayer(canvas, matrix, alpha); 即 BaseLayer.drawLayer這個方法是抽象方法,各layer具體實現
10.1 我們以ImageLayer為例來來看 (重點分析) ImageLayer.drawLayer 首先通過BaseKeyframeAnimation.getValue() 這個就用到前面動畫改變的progress的值,根據差值器獲取到當前的Bitmap
10.2 然后使用canvas來進行繪制,完成圖片的變換
LottieValueAnimator是ValueAnimator的子類,并且實現了Choreographer.FrameCallback接口。通過屬性動畫的進度變換回調以及VSYNC信號的doframe回調來通知Layer進行進度以及值計算,并且通知LottieDrawble進行重新繪制,從而實現json中layers也即各種Layer圖層的動畫和繪制。
而具體的繪制還是有Canvas來實現,可以通過ImageLayer的drawLayer
public void drawLayer(@NonNull Canvas canvas, Matrix parentMatrix, int parentAlpha) {
Bitmap bitmap = getBitmap();
if (bitmap == null || bitmap.isRecycled()) {
return;
}
float density = Utils.dpScale();
paint.setAlpha(parentAlpha);
if (colorFilterAnimation != null) {
paint.setColorFilter(colorFilterAnimation.getValue());
}
//將畫布的當前狀態保存
canvas.save();
//對matrix的變換應用到canvas上的所有對象
canvas.concat(parentMatrix);
//src用來設定要繪制bitmap的區域,即是否進行裁剪
src.set(0, 0, bitmap.getWidth(), bitmap.getHeight());
//dst用來設置在canvas畫布上的顯示區域。這里可以看到顯示的寬高會根據像素密度進行等縮放
dst.set(0, 0, (int) (bitmap.getWidth() * density), (int) (bitmap.getHeight() * density));
//第一個Rect(src) 代表要繪制的bitmap 區域,可以對是對圖片進行裁截,若是空null則顯示整個圖片。第二個 Rect(dst) 是圖片在Canvas畫布中顯示的區域,即要將bitmap 繪制在屏幕的什么地方
// 通過動態的改變dst,可以實現 移動、縮放等效果,以及根據屏幕的像素密度進行縮放,通過改變src 對繪制的圖片需求做處理,也能夠實現很多有趣的效果,比如 顯示一部分,或者逐漸展開等
canvas.drawBitmap(bitmap, src, dst, paint);
//恢復之前保存的畫布狀態,和sava一一對應
canvas.restore();
}
至于ShapeLayer和CompositionLayer有些復雜,下面我們會單獨來分析。
思考: 如果有多個圖層,怎么保證多個圖層之間的關聯性(就像ViewTree一樣,怎么管理他們之間的關系和繪制的順序)。
二、LayerView樹
Lottie中有各種Layer:
那么他們之間是什么關系吶?如何進行管理和層級控制吶?
CompositionLayer的構造
public CompositionLayer(LottieDrawable lottieDrawable, Layer layerModel, List<Layer> layerModels,
LottieComposition composition) {
//主要是TransformKeyframeAnimation的初始化
super(lottieDrawable, layerModel);
LongSparseArray<BaseLayer> layerMap =
new LongSparseArray<>(composition.getLayers().size());
BaseLayer mattedLayer = null;
//根據layers大小,倒序生產每個Layer
for (int i = layerModels.size() - 1; i >= 0; i--) {
Layer lm = layerModels.get(i);
//這個是一個工程方法,根據layerType構造對應的Layer
BaseLayer layer = BaseLayer.forModel(this, lm, lottieDrawable, composition);
if (layer == null) {
continue;
}
layerMap.put(layer.getLayerModel().getId(), layer);
......
}
for (int i = 0; i < layerMap.size(); i++) {
long key = layerMap.keyAt(i);
BaseLayer layerView = layerMap.get(key);
if (layerView == null) {
continue;
}
// 確定layer之間的父子關系
BaseLayer parentLayer = layerMap.get(layerView.getLayerModel().getParentId());
if (parentLayer != null) {
layerView.setParentLayer(parentLayer);
}
}
}
工廠方法:BaseLayer#forModel
static BaseLayer forModel(
CompositionLayer compositionLayer, Layer layerModel, LottieDrawable drawable, LottieComposition composition) {
//對應json中 object->layers->ty
switch (layerModel.getLayerType()) {
//輪廓/形態圖層 這個是再lottie動畫中用的基本上是最多的類型
case SHAPE:
return new ShapeLayer(drawable, layerModel, compositionLayer);
//合成圖層,相當于ViewTree的ViewGroup的角色
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);
//空圖層,可以作為其他圖層的parent
case NULL:
return new NullLayer(drawable, layerModel);
//文本圖層
case TEXT:
return new TextLayer(drawable, layerModel);
case UNKNOWN:
default:
// Do nothing
Logger.warning("Unknown layer type " + layerModel.getLayerType());
return null;
}
}
我們上面看到layerView.setParentLayer(parentLayer);那么這個ParentLayer有什么用吶?
主要在確定每個圖層的邊界和繪制時使用
// BaseLayer#buildParentLayerListIfNeeded
//該方法會在確定當前圖層邊界getBounds以及繪制該圖層的時候調用draw
private void buildParentLayerListIfNeeded() {
if (parentLayers != null) {
return;
}
//如果該圖層有父圖層,則創新
if (parentLayer == null) {
parentLayers = Collections.emptyList();
return;
}
//該圖層的LayerViewTree
parentLayers = new ArrayList<>();
BaseLayer layer = parentLayer;
//遞歸找到該圖層的父圖層、祖父圖層、曾祖圖層等等
while (layer != null) {
parentLayers.add(layer);
layer = layer.parentLayer;
}
}
BaseLayer#getBounds
public void getBounds(
RectF outBounds, Matrix parentMatrix, boolean applyParents) {
rect.set(0, 0, 0, 0);
//確定該圖層的LayerViewTree:parentLayers
buildParentLayerListIfNeeded();
//子圖層的矩陣變換,以作用再父圖層的矩陣變換為基礎
boundsMatrix.set(parentMatrix);
if (applyParents) {
//遞歸調用父圖層額矩陣變換,進行矩陣相乘
if (parentLayers != null) {
for (int i = parentLayers.size() - 1; i >= 0; i--) {
boundsMatrix.preConcat(parentLayers.get(i).transform.getMatrix());
}
} else if (parentLayer != null) {
boundsMatrix.preConcat(parentLayer.transform.getMatrix());
}
}
//最后再乘以當前圖層的矩陣變換,以確定最終的邊界矩陣
boundsMatrix.preConcat(transform.getMatrix());
}
BaseLayer#draw
和BaseLayer#getBounds一樣的矩陣處理方式。
通過parentid確立該圖層的LayerViewTree,再測量繪制時根據LayerView的確定自己的bound和draw。
三、ShapeLayer 的分析
之所以把ShapeLayer單獨拎出來說,是因為他在lottie動畫中很重要,通過
ShapeLayer是一個通過矢量圖形而不是bitmap來繪制的圖層子類。指定顏色和線寬等屬性,用Path來定義要繪制的圖形.
public class ShapeLayer extends BaseLayer {
......
//這個ContentGroup是什么吶?可以看到ShapeLayer的drawLayer和getBound都是通過contentGroup代理的。
private final ContentGroup contentGroup;
ShapeLayer(LottieDrawable lottieDrawable, Layer layerModel, CompositionLayer compositionLayer) {
......
//ContentGroup構造
contentGroup = new ContentGroup(lottieDrawable, this, shapeGroup);
contentGroup.setContents(Collections.<Content>emptyList(), Collections.<Content>emptyList());
}
@Override void drawLayer(@NonNull Canvas canvas, Matrix parentMatrix, int parentAlpha) {
//調用了contentGroup的draw
contentGroup.draw(canvas, parentMatrix, parentAlpha);
}
@Override public void getBounds(RectF outBounds, Matrix parentMatrix, boolean applyParents) {
......
contentGroup.getBounds(outBounds, boundsMatrix, applyParents);
}
......
}
ContentGroup是什么吶?
可以看到ShapeLayer的drawLayer和getBound都是通過contentGroup代理的。
我們看下ContentGroup的draw的實現
public void draw(Canvas canvas, Matrix parentMatrix, int parentAlpha){
//遍歷調用content,如果是DrawingContent則進行draw,那邊什么是DrawingContent吶
for (int i = contents.size() - 1; i >= 0; i--) {
Object content = contents.get(i);
if (content instanceof DrawingContent) {
((DrawingContent) content).draw(canvas, matrix, childAlpha);
}
}
}
遍歷調用content,如果是DrawingContent則進行draw,哪些content是DrawingContent吶?
我們以FillContent為例,來看下其draw的實現
public void draw(Canvas canvas, Matrix parentMatrix, int parentAlpha) {
......
//獲取顏色 透明度等 設置畫筆paint的顏色
int color = ((ColorKeyframeAnimation) this.colorAnimation).getIntValue();
int alpha = (int) ((parentAlpha / 255f * opacityAnimation.getValue() / 100f) * 255);
paint.setColor((clamp(alpha, 0, 255) << 24) | (color & 0xFFFFFF));
//設置colorFilter
if (colorFilterAnimation != null) {
paint.setColorFilter(colorFilterAnimation.getValue());
}
......
//設置path路徑
path.reset();
for (int i = 0; i < paths.size(); i++) {
path.addPath(paths.get(i).getPath(), parentMatrix);
}
//用cavas drawpath
canvas.drawPath(path, paint);
}
可以ShapeContent的DrawingContent也是通過Canvas來進行draw的。
Lottie的動畫和渲染解析部分就到這里,關于BaseKeyframeAnimation主要實現Layer和DrawingContent中動畫的插值計算,沒有詳細分析,有需要再看吧。
思考:能不能通過OpenGL ES來進行渲染繪制吶?
五、Lottie優劣以及和PAG的簡單對比
Lottie的優劣
優點:
支持跨平臺(雖然每個端各自實現一套)
性能好
可以通過配置下發“json和素材”進行更新。
不足點:
Lottie不支持交互和編輯
Lottie不支持壓縮位圖,如果使用png等位圖,需要自行在tiny等壓縮平臺進行圖片壓縮、降低包體積。
Lottie存在mask、matters 時,需要先saveLayer,再調用drawLayer返回。
saveLayer是一個耗時的操作,需要先分配、繪制一個offscreen的緩沖區,這增加了渲染的時間
PAG的優劣簡單介紹
PAG是騰訊昨天剛開源的動畫組件,除lottie的優點外,
支持更多AE特效,
支持文本和序列幀,
支持模版的編輯,
采用二級值文件而不是json,文件大小和解析的性能都會更好些
渲染層面:Lottie渲染層面的實現依賴平臺端接口,不同平臺可能會有所差異。PAG渲染層面使用C++實現,所有平臺共享同一套實現,平臺端只是封裝接口調用,提供渲染環境,渲染效果一致。
PAG的不足,渲染基于google開源的skia 2d來實現。增加了包大小。4.0的版本會有改善,去掉skia 2d。自己實現簡單的渲染封裝(估計也是opengl或者metal 、vulkan)。
rlottie簡單介紹
[Samsung-rlottie](https://github.com/Samsung/rlottie)
rLottie 與 lottie 工作流一致,在 SDK 上實現不一樣,rLottie 沒有使用平臺特定實現,是統一 C++實現,素材支持 lottie 的 json 文件,矢量渲染性能還不錯,但缺少各平臺封裝,支持的 AE 特性不全,也不支持文本、序列幀等
這個還沒有分析它的源碼實現。抽時間可以分析學習下。
六、資料
- Lottie實現思路和源碼分析
- Lottie 動畫原理剖析
- 揭秘Lottie動畫的優劣及原理
- lottie-android 框架使用及源碼解析
- Lottie動畫庫 Android 端源碼淺析
- 騰訊開源的PAG
- Samsung-rlottie
- 從解碼渲染層面對比 PAG 與 lottie
七、收獲
通過本篇的學習分析
- 梳理了lottie動畫和渲染的流程
- LayerView樹的概念和理解,搞清楚lottie是如何管理不同layer之間的關系的
- 重點分析了CompositionLayer、BaseLayer、ImageLayer和ShapeLayer,其中ShapeLayer又包含ContentGroup
- 簡單對比了lottie、PAG、rlottie
感謝你的閱讀
歡迎關注公眾號“音視頻開發之旅”,一起學習成長。
歡迎交流