Android Lottie动画库介绍

        目前支持android上gif动画主要存在两种方式:第一种为将动画做成一张张图片然后进行快速切换,从而形成动画效果,第二种将动画变成json字符串,利用开源库进行解析,然后进行显示从而达到动画效果。其中两种方式的典型代表库为:android-gif-drawable和Lottie。

android-gif-drawable开源库地址:https://github.com/koral--/android-gif-drawable

Lottie开源库地址:https://github.com/airbnb/lottie-android


Lottie是Airbnb最近开源的一个动画库,它能够同时支持iOS,Android与ReactNative的开发.它的原理是利用Adobe After Effects软件做出动画,然后再用bodyMovin插件将动画导出成为一个json字符串,最后用Airbnb的开源库Lottie进行json文件解析最后绘制到设备上面,显而易见这个动画库的好处就是将动画变成了json字符串,极大地减少了文件大小,从而对apk进行了“瘦身”,所以下面总结了一下使用Lottie动画库进行动画显示的好处:

1、由于将动画变成了json字符串,极大地减少了应用大小。

2、因为Lottie是利用json文件生成动画,从而避免了不同分辨率、不同设备尺寸上面动画效果存在差异的问题。

3、只需要进行一动画绘制,生成一次json文件,从而可以在所有端进行使用(android,ios,web)


       上面介绍了Lottie动画库的好处,那么现在我们来具体了解Lottie动画库的工作原理和使用流程:

首先我们需要有一个用AE生成的json动画文件,这个文件由UI美工进行提供,不需要我们程序员进行生成(感兴趣的可以自己试试,反正我是不知道怎么弄),我们尝试打开json文件,得到如下的结果:

Android Lottie动画库介绍_第1张图片

        发现和普通的json文件没有任何区别,它是由layers数组和其他关键数组组成,学过ps的知道一张图片是有多个图层叠加而成,这里的动画也是如此,通过AE做成动画,然后由插件将图层、图片大小、动画时间、关键帧等信息输出到json字符串中,所以我们只需要解析json文件,将关键信息解析出来然后把图层一层一层绘制上去,然后一帧一帧的切换就形成了动画。

我们将Lottie动画下载下来阅读源码,得出Lottie的大致框架图如下:

Android Lottie动画库介绍_第2张图片

从图中可以看出Lottie库主要有三个重要的类型:AnimatableValue、KeyframeAnimation、AnimatableLayer,其中AnimatableLayer是继承Drawable类,所以真正进行动画绘制是依靠Drawable机制进行处理的,所有的关键数组最后都会封装到对应的AnimatableLayer中进行处理,其中流程图大致如下:

Android Lottie动画库介绍_第3张图片

接下来我们从源码角度来了解Lottie的工作过程,首先我们知道Lottie显示动画主要有两种方式:

第一种方式是在xml中配置:



第二种方式是在代码中进行动态设置:

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


首先我们看一下LottieAnimationView类的初始化,发现在构造方法中调用了init方法,下面就是init方法的实现:

private void init(@Nullable AttributeSet attrs) {
        TypedArray ta = getContext().obtainStyledAttributes(attrs,
                R.styleable.LottieAnimationView);
        String fileName = ta
                .getString(R.styleable.LottieAnimationView_lottie_fileName);
        if (!isInEditMode() && fileName != null) {
            setAnimation(fileName);
        }
        if (ta.getBoolean(R.styleable.LottieAnimationView_lottie_autoPlay,
                false)) {
            lottieDrawable.playAnimation();
        }
        lottieDrawable.loop(ta.getBoolean(
                R.styleable.LottieAnimationView_lottie_loop, false));
        ta.recycle();
        setLayerType(LAYER_TYPE_SOFTWARE, null);

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
            float systemAnimationScale = Settings.Global.getFloat(
                    getContext().getContentResolver(),
                    Settings.Global.ANIMATOR_DURATION_SCALE, 1.0f);
            if (systemAnimationScale == 0f) {
                lottieDrawable.systemAnimationsAreDisabled();
            }
        }
    }


我们发现在init方法中读取了xml中的配置信息,然后调用setAnimation()方法,最后调用LottieDrawable的playAnimation()方法开始播放动画,其中setAnimation()有多个重载方法,我们就看最核心的方法:

