lottie入门

lottie

项目地址: https://github.com/jerryyh/MvpAndroidmaster

Lottie支持Jellybean (API 16)及以上的系统

什么是lottie?

Airbnb最近开源了一个名叫Lottie的动画库,它能够同时支持iOS,Android与ReactNative的开发,使用流程如下图所示

如图所示,通过安装AE上的bodymovin的插件,能够将AE中的动画工程文件转换为通用的json格式描述文件(bodymovin插件本身是用于网页上呈现各种AE效果的一个开源库),lottie所做的事情就是实现在不同移动端平台上呈现AE动画的方式,从而达到动画文件的一次绘制、一次转换,随处可用的效果,这个跟Java一次编译随处运行效果一样

很酷炫有木有!

使用准备

在使用这么酷炫的项目前,需要做一下准备

以windows为例,使用到的工具在云盘中

http://pan.baidu.com/s/1c19FLdA

  • 下载AE 安装(AE2007)


安装破解: 
Adobe After Effects CC 2017 安装后不要运行,直接使用 adobe.snr.patch.v2.0-painter.exe 选择产品破解即可

中文语言更改:
安装后默认是英文界面,找到安装目录(默认是:C:\Program Files\Adobe\Adobe After Effects CC 2017\Support Files\AMT)
在 AMT 文件夹内找到 application.xml 文件
使用文本编辑器打开并修改底部一行:
en_GBzh_CN 保存。
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6


- 安装bodymovin插件

该项目地址在https://github.com/bodymovin/bodymovin

安装这个插件有几种方式,采用安装 zxp installer安装bodymovin.zxp(获取这个文件:下载上面的zip文件,在build/extension目录下,这里已经下载好了)方式,

安装云盘中的aescript+aeplugins zxp installer.exe,然后安装bodymovin即可

此时打开AE 在window/extension 文件夹下可以看到bodymovin插件说明就ok了,

  • 开启允许脚本写入文件和访问网络

都需要在AE的编辑->首选项->常规中勾选允许脚本写入文件和访问网络(默认不开启)

制作Json

从这里可以找到一些Lottie中演示过的动画的AE源文件,下载到本地后在AE中打开即可(或者去https://material.uplabs.com选择 Download 选择 -> view all -> 在tools这一栏里面选择 After effects 然后选择一个免费的项目下载下来 用ae打开。 ).这里我们选用EmptyState.aep这个实例工程,稍作修改:

然后使用bodymovin插件导出aep文件对应的数据json(点击Render)

你也可以通过http://svgsprite.com/demo/bm/player.php?render=canvas&bg=fff去浏览你制作的json文件动画

使用Lottie库播放动画

Lottie的引入与使用就如其他库一样,这里以Android平台的使用为例.

在项目的build.gradle文件中加入:


    dependencies {  
        compile 'com.airbnb.android:lottie:1.0.1'
    }
  • 1
  • 2
  • 3
  • 4
  • 5

布局文件中添加

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


    
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

代码中添加


    LottieAnimationView animationView = (LottieAnimationView) findViewById(R.id.animation_view);
    animationView.setAnimation("hello-world.json");
    animationView.loop(true);
  • 1
  • 2
  • 3
  • 4
  • 5

这方法将在后台线程异步加载数据文件,并在加载完之后开始渲染显示动画

或者从网络上加载jsonObject


    LottieAnimationView animationView = (LottieAnimationView) findViewById(R.id.animation_view);
     ...
     LottieComposition composition = LottieComposition.fromJson(getResources(), jsonObject, (composition) -> {
         animationView.setComposition(composition);
         animationView.playAnimation();
     });
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

你也可以通过API控制动画,并且设置一些监听


    animationView.addAnimatorUpdateListener((animation) -> {
    // Do something.
    });
    animationView.playAnimation();
    ...
    if (animationView.isAnimating()) {
        // Do something.
    }
    ...
    animationView.setProgress(0.5f);
    ...
    // 自定义速度与时长
    ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f)
        .setDuration(500);
    animator.addUpdateListener(animation -> {
        animationView.setProgress(animation.getAnimatedValue());
    });
    animator.start();
    ...
    animationView.cancelAnimation();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

在使用遮罩的情况下,LottieAnimationView 使用 LottieDrawable来渲染动画.如果需要的话,你可以直接使用drawable形式:


    LottieDrawable drawable = new LottieDrawable();
    LottieComposition.fromAssetFileName(getContext(), "hello-world.json", (composition) -> {
    drawable.setComposition(composition);
    });
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

如果你需要频繁使用某一个动画,可以使用LottieAnimationView内置的一个缓存策略: 
LottieAnimationView.setAnimation(String, CacheStrategy) 
其中CacheStrategy的值可以是Strong,Weak或者None,它们用来决定LottieAnimationView对已经加载并转换好的动画持有怎样形式的引用(强引用/弱引用).

