安卓APP想加载动图却没兼容androidX,只能用Lottie2.7?动不动就闪退怎么办?

目前设计师的AE能导出的动图有GIF和Json两种格式,在安卓项目中,加载一个普通的GIF使用Glide就行,Json格式的动图则需要使用Airbnb开源的Lottie框架,网上随手就能搜到很多关于Lottie的介绍和使用方法,再次就不再赘述了。顺手附上lottie-android的github地址:https://github.com/airbnb/lottie-android   。然而,在实际项目中接入这个框架时,Lottie在github上的一句话引起了我的注意:

意思是2.8.0开始的版本需要项目兼容androidX才能用,如果一个成熟的大项目切换到androidX需要花费的成本过高,你能用的最高版本只能是2.7.0(截止至2020年6月,最新版本是3.4.0),而低版本有一些严重的问题,下面以加载json的url为例,常见问题如下(如果需要加载静态json文件,处理方式也是一样的)。

1.加载json的url错误,网络返回了404就直接闪退了,连异常也无法捕捉

LottieAnimationView在代码中最简单的应用就两句话:

lottieAnimationView.setAnimationFromUrl(url);
 lottieAnimationView.playAnimation();

别高兴得太早,试试故意写一个错误的url,立刻就闪退,我们看看源码中能不能找到解决方案

安卓APP想加载动图却没兼容androidX,只能用Lottie2.7?动不动就闪退怎么办?_第1张图片

注意这里设置了一个加载失败的回调failureListener,我们来看看它是什么

安卓APP想加载动图却没兼容androidX,只能用Lottie2.7?动不动就闪退怎么办?_第2张图片

failureListener是LottieListener类型的,并且是private final的,所以我们不能获取它也不能修改它,而它只抛出了一个异常,所以就直接闪退了,经过尝试,这个异常是不能直接捕捉到的。我在官方的github上看到了有人提到了这个issue,并且留言回复中有人给出了解决方案,关键在于源码的第一句话

setCompositionTask(LottieCompositionFactory.fromUrl(getContext(), url));

这句话给LottieAnimationView设置了一个LottieTask,显然LottieCompositionFactory.fromUrl(getContext(), url)返回的就是一个LottieTask,我们直接使用这个方法生成一个LottieTask,LottieTask下可以添加自定义的回调,这样我们就能自定义加载成功和失败的回调了,具体使用方法如下:

private void loadLottie(final LottieAnimationView lottie) {
    LottieTask lottieCompositionLottieTask = LottieCompositionFactory.fromUrl(this, url);
    lottieCompositionLottieTask.addListener(new LottieListener() {
        @Override
        public void onResult(LottieComposition result) {
            lottie.setComposition(result);
            lottie.setRepeatCount(ValueAnimator.INFINITE);
            lottie.playAnimation();
        }
    }).addFailureListener(new LottieListener() {
        @Override
        public void onResult(Throwable result) {
            result.printStackTrace();
        }
    });
}

这个方法在官方github的issue中有人提到了,建议大家使用这种方法去加载动画,以防闪退

 

2、高版本Json加载失败闪退

关于这个问题网上也有相关的讨论,问题产生的大致原因是,gif生成json格式的方式升级了,省略了一些无用的帧,大大减少了资源的大小,提升了加载效率,而Lottie在3.0.0以上才支持这种类型的json。网上的解决方法也很粗暴,就是直接让美工不要生成这样的json,选择低一点的版本,让美工做一些妥协,牺牲一些效率,浪费一些用户体验,这种做法显然不怎么好。让我们从源码分析一下加载失败的原因。

lottie.setComposition(result)这句话产生了闪退。闪退发生的地方在源码的FloatKeyframeAnimation类

@Override
Float getValue(Keyframe keyframe, float keyframeProgress) {
    if (keyframe.startValue == null || keyframe.endValue == null) {
        throw new IllegalStateException("Missing values for keyframe.");
    }
...

}

由于新版的json省略了一些无用的帧,keyframe.endValue==null,抛出了异常"Missing values for keyframe.",需要解决的话我们只能修改源码了,因此,我们不直接使用gradle直接加载lottie框架,而是到github直接下载lottie的源码,把lottie moudle导入项目中。

安卓APP想加载动图却没兼容androidX,只能用Lottie2.7?动不动就闪退怎么办?_第3张图片

然后,找到FloatKeyframeAnimation类,把开头改成:

if (keyframe.startValue == null) {
    keyframe.startValue = 0f;
}

if (keyframe.endValue == null) {
    keyframe.endValue = 0f;
}

对null的情况做个特殊处理,也许你发现startValue和endValue都是private final的,不可修改,但是源码都给我们导进来了,怎么改都行,所以就统统改成public。值得一提的是,如果你稍微看一下lottie的源码,会发现除了FloatKeyframeAnimation还有IntegerKeyframeAnimation也需要做同样的修改,字面上很好理解,使用IntegerKeyframeAnimation类型的Json用整数描述图像矢量的起点和终点而FloatKeyframeAnimation使用的是浮点数,更精确一些,原理是一样的。

改完以后运行,我们发现仍然闪退,报错的地方在ScaleKeyframeAnimation

