一. Lottie能做什么

回答这个问题前,我们先想想下面这些动画需要怎么实现:

              

lottie动画在ios偶现播放问题_移动开发

             

lottie动画在ios偶现播放问题_python_02

               

lottie动画在ios偶现播放问题_lottie动画在ios偶现播放问题_03

是不是一脸懵逼,如果不懵逼是不是感觉压力山大?传统方式实现动画,无非以下几种方式:

  • 使用Animation/Animator。这种方式固然可行,但是要么需要添加多张图片,要么需要针对单个控件写一大堆动画,无论是从apk体积方面考虑还是从开发效率上来说都得不偿失势;
  • 使用 GIF。同样面临的问题是所占体积较大,而且需要为各种屏幕尺寸、分辨率做适配,并且Android本是不支持GIF直接展示的;
  • Android 5.0 Lollipop 之后提供了对 SVG 的支持,通过 VectorDrawable、AnimatedVectorDrawable 的结合可以实现一些稍微复杂的动画,但是同样面临上述问题。

那么有什么方法既可以高效的实现动画,又不需要占用过多空间,还能同时支持多个系统环境呢?Lottie应运而生。

Lottie 是Airbnb开源的动画实现项目,支持Android、iOS、ReactNaitve三大平台,Github原文内容请点击这里Lottie 的使用前提是需要先通过插件 bodymovin 将 Adobe After Effects (AE)生成的 aep 动画工程文件转换为通用的 json 格式描述文件( bodymovin 插件本身是用于网页上呈现各种AE效果的一个开源库)。Lottie 所做的事情就是实现在不同移动端平台上呈现AE动画的方式,从而达到动画文件的一次绘制、一次转换,随处可用的效果。

本文主要侧重于讲解 Lottie 在Android 中的使用方式及源码实现过程

 



二. 使用过程



1.添加依赖

现有版本已升级到2.0.0-rc1

dependencies {  
        compile 'com.airbnb.android:lottie:2.0.0-rc1'
    }



2. 使用

Lottie支持ICS (API 14)及以上的系统版本, 最简单的使用方式是直接在布局文件中添加:

<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" />

你也可以选择使用 Java 代码的方式进行动画加载,从app/src/main/assets获取json文件:

LottieAnimationView animationView = (LottieAnimationView) 
    findViewById(R.id.animation_view);
    animationView.setAnimation("hello-world.json");
    animationView.loop(true);//设置动画是否循环播放,true表示循环播放,false表示只播放一次
    animationView.playAnimation();

这种方式会在后台进行一次性的异步文件加载和动画渲染工作 。

如果你想重复利用一个动画效果,例如在列表的每个项目中,或者从一个网络请求的返回中解析JSONObject对象,你可以采用如下方式先生成一个Cancellable, 然后进行设置:

LottieAnimationView animationView = (LottieAnimationView) findViewById(R.id.animation_view);
 ...
 Cancellable compositionCancellable = LottieComposition.Factory.fromJson(getResources(), jsonObject, 
     new OnCompositionLoadedListener() {
         @Override public void onCompositionLoaded(LottieComposition composition) {
                animationView.setComposition(composition);
                animationView.playAnimation();
         }
     });

 // Cancel to stop asynchronous loading of composition
 // compositionCancellable.cancel();

你可以通过如下方式控制动画或者添加监听:

animationView.addAnimatorUpdateListener(//监听动画进度
        new ValueAnimator.AnimatorUpdateListener() {
          @Override public void onAnimationUpdate(ValueAnimator animation) {
               // Do something.
          }
        });

 animationView.playAnimation();//开始动画
 ...
 animationView.cancelAnimation();//结束动画
 ...
 animationView.pauseAnimation();//暂停动画
 ...
 animationView.resumeAnimation();//重启动画
 ...
 animationView.setScaleX(0.5f);//设置X轴方向上的缩放比例,0f为不可见,1f原始大小 Ps.原setScale方法在2.0.0版本后已弃用
 animationView.setScaleY(0.5f);//设置Y轴方向上的缩放比例
 ...
 if (animationView.isAnimating()) {//动画正在进行中
     // Do something.
 }
 ...
 animationView.setProgress(0.5f);//手动设置动画进度
 ...
 // Custom animation speed or duration.
 ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f)//自定义一个属性动画
     .setDuration(500);
 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
      @Override
      public void onAnimationUpdate(ValueAnimator animation) {
          animationView.setProgress(animation.getAnimatedValue());
      }
    });
 animator.start();
 ...

