Fish-Lottie: How to Implement a High-performance Animation Framework with Dart?

Project Architecture

After the Xianyu team researched the official open-source Lottie-Android library, they found that Flutter had provided implementation solutions that outperform the solutions provided by Android in both data parsing and graphic drawing. Therefore, the Xianyu team, in light of the Lottie-Android library, has implemented a pure Dart package to provide Lottie animation support on Flutter. This package has elaborate functions and high performance.

Fish-Lottie project architecture

As shown in the preceding figure, a project consists of basic modules, an interface layer, and a widget layer. The project has capabilities similar to Lottie-Android, such as vector graphic drawing, filling, and stroking. For more information about the capabilities, see Lottie Capabilities.

Basic Modules

The basic modules directly interact with various capabilities provided by the Flutter SDK. These basic modules are subdivided into the data model module, animation rendering module, data parsing module, and tool module. The workflow to process a JSON file, which contains the entire animation information, is as follows: The data parsing module first parses the data and information contained in the JSON file and transmits them to the data model module. Then, data models from the data model module are transmitted to the animation rendering module to draw graphics by using the drawing capabilities provided by Flutter. Lastly, the tool module is responsible for screen information acquisition, string processing, log printing, and other tool capabilities.

The Interface Layer

The interface layer is mainly responsible for obtaining JSON data and controlling animation rendering. After JSON information is parsed, the data parsing module will eventually generate a LottieComposition object, which carries the complete JSON animation information. Then, the object is passed to LottieDrawable, which passes on the object to the animation rendering module, so that the animation rendering module can obtain the animation information. At last, LottieDrawable calls the animation rendering module to draw and refresh the animation.

The Widget Layer

The widget layer has custom components that are implemented by inheriting Flutter Widget, which is also the interface exposed by the framework to developers. To make a simple Lottie animation player, developers only need to create a LottieAnimationView object and pass the JSON file path to it, and then add the LottieAnimationView object to FlutterUI as a widget. The JSON file path supports three formats, that is, the asset, URL, and file. In addition, the animation control interface and the widget layout interface are available. Developers can pass properties, such as width, height, and alignment, to the AnimationController object when creating the LottieAnimationView object to further customize the animation.

Workflow: from JSON to Animation

Overall Design

In Adobe After Effects (AE), an animation is actually composed of different layers. AE provides multiple layers for the designer to choose from, such as solid color layers (usually for the background), shape layers (for various vector graphics), text layers, and image layers. Each of these layers can be translated, rotated, and scaled. Each layer contains multiple elements. For example, a shape layer may have multiple basic vector graphics and pen path graphics to form a well-designed pattern. Every element may also have its own transformation in addition to the basic transformation, such as color and shape transformations. All the preceding layers and elements compose a complete animation.

Data Loading and Display

The widget layer provides three methods to obtain JSON files, that is, asset (built-in resources of the program), URL (network resources), and file (file resources). The flowchart for data loading and display is as follows, with the underlying rendering details omitted:

Animation Rendering and Playback

After we load and display the animation, we also need to make it live. To do this, set the value of AnimationController to the LottieDrawable’s progress by using AnimationBuilder, and then trigger the redrawing operation so that the underlying layer can obtain the animation properties through the progress. In this way, the animation will be live. The sequence diagram is as follows:

Implementation: Android and Flutter

The Widget Layer at the Android End

For Lottie-Android, LottieAnimationView and LottieDrawable compose the entire widget layer. LottieAnimationView inherits from ImageView, and LottieDrawable inherits from Drawable. The entire workflow is basically the same as the preceding procedure, and the developers writes LottieAnimationView in an XML file and sets the JSON file resource path. LottieAnimationView then initiates data acquisition and parsing. After the parsing is completed, the LottieComposition object is passed to LottieDrawable, and then the overridden draw method is called for animation display.

The Widget Layer at the Flutter End

For Flutter, no widget like ImageView and Drawable is available to inherit from and override. Hence, we need to customize a widget, which can be done in any of the following three ways:

@override
void paint(PaintingContext context, Offset offset) {
if (_drawable == null) return;
_drawable.draw(context.canvas, offset & size,
fit: _fit, alignment: _alignment);
}
//The paint method of RenderLottie

Text Rendering at the Android End

Canvas in the Android SDK provides the drawText method so that we can use a canvas to draw text directly. The Android implementation is as follows:

private void drawCharacter(String character, Paint paint, Canvas canvas) {
if (paint.getColor() == Color.TRANSPARENT) {
return;
}
if (paint.getStyle() == Paint.Style.STROKE && paint.getStrokeWidth() == 0) {
return;
}
canvas.drawText(character, 0, character.length(), 0, 0, paint);
}

Text Rendering at the Flutter End

Compared with the Android canvas, Flutter Canvas does not provide methods to draw text. After research, we found that Flutter had provided a special TextPainter for text rendering. The Flutter implementation is as follows:

