Lottie是aribnb发布的开源库,它可以将AE制作的动画在Android、iOS和RN代码中渲染出来。

Lottie的功能及其强大,只需要设计师使用AE设计动画,用bodymovin导出,那么我们只需要简单的几行代码,就能实现非常复杂的动画效果。

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

  • LottieAnimationView 继承自 ImageView ,并且是加载 Lottie 动画的默认和最简单的方法。
  • LottieDrawable 与 LottieAnimationView 有大部分相同的 API,但你可以在任何你想要的视图上使用它。
  • LottieComposition 是动画的无状态model。只要你需要,此文件就可以安全地缓存,并且可以在drawable/view之间自由重用。
  • LottieCompositionFactory 允许您从多个输入创建 LottieComposition。这就是 setAnimation(...) API 在后台使用 LottieDrawable LottieAnimationView 使用的内容。工厂方法也与这些类共享相同的缓存。
  • airbnb.io/lottie/#/an…

    Lottie的使用方法

    加载动画资源的方式:

  • src/main/res/raw 中的 json 动画
  • src/main/assets 中的 json 文件
  • src/main/assets 中的 zip 文件
  • src/main/assets中的 dotLottie 文件(*将Lottie的所有资源打包为一个.lottie文件,有兴趣可查看相关文档)
  • json 或 zip 文件的 Url
  • json 字符串
  • json 或 zip 文件的 InputStream
  • xml中使用方法

    (不再赘述)

    xml文件中Lottie的各属性

    属性 功能
    lottie_fileName 设置播放动画的json文件名称
    Lottie_rawRes 设置播放动画的json文件资源
    Lottie_autoPlay 设置动画是否自动播放(默认为FALSE)
    Lottie_loop 设置动画是否循环(默认为FALSE)
    Lottie_repeatMode 设置动画的重复模式(默认为restart)
    lottie_repeatCount 设置动画的重复次数(默认为-1)
    Lottie_cacheStrategy 设置动画的缓存策略(默认为weak)
    Lottie_colorFilter 设置动画的着色颜色(优先级最低)
    Lottie_scale 设置动画的比例(默认为1f)
    Lottie_progress 设置动画的播放进度
    Lottie_imageAssetsFolder 设置动画依赖的图片资源文件地址

    代码中使用Lottie

    LottieAnimationView animationView = ...
    animationView.setAnimation(R.raw.hello_world);
    // or
    animationView.setAnimation(R.raw.hello_world.json);
    animationView.playAnimation();
    

    默认情况下,所有Lottie动画都使用LRU缓存算法进行缓存,所有从raw或者assets文件夹加载出的动画都将默认创建缓存Key,其他API需要设置缓存key。如果需要对同一个动画并行触发多个动画请求,后续请求将加入现有任务,因此只会被解析一次。

    Lottie 有一些全局配置选项。默认情况下不需要,但它可用于:

  • 从网络加载动画时,使用你自己的网络堆栈而不是 Lottie 的内置堆栈。
  • 为从网络获取的动画提供您自己的缓存目录,而不是使用 Lottie 的默认目录 ( cacheDir/lottie_network_cache)。
  • 启用 systrace 进行调试。
  • 要设置它,在应用程序初始化期间的某个地方,包括:

    Lottie.initialize(
        LottieConfig.Builder()
            .setEnableSystraceMarkers(true)
            .setNetworkFetcher(...)
            .setNetworkCacheDir(...)
    

    注:systrace是Android自带的性能分析工具,详情可以查看文档

    Android Systrace 系列文章

    Lottie可以通过setRepeatMode和setRepeatCount设置循环播放模式,或者通过在xml中设置 lottie_loop="true"

    你同样可以循环动画中的某一段内容,通过调用 setMinFrame, setMaxFrame, or setMinAndMaxFrame,包括帧、进度(从 0.0 到 1.0)或标记名称(在 After Effects 中指定)。

    Lottie适配

    Lottie 将 After Effects 中的所有 px 值转换为设备上的 dps,以便在设备上以相同大小呈现所有内容。这意味着,*Lottie本身已经自带了适配功能, *与其在 After Effects 中制作 1920x1080 的动画,不如在 After Effects 中制作 411x731px,大致对应于当今大多数手机的 dp 屏幕尺寸。

    但是,如果您的动画尺寸不合适,您有两种选择:

    ImageView scaleType

  • LottieAnimationView 是一个包装好的ImageView,它支持centerCrop, centerInsidefitXY所以你可以像使用imageview一样使用此属性。
  • Scaling Up/Down

    LottieAnimationViewLottieDrawable两者都有一个setScale(float)API,您可以使用它来手动放大或缩小动画。这很少有用,但在某些情况下可能有用。

    如果您的动画执行缓慢,请务必查看有关性能的文档。但是,请尝试结合 scaleType 缩小动画。这将减少 Lottie 每帧渲染的数量,特别是Lottie有大的mask或matters,这将特别有用。

    理解AE(After Effects)

    要了解如何在 Lottie 中更改动画属性,首先应该了解动画属性是如何存储在 Lottie 中的。动画属性存储在模仿 After Effects 信息层次结构的数据树中。在 After Effects 中,Composition是一个集合Layers,每个集合都有自己的时间线。Layer对象具有字符串名称,它们的内容可以是图像、形状图层、填充、描边或任何可绘制的内容。After Effects 中的每个对象都有一个名称。Lottie可以使用这些对象和属性的名称通过KeyPath找到它们。

    Lottie json文件的属性含义

  • lottie的最外层结构:
  •  "v": "5.8.0",  //bodymovin的版本  "fr": 60,      //帧率  "ip": 0,       //起始关键帧  "op": 102,     //结束关键帧  "w": 1350,     //动画宽度  "h": 800,      //动画高度  "nm": "recommend_turn page_x0.75_original", //名称  "ddd": 0,       //是否为3d  "assets":[],   //资源信息  "layers":[],   //图层信息  "markers": []  //遮罩 注:时间=(op-ip)/fr
  • assets
  • "assets": [         //资源信息
            "id": "image_0",  //图片id
            "w": 129,               //图片宽度
            "h": 884,                   //图片高度
            "u": "images/",   //图片路径
            "p": "recommend_bg_book_shadow.png",  //名称
            "e": 0
    
  • layers:动画是由一个一个的图层组合起来,并在图层上进行偏移、缩放等操作来实现动画的。图层的解析是lottie的主要功能模块。
  •  "layers": [                            //图层信息
          "ddd": 0,         //是否为3d
          "ind": 1,                     //图层id 唯一性
          "ty": 4,            //图层类型
          "nm": "page back 4",//图层名称
          "refId": "comp_0", // 引用的资源,图片/预合成层
          "td": 1,
          "sr": 1,
          "ks": {...},              // 变换。对应AE中的变换设置
          "ao": 0,
          ”layer“: [],         // 该图层包含的子图层
          “shaps”: [],         // 形状图层
          "ip": 12,                     //该图层起始关键帧
          "op": 1782,         //该图层结束关键帧
          "st": -18,         
          "bm": 0
    
  • ks:对应AE中图层的变换属性,可以通过设置锚点、位置、旋转、缩放、透明度等来控制图层,并设置这些属性的变换曲线,来实现动画。
  • "ks": { // 变换。对应AE中的变换设置
        "o": { // 透明度
            "a": 0,
            "k": 100,
            "ix": 11
        "r": { // 旋转
            "a": 0,
            "k": 0,
            "ix": 10
        "p": { // 位置
            "a": 0,
            "k": [-167, 358.125, 0],
            "ix": 2
        "a": { // 锚点
            "a": 0,
            "k": [667, 375, 0],
            "ix": 1
        "s": { // 缩放
            "a": 0,
            "k": [100, 100, 100],
            "ix": 6
    
  • shape:对应AE中图层的内容中的形状设置的内容,其主要用于绘制图形
  • "shapes": [{
      "ty": "gr", // 类型。混合图层
      "it": [{ // 各图层json
          "ind": 0,
          "ty": "sh", // 类型,sh表示图形路径
          "ix": 1,
          "ks": {
              "a": 0,
              "k": {
                  "i": [ // 内切线点集合
                      [0, 0],
                      [0, 0]
                  "o": [ // 外切线点集合
                      [0, 0],
                      [0, 0]
                  "v": [ // 顶点坐标集合
                      [182, -321.75],
                      [206.25, -321.75]
                  "c": false // 贝塞尔路径闭合
              "ix": 2
          "nm": "路径 1",
          "mn": "ADBE Vector Shape - Group",
          "hd": false
        "ty": "st", // 类型。图形描边
        "c": { // 线的颜色
            "a": 0,
            "k": [0, 0, 0, 1],
            "ix": 3
        "o": { // 线的不透明度
            "a": 0,
            "k": 100,
            "ix": 4
        "w": { // 线的宽度
            "a": 0,
            "k": 3,
            "ix": 5
        "lc": 2, // 线段的头尾样式
        "lj": 1, // 线段的连接样式
        "ml": 4, // 尖角限制
        "nm": "描边 1",
        "mn": "ADBE Vector Graphic - Stroke",
        "hd": false
    

    动态修改属性方法:

    如果需要在运行时动态修改属性,需要以下三点:

  • KeyPath
  • LottieProperty
  • LottieValueCallback
  • KeyPath

    KeyPath用于定位特定内容或将要更新的一组内容。KeyPath由字符串列表指定,这些字符串对应于原始动画中After Effectsd的内容层级结构。

    KeyPaths 可以包含内容的特定名称或通配符:

    Wildcard(通配符)

  • 通配符匹配其在keypath中位置的任意单个内容名称
  • Globstar(全局星标)

  • globstar匹配0个或多个层级。
  • KeyPath resolution

    KeyPath能够存储对其解析的内容的内部引用。当您创建一个新的KeyPath对象时,它将被解析。LottieDrawable和LottieAnimationView有一个resolveKeyPath()方法,它接受一个KeyPath并返回一个由零个或多个已解析的KeyPath组成的列表,每个都在内部解析为一个内容片段。如果你不知道,这可以用来发现你的动画结构。为此,在开发环境中,解析新的KeyPath("")并记录返回的列表。然而,你不应该单独使用和ValueCallback,因为它会被应用到动画中的每一个内容片段。如果您解析了您的keypath,并希望随后添加一个值回调,请使用从该方法返回的keypath,因为它们将在内部解析,而不需要执行树遍历来再次查找内容。

    LottieProperty

    LottieProperty 是可以设置的属性的枚举。它们对应于 After Effects 中的动画值,可用属性在上面和文档中列出LottieProperty

    以下属性可以运行时修改:

    TransformLayerFillStrokeEllipsePolystarRepeater
    TRANSFORM_ANCHOR_POINTTRANSFORM_ANCHOR_POINTCOLORCOLORELLIPSE_SIZEPOLYSTAR_POINTSREPEATER_COPIES
    TRANSFORM_POSITIONTRANSFORM_POSITIONOPACITYOPACITYPOSITIONPOLYSTAR_ROTATIONREPEATER_OFFSET
    TRANSFORM_OPACITYTRANSFORM_OPACITYCOLOR_FILTERCOLOR_FILTERPOSITIONTRANSFORM_ROTATION
    TRANSFORM_SCALETRANSFORM_SCALESTROKE_WIDTHPOLYSTAR_OUTER_RADIUSTRANSFORM_START_OPACITY
    TRANSFORM_ROTATIONTRANSFORM_ROTATIONPOLYSTAR_OUTER_ROUNDEDNESSTRANSFORM_END_OPACITY
    TIME_REMAPPOLYSTAR_INNER_RADIUS
    ValueCallback

    ValueCallback 是每次渲染动画时调用的内容。回调提供:

  • 当前关键帧的起始帧。
  • 当前关键帧的结束帧。
  • 当前关键帧的起始值。
  • 当前关键帧的结束值。
  • 当前关键帧中从 0 到 1 的进度,没有任何时间插值。
  • 当前关键帧的进度(存在插值器)。
  • 整体动画进度从0到1。
  • ValueCallback类
  • LottieValueCallback:可以在构造函数中设置静态值,也可以覆盖getValue()来设置每一帧的值。
  • LottieRelativeTYPEValueCallback:可以在构造函数中设置一个静态值,也可以覆盖getOffset()来设置一个值,该值将被应用于每一帧上的实际动画值的偏移量。TYPE与LottieProperty参数的类型相同。
  • LottieInterpolatedTYPEValue:提供一个开始值、结束值和可选的插值器,使值在整个动画中自动插入。TYPE与LottieProperty参数的类型相同。
  • 动态修改属性的用法:
  • 动态修改颜色
  • KeyPath shirt = new KeyPath("Shirt", "Group 5", "Fill 1");
    turnpagesLotv.addValueCallback(shirt, LottieProperty.COLOR, new LottieValueCallback<Integer>(){
        @Nullable
        @Override
        public Integer getValue(LottieFrameInfo<Integer> frameInfo) {
                 return frameInfo.getOverallProgress() > 0.5f ?
                            COLORS[index] :
                            COLORS[index++];
    
  • 修改弹跳高度
  •  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;
    
  • 事件绑定 (与手势事件绑定,本质上还是对position进行操作)
  •         LottieRelativePointValueCallback largeValueCallback = new LottieRelativePointValueCallback(new PointF(0f, 0f));
            lottieAnimationView.addValueCallback(new KeyPath("First"),
                    LottieProperty.TRANSFORM_POSITION, largeValueCallback);
            LottieRelativePointValueCallback mediumValueCallback = new LottieRelativePointValueCallback(new PointF(0f, 0f));
            lottieAnimationView.addValueCallback(new KeyPath("Fourth"),
                    LottieProperty.TRANSFORM_POSITION, mediumValueCallback);
            LottieRelativePointValueCallback smallValueCallback = new LottieRelativePointValueCallback(new PointF(0f, 0f));
            lottieAnimationView.addValueCallback(new KeyPath("Seventh"),
                    LottieProperty.TRANSFORM_POSITION, smallValueCallback);
            ViewDragHelper viewDragHelper = ViewDragHelper.create(container, new ViewDragHelper.Callback() {
                @Override
                public boolean tryCaptureView(@NonNull View child, int pointerId) {
                    return child == targetView;
                @Override
                public int clampViewPositionVertical(@NonNull View child, int top, int dy) {
                    return top;
                @Override
                public int clampViewPositionHorizontal(@NonNull View child, int left, int dx) {
                    return left;
                 * 拖动的这个View的位置发生变化
                 * @param changedView  当前拖动的这个View
                 * @param left         距离左边的距离
                 * @param top          距离右边的距离
                 * @param dx           x轴的变化量
                 * @param dy           y轴的变化量
                @Override
                public void onViewPositionChanged(@NonNull View changedView, int left, int top, int dx, int dy) {
                    totalDx += dx;
                    totalDy += dy;
                    //控制的是圆心然后触发重新绘制,就是位置的距离转换一下设置给新的圆心
                    //这个触摸绑定交互可能不具有参考意义,因为动画没有特别复杂,直接canvas画三个圆也能达到同样的效果
                    smallValueCallback.setValue(getPoint(totalDx, totalDy, 1.2f));
                    mediumValueCallback.setValue(getPoint(totalDx, totalDy, 1f));
                    largeValueCallback.setValue(getPoint(totalDx, totalDy, 0.75f));
            container.setViewDragHelper(viewDragHelper);
    

    注意:KeyPath构造函数中的字符串对应Lottie的json文件内不同层级的nm字段,通过nm字段,Lottie可以定位到需要动态修改属性的位置,不过当Lottie资源复杂时,比较难以找到对应字段。

  • 更换图片资源
  • //imageId--图片资源id
    lottieView.updateBitmap(imageId, bitmap);
    

    LottieAnimationView继承自AppCompatImageView,Lottie动画能够实现的核心在于LottieDrawable。

    以下为Lottie工作的简要流程:

  • LottieComposition: After Effects/Bodymovin合成模型,这是创建动画的序列化模型。它被设计成无状态、可缓存和可共享的,这是json文件转换后的结果。
  • LottieDrawable: 将LottieComposition封装为可以调用draw()方法的BaseLayer。
  • BaseLayer: 当LottieAnimationView需要绘制时,将会逐层调用BaseLayer,从而将图像绘制出来。
  • Lottie第一步: json解析

    通过LottieAnimationView的setAnimation()方法,可以看到

    public void setAnimation(@RawRes final int rawRes) {
      this.animationResId = rawRes;
      animationName = null;
      setCompositionTask(fromRawRes(rawRes));
    public void setAnimation(final String assetName) {
        this.animationName = assetName;
        animationResId = 0;
        setCompositionTask(fromAssets(assetName));
    

    进入fromAssets方法:

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

    然后对字节流内容进行解析

    LottieComposition composition = LottieCompositionMoshiParser.parse(reader);
        if (cacheKey != null) {
          LottieCompositionCache.getInstance().put(cacheKey, composition);
    

    解析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
        "fonts", // 8
        "chars", // 9
        "markers" // 10
    
    public static LottieComposition parse(JsonReader reader) throws IOException {
      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<>();
      ......
    

    Lottie第二步: LottieAnimationView将解析后生成的LottieComposition对象传递给LottieDrawer

    * 设置一个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);
    

    Lottie第三步: 播放Lottie动画

    通过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);
      if (timeRemapping != null) {
        // The duration has 0.01 frame offset to show end of animation properly.
        // https://github.com/airbnb/lottie-android/pull/766
        // Ignore this offset for calculating time-remapping because time-remapping value is based on original duration.
        float durationFrames = lottieDrawable.getComposition().getDurationFrames() + 0.01f;
        float compositionDelayFrames = layerModel.getComposition().getStartFrame();
        float remappedFrames = timeRemapping.getValue() * layerModel.getComposition().getFrameRate() - compositionDelayFrames;
        progress = remappedFrames / durationFrames;
      if (timeRemapping == null) {
        progress -= layerModel.getStartProgress();
      //Time stretch needs to be divided if is not "__container"
      if (layerModel.getTimeStretch() != 0 && !"__container".equals(layerModel.getName())) {
        progress /= layerModel.getTimeStretch();
      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动画就跑起来了,流程图如下:

    Lottie性能优化

    开发过程中经常会出现Lottie跳帧的问题,那么首先要明白,Lottie为何会跳帧?

    进入LottieValueAnimator的playAnimation方法,可以看到

    @MainThread
    public void playAnimation() {
      running = true;
      notifyStart(isReversed());
      setFrame((int) (isReversed() ? getMaxFrame() : getMinFrame()));
      //lastFrameTimeNs这个时间戳代表上一帧动画的时间,第一帧为0
      lastFrameTimeNs = 0;
      repeatCount = 0;
      //开启动画之后,post了一个frameCallback
      postFrameCallback();
    
    protected void postFrameCallback() {
      if (isRunning()) {
        removeFrameCallback(false);
        //每次界面绘制完一帧,都会回调一次这个接口,主流帧率监测的方案都是通过这个接口
        Choreographer.getInstance().postFrameCallback(this);
    

    继续顺藤摸瓜,找到FrameCallback的实现doFrame方法:

    @Override public void doFrame(long frameTimeNanos) {
      postFrameCallback(); //重新回调
      if (composition == null || !isRunning()) {
        return;
      L.beginSection("LottieValueAnimator#doFrame");
      //这里会拿到lastFrameTimeNs,计算两次进入回调后的时间差
      long timeSinceFrame = lastFrameTimeNs == 0 ? 0 : frameTimeNanos - lastFrameTimeNs;
      //动过这个时间差,计算下一帧的播放进度
      float frameDuration = getFrameDurationNs();
      float dFrames = timeSinceFrame / frameDuration;
      //这里便是跳帧发生的位置,frame代表帧数,如果dFrames这个时间差越大,那么frame的值也就越大,跳帧便发生了
      frame += isReversed() ? -dFrames : dFrames;
      boolean ended = !MiscUtils.contains(frame, getMinFrame(), getMaxFrame());
      frame = MiscUtils.clamp(frame, getMinFrame(), getMaxFrame());
      //这里重新标记上一帧的时间
      lastFrameTimeNs = frameTimeNanos;
      notifyUpdate();
      if (ended) {
        if (getRepeatCount() != INFINITE && repeatCount >= getRepeatCount()) {
          frame = speed < 0 ? getMinFrame() : getMaxFrame();
          removeFrameCallback();
          notifyEnd(isReversed());
        } else {
          notifyRepeat();
          repeatCount++;
          if (getRepeatMode() == REVERSE) {
            speedReversedForRepeatMode = !speedReversedForRepeatMode;
            reverseAnimationSpeed();
          } else {
            frame = isReversed() ? getMaxFrame() : getMinFrame();
          lastFrameTimeNs = frameTimeNanos;
    

    理解了Lottie跳帧的机制,那么如何进行优化呢?

  • 往往Lottie跳帧是主线程进行了耗时操作,那么最有方案便是优化此耗时操作,放到子线程等。
  • 看Lottie的json结构,如果没有用到遮罩mask(掩膜)或者matte(前景蒙版)标签,那正常来讲性能开销没啥问题,这两个标签会创建bitmap,大幅拉高内存,特别是在recyclerview中。
  • 导出的矢量图层使用1x一倍图,这一点十分重要,Lottie会自动适配屏幕密度
  • 尽量保持图层简洁,预合成嵌套越少越好
  • 开启硬件加速,lotv.setRenderMode(RenderMode.HARDWARE),但是注意开启硬件加速后不支持抗锯齿、笔画上限(API 18前)和其他一些功能。
  • 腾讯VTeam技术团队
    私信