Lottie 作为一个可以轻松实现复杂动画的跨平台开源动画库,从发布至今,受到了越来越多开发者的推崇。笔者所在项目从今年 8 月开始接入 lottie 库,使用的是 2.0.0 的版本。接入初期,在享受 lottie 带来的便利和高效同时,有时会遇到动画效果和预期不符的情况,在修正了使用方法后,问题大多都能解决。最近又遇到了一个问题,解决起来花费了些精力,记录下来。
问题描述
项目中有个“点赞”的动效是用 lottie 实现的,而展示效果出现了偶现的 bug,LottieAnimationView 显示过大:
开启布局边界可以清楚的看到“点赞” icon 显示大小确实过大了。该 icon 的布局代码:
布局指定了 LottieAnimationView 的动画资源路径、loop 和 scale 属性,宽、高为 wrap_content。
这里 scale 设置为 0.33 需要解释一下。我们知道 lottie 的 JSON 格式的动画资源文件中是有动画的宽、高属性的,并且 lottie-android 库将 JSON 中的宽、高的单位视为 dp,也就是动画的实际显示大小是 JSON 文件中设定的大小乘以手机系统的 density 值。那么在 wrap_content 下,为了使动画显示大小在数值上和 JSON 中设定的一致,就需要指定一个 scale 值,scale 值大小为 density 的倒数,以 density = 3 为例,这里 scale 即为 0.33。
但是从这个 bug 来看,动画 LottieAnimationView 并没有按照预期的 scale 缩放,显示的大小是未缩放的大小。
分析问题
View 大小显示不正常,应该是 measure 过程出了问题。LottieAnimationView 自己没有 onMeasure() 方法,于是查看一下其父类 ImageView 的 onMeasure() 方法。出现 bug 的手机的系统是 Android 5.1 系统,查看 Android 5.1 的 ImageView 源码可以看到,onMeasure() 处理时是基于 mDrawableWidth 和 mDrawableHeight 来确定最终的大小,因此猜测这两个变量的赋值出现了问题。 mDrawableWidth 和 mDrawableHeight 的赋值在 updateDrawable 方法中,如下所示。
LottieDrawable 重写了 getIntrinsicWidth() 和 getIntrinsicHeight() 方法
可以看到返回的结果已经考虑 scale 值了。如果返回值不符合预期,那么一定是 scale 值不正确,这里先留着,后面继续分析。
从 updateDrawable() 方法开始向上层层追踪调用,可以找到调用 updateDrawable() 方法的调用链:
由此可知,LottieAnimationView 在解析动画文件成 LottieComposition 后,setComposition() 时会调用到 updateDrawable() 来获取 drawable 的大小,进而确定自身的大小。
由于我们把动画资源写到了 xml 布局文件中,所以 LottieComposition 的解析时机是在 LottieAnimationView 的 init() 方法中。
setAnimation() 方法首先在缓存池中查找是否存在解析好的相同文件名的动画文件,如果存在直接调用 setComposition() 使用;如果不存在,则启动 loader 加载、解析动画文件,在回调函数中调用 setComposition(),相关代码如下:
到这里,代码逻辑已经梳理得比较清晰了:在 LottieAnimationView 中解析动画文件成 LottieComposition,然后调用 setComposition() 保存、处理 LottieComposition,最终调用到 ImageView 的 updateDrawable() 方法,获取 LottieDrawable 的尺寸,反映到 LottieAnimationView 上。
这个流程看起来没什么问题。但是从 bug 上来看,最终获取的 LottieDrawable 尺寸是错误的。根据前面的结论,此时应该是 scale 值不正确了。继续看一下 scale 设置的时机。
从上述 LottieAnimationView 的 init() 方法中看到,scale 值从 xml 布局文件中解析得来,数据肯定不会有错误。但是设置的时机是在 setAnimation() 之后。这会有什么问题呢?
当然有问题了,setAnimation() 时如果缓存中有解析好的动画资源,那么就会直接获取使用,继续执行到 ImageView.updateDrawable(),此时 scale 值还未设置,初始为 1f,所以获取到的 LottieDrawable 大小就是未缩放的大小了,LottieAnimationView 的大小也就偏大了。
那本文开头提到的 bug 原因是这样的吗?答案是肯定的。这个“点赞”动画资源在其他的业务场景中也有使用,并且,由于是在列表中使用,因此做了强缓存设置。所以只要这个“点赞”资源加载、解析过,那么就会缓存下来,进入到其他页面再次使用时,此 bug 就会复现。
问题原因找到了,赶紧修复吧。等等,笔者发现按照上面提到的复现路径,在 Android 6.0 以上系统上并没有出现这个 bug,这是怎么回事呢?
查看 ImageView 的源码发现,Android 6.0 以上系统里,ImageView 中 mDrawableWidth 和 mDrawableHeight 多了一个赋值时机,而这个时机是在 Android 5.x 系统里没有的。
对比 Android 6.0+ 和 Android 5.x 的 ImageView 的 invalidateDrawable() 可知,Android 6.0+ 系统上会根据获取到的 drawable 大小来更新mDrawableWidth、mDrawableHeight 的值。
这就不难解释为什么 Android 6.0 以上系统不会出现这个 bug 了。LottieDrawable 绘制时调用自己的 invalidateSelf(),invalidateSelf() 方法会调用到 ImageView 的 invalidateDrawable(),此时 scale 值已经设置完毕,就可以保证获取到的 LottieDrawable 的大小是正确的了。
根据以上分析,LottieAnimationView 缩放无效的 bug 出现在 Android 5.x 系统上,由于缩放参数 scale 设置时机在动画解析之后,所以缓存中有动画资源时,还没等到 scale 设置好,就直接获取 drawable 的大小作为 LottieAnimationView onMeasure() 参考的大小了。
解决问题
问题原因找到了,就好办了。
首先考虑是否可以修正 lottie 的使用姿势来避免这个问题呢?比如在 java 代码中通过 setAnimation() 来设置动画文件,而不是在 xml 布局文件中设置。这样就能够确保 setAnimation() 的调用在设置 scale 之后了。这种方式理论上是可以解决问题的,但是实际操作上是不可行的。因为这样做相当于设定了一个使用 lottie 开发的规则,削减了 lottie 开发的便利性的同时,让团队每个人都遵守起来成本很高,而且难以保证不会出错。
那么就只能通过修改 lottie 源码来解决了。这样做也是合理的,因为获取 drawable 尺寸时依赖于 scale 的值,逻辑上,此时 scale 值必须设置完毕才行。因此,我们将 LottieAnimationView 的 init() 方法修改了一下,将 scale 值的设置提前。