Lottie is a set of cross-platform framework developed by Airbnb, which can show the animation effects generated by AE on various platforms, supporting Android, iOS, Web, MacOS, Windows, etc. Taking Android as an example, designers use AE to design animations, export AE project files as JSON files through the plug-in Bodymovin, app parses the corresponding data structure through Lottie, and finally draws through Canvas.

If you want to test it yourself, you can download the json animation file directly from LottieFiles, which is rich in content. Look at a chestnut, see the effect:

This animation can be exported as robotwave. json after Effects, as shown in Demo. The format is as follows:

{
    "v":"4.10.1", version of # Bodymovin"fr":60, # Number of animation frames, where 1s is performed60frame"ip":0, # Start frame of animation"op":123, # op-ip gets the total number of frames for the animation"w":400."h":400, # Animation width and height, Lottie will adapt to the phone screen"nm":"AndroidWave", # name"ddd":0#,3d
    "assets":Array[], # animate resource file"layers":Array[] # layer information to drawCopy the code

How do developers use this JSON file once they have it? Here’s an example of Lottie-Android:

Implementation 'com. Reality. The android: Lottie: 3.7.0'Copy the code

Code implementation

Within our XML file, add the LottieAnimationView (which supports dynamic creation) :

    <com.airbnb.lottie.LottieAnimationView
        android:id="@+id/animationView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        airbnb:lottie_fileName="RobotWave.json"
        airbnb:lottie_autoPlay="true"
        airbnb:lottie_loop="true"/>
Copy the code

In our code, we can use it directly, is it very convenient?

    private lateinit var lottieAnimaView: LottieAnimationView

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.lottie_layout)
        
        lottieAnimaView = findViewById(R.id.animationView)
        lottieAnimaView.playAnimation()
    }
Copy the code

Json source

Lottie not only loads JSON inside assets folders, but also supports:

  • Json animation in SRC /main/res/raw
  • Zip file under SRC /main/assets
  • Json or ZIP file URL
  • A JSON string from anywhere
  • From the Json InputStream

The LottieAnimationView provides a set method. You can also use the LottieCompositionFactory method. Just take the URL as an example:

    private val url = "https://assets1.lottiefiles.com/packages/lf20_gygeywbl.json"
    private var cacheKey : String? = null;
    
    fun playAnimaFromUrl(a) {
         LottieCompositionFactory.fromUrl(application, url, cacheKey)
                .addListener() {lottieAnimaViewByUrl.setComposition(it)}
        /* lottieAnimaViewByUrl.setAnimationFromUrl(url, cacheKey) */
        lottieAnimaViewByUrl.playAnimation()
    }
Copy the code

The results are as follows:

I won’t go into apis like registering for listening or basic animation configuration, but just check the Lottie website to use it. Here’s how Lottie makes animations work with JSON.

The animation principles

JSON to LottieComposition

Lottie’s first step is to parse JSON, mapping the JSON node data into the fields of the LottieComposition class. In this example, we will first convert the JSON to an InputStream and pass it in through the simple LottieCompositionFactory:

  public void setAnimationFromJson(String jsonString, @Nullable String cacheKey) {
    setAnimation(new ByteArrayInputStream(jsonString.getBytes()), cacheKey);
  }
  public void setAnimation(InputStream stream, @Nullable String cacheKey) {
    setCompositionTask(LottieCompositionFactory.fromJsonInputStream(stream, cacheKey));
  }
Copy the code

Here we have a buffer read mechanism that converts stream incoming to synchronous loading when there is no cache to read:

  public static LottieTask<LottieComposition> fromJsonInputStream(final InputStream stream, @Nullable final String cacheKey) {
    // Return LottieTask
      
    // If not, a call() callback is performed within the LottieTask
    return cache(cacheKey, new Callable<LottieResult<LottieComposition>>() {
      @Override
      public LottieResult<LottieComposition> call(a) {
        returnfromJsonInputStreamSync(stream, cacheKey); }}); }Copy the code