你可以给整个动画、一个特定的图层或者一个图层的特定内容添加一个颜色过滤器:

// 任何符合颜色过滤界面的类
final PorterDuffColorFilter colorFilter = new PorterDuffColorFilter(Color.RED, PorterDuff.Mode.LIGHTEN);

// 在整个视图中添加一个颜色过滤器
animationView.addColorFilter(colorFilter);

//在特定的图层中添加一个颜色滤镜
animationView.addColorFilterToLayer("hello_layer", colorFilter);

// 添加一个彩色过滤器特效“hello_layer”上的内容
animationView.addColorFilterToContent("hello_layer", "hello", colorFilter);

// 清除所有的颜色滤镜
animationView.clearColorFilters();

你也可以在布局文件中为动画控件添加一个颜色过滤器:

<com.airbnb.lottie.LottieAnimationView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:lottie_fileName="hello-world.json"
        app:lottie_colorFilter="@color/blue" />

注意:颜色过滤器只适用于图层,如图像层和实体层,以及包含填充、描边或组内容的内容。

在内部, LottieAnimationView 使用 LottieDrawable 作为其代理的方式呈现其动画,您甚至可以直接使用 drawable 表单:

LottieDrawable drawable = new LottieDrawable();
    LottieComposition.Factory.fromAssetFileName(getContext(), "hello-world.json", 
        new OnCompositionLoadedListener() {
            @Override public void onCompositionLoaded(LottieComposition composition) {
                drawable.setComposition(composition);
            }
    });

如果你的动画会经常重用,LottieAnimationView内置了一个可选的缓存策略。使用LottieAnimationView .setAnimation(String,CacheStrategy)。CacheStrategy可以为Strong, Weak, 或者None。LottieAnimationView对加载和解析的动画持有强引用或弱引用,弱或强表示缓存中组合的回收对象的优先级。

 



三. Image 支持

如果你的动画是从assets中加载的,并且你的图像文件位于assets 的子目录中,那么你可以对图像进行动画处理。你可以使用 LottieAnimationView 或者 LottieDrawable 对象调用 setImageAssetsFolder(String) 方法读取assets目录中的文件,确保图像 bodymovin 生成的图像文件所保存的文件夹以 img_ 开头。如果直接使用 LottieDrawable, 当你完成时您必须调用 recycleBitmaps方法。

如果你需要提供你自己的位图,如果你从网络或其他地方下载,你可以提供一个委托来做这个工作:

animationView.setImageAssetDelegate(new ImageAssetDelegate() {
         @Override public Bitmap fetchBitmap(LottieImageAsset asset) {
           getBitmap(asset);
         }
    });

 



四. 实现原理

Lottie使用json文件来作为动画数据源,json文件是通过 AE 插件 Bodymovin 导出的,查看sample中给出的json文件,其实就是把图片中的元素进行来拆分,并且描述每个元素的动画执行路径和执行时间。Lottie的功能就是读取这些数据,然后绘制到屏幕上。

现在思考如果我们拿到一份json格式动画如何展示到屏幕上:首先要解析json,建立数据到对象的映射(LottieComposition),然后根据数据对象创建合适的 Drawable (LottieDrawable)并绘制到 View (LottieAnimationView)上,动画的实现可以通过操作读取到的元素完成,如下图所示:

              

lottie动画在ios偶现播放问题_android_04



1. json文件到对象的映射

在分析映射过程之前,我们先来看看由 Bodymovin导出的 json 文件的格式:

{
  "assets": [
    
  ],
  "layers": [
    {
      "ddd": 0,
      "ind": 0,
      "ty": 1,
      "nm": "MASTER",
      "ks": {
        "o": {
          "k": 0
        },
        "r": {
          "k": 0
        },
        "p": {
          "k": [
            164.457,
            140.822,
            0
          ]
        },
        "a": {
          "k": [
            60,
            60,
            0
          ]
        },
        "s": {
          "k": [
            100,
            100,
            100
          ]
        }
      },
      "ao": 0,
      "sw": 120,
      "sh": 120,
      "sc": "#ffffff",
      "ip": 12,
      "op": 179,
      "st": 0,
      "bm": 0,
      "sr": 1
    },
    ……
  ],
  "v": "4.4.26",
  "ddd": 0,
  "ip": 0,
  "op": 179,
  "fr": 30,
  "w": 325,
  "h": 202
}

