lottie

作者: Shmily鱼 | 来源:发表于2022-05-31 12:08 被阅读0次

    概念

    Lottie 是 Airbnb开源的一套跨平台的完整的动画效果解决方案,它可以使用Bodymovin解析以json导出的Adobe After Effects动画,并在移动设备上进行本地渲染。
    可以直接运用在 iOSAndroidWebReact Native之上。开发者无需关注动画中的实现细节。

    特点

    • 传统动画
      使用序列帧,开发者需要设计时序、位移、透明度、尺寸、插值器等各类参数信息,再播放出动画。

    • lottie的设计方案
      将设计软件中的时间轴完整地导出来,包括里面的各种关键帧信息、矢量路径、层级、样式等等。
      即动画的描述文件导出,再将动画元素导出,然后在对应的客户端,解析描述文件,还原出整个动画。

    • 大小
      导出的json文件比gif文件小很多
    • 性能
      性能也更好(应用到矢量图,内存占用小,缩放效果好)
    • 使用
      API简单,代码实现简单,开发无需编写动画,降低动画的开发成本
    • 灵活度
      可动态配置下发,更换替换动画效果,易于调试和维护。
    • 适配
      不同的手机分辨率无需适配
    • 通用
      跨平台,设计稿导出一份动画描述文件,android,ios,react native,web多端通用
    • 效果
      几乎与设计出的动画无差别

    使用流程

    image.png

    使用方法

    由于API文档是1.0.3未及时更新,建议在源码里看方法
    API文档

    2.7.0版本

     动画来源可以从本地,网络等
     public void setAnimation(@RawRes final int rawRes)  //src/main/res/raw
     public void setAnimation(final String assetName)        //src/main/assets
     public void setAnimationFromJson(String jsonString)
     public void setAnimationFromJson(String jsonString, @Nullable String cacheKey) {
     public void setAnimation(JsonReader reader, @Nullable String cacheKey)  //JSON文件或zip文件的InputStream
     public void setAnimationFromUrl(String url)  //json或zip文件的网址
    
    public boolean addLottieOnCompositionLoadedListener(lottieOnCompositionLoadedListener)
    public boolean removeLottieOnCompositionLoadedListener(lottieOnCompositionLoadedListener) 
    public void playAnimation()
    public void pauseAnimation()
    public void setProgress(float progress)
    ...
    public void setImageBitmap(Bitmap bm)
    public void setImageResource(int resId)
    public void setImageDrawable(Drawable drawable) 
    ...
    AnimatorListener 、AnimatorUpdateListener 接口的支持
    

    使用示例

      <....ui.lottie.RecyclableLottieAnimationView
        android:id="@+id/title_bar_toolbox_ani"
        android:layout_width="@dimen/titlebar_web_action_width"
        android:layout_height="@dimen/titlebar_web_action_width"
        android:layout_centerVertical="true"
        android:layout_alignParentRight="true">
    
    mToolBoxAniView = mTopView.findViewById(R.id.title_bar_toolbox_ani);
    mToolBoxAniView.setAnimation("sniffer_animation.json");
    mToolBoxAniView.playAnimation();
    
    public class RecyclableLottieAnimationView extends LottieAnimationView {
        public RecyclableLottieAnimationView(Context context) {
            super(context);
        }
    
        public RecyclableLottieAnimationView(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        public RecyclableLottieAnimationView(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
        }
    
        @Override
        protected void onDetachedFromWindow() {
            super.onDetachedFromWindow();
            /**
             * lottie依赖 onDetachedFromWindow停止动画,回收动画资源
             * 但动画的 play可能是异步的,
             * 如果有post出去的异步任务,在detach后动画仍会执行
             */
            cancelAnimation();
        }
    }
    

    兼容性

    版本支持 sdkVersion: >=16

    = 2.8以上需AndroidX
    依赖库的版本与动画导出版本有关
    效果支持

    引入后的影响

    包大小新增: 71K

    Json文件结构

    json格式3.png
    • 回忆: 帧动画的播放.

    核心类

    LottieComposition 将json文件解析成数据对象.
    LottieDrawable 承载所有的绘制工作, 将LottieComposition 解析的数据对象, 绘制成 drawable。
    LottieAnimationView 提供了异步加载, 反序列化,显示, 封装了一些动画的操作,并处理了图片的回收onDetachWindow . 控制动画的实际操作委托给LottieDrawable
    备注:
    如直接使用LottieDrawable,需在合适的时机 invoke recycleBitmaps,否则内存泄漏.

    解析流程.jpg 类结构.png

    动画播放原理

        #LottieAnimationView.java
        @MainThread
        public void playAnimation() {
          lottieDrawable.playAnimation();
          enableOrDisableHardwareLayer();
        }
    
        #LottieDrawable.java
        @MainThread
        public void playAnimation() {
          if (compositionLayer == null) {
            lazyCompositionTasks.add(new LazyCompositionTask() {
              @Override public void run(LottieComposition composition) {
                playAnimation();
              }
            });
            return;
          }
          animator.playAnimation();  // LottieValueAnimator
        }
    
     #LottieValueAnimator.java
      @MainThread
      public void playAnimation() {
        running = true;
        notifyStart(isReversed());
       // update frame and notifyUpdate
        setFrame((int) (isReversed() ? getMaxFrame() : getMinFrame())); 
        lastFrameTimeNs = System.nanoTime();
        repeatCount = 0;
        postFrameCallback();
      }
    
    // 说明
    BaseLottieAnimator  extends ValueAnimator
    LottieValueAnimator extends BaseLottieAnimator 
    BaseLottieAnimator 提供了 notifyStart 、notifyEnd、notifyCancel 、notifyUpdate、 notifyRepeat 等notifyX方法.
    通知 所有listeners : ValueAnimator.AnimatorUpdateListener 与ValueAnimator.AnimatorListener
    对应者各自的回调方法 eg: onAnimationStart  、onAnimationEnd、onAnimationX方法
    
    #LottieDrawable.java
    public LottieDrawable() {
        this.animator.addUpdateListener(new AnimatorUpdateListener() {
            public void onAnimationUpdate(ValueAnimator animation) {
                if (LottieDrawable.this.compositionLayer != null) {
                    LottieDrawable.this.compositionLayer.setProgress(LottieDrawable.this.animator.getAnimatedValueAbsolute());
                }
            }
        });
    }
    
    #CompositionLayer.java
    @Override public void setProgress(@FloatRange(from = 0f, to = 1f) float progress) {
      super.setProgress(progress);
        //... update the progress
      for (int i = layers.size() - 1; i >= 0; i--) {
        layers.get(i).setProgress(progress);
      }
    }
    
    #BaseLayer.java
    void setProgress(@FloatRange(from = 0f, to = 1f) float progress) {
      //... layout.setProgress
      for (int i = 0; i < animations.size(); i++) {
      // List<BaseKeyframeAnimation<?, ?>> animations 
        animations.get(i).setProgress(progress);   
      }
    }
    
    #BaseKeyframeAnimation.java
    public void setProgress(@FloatRange(from = 0f, to = 1f) float progress) {
        //... update the progress 
       notifyListeners();
    }
    
    public void notifyListeners() {
      for (int i = 0; i < listeners.size(); i++) {
        listeners.get(i).onValueChanged();
      }
    }
    
    #BaseLayer
      @Override public void onValueChanged() {
        invalidateSelf();
      }
    
      private void invalidateSelf() {
        lottieDrawable.invalidateSelf();
      }
    
    
    动画播放时序图.jpg

    动画适配原理

    ① Android开发中, 不同屏幕分辨率适配.
    ② 为自定义View为其添加缩放属性

    LottieAnimationView.java
    private void init(@Nullable AttributeSet attrs) {
       ...
        if (ta.hasValue(R.styleable.LottieAnimationView_lottie_scale)) {
          lottieDrawable.setScale(ta.getFloat(R.styleable.LottieAnimationView_lottie_scale, 1f)); 
        }
       ...
    }
    
    LottieAnimationView$setScale 委托给LottieDrawable
    #LottieDrawable.java
    public void setScale(float scale) {
      this.scale = scale;
      updateBounds();
    }
    
    #LottieDrawable.java
    private void updateBounds() {
      if (composition == null) {
        return;
      }
      float scale = getScale();
      // Drawable#setBounds
      setBounds(0, 0, (int) (composition.getBounds().width() * scale),
          (int) (composition.getBounds().height() * scale));
    }
    
    #LottieDrawable.java
    private float getMaxScale(@NonNull Canvas canvas) {
      float maxScaleX = canvas.getWidth() / (float) composition.getBounds().width();
      float maxScaleY = canvas.getHeight() / (float) composition.getBounds().height();
      return Math.min(maxScaleX, maxScaleY);
    }
    
      @Override public void draw(@NonNull Canvas canvas) {
        float scale = this.scale;
        float extraScale = 1f;
        float maxScale = getMaxScale(canvas);
        if (scale > maxScale) {
          scale = maxScale;
          extraScale = this.scale / scale;
        }
    
        if (extraScale > 1) { 
        // ...  translate and scale
        }
    
        matrix.reset();
        matrix.preScale(scale, scale);
        compositionLayer.draw(canvas, matrix, alpha);
      }
    
     #LottieCompositionParser.java
      public static LottieComposition parse(JsonReader reader) throws IOException {
        float scale = Utils.dpScale();
        // ...parse
        int scaledWidth = (int) (width * scale);
        int scaledHeight = (int) (height * scale);
        Rect bounds = new Rect(0, 0, scaledWidth, scaledHeight);
        composition.init(bounds, startFrame, endFrame, frameRate, layers, layerMap, precomps,
            images, characters, fonts);
      }
    
    

    小结:
    Lottie 适配原理:
    解析json文件,获得取宽高之后, 乘以手机的相对密度。得到初始的Rect边界.
    在使用的时候判断适配后的宽高是否超过屏幕的宽高,如果超过则再进行缩放。以此保障 Lottie 在 Android 平台的显示效果。

    绘制原理

    猜想: 参考动画播放思路,猜想下绘制流程

     BaseLayer.java
      @Override
      public void draw(Canvas canvas, Matrix parentMatrix, int parentAlpha) {
        if (!visible) {
          return;
        }
        buildParentLayerListIfNeeded();
        if (!hasMatteOnThisLayer() && !hasMasksOnThisLayer()) {
          matrix.preConcat(transform.getMatrix());
          drawLayer(canvas, matrix, alpha);
          recordRenderTime(L.endSection(drawTraceName));
          return;
        }
    
        if (hasMasksOnThisLayer()) {
            //...draw maskLayer 
          applyMasks(canvas, matrix); 
    
        }
        if (hasMatteOnThisLayer()) {
            //...draw matteLayer
            matteLayer.draw(canvas, parentMatrix, alpha);
        } 
      }
    
      CompositionLayer.java
      @Override void drawLayer(Canvas canvas, Matrix parentMatrix, int parentAlpha) {    
      //...    
        for (int i = layers.size() - 1; i >= 0 ; i--) {
          boolean nonEmptyClip = true;
          if (!newClipRect.isEmpty()) {
            nonEmptyClip = canvas.clipRect(newClipRect);
          }
          if (nonEmptyClip) {
            BaseLayer layer = layers.get(i);
            layer.draw(canvas, parentMatrix, parentAlpha);
          }
        }
        //...
      }
    
    遇到的问题与解决方案

    ①依赖库的版本与导出动画版本

    java.lang.IllegalStateException: Missing values for keyframe.
            at com.airbnb.lottie.animation.keyframe.FloatKeyframeAnimation.getValue(FloatKeyframeAnimation.java:16)
            at com.airbnb.lottie.animation.keyframe.FloatKeyframeAnimation.getValue(FloatKeyframeAnimation.java:8)
            at com.airbnb.lottie.animation.keyframe.BaseKeyframeAnimation.getValue(BaseKeyframeAnimation.java:125)
            at com.airbnb.lottie.animation.keyframe.TransformKeyframeAnimation.getMatrix(TransformKeyframeAnimation.java:113)
    

    原因:
    json格式与解析规则不匹配
    Lottie 3.0和Bodymovin 5.5有一些重要的json优化,可以节省json大小和解析速度的1/3。 但是,必须在3.0以上生效,否则就在bodymovin设置中启用“导出为旧格式”

    ②issues: zip 播放的问题
    https://github.com/airbnb/lottie-android/issues/1009

    ③. 内存泄漏问题

    图片在回收时机
      @Override protected void onDetachedFromWindow() {
        if (isAnimating()) {  // 动画的加载play是异步的 
          cancelAnimation();  
          wasAnimatingWhenDetached = true;
        }
        recycleBitmaps();
        super.onDetachedFromWindow();
      }
    
    以此可能引发的内存抖动的场景
    假设在RecyclerView中使用包涵mattes或者mask的动画
    

    ④ 内存抖动的风险
    bitmap在动画加载到window时被创建,onDetachedFromWindow删除时回收。所以不宜在RecyclerView中使用包涵mattes或者mask的动画,否则会引起bitmap抖动。
    ⑤ 版本变更比较多, API变化比较大 解决方案: 封装,提供统一接口外观

    与SVGA对比

    SVGA里面的每一帧都是关键帧,SVGA已经在导出动画的时候,把每一帧的信息都计算好了,如此一来,
    播放时无需关心插值计算的过程。
    通过帧率去刷每一帧的画面,这个思路跟gif很像,SVGA可以同时支持Flash和After Effects的导出.
    且通过配置使得动画过程中图片都可以得到复用。

    1. 由于拥有所有帧, 不用解析高阶插值(二次线性方程,贝塞尔曲线方程),节省了CPU
    2. 2.x之后的svga,使用Protocol Buffers 来做序列化,序列化的数据体更小,传递效率比xml,json 更高。
      Lottie关键帧Keyframes, 是通过传参的方式,交由cpu去运算. 所以复杂动画实现耗费cpu

    与SVGA对比

    SVGA动画原理
    逐帧渲染,每一帧均为关键帧,只需渲染每个元素无需插值计算
    播放前一次性上传纹理到 GPU,并在动画过程中复用纹理
    2.x之后的svga,使用Protocol Buffers 来做序列化,序列化的数据体更小,传递效率比xml,json 更高。

    Lottie动画原理
    逐层渲染,完全按照设计工具的设计思路还原
    播放解析多个图层配置并添加相应动画,并在动画过程中复用图层
    当需要解析高阶插值,性能相对差一些 (关键帧Keyframes,是通过传参的方式,交由cpu去运算. 所以复杂动画实现耗费cpu)

    通过帧率去刷每一帧的画面,与gif很像,所以SVGA可以同时支持Flash和After Effects的导出.

    1. 由于拥有所有帧, 不用解析高阶插值(二次线性方程,贝塞尔曲线方程),节省了CPU
      Lottie关键帧Keyframes, 是通过传参的方式,交由cpu去运算. 所以复杂动画实现耗费cpu

    位图与json
    SVGA是将图片与描述文件集成在.svga文件当中的,而Lottie则是把二者分离开。
    Lottie可以在导出后,再对图片进行文件大小优化;而SVGA最好是在事先就对图片进行大小优化。

    SVGA应用场景:
    在直播应用场景,礼物播放,游戏炫酷动画.

    Lottie应用场景:
    高德地图,支付宝,全民K歌
    阿里提供的犸良动画
    它最底层采用的技术就是Lottie,阿里对其二次封装了许多预设的动画效果,
    可以自定义其中的元素与参数,然后试着导出你的第一个json文件~

    文献参考

    Lottie生态
    https://github.com/airbnb/lottie-android 31.1K star
    https://github.com/airbnb/lottie-ios
    https://github.com/airbnb/lottie-web
    Lottie doc

    导出工具
    https://github.com/bodymovin/bodymovin
    动画预览器
    https://lottiefiles.com/preview
    https://airbnb.design/lottie/

    SVGA生态
    开源iOS/Android/Web三个平台的源码。
    https://github.com/yyued/SVGAPlayer-Android 2.5K star
    https://github.com/yyued/SVGAPlayer-iOS
    https://github.com/yyued/SVGAPlayer-Web

    设计师工具
    http://svga.io/designer.html

    动画预览器
    http://svga.io/svga-preview.html

    相关文章

      网友评论

          本文标题:lottie

          本文链接:https://www.haomeiwen.com/subject/mgcrjltx.html