Lottie 动画原理剖析

456 阅读5分钟

一、核心架构与核心流程

Lottie 的核心是将设计师用 Adobe After Effects 制作的动画,通过 Bodymovin 插件导出为轻量级 JSON 文件,然后在移动端解析并渲染为流畅动画。整个流程可分为三大阶段:

  1. JSON 解析阶段

    • 解析器将 JSON 文件转换为 LottieComposition 对象,包含动画的宽高、帧率、图层结构等基础信息。
    • 关键类 LayerParser 递归解析 JSON 中的 layers 字段,根据 ty 字段创建不同类型的 BaseLayer 子类(如 ShapeLayerImageLayer),并构建图层树结构。
  2. 动画控制阶段

    • LottieDrawable 通过 ValueAnimator 和 Choreographer 同步屏幕刷新率(VSYNC 信号),驱动动画进度更新1011。
    • 核心方法 setProgress() 将进度传递给所有图层,触发关键帧动画计算和重绘请求。
  3. 渲染阶段

    • 每个 BaseLayer 负责将自身内容绘制到 Canvas 上,支持图层叠加、变换(缩放 / 旋转 / 位移)等效果17。
    • 复杂动画(如遮罩、路径动画)通过 KeyframeAnimation 子类实现插值计算,例如 TransformKeyframeAnimation 处理图层变换属性。

二、源码级实现细节

1. JSON 解析流程
// LottieCompositionFactory.java
public static void fromAssetFileName(Context context, String fileName,
                                     @NonNull CompositionListener listener) {
    // 异步解析 JSON 文件
    new AsyncTask<Void, Void, LottieComposition>() {
        @Override
        protected LottieComposition doInBackground(Void... params) {
            try (InputStream is = context.getAssets().open(fileName)) {
                return LottieCompositionParser.parse(is);
            }
        }

        @Override
        protected void onPostExecute(LottieComposition composition) {
            listener.onCompositionLoaded(composition);
        }
    }.execute();
}

// LottieCompositionParser.java
public static LottieComposition parse(InputStream inputStream) {
    JsonReader reader = new JsonReader(new InputStreamReader(inputStream));
    reader.beginObject();
    while (reader.hasNext()) {
        String name = reader.nextName();
        if (name.equals("layers")) {
            // 解析图层数组
            List<Layer> layers = parseLayers(reader);
            composition.setLayers(layers);
        }
        // 解析其他字段(帧率、尺寸等)
    }
    return composition;
}

// LayerParser.java
public static BaseLayer parse(JsonReader reader, LottieDrawable drawable) {
    reader.beginObject();
    while (reader.hasNext()) {
        String name = reader.nextName();
        if (name.equals("ty")) {
            int layerType = reader.nextInt();
            switch (layerType) {
                case 1: // 形状图层
                    return new ShapeLayer(drawable, layerModel);
                case 2: // 图片图层
                    return new ImageLayer(drawable, layerModel);
                // 其他图层类型...
            }
        }
    }
}

关键机制

  • 流式解析:使用 JsonReader 避免一次性加载整个 JSON 到内存,降低 OOM 风险。
  • 图层树构建:按 JSON 中 layers 的顺序倒序创建图层,确保绘制顺序与 AE 一致(后定义的图层覆盖先定义的)。
2. 动画驱动与渲染
// LottieDrawable.java
public void playAnimation() {
    if (animator == null) {
        animator = ValueAnimator.ofFloat(0f, 1f);
        animator.addUpdateListener(animation -> setProgress(animator.getAnimatedFraction()));
    }
    animator.start();
}

public void setProgress(float progress) {
    if (compositionLayer != null) {
        // 递归设置所有图层的进度
        compositionLayer.setProgress(progress);
        invalidateSelf(); // 触发重绘
    }
}

// BaseLayer.java
public void setProgress(float progress) {
    // 更新变换动画(缩放、旋转等)
    transform.setProgress(progress);
    // 更新其他关键帧动画
    for (KeyframeAnimation<?> animation : animations) {
        animation.setProgress(progress);
    }
}

// ImageLayer.java
@Override
public void drawLayer(Canvas canvas, Matrix parentMatrix, float alpha) {
    // 获取当前帧的 Bitmap
    Bitmap bitmap = imageAsset.getBitmap();
    // 应用变换矩阵
    Matrix matrix = new Matrix(parentMatrix);
    transform.getMatrix().postConcat(matrix);
    // 绘制 Bitmap
    canvas.drawBitmap(bitmap, matrix, paint);
}

