Android 加载json动画

01.如何解析json动画

  • 如下所示,代码很简单

  • 在布局中

    1
    2
    3
    4
    5
    6
    7
    <com.airbnb.lottie.LottieAnimationView
    android:id="@+id/lottie_view"
    android:layout_width="400dp"
    android:layout_height="400dp"
    app:lottie_fileName="loading.json"
    app:lottie_loop="true"
    app:lottie_autoPlay="true"/>
  • 代码中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    /**
    * 初始化动画操作
    */
    private void initAnim(){
    try {
    //None无缓存
    //在assets目录下的动画json文件名。
    mLottieView.setAnimation("loading.json");
    } catch (Exception e){
    e.printStackTrace();
    }

    /*LottieComposition.Factory.fromAssetFileName(this, "loading.json",
    new OnCompositionLoadedListener() {
    @Override
    public void onCompositionLoaded(@Nullable LottieComposition composition) {
    if (composition != null) {
    mLottieView.setComposition(composition);
    }
    mLottieView.setProgress(0.333f);
    mLottieView.playAnimation();
    }
    });*/

    //设置动画循环播放
    mLottieView.loop(false);
    //assets目录下的子目录,存放动画所需的图片
    //mLottieView.setImageAssetsFolder("anim/");
    //播放动画
    //mLottieView.playAnimation();
    //是否在播放
    //boolean animating = mLottieView.isAnimating();
    //播放
    //mLottieView.playAnimation();
    //暂停
    //mLottieView.pauseAnimation();
    //取消
    //mLottieView.cancelAnimation();
    //获取动画时长
    mLottieView.getDuration();
    //添加动画监听
    mLottieView.addAnimatorListener(new Animator.AnimatorListener() {
    @Override
    public void onAnimationStart(Animator animation) {
    LogUtil.d("addAnimatorListener---"+"onAnimationStart");
    }

    @Override
    public void onAnimationEnd(Animator animation) {
    LogUtil.d("addAnimatorListener---"+"onAnimationEnd");
    toMain();
    }

    @Override
    public void onAnimationCancel(Animator animation) {
    LogUtil.d("addAnimatorListener---"+"onAnimationCancel");
    toMain();
    }

    @Override
    public void onAnimationRepeat(Animator animation) {
    LogUtil.d("addAnimatorListener---"+"onAnimationRepeat");
    }
    });
    startAnimating();
    }


    /**
    * 开始动画
    */
    private void startAnimating(){
    boolean inPlaying = mLottieView.isAnimating();
    if (!inPlaying) {
    mLottieView.setProgress(0f);
    mLottieView.playAnimation();
    }
    }

    /**
    * 停止动画
    */
    private void stopAnimating(){
    boolean inPlaying = mLottieView.isAnimating();
    if (inPlaying) {
    mLottieView.cancelAnimation();
    }
    }

    /**
    * 取消动画
    */
    @Override
    protected void onStop() {
    super.onStop();
    stopAnimating();
    }

    /**
    * 注意销毁的时候移除监听
    */
    @Override
    protected void onDestroy() {
    super.onDestroy();
    if (mLottieView!=null){
    mLottieView.removeAllLottieOnCompositionLoadedListener();
    mLottieView.removeAllAnimatorListeners();
    mLottieView.removeAllLottieOnCompositionLoadedListener();
    }
    }

02.加载动画优化点

  • 可以这样操作

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    /**
    * 取消动画
    */
    @Override
    protected void onStop() {
    super.onStop();
    stopAnimating();
    }

    /**
    * 注意销毁的时候移除监听
    */
    @Override
    protected void onDestroy() {
    super.onDestroy();
    if (mLottieView!=null){
    mLottieView.removeAllLottieOnCompositionLoadedListener();
    mLottieView.removeAllAnimatorListeners();
    mLottieView.removeAllLottieOnCompositionLoadedListener();
    }
    }
  • 判断assets文件夹下的文件是否存在

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    /**
    * 判断assets文件夹下的文件是否存在
    * @param filename 文件名称
    * @param file 文件夹名称
    * @return alse 不存在 true 存在
    */
    private boolean isFileExists(String filename , String file) {
    if (file==null){
    file = "";
    }
    AssetManager assetManager = this.getAssets();
    try {
    //获取assets文件下子目录文件夹file中的所有文件的名称
    String[] names = assetManager.list(file);
    if (names != null) {
    for (String name : names) {
    LogUtil.e(name);
    if (name.equals(filename.trim())) {
    System.out.println(filename + "存在");
    return true;
    }
    }
    }
    } catch (IOException e) {
    e.printStackTrace();
    System.out.println(filename + "不存在");
    return false;
    }
    System.out.println(filename + "不存在");
    return false;
    }