使用小结

关于方法数

使用ClassShark分析官方demo,lottie库其实只占用了783个方法数,还是比较少的

关于性能

官方说法

  • 如果没有mask和mattes,那么性能和内存非常好,没有bitmap创建,大部分操作都是简单的cavas绘制。
  • 如果存在mattes,将会创建2~3个bitmap。bitmap在动画加载到window时被创建,被window删除时回收。所以不宜在RecyclerView中使用包涵mattes或者mask的动画,否则会引起bitmap抖动。除了内存抖动,mattes和mask中必要的bitmap.eraseColor()和canvas.drawBitmap()也会降低动画性能。对于简单的动画,在实际使用时性能不太明显。
  • 如果在列表中使用动画,推荐使用缓存LottieAnimationView.setAnimation(String, CacheStrategy) 。

原理

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

首先要解析json,建立数据到对象的映射,然后根据数据对象创建合适的Drawable绘制到view上,动画的实现可以通过操作读取到的元素完成。

具体过程如下所示

json文件——>Component——>Drawable——>View

通过如下3个核心类来来完成整个工作流程,因而使用起来比较简单

  • LottieComposition(json->数据对象)

Lottie使用LottieComposition来作为After Effects的数据对象,即把Json文件映射为到LottieComposition,该类中提供了解析json的静态方法

  • LottieDrawable(数据对象->Drawable)

绘制

  • LottieAnimationView(绘制)

操作集合,LottieAnimationView 继承自 AppCompatImageView,封装了一些动画的操作,具体的绘制时委托为 LottieDrawable 完成的

LottieComposition(json->数据对象)

定义两个接口


    //定义两个通用接口
     public interface OnCompositionLoadedListener {
        void onCompositionLoaded(LottieComposition composition);
     }
     interface Cancellable {
      void cancel();
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

看下这个类的关键函数调用情况

简单介绍下,通过提供

  • fromAssetFileName(资源file)
  • fromFileSync(异步文件,通常是网络数据)
  • fromJson(直接的json)

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

主要分为以下层数据

width = json.getInt(“w”);

height = json.getInt(“h”);

将根据这两个得到一块矩形区域


    composition.bounds = new Rect(0, 0, scaledWidth, scaledHeight);
  • 1
  • 2

composition.startFrame = json.getLong(“ip”);

composition.endFrame = json.getLong(“op”);

composition.frameRate = json.getInt(“fr”);

将根据这几个得到,动画帧持续的时间


        long frameDuration = composition.endFrame - composition.startFrame;
        composition.duration = (long) (frameDuration / (float) composition.frameRate * 1000);
  • 1
  • 2
  • 3
  • 4

JSONArray jsonLayers = json.getJSONArray(“layers”);

其中Layers是包含了几层的参数,层级非常丰富,具体可以查看Layer类中的json解析(该类中除了解析参数之外还实现了属性动画一些基本类,核心方法是 Layer fromJson)

这些数据解析完后,都被存在如下变量中,用以描述After Effects中的动画


      private final LongSparseArray layerMap = new LongSparseArray<>();
      private final List layers = new ArrayList<>();
      private Rect bounds;
      private long startFrame;
      private long endFrame;
      private int frameRate;
      private long duration;
      private boolean hasMasks;
      private boolean hasMattes;
      private float scale;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

从数据变量来看,startFrame、endFrame、duration、scale等都是动画中常见的参数,List layers为映射拆分后的图层数据

LottieDrawable(数据对象->Drawable)

先看下继承关系

LottieDrawable extends AnimatableLayer extends Drawable

AnimatableLayer

首先看下AnimatableLayer继承了Drawable主要重写了draw,在代码中可以看出,借用canvas的saverestoreToCount来实现像PS那种图层叠加的效果


     @Override
      public void draw(@NonNull Canvas canvas) {
        int saveCount = canvas.save();
        applyTransformForLayer(canvas, this);

        int backgroundAlpha = Color.alpha(backgroundColor);
        if (backgroundAlpha != 0) {
          int alpha = backgroundAlpha;
          if (this.alpha != null) {
            alpha = alpha * this.alpha.getValue() / 255;
          }
          solidBackgroundPaint.setAlpha(alpha);
          if (alpha > 0) {
            canvas.drawRect(getBounds(), solidBackgroundPaint);
          }
        }
        for (int i = 0; i < layers.size(); i++) {
          layers.get(i).draw(canvas);
        }
        canvas.restoreToCount(saveCount);
      }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

特别是for循环那段,更是体现了将之前json解析的元素图层一层层的画出来,如同PS一般

好了,AnimatableLayer如其名一样负责将将之前的Layer层动画显示出来,下面再来看LottieDrawable

LottieDrawable

这个类的核心方法


      void 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.");
        }
        //清除之前的数据
        clearComposition();
        this.composition = composition;
        animator.setDuration(composition.getDuration());
        setBounds(0, 0, composition.getBounds().width(), composition.getBounds().height());
        //核心函数:即根据lottieComposition建立多个layerView,此时已经创建好了多个Drawable,并通过List建立的为以lottieDrawable为根的一个drawable树。
        buildLayersForComposition(composition);
        getCallback().invalidateDrawable(this);
      }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

