Lottie系列三 :原理分析

一 原理概述

1.1 核心类

  • LottieAnimationView 扩展了 ImageView,是加载 Lottie 动画的默认且最简单的方法。
  • LottieDrawable 具有与 LottieAnimationView 相同的大部分 API,但是您可以在任何您想要的视图上使用它。
  • LottieComposition 组合是动画的无状态模型表示。只要您需要,这个文件可以安全地缓存很长时间,并且可以在drawables/views之间自由地重用。
  • LottieCompositionFactory 允许您从许多输入创建 LottieCompostionFactory。这就是 LottieDrawable 和 LottieAnimationView 上的 setAnimation (…) API 在底层使用的。工厂方法也与这些类共享相同的缓存。

1.2 概述

LottieAnimationView继承自ImageView,通过当前时间绘制canvas显示到界面上。这里有两个关键类:LottieComposition 负责解析json描述文件,把json内容转成Java数据对象;LottieDrawable负责绘制,把LottieComposition转成的数据对象绘制成drawable显示到View上。顺序如下:
image-20220718112821577

由上述过程可以看到,Lottie解析并展示动画的流程主要可分为三步:解析Json为LottieComposition;LottieComposition转为LottieDrawer;LottieDrawer播放动画;下面来一步步解析:

二 解析Josn文件

image-20220718112821577

进入setCompositionTask方法:

public void setAnimation(@RawRes final int rawRes) {
    
    setCompositionTask(fromRawRes(rawRes));
}

进入fromRawRes方法:

private LottieTask<LottieComposition> fromAssets(final String assetName) {
    
    
    if (isInEditMode()) {
    
     
         //避免可视化编辑报错问题
         ...
    } else {
    
    
   	 //cacheComposition记录是否已缓存
   	 //最终拿到json文件的LottieComposition数据模型
   	 return cacheComposition ?
       LottieCompositionFactory.fromAsset(getContext(), assetName) : LottieCompositionFactory.fromAsset(getContext(), assetName, null);
   }
}

进入LottieCompositionFactory.fromAsset方法,最终一连串函数调用,走到LottieCompositionMoshiParser
.parse方法:

// 对字节流内容进行解析:
LottieComposition composition = LottieCompositionMoshiParser
.parse(reader);
   if (cacheKey != null) {
     LottieCompositionCache.getInstance().put(cacheKey, composition);
}

parse过程需要对json的每个字段进行映射对应,并进行解析,映射关系如下:

// 解析json字段:
private static final JsonReader.Options NAMES = JsonReader.Options.of(
   "w", // 0
   "h", // 1
   "ip", // 2
   "op", // 3
   "fr", // 4
   "v", // 5
   "layers", // 6
   "assets", // 7
...
);

Options是JsonReader的 static 内部类,of是方法;

四 生成LottieDrawer

LottieAnimationView将解析后生成的LottieComposition对象传递给LottieDrawer;

扫描二维码关注公众号,回复: 15783980 查看本文章
/**
 * 设置一个composition.
 * 如果这个视图使用R.attr.lottie_cacheComposition填充xml,则可以设置默认缓存策略。
 */