03.加载json动画原理

  • Lottie实现原理

    • 设计师把一张复杂的图片使用多个图层来表示,每个图层展示一部分内容,图层中的内容也可以拆分为多个元素。拆分元素之后,根据动画需求,可以单独对图层或者图层中的元素做平移、旋转、收缩等动画。
    • Lottie的使用的资源是需要先通过bodymovin(bodymovin插件本身是用于网页上呈现各种AE效果的一个开源库)将 Adobe After Effects(AE)生成的aep动画工程文件转换为通用的json格式描述文件。Lottie则负责解析动画的数据,计算每个动画在某个时间点的状态,准确地绘制到屏幕上。
  • Lottie主要类图:

    • Lottie对外通过控件LottieAnimationView暴露接口,控制动画。
    • LottieAnimationView继承自ImageView,通过当前时间绘制canvas显示到界面上。这里有两个关键类:LottieComposition 负责解析json描述文件,把json内容转成Java数据对象;LottieDrawable负责绘制,把LottieComposition转成的数据对象绘制成drawable显示到View上。顺序如下:
  • 解析json外部结构

    • LottieComposition封装整个动画的信息,包括动画大小,动画时长,帧率,用到的图片,字体,图层等等。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    {
    "v": "4.6.0", //bodymovin的版本
    "fr": 29.9700012207031, //帧率
    "ip": 0, //起始关键帧
    "op": 141.000005743048, //结束关键帧
    "w": 800, //动画宽度
    "h": 800, //动画高度
    "ddd": 0,
    "assets": [...] //资源信息
    "layers": [...] //图层信息
    }
  • 复制代码解析图片资源

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    "assets": [                 //资源信息
    { //第一张图片
    "id": "image_0", //图片id
    "w": 58, //图片宽度
    "h": 31, //图片高度
    "u": "images/", //图片路径
    "p": "img_0.png" //图片名称
    },
    {...} //第n张图片
    ]
  • 复制代码解析图层

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    "layers": [                 //图层信息
    { //第一层动画
    "ddd": 0,
    "ind": 0, //layer id 图层 id
    "ty": 4, //图层类型
    "nm": "center_circle",
    "ks": {...}, //动画
    "ao": 0,
    "shapes": [...],
    "ip": 0, //inFrame 该图层起始关键帧
    "op": 90, //outFrame 该图层结束关键帧
    "st": 0, //startFrame 开始
    "bm": 0,
    "sr": 1
    },
    {...} //第n层动画
    ]

04.部分源码解析说明

  • 利用属性动画控制进度,每次进度改变通知到每一层,触发LottieAnimationView重绘。
    代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public LottieDrawable() {
    animator.setRepeatCount(0);
    animator.setInterpolator(new LinearInterpolator());
    animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
    if (systemAnimationsAreDisabled) {
    animator.cancel();
    setProgress(1f);
    } else {
    setProgress((float) animation.getAnimatedValue());
    }
    }
    });
    }
  • 复制代码通过CompositionLayer把进度传递到各个图层

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    @Override
    public void setProgress(@FloatRange(from = 0f, to = 1f) float progress) {
    super.setProgress(progress);
    if (timeRemapping != null) {
    long duration = lottieDrawable.getComposition().getDuration();
    long remappedTime = (long) (timeRemapping.getValue() * 1000);
    progress = remappedTime / (float) duration;
    }
    if (layerModel.getTimeStretch() != 0) {
    progress /= layerModel.getTimeStretch();
    }
    progress -= layerModel.getStartProgress();
    for (int i = layers.size() - 1; i >= 0; i--) {
    layers.get(i).setProgress(progress);
    }
    }
  • 复制代码通知进度改变

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void setProgress(@FloatRange(from = 0f, to = 1f) float progress) {
if (progress < getStartDelayProgress()) {
progress = 0f;
} else if (progress > getEndProgress()) {
progress = 1f;
}

if (progress == this.progress) {
return;
}
this.progress = progress;

for (int i = 0; i < listeners.size(); i++) {
listeners.get(i).onValueChanged();
}
}
  • 复制代码最终回调到LottieAnimationView的invalidateDrawable

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @Override
    public void invalidateDrawable(@NonNull Drawable dr) {
    if (getDrawable() == lottieDrawable) {
    // We always want to invalidate the root drawable so it redraws the whole drawable.
    // Eventually it would be great to be able to invalidate just the changed region.
    super.invalidateDrawable(lottieDrawable);
    } else {
    // Otherwise work as regular ImageView
    super.invalidateDrawable(dr);
    }
    }
  • 复制代码最后触发LottieDrawable重绘

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Override
    public void draw(@NonNull Canvas canvas) {
    ...
    matrix.reset();
    matrix.preScale(scale, scale);
    compositionLayer.draw(canvas, matrix, alpha); //这里会调用所有layer的绘制方法
    if (hasExtraScale) {
    canvas.restore();
    }
    }

05.性能与常见动画分析

  • 1.官方说明
    • 如果没有mask和mattes,那么性能和内存非常好,没有bitmap创建,大部分操作都是简单的cavas绘制。
    • 如果存在mattes,将会创建2~3个bitmap。bitmap在动画加载到window时被创建,被window删除时回收。所以不宜在RecyclerView中使用包涵mattes或者mask的动画,否则会引起bitmap抖动。除了内存抖动,mattes和mask中必要的bitmap.eraseColor()和canvas.drawBitmap()也会降低动画性能。对于简单的动画,在实际使用时性能不太明显。
    • 如果在列表中使用动画,推荐使用缓存LottieAnimationView.setAnimation(String, CacheStrategy) 。
  • 2.属性动画和Lottie动画对比
    • Lottie动画在未开启硬件加速的情况下,帧率、内存,CPU都比属性动画差,开启硬件加速后,性能差不多。
  • 3.未开启硬件加速,Lottie动画大小帧率对比
    • 主要耗时在draw方法,绘制区域越小,耗时越小
  • 4.劣势
    • 性能不够好—某些动画特效,内存和性能不够好;相对于属性动画,在展示大动画时,帧率较低
  • 5.优势
    • 开发效率高—代码实现简单,更换动画方便,易于调试和维护。
    • 数据源多样性—可从assets,sdcard,网络加载动画资源,能做到不发版本,动态更新
    • 跨平台—设计稿导出一份动画描述文件,android,ios,react native通用
    • Lottie使用简单,易于上手,非常值得尝试。

06.可能出现的异常

  • java.lang.IllegalStateException: Missing values for keyframe.
  • 解决办法:Bodymovin 5.5有一些重要的json优化,可以节省json大小和解析速度的1/3。但是,您必须在3.0或在bodymovin设置中启用“导出为旧格式”。
  • 参考内容:https://github.com/airbnb/lottie-android/issues/1177