public void setAnimation(final String animationName,
            final CacheStrategy cacheStrategy) {
        this.animationName = animationName;
        if (weakRefCache.containsKey(animationName)) {
            WeakReference compRef = weakRefCache
                    .get(animationName);
            if (compRef.get() != null) {
                setComposition(compRef.get());
                return;
            }
        } else if (strongRefCache.containsKey(animationName)) {
            setComposition(strongRefCache.get(animationName));
            return;
        }

        this.animationName = animationName;
        lottieDrawable.cancelAnimation();
        cancelLoaderTask();
        compositionLoader = LottieComposition.fromAssetFileName(getContext(),
                animationName,
                new LottieComposition.OnCompositionLoadedListener() {
                    @Override
                    public void onCompositionLoaded(
                            LottieComposition composition) {
                        if (cacheStrategy == CacheStrategy.Strong) {
                            strongRefCache.put(animationName, composition);
                        } else if (cacheStrategy == CacheStrategy.Weak) {
                            weakRefCache.put(animationName,
                                    new WeakReference<>(composition));
                        }

                        setComposition(composition);
                    }
                });
    }


在这个方法中有两个参数一个是AnimationName,一个是CacheStrategy,毫无疑问AnimationName就是json文件的路径,而CacheStragy是什么呢?我们跟踪代码发现这个是缓存工具类,它是一个枚举类型,它提供了三种缓存技巧:Weak(弱引用缓存)、Strong(直接缓存在内存中)、None(不缓存),他们分别缓存在两个map中:

private static final Map strongRefCache = new HashMap<>();
private static final Map> weakRefCache = new HashMap<>();


在检查缓存之后利用LottieComposition的静态方法fromAssetFileName()去加载json文件,最后调用了fromInputStream()方法。

public static Cancellable fromInputStream(Context context,
            InputStream stream, OnCompositionLoadedListener loadedListener) {
        FileCompositionLoader loader = new FileCompositionLoader(
                context.getResources(), loadedListener);
        loader.execute(stream);
        return loader;
    }


其中FileCompositionLoader是AyscTask的子类,所以利用它进行了数据的异步加载(因为读取文件是耗时操作),当数据读取完成之后剩下来就是进行json字符串的解析了。

static LottieComposition fromJsonSync(Resources res, JSONObject json) {
        LottieComposition composition = new LottieComposition(res);

        int width = -1;
        int height = -1;
        try {
            width = json.getInt("w");
            height = json.getInt("h");
        } catch (JSONException e) {
            // ignore.
        }
        if (width != -1 && height != -1) {
            int scaledWidth = (int) (width * composition.scale);
            int scaledHeight = (int) (height * composition.scale);
            if (Math.max(scaledWidth, scaledHeight) > MAX_PIXELS) {
                float factor = (float) MAX_PIXELS
                        / (float) Math.max(scaledWidth, scaledHeight);
                scaledWidth *= factor;
                scaledHeight *= factor;
                composition.scale *= factor;
            }
            composition.bounds = new Rect(0, 0, scaledWidth, scaledHeight);
        }

        try {
            composition.startFrame = json.getLong("ip");
            composition.endFrame = json.getLong("op");
            composition.frameRate = json.getInt("fr");
        } catch (JSONException e) {
            //
        }

        if (composition.endFrame != 0 && composition.frameRate != 0) {
            long frameDuration = composition.endFrame - composition.startFrame;
            composition.duration = (long) (frameDuration
                    / (float) composition.frameRate * 1000);
        }

        try {
            JSONArray jsonLayers = json.getJSONArray("layers");
            for (int i = 0; i < jsonLayers.length(); i++) {
                Layer layer = Layer.fromJson(jsonLayers.getJSONObject(i),
                        composition);
                addLayer(composition, layer);
            }
        } catch (JSONException e) {
            throw new IllegalStateException("Unable to find layers.", e);
        }

        // These are precomps. This naively adds the precomp layers to the main
        // composition.
        // TODO: Significant work will have to be done to properly support them.
        try {
            JSONArray assets = json.getJSONArray("assets");
            for (int i = 0; i < assets.length(); i++) {
                JSONObject asset = assets.getJSONObject(i);
                JSONArray layers = asset.getJSONArray("layers");
                for (int j = 0; j < layers.length(); j++) {
                    Layer layer = Layer.fromJson(layers.getJSONObject(j),
                            composition);
                    addLayer(composition, layer);
                }
            }
        } catch (JSONException e) {
            // Do nothing.
        }

        return composition;
    }



从上面方法的返回值可以发现将json字符串中的所有信息都封装在LottieCompositon类中,在这个类中保存了动画时长、帧率等关键信息,其中将json文件中的图层layer封装成了Layer对象并保存在图层list列表layers中,而Layer类负责对图层JsonObject对象进行解析,阅读源码发现Layer类中解析出了scale、rotation、opacity、shape等动作,并将这些动作用对应的AnimatableValue类进行封装,至此json文件的解析过程就已经完成了,接下里就是如何实现动画的绘制了。