安卓APP想加载动图却没兼容androidX,只能用Lottie2.7?动不动就闪退怎么办?_第4张图片还是一样的套路,在方法的第一行因为缺失了帧,直接抛出了异常,在这个地方我们返回一个空的ScaleXY而不直接抛出异常:

if (keyframe.startValue == null || keyframe.endValue == null) {
  return new ScaleXY();
}

接下来,我们继续跟踪到ShapeKeyframeAnimation类

安卓APP想加载动图却没兼容androidX,只能用Lottie2.7?动不动就闪退怎么办?_第5张图片

这里也会抛出一个空指针,我们需要做一个判空

if (startShapeData != null && endShapeData != null) {
    tempShapeData.interpolateBetween(startShapeData, endShapeData, keyframeProgress);
    MiscUtils.getPathFromData(tempShapeData, tempPath);
}

如果startShapeData和endShapeData有一个为空的话会直接返回一个空的路径new Path(),那么这一小段就直接废弃而不抛出异常了。

做完以上几步lottie2.7.0就能正常加载高版本的json了,虽然我们对源码的处理会损失少量图像的数据,可能会造成跳帧,但实际测试的结果是,用户完全无感知,基本不影响体验,而加载json动图的效率明显提高了。

 

3、scaleType不能设置为FIT_XY

这个问题在官方github的issue上也有人提了,传送门:https://github.com/airbnb/lottie-android/issues/1384   开发人员给出了回答:

安卓APP想加载动图却没兼容androidX,只能用Lottie2.7?动不动就闪退怎么办?_第6张图片

总之就是一句话,Lottie就是不支持FIT_XY,我也没办法,在另一个issue,这个开发人员还说,能不能考虑用setScaleX和setScaleY来自行解决一下,具体的原因就不再解释了,有兴趣的读者可以上github看看这个issue。

其实做尺寸适配的话,其实理论上有很多中方案,因为我们的项目中切实有这样的需求,需要加载大屏广告适应view的宽高,我尝试了setScaleX、setScaleY、setScale,效果全都不理想,找了LottieComposition里的很多属性,如图层、图片尺寸等,也没有解决,花了将近一天的时间,终于从图片的变换矩阵中找到了突破口。

首先我们写一个自定义控件集成LottieAnimationView,我们要在控件内部获取控件的宽高(一定要在onMeasure之后),然后再获取控件上图片绘制的大小,计算他们的比例,最后给图像设置一个适应外部view的变换矩阵,手动实现ScaleType.FIT_XY。具体代码如下:

setScaleType(ScaleType.MATRIX);//让图像根据图形矩阵显示
Matrix m = getImageMatrix();//获取图像的图形矩阵
float[] values = new float[9];
m.getValues(values);
int transX = (int) values[Matrix.MTRANS_X];//x轴的旋转角度
int transY = (int) values[Matrix.MTRANS_Y];//y轴的旋转角度
if ((transX + transY) / 90 % 2 == 1) {//x轴和y轴的旋转角度之和如果是90的倍数而不是180的倍数,则图像的宽高交换
    values[Matrix.MSCALE_X] = (float) getMeasuredWidth() / (float)(getDrawable().getIntrinsicHeight());
    values[Matrix.MSCALE_Y] = (float) getMeasuredHeight() / (float)(getDrawable().getIntrinsicWidth());
} else {
    values[Matrix.MSCALE_X] = (float) getMeasuredWidth() / (float) (getDrawable().getIntrinsicWidth());
    values[Matrix.MSCALE_Y] = (float) getMeasuredHeight() / (float) (getDrawable().getIntrinsicHeight());
}//让图像的宽高正好适配到View的宽高
                    //旋转角度清0
values[Matrix.MTRANS_X] = 0;
values[Matrix.MTRANS_Y] = 0;

m.setValues(values);
setImageMatrix(m);
getImageMatrix()是ImageView的方法,能获取到ImageView内的图片的变换矩阵,这个矩阵大小是3X3的,所以用一个长度为9的float数组保存。
数组的第0位是x轴的放大倍数,第4位是y轴的放大倍数,第2位是x轴旋转角度,第5位是y轴旋转角度。图像的像素数组乘上这个变换矩阵,根据高中学的行列式乘法,我们就会得到对应的效果。

在开头记得 setScaleType(ScaleType.MATRIX); 让变换矩阵生效,末尾处setImageMatrix设置变换后的矩阵。

getMeasuredWidth获取的是当前控件的测量宽度,getDrawable().getIntrinsicHeight()获取的是json生成的动图的实际尺寸,把他们的比值作为x轴方向的放大倍数,就适配好了控件的宽度,高度也是一样的道理。

值得注意的是,经过Lottie处理,图像资源可能会发生翻转,所以我在这里计算了x轴旋转的角度和y轴的旋转角度之和,如果旋转角度是90度或270度的话图像的宽高数值会调换过来,所以做了这样的判断。

完成了以上的操作,我们就手动实现了FIT_XY,解决了lottie的这个bug

 

以上就是我在公司的项目中切实遇到的坑和解决方案。Lottie其实支持多个平台,不但有android也有ios、rn、h5,然而在和其他开发的沟通中我发现,以上提到的所有问题只发生在安卓端。。。。。。

你可能感兴趣的:(安卓)