层级非常丰富,除了包含动画宽、高、帧率等基本属性外,还包含了重要的的图层信息layers,以及包含其他动画信息的递归子集assets。

然后我们在来观察 LottieComposition 这个类的结构:

             

lottie动画在ios偶现播放问题_移动开发_05

可以看到startFrame、endFrame、duration、scale等都是动画中常见的属性,Factory是静态内部类(后文会进行分析),剩余的几个属性就值得玩味了:

  • precomps:存储assets递归子集的 Layer 类型属性的HashMap集合;
  • images:存储assets递归子集中的 LottieImageAsset 类型属性的HashMap集合;
  • layerMap:存储外层layers中的Layer类型属性的LongSparseArray集合。

我们再看静态内部类 Factory ,首先从命名上我们可以看到他有如下几个入口:

              

lottie动画在ios偶现播放问题_移动开发_06

经过梳理发现这几个函数的调用关系如下:

             

lottie动画在ios偶现播放问题_android_07

也就是入口函数实际只有这三个:

  • fromAssetFileName(Context context, String fileName, OnCompositionLoadedListener loadedListener);
  • fromFileSync(Context context, String fileName);
  • fromJson(Resources res, JSONObject json, OnCompositionLoadedListener loadedListener)。

正是通过这三个入口接收json文件、json流,然后通过AsynTask进行异步处理,最终核心处理都是在 fromJsonSync 中进行json数据的解析。

再来看fromJsonSync函数中的处理过程:

首先获取动画区域的宽高:

...
      int width = json.optInt("w", -1);
      int height = json.optInt("h", -1);
      ...

然后根据缩放比例换算实际所需要的宽高:

...
        int scaledWidth = (int) (width * scale);
        int scaledHeight = (int) (height * scale);
        ...

再根据实际宽高得到一块矩形区域

...
        bounds = new Rect(0, 0, scaledWidth, scaledHeight);
        ...

然后获取动画的初始帧,结束帧和帧率,并初始化 LottieComposition对象:

...
      long startFrame = json.optLong("ip", 0);
      long endFrame = json.optLong("op", 0);
      int frameRate = json.optInt("fr", 0);
      LottieComposition composition =
          new LottieComposition(bounds, startFrame, endFrame, frameRate, scale);
      ...

最后去解析assets 层级中的 LottieImageAsset属性并存储在images属性中,解析assets层级中的Layer属性并存储在precomps属性中解析外层的Layer属性并存储在layers属性中,返回 LottieComposition 对象:

...
      JSONArray assetsJson = json.optJSONArray("assets");
      parseImages(assetsJson, composition);
      parsePrecomps(assetsJson, composition);
      parseLayers(json, composition);
      return composition;
      ...



2. 根据对象创建drawable并绘制到View上

当LottieCompostion 返回后,会回调 LottieAnimationView.setComposition 方法。LottieAnimationView则通过代理属性--一个LottieDrawable对象,调用其内部的 setComposition 方法:

...
    boolean isNewComposition = lottieDrawable.setComposition(composition);
    if (!isNewComposition) {
      // We can avoid re-setting the drawable, and invalidating the view, since the composition
      // hasn't changed.
      return;
    }
    ...

我们看到 LottieDrawable 中的 setComposition 方法:

/**
   * @return True if the composition is different from the previously set composition, false otherwise.
   */
  @SuppressWarnings("WeakerAccess") public boolean setComposition(LottieComposition composition) {
    if (getCallback() == null) {
      throw new IllegalStateException(
          "You or your view must set a Drawable.Callback before setting the composition. This " +
              "gets done automatically when added to an ImageView. " +
              "Either call ImageView.setImageDrawable() before setComposition() or call " +
              "setCallback(yourView.getCallback()) first.");
    }

    if (this.composition == composition) {
      return false;
    }

    clearComposition();
    this.composition = composition;
    setSpeed(speed);
    setScale(1f);
    updateBounds();
    buildCompositionLayer();
    applyColorFilters();

    setProgress(progress);
    if (playAnimationWhenCompositionAdded) {
      playAnimationWhenCompositionAdded = false;
      playAnimation();
    }
    if (reverseAnimationWhenCompositionAdded) {
      reverseAnimationWhenCompositionAdded = false;
      reverseAnimation();
    }

    return true;
  }