至此我们来总结一下json文件的解析过程,首先在LottieAnimationView的init方法中读取xml属性中动画文件,然后解析成LottieComposition对象,其中的解析过程是利用LottieCompostion类的静态方法进行json字符串的第一次基本解析,然后利用Layer对象将每个图层字符串解析变成一个Layer对象并保存在LottieComposition的成员变量layer集合中。在前面提到过我是利用AysncTask子类进行异步解析json字符串的,所以解析完成之后就会调用onPostExecute()方法

protected void onPostExecute(LottieComposition composition) {
     loadedListener.onCompositionLoaded(composition);
}

//这个类是在LottieAnimationView中进行定义的
private final LottieComposition.OnCompositionLoadedListener loadedListener = new LottieComposition.OnCompositionLoadedListener() {
        @Override
        public void onCompositionLoaded(LottieComposition composition) {
            setComposition(composition);
            compositionLoader = null;
        }
    };


所以解析完成之后开始调用LottieAnimationView的setComposition()方法,将LottieComposition对象设置到LottieDrawable对象

@Override
public void setComposition(@NonNull LottieComposition composition) {
        if (L.DBG) {
            Log.v(TAG, "Set Composition \n" + composition);
        }
        lottieDrawable.setCallback(this);
        lottieDrawable.setComposition(composition);
        // If you set a different composition on the view, the bounds will not
        // update unless
        // the drawable is different than the original.
        setImageDrawable(null);
        setImageDrawable(lottieDrawable);

        this.composition = composition;

        requestLayout();
    }

将LottieComposition设置个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;
    setSpeed(speed);
    setBounds(0, 0, composition.getBounds().width(), composition.getBounds().height());
    buildLayersForComposition(composition);

    setProgress(getProgress());
  }

到此为止我们就将整个动画设置给了View,接下来就是分析如何将动画运行起来了,我们发现在LottieAnimationView提供了playAnimation()方法,我们跟进去之后发现最终调用到了LottieDrawable对象的playAnimation()方法,我们分析一下LottieDrawable对象的playAnimation()是如何实现的:

void playAnimation() {
    if (layers.isEmpty()) {
      playAnimationWhenLayerAdded = true;
      reverseAnimationWhenLayerAdded = false;
      return;
    }
    animator.setCurrentPlayTime((long) (getProgress() * animator.getDuration()));
    animator.start();
  }
我们发现在LottieDrawable中实际上调用的是ValueAnimator属性动画,但是这个与我们说讲述的动画有什么关联呢?我们接下来发现了ValueAnimator中添加了一个动画的监听器,在监听器中我们的动画随着属性动画的值改变而发生改变。

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((float) animation.getAnimatedValue());
        }
      }
    });
  }
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);
    }
  }
 
  
接下来的处理就交给了每个图层进行处理,然后进行绘制,后面的处理逻辑就不进行叙述了。


通过以上分析,我们来总结一下Lottie动画库显示动画的整个流程:首先将json文件解析成为LottieComposition对象并将这个对象设置个LottieDrawable,在LottieDrawable对象将将LottieComposion中的图层Layer数据实体变成LayerView对象(这个对象里面处理了每个图层的控制逻辑),然后在LottieDrawable对象中利用属性动画将这个Lottie动画绘制起来,

将Lottie动画库中的每个图层一层一层的绘制,这个属性动画相当于整个动画的一个引子,将所有的图层联系起来。


下面我们来比较一下展示动画的两种常用方式:

(1)android-gif-drawable开源库,利用.gif文件进行动画展示

(2)Lottie动画库,利用json文件进行动画展示


我们将从三个方面来进行比较:

(1)原理方面:

android-gif-drawable使用c层进行gif图片解析,然后按照关键帧一张一张的显示,所以这个过程中存在gift文件解析、图片解码、图片渲染展示这些过程。

       Lottie则是根据Json文件利用canvas进行绘制,所以它的过程为:解析json文件,canvas绘制动画。


(2)实现效果方面

android-gif-drawable由于是使用了图片,所以图片尺寸大小就限定了动画的展示效果,所以他们在不同尺寸的设备上得展现效果存在差异.

Lottie动画库由于使用json文件,所以不存在这个问题,它在所有设备上得展现效果都一样


(2)系统资源占用方面

 Android Lottie动画库介绍_第4张图片

gif方式


    Android Lottie动画库介绍_第5张图片

Lottie方式


对于上面的比较,我们发现这两方式各有各的特点,各有各的优势,至于如何选择得看具体的业务场景,Lottie个人觉得适合做宣介、引导等动画,而gif方式则适合做局部复杂UI动画。

你可能感兴趣的:(android)