void _drawCharacter(
String character, TextStyle textStyle, Paint paint, Canvas canvas) {
if (paint.color.alpha == 0) {
return;
}
if (paint.style == PaintingStyle.stroke && paint.strokeWidth == 0) {
return;
}
if (paint.style == PaintingStyle.fill) {
textStyle = textStyle.copyWith(foreground: paint);
} else if (paint.style == PaintingStyle.stroke) {
textStyle = textStyle.copyWith(background: paint);
}
var painter = TextPainter(
text: TextSpan(text: character, style: textStyle),
textDirection: _textDirection,
);
painter.layout();
painter.paint(canvas, Offset(0, -textStyle.fontSize));
}

The Bezier Curve

Animation at the Android End

In the background section, we mentioned that the Bezier curve is one of the three elements that make up the animation. As the animation is usually not played linearly, if we need to achieve the effect of moving fast first and then slowing down, we need to use a Bezier curve to map the progress to a property value in order to obtain the property value through the progress. The Android SDK provides PathInterpolator to obtain the property value. To do this, we can use two control points in the JSON file to describe the Bezier curve, pass the coordinates of these two control points to PathInterpolator, and then call the getInterpolation method of the interpolator to obtain the mapped property value. The implementation is as follows:

interpolator = PathInterpolatorCompat.create(cp1.x, cp1.y, cp2.x, cp2.y);public static Interpolator create(float controlX1, float controlY1,
float controlX2, float controlY2) {
if (Build.VERSION.SDK_INT >= 21) {
return new PathInterpolator(controlX1, controlY1, controlX2, controlY2);
}
return new PathInterpolatorApi14(controlX1, controlY1, controlX2, controlY2);
}
public PathInterpolator(float controlX1, float controlY1, float controlX2, float
controlY2) {
initCubic(controlX1, controlY1, controlX2, controlY2);
}
private void initCubic(float x1, float y1, float x2, float y2) {
Path path = new Path();
path.moveTo(0, 0);
path.cubicTo(x1, y1, x2, y2, 1f, 1f);
initPath(path);
}
//The key method for generating Bezier curves built in Android

Animation at the Flutter End

As no ready-made path interpolator is provided in Flutter, we have to implement it ourselves according to the source code. After checking the related source code, we found that we only need to pass the coordinates of the two control points in the JSON file to the cubicTo method in the Flutter path to generate the Bezier curve. Then, to obtain the property value, we can implement a method whose input parameter is the t time and return value is the p progress after mapping. The implementation details are similar as the getInterpolation method in PathInterpolator. The implementation is as follows:

interpolator = PathInterpolator.cubic(cp1.dx, cp1.dy, cp2.dx, cp2.dy);factory PathInterpolator.cubic(
double controlX1, double controlY1, double controlX2, double controlY2) {
return PathInterpolator(
_initCubic(controlX1, controlY1, controlX2, controlY2));
}
static Path _initCubic(
double controlX1, double controlY1, double controlX2, double controlY2) {
final path = Path();
path.moveTo(0.0, 0.0);
path.cubicTo(controlX1, controlY1, controlX2, controlY2, 1.0, 1.0);
return path;
}
//The key method for customizing Flutter Bezier curves

Effects: Android and Flutter

We have implemented a closed-loop demo project with Fish-Lottie. We also used the Lottie JSON file from the Lottie-Android project for testing and found that the release package is comparable to the official sample app in both the fluency and accuracy of animation rendition. Next, I will compare them with a few examples:

Prospects: from Static to Interactive

Currently, Lottie is only used for the static playback of animations. For example, the thumb animation that appears after you click Like, and the heart-shaped animation that appears after you click Favorites. At best, you can control the playback of the entire animation by using the progress bar. However, in the process of implementing the entire framework, I realized that Lottie-Android already has some interactive capabilities. The implementation is as follows:

val shirt = KeyPath("Shirt", "Group 5", "Fill 1")
animationView.addValueCallback(shirt, LottieProperty.COLOR) { Colors.XXX } // The color to customize

The Lottie-Android Solution

From the preceding code we discovered that, to implement dynamic property control, we need to pass three parameters. The first parameter is similar to a locator, which locates vector graphics whose properties we want to control. The second parameter is a property enumeration variable, which indicates the property type. The last parameter is a callback function, which returns the dynamically changed target value.

The Fish-Lottie Solution

Generally, we do not obtain a widget reference in Flutter to call its methods due to the differences in the implementations of widgets and UI building features between both ends. Therefore, we cannot directly use lottieAnimationView.addValueCallback() to control dynamic properties, like in Lottie-Android. In fact, we have encountered the same problem when controlling the progress of an animation. So, our idea is actually the same as implementing AnimationController. That is, create a PropertiesController object and pass the target graphics, target properties, and callback functions that we want to modify to this controller. Then, pass this controller to LottieDrawable as a parameter of the LottieAnimationView constructor. At last, this property controller initiates the matching for the target graphic rendering class and the setting for callback functions. The underlying methods in the drawing class and frame animation class are consistent with those in Lottie-Android. The basic idea is the same as that of Lottie-Android, except that LottieAnimationView is no longer responsible for property control, but PropertiesController is.

Implementation

With the interaction capability, we can achieve more than controlling the playback of animations. For example, we can make some complex interactions by providing feedback to users’ click events.

Original Source:

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store