可以看到 LottieDraw 先清理了旧的 compositionLayer 对象,重新建立了对 compostion 对象的引用,设置了 speed、setScale 等属性,然后通过 buildCompositionLayer 方法重新创建 compostionLayer 对象。

看一看 buildCompositionLayer 方法做了什么:

private void buildCompositionLayer() {
    compositionLayer = new CompositionLayer(
        this, Layer.Factory.newInstance(composition), composition.getLayers(), composition);
  }

通过 compostion 创建了一个 Layer 对象,并将自身、 compostion 对象中的 layers 属性及 composition 对象作为参数初始化了一个 CompositionLayer 对象:

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(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;
        }
      }
    }

    for (int i = 0; i < layerMap.size(); i++) {
      long key = layerMap.keyAt(i);
      BaseLayer layerView = layerMap.get(key);
      BaseLayer parentLayer = layerMap.get(layerView.getLayerModel().getParentId());
      if (parentLayer != null) {
        layerView.setParentLayer(parentLayer);
      }
    }
  }

大致就是将lottieDrawable、Layer传递给了Parent class, 并将外部 layer 都转换为 BaseLayer 并存储到了一个LongSparseArray中,并为所有BaseLayer设置了他的父亲 BaseLayer属性。

然而 CompositionLayer 又继承于 BaseLayer, 我们来看看它的 draw 方法:

@Override
  public void draw(Canvas canvas, Matrix parentMatrix, int parentAlpha) {
    if (!visible) {
      return;
    }
    buildParentLayerListIfNeeded();
    matrix.reset();
    matrix.set(parentMatrix);
    for (int i = parentLayers.size() - 1; i >= 0; i--) {
      matrix.preConcat(parentLayers.get(i).transform.getMatrix());
    }
    int alpha = (int)
        ((parentAlpha / 255f * (float) transform.getOpacity().getValue() / 100f) * 255);
    if (!hasMatteOnThisLayer() && !hasMasksOnThisLayer()) {
      matrix.preConcat(transform.getMatrix());
      drawLayer(canvas, matrix, alpha);
      return;
    }

    rect.set(0, 0, 0, 0);
    getBounds(rect, matrix);
    intersectBoundsWithMatte(rect, matrix);

    matrix.preConcat(transform.getMatrix());
    intersectBoundsWithMask(rect, matrix);

    rect.set(0, 0, canvas.getWidth(), canvas.getHeight());

    canvas.saveLayer(rect, contentPaint, Canvas.ALL_SAVE_FLAG);
    // Clear the off screen buffer. This is necessary for some phones.
    clearCanvas(canvas);
    drawLayer(canvas, matrix, alpha);

    if (hasMasksOnThisLayer()) {
      applyMasks(canvas, matrix);
    }

    if (hasMatteOnThisLayer()) {
      canvas.saveLayer(rect, mattePaint, SAVE_FLAGS);
      clearCanvas(canvas);
      //noinspection ConstantConditions
      matteLayer.draw(canvas, parentMatrix, alpha);
      canvas.restore();
    }

    canvas.restore();
  }

可以看到 BaseLayer 先绘制了最底层的内容,然后开始绘制包含的 layers 的内容,这个过程类似与界面中的 ViewGroup 嵌套绘制,其中需要用到 drawLayer 来进行layers的绘制,那我们再回到 CompostionLayer中的 drawLayer方法:

@Override void drawLayer(Canvas canvas, Matrix parentMatrix, int parentAlpha) {
    canvas.getClipBounds(originalClipRect);
    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) {
        layers.get(i).draw(canvas, parentMatrix, parentAlpha);
      }
    }
    if (!originalClipRect.isEmpty()) {
      canvas.clipRect(originalClipRect, Region.Op.REPLACE);
    }
  }

至此,LottieAnimationView的绘制流程结束。