该方法在LottieAnimationView中调用,该方法中实际调用的核心函数是


    void buildLayersForComposition(LottieComposition composition)
  • 1
  • 2
  • 3

这个函数的重点做的是为AnimatableLayer创建关键信息:

  • 将得到的bitmap(mainBitmap, maskBitmap, matteBitmap)+ layer(通过之前setComposition获得的)信息合成 
    LayerView

  • 将LayerView通过super.addLayer(AnimatableLayer#layers),在AnimatableLayer#layers中去draw

详见AnimatableLayer#draw方法


        //这里的layer来自于LottieDrawabe调用super.addLayers
        for (int i = 0; i < layers.size(); i++) {
          layers.get(i).draw(canvas);
        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

绘制动画的载体LottieAnimationView

这个view将加载、转换、和显示由AE动画插件bodymovin导出的json文件,并且支持设置动画的进度。,其实LottieAnimationView仅仅是个载体,通过管理

  • 数据源(LottieComposition)
  • 动画执行者(LottieDrawable实际上是AnimatableLayer!,LottieDrawable继承自AnimatableLayer)

LottieAnimationView继承了AppCompatImageView,并且封装了一些动画操作,如入口和进度控制,开启、取消、暂停动画

通过重载 setAnimation函数间接调用之前LottieComposition解析文件的三种方式

  • 入口

void setAnimation(final JSONObject json)——>LottieComposition.fromJson(getResources(), json, loadedListener)

void setAnimation(final String animationName, final CacheStrategy cacheStrategy)——LottieComposition.fromAssetFileName(getContext(), animationName,new LottieComposition.OnCompositionLoadedListener()

可以看出动画的设置是通过LottieComposition来代理的

  • 进度控制

setProgress(@FloatRange(from = 0f, to = 1f) float progress)——>lottieDrawable.setProgress(progress)

  • 开启动画

playAnimation()——>lottieDrawable.playAnimation();

  • 取消动画

cancelAnimation()——>lottieDrawable.cancelAnimation();

  • 暂停动画

pauseAnimation()——>lottieDrawable.cancelAnimation(); setProgress(progress);

因此可以看出动画的控制是通过lottieDrawable来代理进行的。

下面将详细的总结下LottieAnimationView如何工作的

  • 创建 LottieAnimationView lottieAnimationViewLottieDrawable lottieDrawable
  • 提供入口setAnimation,实际中是通过LottieComposition中的静态方法解析json文件创建LottieComposition lottieComposition,并且这个过程中已经解析出多个Layer对象。

这里要重点提一下 lottieDrawable.setComposition(lottieComposition),因为每个setAnimation都会去调用setComposition(@NonNull LottieComposition composition)里面会调用前面的方法。前面部分已经详述

  • 启动动画、取消动画、暂停动画

直接委托给了lottieDrawable,lottieDrawable中有private final ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);

在lottieDrawable中有private final ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);,并且在构造函数中初始化


      LottieDrawable() {
        super(null);
        animator.setRepeatCount(0);
        animator.setInterpolator(new LinearInterpolator());
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
          @Override public void onAnimationUpdate(ValueAnimator animation) {
            if (systemAnimationsAreDisabled) {
              animator.cancel();
              setProgress(1f);
            } else {
              setProgress(animation.getAnimatedFraction());
            }
          }
        });
      }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 进度控制setProgress方法

还是在lottieDrawable中实现,


      public void setProgress(@FloatRange(from = 0f, to = 1f) float progress) {
        this.progress = progress;
        for (int i = 0; i < animations.size(); i++) {
          animations.get(i).setProgress(progress);
        }

        for (int i = 0; i < layers.size(); i++) {
          layers.get(i).setProgress(progress);
        }
      }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

最终还是调用了private final List


      void setProgress(@FloatRange(from = 0f, to = 1f) float progress) {
        if (progress < getStartDelayProgress()) {
          progress = 0f;
        } else if (progress > getDurationEndProgress()) {
          progress = 1f;
        } else {
          progress = (progress - getStartDelayProgress()) / getDurationRangeProgress();
        }
        if (progress == this.progress) {
          return;
        }
        this.progress = progress;

        T value = getValue();
        for (int i = 0; i < listeners.size(); i++) {
          //在onValueChanged时,各个创建好的Drawable会根据需求进行重绘,达到动画的效果。
          listeners.get(i).onValueChanged(value);
        }
      }

你可能感兴趣的:(lottie入门)