Lottie-android简析

4,968 阅读5分钟

简介

使用方法

  1. 布局中添加LottieAnimationView控件

如果json文件在assets子文件夹中,lottie_fileName="lottieani/hello-world.json"

 <com.airbnb.lottie.LottieAnimationView
      android:id="@+id/animation_view"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      app:lottie_fileName="hello-world.json"
      app:lottie_loop="true"
      app:lottie_autoPlay="true" />

同时也支持

    //assets
    <attr name="lottie_fileName" format="string" />
    //raw
    <attr name="lottie_rawRes" format="reference" />
    //链接
    <attr name="lottie_url" format="string" />
  1. 获取到LottieAnimationView对象进行动画操作
 // 布局中不指定文件可以在此设置,路径设置同布局文件
 //assets下的文件
 animationView.setAnimation("match_success.json");
 //R.raw.xxx
 animationView.setAnimation(R.raw.match_success);
 //从链接获取Json
 animationView.setAnimationFromUrl("https://assets7.lottiefiles.com/private_files/lf3 rBgRS1.json");
 //json串
 animationView.setAnimationFromJson("json", "cachekey");
 
 // 是否循环播放
 animationView.loop(true);
 // 设置播放速率,例如:2代表播放速率是不设置时的二倍
 animationView.setSpeed(2f);
 // 开始播放
 animationView.playAnimation();
 // 暂停播放
 animationView.pauseAnimation();
 // 取消播放
 animationVIew.cancelAnimation();
 // 设置播放进度
 animationView.setProgress(0.5f);
 // 判断是否正在播放
 animationView.isAnimating();
  1. 添加监听 LottieAnimationView里使用的是ValueAnimator控制进度
  //json转化LottieComposition,准备好动画的回调
  animationView.addLottieOnCompositionLoadedListener(***)
 
 //动画监听
 animationView.addAnimatorUpdateListener(ValueAnimator.AnimatorUpdateListene updateListener);
 animationView.addAnimatorListener(Animator.AnimatorListener listener);
 //自定义动画的速率和时长
  ValueAnimator valueAnimator = ValueAnimator
                .ofFloat(0f, 1f)
                .setDuration(5000);
  valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            animationView.setProgress((Float) animation.getAnimatedValue());
        }
    });
    valueAnimator.start();
  1. 替换动画元素
//参考PPLottieAnimationView
animationView.setAnimation(R.raw.match_success);
animationView.addLottieOnCompositionLoadedListener(new LottieOnCompositionLoadedListener() {
    @Override
    public void onCompositionLoaded(LottieComposition composition) {
        String imageUrl = "https://ae01.alicdn.com/kf/U75499bbca2264e6badd85f7695dd9ab5H.jpg";
        animationView.setDynamicImage("image_0", imageUrl);
        animationView.setDynamicImage("image_1", imageUrl);
        animationView.playAnimation();
    }
});

更多详细的使用可以浏览官网 链接在上方

源码解析

加载流程

  • LottieComposition:承载json文件解析好的数据
    • LottieAnimationView:可以认为是LottieDrawable的代理类
  • LottieDrawable:真正干活的类负责绘制
  • LottieValueAnimator:通过监听Vsync,一帧一帧更新进度
  • LottieCompositionFactory:创建和缓存LottieComposition
  • LottieTask:运行异步任务并回调结果

json结构

整个动画是由一层一层图层组成的

{
    "v": "4.6.0",               //bodymovin的版本,AE上的插件,生成Json文件
    "fr": 29.9700012207031,     //帧率
    "ip": 0,                    //起始关键帧
    "op": 141.000005743048,     //结束关键帧
    "w": 800,                   //动画宽度
    "h": 800,                   //动画高度
    "ddd": 0, 
    "assets": [...]             //资源信息
    "layers": [...]             //图层信息
}

setAnimation做了什么

当调用setAnimation的方法时,目的是将Json文件转换成LottieComposition,在将LottieComposition设置进LottieDrawable,然后调用playAnimation()时就开始动画。

