Introduction to Android Lottie Animation Library

Keywords: JSON Android Attribute xml

At present, there are two main ways to support GIF animation on android: the first way is to make animation into one picture and switch it quickly to form animation effect; the second way is to change animation into json string, parse it using open source library, and then display it to achieve animation effect. Two typical representational libraries are android-gif-drawable and Lottie.

android-gif-drawable open source library address: https://github.com/koral--/android-gif-drawable

Lottie Open Source Library Address: https://github.com/airbnb/lottie-android


Lottie is Airbnb's latest open source animation library, which supports the development of iOS,Android and ReactNative at the same time. Its principle is to use Adobe After Effects software animation, and then use the bodyMovin plug-in to export the animation into a json string, and finally use Airbnb's open source library Lottie to parse the json file and draw it on the device. Obviously, the advantage of this animation library is to turn the animation into a json string, which greatly reduces the file size, thus making the apk "thin", so the following summary is made. Here are the benefits of using Lottie animation libraries for animation display:

1. Because the animation is changed into a json string, the application size is greatly reduced.

2. Because Lottie uses json file to generate animation, it avoids the problem of animation effect difference on different resolution and different device size.

3. Only one animation is needed to generate a json file once, so that it can be used at all ends (android, ios, web)


The advantages of Lottie animation library are introduced above. Now let's understand the working principle and using process of Lottie animation library in detail.

First, we need to have a json animation file generated by AE, which is provided by UI artists, and does not need to be generated by our programmers. (Interested in it, you can try it yourself, I don't know how to do it anyway). We try to open the json file and get the following results:


It is found that there is no difference from ordinary json file. It is composed of layers array and other key arrays. We learned that a picture is made of multiple layers, and so is the animation here. The animation is made by AE, and then the information of layer, picture size, animation time, key frame is output to json string by plug-in, so we only need to To parse the json file, parse out the key information, draw the layers one by one, and then switch frame by frame to form an animation.

We downloaded Lottie animation to read the source code, and got the general framework of Lottie as follows:


From the figure, we can see that Lottie library has three important types: Animatable Value, Keyframe Animation and Animatable Layer. Animatable Layer inherits Drawable class, so the real animation drawing is handled by Drawable mechanism. All key arrays are finally encapsulated in the corresponding Animatable Layer for processing. The flow chart is roughly as follows:

Next, we learn about Lottie's working process from the source point of view. First of all, we know that Lottie shows animation in two main ways:

The first way is to configure in xml:

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


The second way is to set up dynamically in the code:

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


First, we look at the initialization of the LottieAnimationView class and find that the init method is called in the constructor. Here is the implementation of the init method:

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


We find that we read the configuration information in xml in init method, then call setAnimation() method, and finally call LottieDrawable's playAnimation() method to start playing animation. setAnimation() has several overloading methods, so we look at the core method:

public void setAnimation(final String animationName,
            final CacheStrategy cacheStrategy) {
        this.animationName = animationName;
        if (weakRefCache.containsKey(animationName)) {
            WeakReference<LottieComposition> 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);
                    }
                });
    }


There are two parameters in this method: Animation Name and CacheStrategy. There is no doubt that Animation Name is the path of json file. What is CacheStragy? We tracked the code and found that this is a caching tool class, which is an enumeration type. It provides three caching techniques: Weak (weak reference caching), Strong (direct caching in memory), None (no caching), which are cached in two map s:

private static final Map<String, LottieComposition> strongRefCache = new HashMap<>();
private static final Map<String, WeakReference<LottieComposition>> weakRefCache = new HashMap<>();


After checking the cache, the static method fromAssetFileName() of LottieComposition is used to load the json file, and the fromInputStream() method is finally called.

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


FileCompositionLoader is a subclass of AyscTask, so it is used to load data asynchronously (because reading files is time-consuming), and when the data is read, the rest is to parse the json string.

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



