Lottie的使用及原理浅析

来源:互联网 发布:linux mv移动多个文件 编辑:程序博客网 时间:2024/05/21 09:49

lottie

项目地址: https://github.com/xsfelvis/lottie-android

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 文件使用文本编辑器打开并修改底部一行:<Data key=”installedLanguages”>en_GB</Data> 为 <Data key=”installedLanguages”>zh_CN</Data> 保存。

`
- 安装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'    }

布局文件中添加

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

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

代码中添加

    LottieAnimationView animationView = (LottieAnimationView) findViewById(R.id.animation_view);    animationView.setAnimation("hello-world.json");    animationView.loop(true);

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

或者从网络上加载jsonObject

    LottieAnimationView animationView = (LottieAnimationView) findViewById(R.id.animation_view);     ...     LottieComposition composition = LottieComposition.fromJson(getResources(), jsonObject, (composition) -> {         animationView.setComposition(composition);         animationView.playAnimation();     });

你也可以通过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();

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

    LottieDrawable drawable = new LottieDrawable();    LottieComposition.fromAssetFileName(getContext(), "hello-world.json", (composition) -> {    drawable.setComposition(composition);    });

如果你需要频繁使用某一个动画,可以使用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();    }

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

简单介绍下,通过提供

  • 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);

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);

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

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

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

      private final LongSparseArray<Layer> layerMap = new LongSparseArray<>();      private final List<Layer> 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;

从数据变量来看,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);      }

特别是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);      }

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

    void buildLayersForComposition(LottieComposition composition)

这个函数的重点做的是为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);        }

绘制动画的载体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());            }          }        });      }
  • 进度控制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);        }      }

最终还是调用了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);        }      }

参考

  • https://github.com/xsfelvis/lottie-android
  • http://www.jianshu.com/p/81be1bf9600c
1 0