以setAniamtion(String assetName)为例,其他也类似

LottieAnimationView,看到fromAsset方法,继续跟下去

    public void setAnimation(final String assetName) {
        this.animationName = assetName;
        animationResId = 0;
        //1. 获取LottieTask, LottieTask里封装了线程池和FutureTask,做解析并返回
        setCompositionTask(LottieCompositionFactory.fromAsset(getContext(), assetName));
    }

LottieCompositionFactory 看见cache方法,在这里处理创建和缓存LottieComposition, 如果没有缓存走2处,有缓存走3处返回

    //用于保存LottieTask,
    private static final Map<String, LottieTask<LottieComposition>> taskCache = new HashMap<>();

    public static LottieTask<LottieComposition> fromAsset(Context context, final String fileName) {
        final Context appContext = context.getApplicationContext();
        //LottieCompositionFactory生成LottieTask或从缓存中取出
        //asset文件会以filename去做缓存的key,raw资源下的会以raw_resId, url会以url
        return cache(fileName, new Callable<LottieResult<LottieComposition>>() {
            @Override
            public LottieResult<LottieComposition> call() {
                // 2.没有缓存
                return fromAssetSync(appContext, fileName);
            }
        });
    }
    
    private static LottieTask<LottieComposition> cache(
            @Nullable final String cacheKey, Callable<LottieResult<LottieComposition>> callable) {
        //LottieCompositionCache是LruCache,这里先判断有没有缓存过LottieComposition,有的话就直接返回
        final LottieComposition cachedComposition = LottieCompositionCache.getInstance().get(cacheKey);
        if (cachedComposition != null) {
            return new LottieTask<>(new Callable<LottieResult<LottieComposition>>() {
                @Override
                public LottieResult<LottieComposition> call() {
                    // 3.返回缓存
                    return new LottieResult<>(cachedComposition);
                }
            });
        }
        //如果当前已经有执行的task直接返回
        if (taskCache.containsKey(cacheKey)) {
            return taskCache.get(cacheKey);
        }
        //callable 在上方2处哪里处理
        LottieTask<LottieComposition> task = new LottieTask<>(callable);
        task.addListener(new LottieListener<LottieComposition>() {
            @Override
            public void onResult(LottieComposition result) {
                //执行成功保存LottieComposition
                if (cacheKey != null) {
                    LottieCompositionCache.getInstance().put(cacheKey, result);
                }
                taskCache.remove(cacheKey);
            }
        });
        task.addFailureListener(new LottieListener<Throwable>() {
            @Override
            public void onResult(Throwable result) {
                taskCache.remove(cacheKey);
            }
        });
        //添加task
        taskCache.put(cacheKey, task);
        return task;
    }

当LottieCompositionFactory处理完回到LottieAnimationView,使用获取到LottieTask对象添加监听并等待回调。

    //在看回1处 获取到LottieTask
    private void setCompositionTask(LottieTask<LottieComposition> compositionTask) {
        clearComposition();
        cancelLoaderTask();
        //添加监听 等待结果回调
        this.compositionTask = compositionTask
                //4 添加监听
                .addListener(loadedListener)
                .addFailureListener(failureListener);
    }

    private final LottieListener<LottieComposition> loadedListener = new LottieListener<LottieComposition>() {
        @Override public void onResult(LottieComposition composition) {
            //获取到结果
            setComposition(composition);
        }
    };

    public void setComposition(@NonNull LottieComposition composition) {
        this.composition = composition;
        //设置进LottieDrawable
        boolean isNewComposition = lottieDrawable.setComposition(composition);
        if (getDrawable() == lottieDrawable && !isNewComposition) {
            return;
        }
        setImageDrawable(null);
        //注意:这里面会调用bitmap.recycle(),将以前的bitmap回收; 所以注意bitmap重用时会报已回收的异常
        setImageDrawable(lottieDrawable);
        requestLayout();
        //加载成功的回调在这里触发
        for (LottieOnCompositionLoadedListener lottieOnCompositionLoadedListener : lottieOnCompositionLoadedListeners) {
            lottieOnCompositionLoadedListener.onCompositionLoaded(composition);
        }
        //至此完成整理加载和缓存的逻辑
    }    
 