public void setComposition(@NonNull LottieComposition composition) {
    
    
  if (L.DBG) {
    
    
    Log.v(TAG, "Set Composition \n" + composition);
  }
  lottieDrawable.setCallback(this);

  this.composition = composition;
  ignoreUnschedule = true;
  //将解析后的LottieComposition传递给LottieDrawable
  boolean isNewComposition = lottieDrawable.setComposition(composition);
  ignoreUnschedule = false;
  enableOrDisableHardwareLayer();
  if (getDrawable() == lottieDrawable && !isNewComposition) {
    
    
    // We can avoid re-setting the drawable, and invalidating the view, since the composition
    // hasn't changed.
    //我们可以避免重新设置drawable,并使视图无效,因为合成并没有改变。
    return;
  } else if (!isNewComposition) {
    
    
    // The current drawable isn't lottieDrawable but the drawable already has the right composition.
    // 当前的drawable不是lottieDrawable,但drawable已经有正确的组成。
    setLottieDrawable();
  }

  // This is needed to makes sure that the animation is properly played/paused for the current visibility state.
  // 需要确保动画在当前可见状态是正确播放/暂停。
  // It is possible that the drawable had a lazy composition task to play the animation but this view subsequently
  // became invisible. Comment this out and run the espresso tests to see a failing test.
  onVisibilityChanged(this, getVisibility());

  requestLayout();

  for (LottieOnCompositionLoadedListener lottieOnCompositionLoadedListener : lottieOnCompositionLoadedListeners) {
    
    
    lottieOnCompositionLoadedListener.onCompositionLoaded(composition);
  }
}

LottieDrawable将LottieComposition对象构造为CompositionLayer

public boolean setComposition(LottieComposition composition) {
    
    
  if (this.composition == composition) {
    
    
    return false;
  }

  isDirty = false;
  clearComposition();
  this.composition = composition;
  buildCompositionLayer();
  ...
 private void buildCompositionLayer() {
    
    
    compositionLayer = new CompositionLayer(
        this, LayerParser.parse(composition), composition.getLayers(), composition);
    if (outlineMasksAndMattes) {
    
    
      compositionLayer.setOutlineMasksAndMattes(true);
    }
  }

CompositionLayer继承自Baselayer,并且在构造时会遍历所有layer图层,转换为BaseLayer对象。

public CompositionLayer(LottieDrawable lottieDrawable, Layer layerModel, List<Layer> layerModels,
    LottieComposition composition) {
    
    
  super(lottieDrawable, layerModel);
  ...
  LongSparseArray<BaseLayer> layerMap =
      new LongSparseArray<>(composition.getLayers().size());
  BaseLayer mattedLayer = null;
  for (int i = layerModels.size() - 1; i >= 0; i--) {
    
    
    Layer lm = layerModels.get(i);
    BaseLayer layer = BaseLayer.forModel(this, lm, lottieDrawable, composition);
   ...
  }
}

这里通过BaseLayer的forModel方法,将BaseLayer的各个子类型抽象出来

@Nullable
static BaseLayer forModel(
    CompositionLayer compositionLayer, Layer layerModel, LottieDrawable drawable, LottieComposition composition) {
    
    
  switch (layerModel.getLayerType()) {
    
    
    case SHAPE:
      return new ShapeLayer(drawable, layerModel, compositionLayer);
    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);
    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;
  }
}

以下是Lottie的不同layer类型:
在这里插入图片描述

到这里,LottieDrawable就通过CompositionLayer将各个类型的layer实例化,然后在LottieDrawable的draw()方法中完成所有图层的绘制:

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public void draw(Canvas canvas, Matrix matrix) {
    
    
  CompositionLayer compositionLayer = this.compositionLayer;
  if (compositionLayer == null) {
    
    
    return;
  }
  compositionLayer.draw(canvas, matrix, alpha);
}

五 播放动画

在这里插入图片描述
通过LottieAnimationView的playAnimation方法可以看到,内部会调用LottieDrawable的playAnimation方法,然后会触发LottieValueAnimator的playAnimation方法。LottieValueAnimator实际也是一个ValueAnimator,所以本质上Lottie也是属性动画驱动的。

具体在LottieDrawable中可以看到,LottieValueAnimator调用updateListener后,会刷新CompositionLayer的progress。

private final ValueAnimator.AnimatorUpdateListener progressUpdateListener = new ValueAnimator.AnimatorUpdateListener() {
    
    
  @Override
  public void onAnimationUpdate(ValueAnimator animation) {
    
    
    if (compositionLayer != null) {
    
    
      compositionLayer.setProgress(animator.getAnimatedValueAbsolute());
    }
  }
};

进入setProgress可以看到,CompositionLayer会遍历所有layer图层,并逐个调用其setProgress方法。

