Implementing Lottie Animations in Flutter and Android: Architecture and Code Overview
The Xianyu team built a pure‑Dart Lottie library for Flutter, featuring a three‑layer architecture (base, interface, component), a custom RenderLottie RenderObject for canvas drawing, full text‑animation support via TextPainter, cubic‑Bezier interpolation matching Android, and plans for interactive property controllers.
Lottie is an open‑source animation solution from Airbnb that uses JSON to describe vector‑based animations, reducing the cost of implementing complex animations on Android, iOS, Web and other platforms.
The Xianyu team adopted Flutter early and needed a Lottie‑Flutter implementation. Existing third‑party solutions either parse and render on the native side (causing flicker and performance issues) or parse directly in Flutter but lack features such as text animation. This motivated the creation of a pure‑Dart Lottie package with full feature parity and high performance.
Project Architecture
The framework consists of three layers: a base module, an interface layer, and a component layer. The base module handles data models, animation drawing, JSON parsing and utilities. The interface layer converts JSON into a LottieComposition object and forwards it to LottieDrawable . The component layer exposes a LottieAnimationView widget that developers can place in a Flutter UI, supporting asset, URL and file sources.
Workflow
Designers create animations in After Effects (AE). Each layer (solid, shape, text, image) can have transforms, fills, strokes, etc. The BodyMovin plugin exports the composition to a JSON file containing all layer and keyframe data.
At runtime the JSON is parsed into a LottieComposition . LottieDrawable receives the composition and uses Flutter’s Canvas to draw the vector graphics. An AnimationBuilder drives the progress value, which triggers redraws based on the current keyframe values.
Android Component Layer
In the Android implementation, AnimationView (subclass of ImageView ) holds a LottieDrawable . The drawable’s ValueAnimator drives a 0‑1 progress value that the view forwards to the drawing logic.
Flutter Component Layer
Flutter does not provide a direct ImageView/Drawable pair, so a custom widget is required. Three approaches were considered:
Native widget composition – unsuitable because the canvas is needed.
CustomPainter – cannot access layout constraints for automatic sizing.
Custom RenderObject – chosen. A RenderLottie (subclass of RenderBox ) overrides paint to forward the canvas to LottieDrawable . A LeafRenderObjectWidget creates the render object and updates its progress.
Key code for the RenderObject implementation:
@override
void paint(PaintingContext context, Offset offset) {
if (_drawable == null) return;
_drawable.draw(context.canvas, offset & size, fit: _fit, alignment: _alignment);
}
//RenderLottie paint methodText Rendering
Android uses Canvas.drawText . Example:
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);
}Flutter lacks a direct text‑draw method, so TextPainter is used:
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));
}Bezier Curve Interpolation
Both Android and Flutter need custom cubic interpolation for non‑linear timing. Android uses PathInterpolatorCompat.create(cp1.x, cp1.y, cp2.x, cp2.y) . Example:
public static Interpolator create(float cx1, float cy1, float cx2, float cy2) {
if (Build.VERSION.SDK_INT >= 21) {
return new PathInterpolator(cx1, cy1, cx2, cy2);
}
return new PathInterpolatorApi14(cx1, cy1, cx2, cy2);
}
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);
}Flutter implements the same idea with its own PathInterpolator factory:
factory PathInterpolator.cubic(double cx1, double cy1, double cx2, double cy2) {
return PathInterpolator(_initCubic(cx1, cy1, cx2, cy2));
}
static Path _initCubic(double cx1, double cy1, double cx2, double cy2) {
final path = Path();
path.moveTo(0.0, 0.0);
path.cubicTo(cx1, cy1, cx2, cy2, 1.0, 1.0);
return path;
}Effect Comparison
Demo videos show that fish‑lottie on Flutter matches the smoothness and fidelity of the native lottie‑android implementation, even for dynamic text animations. The Flutter version also exposes richer text‑style APIs not available in the Android library.
Future Outlook
Beyond static playback, the team plans to add interactive capabilities: property controllers that map KeyPath objects to dynamic callbacks, enabling color changes, shape morphing, and sensor‑driven effects (e.g., gyroscope‑controlled cloud movement). This will allow complex UI feedback such as animated search‑box backgrounds that react to user taps.
Recruitment Notice (non‑academic segment)
The article ends with a hiring call for Xianyu engineers, which is promotional and not part of the technical content.
Xianyu Technology
Official account of the Xianyu technology team
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.