LottieTask主要负责异步处理任务并返回,主要在startTaskObserverIfNeeded方法里监听task是否完成

  //public static修饰的,可换成项目里的
  public static Executor EXECUTOR = Executors.newCachedThreadPool();
    
  //有回调的任务
  private final FutureTask<LottieResult<T>> task;   
    
  //添加监听的方法
  public synchronized LottieTask<T> addListener(LottieListener<T> listener) {
    if (result != null && result.getValue() != null) {
      listener.onResult(result.getValue());
    }

    successListeners.add(listener);
    //里面是开启线程,里面有一个while循环,等待结果回调
    startTaskObserverIfNeeded();
    return this;
  }

    

播放动画

点进LottieAnimationView的playAnimation方法,发现实际调用的是LottieDrawable的playAnimation

public LottieDrawable() {

 private final LottieValueAnimator animator = new LottieValueAnimator();

  public LottieDrawable() {
  //先记住这个监听
    animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
      @Override public void onAnimationUpdate(ValueAnimator animation) {
        if (compositionLayer != null) {
        //通过compositionLayer.setProgress通知到每一个图层去变化实现动画效果
          compositionLayer.setProgress(animator.getAnimatedValueAbsolute());
        }
      }
    });
  }

  public void playAnimation() {
    if (compositionLayer == null) {
      lazyCompositionTasks.add(new LazyCompositionTask() {
        @Override public void run(LottieComposition composition) {
          playAnimation();
        }
      });
      return;
    }
    animator.playAnimation();
  }
    
}

现在来看LottieValueAnimator,是通过实现Choreographer.getInstance().postFrameCallback(this)注册下一帧去进行动画和回调监听的,主要处理逻辑看doFrame()

public class LottieValueAnimator extends BaseLottieAnimator implements Choreographer.FrameCallback {

    @MainThread
    public void playAnimation() {
        running = true;
        notifyStart(isReversed());
        setFrame((int) (isReversed() ? getMaxFrame() : getMinFrame()));
        lastFrameTimeNs = System.nanoTime();
        repeatCount = 0;
        //注册下一帧去开始动画
        postFrameCallback();
    }

    @Override 
    public void doFrame(long frameTimeNanos) {
        //注册下一帧回调
        postFrameCallback();
        ...
        //回调在LottieDrawable注册的监听(animator.addUpdateListener)
        //就是这里去更新动画进度
        notifyUpdate();
        if (ended) {
            if (getRepeatCount() != INFINITE && repeatCount >= getRepeatCount()) {
                frame = getMaxFrame();
                removeFrameCallback();
                //回调AnimatorListener.onAnimationEnd
                notifyEnd(isReversed());
            } else {
                //回调AnimatorListener.onAnimationRepeat
                notifyRepeat();
                repeatCount++;
                if (getRepeatMode() == REVERSE) {
                    speedReversedForRepeatMode = !speedReversedForRepeatMode;
                    reverseAnimationSpeed();
                } else {
                    frame = isReversed() ? getMaxFrame() : getMinFrame();
                }
                lastFrameTimeNs = now;
            }
        }

        verifyFrame();
    }
}

缓存

主要有两处,都是在LottieCompositionFactory里使用

  1. LottieCompositionCache类,使用LruCache缓存LottieComposition,最多10MB

  2. 当调用setAnimationFromUrl时,LottieCompositionFactory实际是NetworkFetcher去做网络访问获取,使用NetworkCache保存到本地

最后

至于Lottie怎么根据进度做绘制和变化这块没有细看,有兴趣的同学可以去研究研究
写得不是很好,完全按照自己的思路写的,如果发现哪里有问题,欢迎指正和讨论

附上简单的使用Demo:LottieDemo