From the return value of the above method, we can find that all information in json string is encapsulated in Lottie Composite class, in which key information such as animation time and frame rate are saved. Layer object in json file is encapsulated into Layer object and stored in Layer list layers. Layer class is responsible for parsing layer JsonObject object and reading source code to discover Layer. The scale, rotation, opacity, shape and other actions are parsed in yer class, and the corresponding AnimatableValue classes of these actions are encapsulated. So far, the parsing process of json file has been completed, and then how to draw the animation is realized.

So let's summarize the parsing process of json file. First, we read the animation file in the xml attribute in the init method of Lottie Animation View, and then parse it into Lottie Composition object. The parsing process is to use the static method of Lottie Composition class for the first basic parsing of json string, and then use Layer object to parse each layer string into an L. The Ayer object is stored in the member variable layer collection of LottieComposition. As mentioned earlier, I used the AysncTask subclass to asynchronously parse json strings, so after parsing is complete, the onPostExecute() method is called.

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

//This class is defined in Lottie Animation View
private final LottieComposition.OnCompositionLoadedListener loadedListener = new LottieComposition.OnCompositionLoadedListener() {
        @Override
        public void onCompositionLoaded(LottieComposition composition) {
            setComposition(composition);
            compositionLoader = null;
        }
    };


So when the parsing is complete, the setComposition() method of LottieAnimationView is called to set the LottieComposition object to the LottieDrawable object.

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

After setting Lottie Composition as a lottieDrawable object, the framework transforms the previously parsed data into a set of layer objects, each containing a set of operations on the layer.

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

So far, we have set the whole animation to View. Next, we analyze how to run the animation. We find that Lottie Animation View provides the playAnimation() method. After we follow up, we find that the playAnimation() method of LottieDrawable object is finally called. We analyze how the playAnimation() of LottieDrawable object is implemented:

void playAnimation() {
    if (layers.isEmpty()) {
      playAnimationWhenLayerAdded = true;
      reverseAnimationWhenLayerAdded = false;
      return;
    }
    animator.setCurrentPlayTime((long) (getProgress() * animator.getDuration()));
    animator.start();
  }
We find that in Lottie Drawable we actually call the Value Animator property animation, but what does this have to do with the animation we're talking about? We then found that a listener for animation was added to Value Animator, in which our animation changed with the value of the attribute animation.

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


The next processing is handed over to each layer for processing, and then for drawing, the following processing logic is not described.


Through the above analysis, we summarize the whole process of Lottie animation library displaying animation: first, we parse the json file into Lottie Composition object and set this object as Lottie Drawable. In Lottie Drawable object, the Layer data entity in Lottie Composition will be transformed into LayerView object (which handles the control logic of each layer), and then in Lottie Drawable object. The Lottie animation is drawn in the ttieDrawable object by using attribute animation.

Drawing each layer in the Lottie animation library layer by layer, this attribute animation is equivalent to a lead of the whole animation, linking all the layers.


Now let's compare two common ways of showing animation:

(1) android-gif-drawable open source library, using. GIF file for animation display

(2) Lottie animation library, using json file for animation display


We will compare them in three ways:

(1) Principles:

android-gif-drawable uses c layer to parse gif pictures, and then displays them one by one according to the key frames, so there are gift file parsing, picture decoding, and picture rendering in this process.

Lottie uses canvas to draw according to Json file, so its process is: parsing JSON file and drawing animation by canvas.


(2) Achieving effect

android-gif-drawable uses pictures, so the size of pictures limits the effect of animation, so they have different effects on different sizes of devices.

The Lottie animation library does not have this problem because it uses json files. It works the same on all devices.


(2) System resource occupancy

 

gif mode


    

Lottie approach


For the above comparison, we find that these two methods have their own characteristics and advantages. As for how to choose a specific business scenario, Lottie personally feels suitable for animation such as publicity and guidance, while gif mode is suitable for local complex UI animation.

Posted by Zepo. on Mon, 24 Jun 2019 14:12:16 -0700