In this case, we will convert the InputStream to a JsonReader stream, so that we can avoid the OOM problems caused by loading it all into memory at once:

  @WorkerThread
  private static LottieResult<LottieComposition> fromJsonInputStreamSync(InputStream stream, @Nullable String cacheKey, boolean close) {
    return fromJsonReaderSync(JsonReader.of(buffer(source(stream))), cacheKey);
  }
  @WorkerThread
  public static LottieResult<LottieComposition> fromJsonReaderSync(com.airbnb.lottie.parser.moshi.JsonReader reader, @Nullable String cacheKey) {
    return fromJsonReaderSyncInternal(reader, cacheKey, true);
  }
Copy the code

Finally through LottieCompositionMoshiParser. Flow from the parse method and resolve the complete LottieComposition object, LottieComposition contains all nodes described above Json data:

  private static LottieResult<LottieComposition> fromJsonReaderSyncInternal(
      com.airbnb.lottie.parser.moshi.JsonReader reader, @Nullable String cacheKey, boolean close) {
    LottieComposition composition = LottieCompositionMoshiParser.parse(reader);
      if(cacheKey ! =null) {
        LottieCompositionCache.getInstance().put(cacheKey, composition);
      }
      return new LottieResult<>(composition);
  }
Copy the code

BaseLayer to LottieComposition

Lottie’s second step is to generate each level of BaseLayer via LottieComposition. Here LottieAnimationView delegates composition to lottieDrawable:

  public void setComposition(@NonNull LottieComposition composition) {...booleanisNewComposition = lottieDrawable.setComposition(composition); . }public boolean setComposition(LottieComposition composition) {... buildCompositionLayer(); . }Copy the code

Here we create a CompositionLayer object that internally manages the layers of each Layer:

  private void buildCompositionLayer(a) {
    compositionLayer = new CompositionLayer(
        this, LayerParser.parse(composition), composition.getLayers(), composition); . }Copy the code

Create BaseLayer concrete classes based on the type of the Layer set within the composition and store them in the layerMap set:

  public CompositionLayer(LottieDrawable lottieDrawable, Layer layerModel, List
       
         layerModels, LottieComposition composition)
        {
          LongSparseArray<BaseLayer> layerMap =
        newLongSparseArray<>(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); . }... }Copy the code

This is the BaseLayer class conversion utility class:

  static BaseLayer forModel( Layer layerModel, LottieDrawable drawable, LottieComposition composition) {
    switch (layerModel.getLayerType()) {
      case SHAPE:
        return new ShapeLayer(drawable, layerModel);
      case PRE_COMP:
        return new CompositionLayer(drawable, layerModel,
            composition.getPrecomps(layerModel.getRefId()), composition);
      case SOLID:
        return new SolidLayer(drawable, layerModel);
      case IMAGE:
        return new ImageLayer(drawable, layerModel);
      case NULL:
        return new NullLayer(drawable, layerModel);
      case TEXT:
        return new TextLayer(drawable, layerModel);
      case UNKNOWN:
      default:
        // Do nothing
        Logger.warning("Unknown layer type " + layerModel.getLayerType());
        return null; }}Copy the code

3. Play the animation

When played, LottieAnimationView delegates lottieDrawable, lottieDrawable delegates the Animator to execute the playAnimation method, The animator is an object of the LottieValueAnimator, which controls the progress and updates of the entire animation:

  @MainThread
  public void playAnimation(a) {
    if(isShown()) { lottieDrawable.playAnimation(); }... }public void playAnimation(a) {...if (animationsEnabled() || getRepeatCount() == 0) { animator.playAnimation(); }... }Copy the code

The notifyStart method notifies the animation to start. The setFrame method sets the current frame data, and the postFrameCallback method is the core. See below:

  @MainThread
  public void playAnimation(a) {
    running = true;
    / / notifyStart will notify the listener. OnAnimationStart (this, isReverse);
    notifyStart(isReversed());
    setFrame((int) (isReversed() ? getMaxFrame() : getMinFrame()));
    lastFrameTimeNs = 0;
    repeatCount = 0;
    postFrameCallback();
  }
Copy the code

The postFrameCallback method will request a VSYNC signal:

  protected void postFrameCallback(a) {
    if (isRunning()) {
      removeFrameCallback(false);
      Choreographer.getInstance().postFrameCallback(this); }}Copy the code