核心原理

  • 属性动画驱动ValueAnimator 计算 0~1 的进度值,通过 setProgress() 传递给所有图层。
  • VSYNC 同步Choreographer 确保动画帧与屏幕刷新率同步,避免卡顿。
  • 增量渲染:仅重绘变化的图层,利用 Canvas.save()/restore() 实现图层叠加。
3. 关键帧动画插值计算
// TransformKeyframeAnimation.java
public Matrix getValue() {
    Matrix matrix = new Matrix();
    // 计算缩放
    PointF scale = scaleAnimation.getValue();
    matrix.postScale(scale.x, scale.y);
    // 计算旋转
    float rotation = rotationAnimation.getValue();
    matrix.postRotate(rotation);
    // 计算位移
    PointF position = positionAnimation.getValue();
    matrix.postTranslate(position.x, position.y);
    return matrix;
}

// LinearKeyframeAnimation.java
@Override
public T getValue() {
    float fraction = getCurrentFraction();
    // 线性插值:startValue + (endValue - startValue) * fraction
    return (T) (startValue + (endValue - startValue) * fraction);
}

// BezierKeyframeAnimation.java
@Override
public T getValue() {
    float fraction = getCurrentFraction();
    // 贝塞尔曲线插值
    float bezierFraction = BezierEvaluator.evaluate(fraction, inTangent, outTangent);
    return (T) (startValue + (endValue - startValue) * bezierFraction);
}

技术细节

  • 插值器类型:支持线性插值、贝塞尔曲线插值(用于缓动效果)、路径插值(如 PositionKeyframeAnimation)。
  • 浮点精度:动画进度允许浮点值(如 15.25 帧),确保过渡平滑。

三、性能优化策略

  1. 硬件加速

    • 默认启用 Canvas 硬件加速,复杂图层通过 RenderNode 缓存渲染结果。
    • 避免使用 saveLayer()(如遮罩),因其会创建离屏缓冲区,导致性能下降。
  2. 内存管理

    • 图片缓存LottieImageAsset 缓存加载的 Bitmap,支持 CacheStrategy 控制内存占用35。
    • 对象池:复用 MatrixPaint 等绘图对象,减少 GC 压力。
  3. 按需渲染

    • 仅在动画可见时更新进度,通过 ViewTreeObserver 监听可见性。
    • 列表中使用时,建议通过 setAnimation(String, CacheStrategy) 启用缓存。

四、复杂动画实现原理

  1. 遮罩(Mask)与蒙版(Matte)

    • 遮罩:通过 MaskLayer 定义可见区域,使用 Canvas.clipPath() 实现硬裁剪。
    • 蒙版MatteLayer 支持透明度混合,需多次渲染到离屏缓冲区,性能开销较大。
  2. 路径动画

    • ShapeLayer 解析 JSON 中的 shapes 字段,构建 Path 对象。
    • PathKeyframeAnimation 使用贝塞尔曲线插值计算路径点,实现平滑移动。
  3. 文本动画

    • TextLayer 解析 JSON 中的字体、颜色、位置等属性,通过 StaticTextKeyframeAnimation 实现文本内容和样式的动态变化。

五、总结与最佳实践

  • 核心优势

    1. 跨平台一致性:同一份 JSON 可在 Android、iOS、Web 上渲染出相同效果39。
    2. 轻量高效:JSON 体积远小于帧动画,内存占用低,支持硬件加速17。
    3. 动态控制:可实时调整动画进度、速度,添加事件监听811。
  • 使用建议

    1. 避免过度绘制:减少图层嵌套和复杂遮罩,优先使用矢量图形17。

    2. 缓存策略:列表中使用 CacheStrategy 避免重复解析 JSON 和加载图片35。

    3. 性能监控:通过 LottieDrawable 的 addAnimatorListener 监听帧率,结合 StrictMode 检测卡顿711。

Lottie 的设计哲学是将动画创作与工程实现解耦,通过声明式 JSON 描述和高效渲染引擎,让开发者专注于业务逻辑,而无需手工实现复杂动效。理解其核心原理后,可更灵活地优化动画性能,并扩展自定义渲染逻辑(如结合 OpenGL 实现 3D 效果)。