@Override public void setProgress(@FloatRange(from = 0f, to = 1f) float progress) {
    
    
  super.setProgress(progress);
  ...
  for (int i = layers.size() - 1; i >= 0; i--) {
    
    
    layers.get(i).setProgress(progress);
  }
}

进入BaseLayer的setProgress方法会发现,会调用所有BaseKeyframeAnimation的setProgress方法,并会在BaseLayer中回调调用invalidateSelf()方法。

private void invalidateSelf() {
    
    
  lottieDrawable.invalidateSelf();
}

回调invalidateSelf()方法后,LottieDrawable会回调draw方法

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public void draw(Canvas canvas, Matrix matrix) {
    
    
  CompositionLayer compositionLayer = this.compositionLayer;
  if (compositionLayer == null) {
    
    
    return;
  }
  compositionLayer.draw(canvas, matrix, alpha);
}

进入CompositionLayer的draw方法

@Override
public void draw(Canvas canvas, Matrix parentMatrix, int parentAlpha) {
    
    
  L.beginSection(drawTraceName);
  if (!visible || layerModel.isHidden()) {
    
    
    L.endSection(drawTraceName);
    return;
  }
  buildParentLayerListIfNeeded();
  L.beginSection("Layer#parentMatrix");
  matrix.reset();
  matrix.set(parentMatrix);
  for (int i = parentLayers.size() - 1; i >= 0; i--) {
    
    
    matrix.preConcat(parentLayers.get(i).transform.getMatrix());
  }
  L.endSection("Layer#parentMatrix");
  int opacity = transform.getOpacity() == null ? 100 : transform.getOpacity().getValue();
  int alpha = (int)
      ((parentAlpha / 255f * (float) opacity / 100f) * 255);
  if (!hasMatteOnThisLayer() && !hasMasksOnThisLayer()) {
    
    
    matrix.preConcat(transform.getMatrix());
    L.beginSection("Layer#drawLayer");
    drawLayer(canvas, matrix, alpha);
    L.endSection("Layer#drawLayer");
    recordRenderTime(L.endSection(drawTraceName));
    return;
  }
  ……

实际这样构成了一个循环,随着animator动画的进行,LottieDrawable会不断的绘制,这样Lottie动画就跑起来了,流程图如下:

截屏2022-02-14 上午10.59.52.png

六 总结

首先会通过LottieCompositionFactory的对应类型设置json资源文件;

然后再fromJsonSync方法里面会把json文件解析出图层的大小并且绘制相应的图片资源文件和图层;

资源加载完后,会在回调里面设置LottieAnimationView的Composition,从而调用LottieDrawable的setComposition()方法
在setComposition方法里面会通过buildCompositionLayer()方法去创建一个CompositionLayer图层,其中CompositionLayer继承BaseLayer,通过BaseLayer的forModel()静态方法获取不同的图层类型;

然后LottieDrawable的setComposition()方法里面会开始执行一个ValueAnimation动画,这个动画会驱使BaseLayer的draw()方法不断执行,通过Matrix的矩阵形式不断的绘制各个图层从而形成动画,而这些图层的矩阵变换的数据来源于BaseKeyframeAnimation里面有一个Keyframe对象会去Json里面获取相应得数据。

附图(参考):

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Rv4eJcr5-1658136002844)(/Users/a58/Library/Application Support/typora-user-images/image-20220718145339827.png)]

源码

例子源码 : https://github.com/LucasXu01/lottiedemo

参考:

Lottie官网

Lottie进阶和原理分析

支持点击交互的Lottie-Android

Lottie—json文件解析

Lottie动画使用及原理分析

如何在Lottie动画中获得可用的总帧数

Flutter进阶教程——Flutter中使用Lottie动画

从设计师和开发的角度使用 Lottie

猜你喜欢

转载自blog.csdn.net/LucasXu01/article/details/125855992
今日推荐