This will then call back to the doFrame method inside the LottieValueAnimator and we can see that the postFrameCallback method will be executed with the VSYNC signal at an interval of 16.6ms. So this method updates the value of the next frame every 16.6ms and calls notifyUpdate to notify LottieDrawable’s onAnimationUpdate callback:

  @Override public void doFrame(long frameTimeNanos) {
    postFrameCallback();
    if (composition == null| |! isRunning()) {return;
    }

    L.beginSection("LottieValueAnimator#doFrame");
    long now = frameTimeNanos;
    long timeSinceFrame = lastFrameTimeNs == 0 ? 0 : now - lastFrameTimeNs;
    float frameDuration = getFrameDurationNs();
    float dFrames = timeSinceFrame / frameDuration;

    frame += isReversed() ? -dFrames : dFrames;
    booleanended = ! MiscUtils.contains(frame, getMinFrame(), getMaxFrame()); frame = MiscUtils.clamp(frame, getMinFrame(), getMaxFrame()); lastFrameTimeNs = now; notifyUpdate(); . }void notifyUpdate(a) {
    for (ValueAnimator.AnimatorUpdateListener listener : updateListeners) {
      listener.onAnimationUpdate(this); }}Copy the code

The progress value is updated after the onAnimationUpdate callback of LottieDrawable:

  private final ValueAnimator.AnimatorUpdateListener progressUpdateListener = new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
      if(compositionLayer ! =null) { compositionLayer.setProgress(animator.getAnimatedValueAbsolute()); }}};Copy the code

LottieDrawable notifies each layers to update their progress values:

  @Override public void setProgress(@FloatRange(from = 0f, to = 1f) float progress) {
    super.setProgress(progress); .for (int i = layers.size() - 1; i >= 0; i--) { layers.get(i).setProgress(progress); }}Copy the code

BaseLayer will notify onValueChanged after updating the progress value:

  void setProgress(@FloatRange(from = 0f, to = 1f) float progress) {
    // Time stretch should not be applied to the layer transform.transform.setProgress(progress); .if(matteLayer ! =null) {
      // The matte layer's time stretch is pre-calculated.
      float matteTimeStretch = matteLayer.layerModel.getTimeStretch();
      matteLayer.setProgress(progress * matteTimeStretch);
    }
    for (int i = 0; i < animations.size(); i++) { animations.get(i).setProgress(progress); }}public void setProgress(@FloatRange(from = 0f, to = 1f) float progress) {...this.progress = progress;
    if(keyframesWrapper.isValueChanged(progress)) { notifyListeners(); }}public void notifyListeners(a) {
    for (int i = 0; i < listeners.size(); i++) { listeners.get(i).onValueChanged(); }}Copy the code

The BaseLayer then tells lottieDrawable to update, which then triggers lottieDrawable’s draw method:

  /* BaseLayer.java */
  @Override public void onValueChanged(a) {
    invalidateSelf();
  }
  
  private void invalidateSelf(a) {
    lottieDrawable.invalidateSelf();
  }
  
  /* LottieDrawable.java*/
  @Override public void draw(@NonNull Canvas canvas) {
   if (safeMode) {
      try {
        drawInternal(canvas);
      } catch (Throwable e) {
        Logger.error("Lottie crashed in draw!", e); }}... }Copy the code

LottieDrawable then tells the compositionLayer to draw:

  private void drawInternal(@NonNull Canvas canvas) {
    if(! boundsMatchesCompositionAspectRatio()) { drawWithNewAspectRatio(canvas); }else{ drawWithOriginalAspectRatio(canvas); }}private void drawWithOriginalAspectRatio(Canvas canvas) {... compositionLayer.draw(canvas, matrix, alpha); . }Copy the code

Finally, the compositionLayer notifies each layer, and the drawing is complete.

  public void draw(Canvas canvas, Matrix parentMatrix, int parentAlpha) {...if(! hasMatteOnThisLayer() && ! hasMasksOnThisLayer()) { drawLayer(canvas, matrix, alpha);return; }... }Copy the code

That’s the end of this article. If this article is useful to you, give it a thumbs up, everyone’s affirmation is also the motivation of dumb I to keep writing.