Lottie伸缩规则分析

作者: blingblingson | 来源:发表于2018-08-05 15:47 被阅读17次

1. 背景

上篇Lottie运行流程分析结尾说到 Lottie 适合的是一段动画的播放这样的场景使用。 它不太适合有动态内容的动画,但如果遇到这种需求怎么办? 譬如下面的场景:

上图的动画是铺满全屏的,也就是 LottieAnimationView 的宽高都是 match_parent 的

你会发现 888 这几个数字显然不是能够事先知道的,所以不能靠设计师作图时就写死在里面。 你可能会想那我在程序中读出动画的 json 字符串,动态在里面插入一些内容不就行了。 如果你看过 json 文件的内容,估计就会放弃了,特别是如果还要给文字添加样式,那就更加绝望了

转念一想,其实也挺简单,在上面层叠一个文本框不就行了。 对(至少我也是这么干的-.-!),但随之而来的一个问题就是文本框的位置放在哪? 更具体的就是 TextView 的 margintop 和 marginleft 应该设置为多少。 或许你是根据设计稿的比例去计算的,又或者你是大概估了个值,刚好就把文本放到了框中。 当你满心欢喜的交了差,估计不久后产品经理就会拿着平板来找你了,你一看,傻眼了,在平板上的文字竟然错位了

这就引出了这篇文章想解决的一个问题,你必须要知道 Lottie 是怎样做伸缩的,才能根据这个规则去计算文本框的位置,解决不同屏幕的文本位置适配问题。 下面的内容需要点预备知识,你需要先了解 Lottie 的运行流程,可以参考上篇文章

2. Lottie伸缩分析

这里以一个简单的 SolideLayer 的位移动画为例子进行分析,手机上效果如下所示:

我在 AE 上建了一张 1920x1080 的画布,在上面添加了一个 SolidLayer,并给这个 SoliderLayer 的 position 属性打上了几个关键帧,而手机是 1080x1920 的。 这些参数都不重要,因为我只是想知道这个 SolidLayer 从 AE 画布到 app Canvas 上的位置的一个映射关系,换句话就是 (x, y) = f(x0, y0),这个 f 是什么

根据上篇 Lottie 的原理分析中,当动画进度(progress)发生变化时,最终会回调到 LottieDrawable.draw() 进行重新绘制,那就从这个方法开始分析起吧

2.1 LottieDrawable.draw()

三个小地方都在上面圈出来了,并做了一些注释

第 1 处,在没有设置 scale 情况下,getScale() 为 1,就暂时忽略这个值了。 为了解释为什么这两句的效果是坐标轴回到原点并且坐标刻度放大 extraScale 倍,这里用公式推导来验证,就不作图了(好吧,其实是作图难解释也麻烦-.-!),感兴趣的可以自己动手画一画。 另外,对 Canvas 变换不熟的可以参考这篇文章

已知:
scale = Min(widthScale, heightScale)
extraScale = 1/scale

scaledHalfWidth = halfWidth * scale
scaledHalfHeight = halfHeight * scale

推导:
(x, y) → (x + hW - sHW, y + hH - sHH)
→ (x + (hW – sHW) + sHW, y + (hH – sHH) + sHH)
→ (x + (hW –sHW) + sHW - eS*sHW, y+ (hH –SHH) + sHH - eS*sHH)
→ (x, y)

估计你会觉得既然只是让坐标轴放大 extraScale 倍,为什么要这么麻烦。 我猜这里和是否设置了 scale 有关,还记得吗,你没有设置 scale 的情况下,getScale() 结果是 1 的,所以上面我们忽略这个值了,如果你设置了 scale,那就不能忽略了,这里就不做这个分析了。 至于为什么要把坐标刻度放大 extraScale 倍,这里先埋个伏笔,到后面再解释

第 2 处,得到一个矩阵 scale matrix,它长下面这个样子:

对 Matrix 类不熟的,可以参考这篇文章,就不过多解释了

第 3 处,执行 compositionLayer.draw(),一个 Composition 包含了所有的图层,它其实只是遍历了一遍每个 Layer,对每个 Layer 调用它的 draw() 方法,因为这个例子只有一个 SolidLayer,所以自然就执行到 SolidLayer 的 draw(),不过 draw() 是定义在 SolidLayer 的父类 BaseLayer 中的

2.2 BaseLayer.draw()

第 1 处,保存传过来的矩阵 scale matrix,没什么好说的
第 2 处,因为 AE 中作图时我没有给 SolidLayer 设置 parent,所以这里不走循环
第 3 处,这里才是关键,transform 保存了你在 AE 中对这个图层(SolidLayer)所做的所有变换(a、p、s、r、o),transform.getMatrix() 就是综合了所有这些变换得到的一个矩阵 transform matrix

第 4 处,调用 drawLayer,这是个抽象方法,定义在子类自身中

2.3 SolidLayer.drawLayer()

这部分就没什么好解释的了,梳理了一下整个流程中的出现的变换,如下:

2.4 伸缩公式

到这里,很快你会发现 AE 画布上的点 (x0, y0) 与 app Canvas 上对应的 (x, y) 有下面的等式成立:

回到这个例子上,我要计算 SolidLayer 在 app Canvas 上四个角的坐标,应该有如下的计算过程:

经过整理,就得到了下面的计算公式:

left = scale*posx
top = scale*posy
right = scale*(width+posx)
bottom = scale*(height+posy)

2.5 最后一步

如果你用上面的 left、top、right、bottom 去进行设置,会发现总是有那么一点误差。 因为这里还有一个地方没考虑进去,那就是 ImageView(LottieAnimationView 是 ImageView 的子类) 有一个默认的 ScaleType,值是 FIT_CENTER,效果是将画布按比例扩大/缩小到能放入 View 中,居中显示。 也就是说在 2.1 中分析 LottieDrawable.draw() 时传进来的那张 Canvas 是已经被缩小且居中过的(伏笔:所以才要把刻度给放大回去呀!),我们上面的分析都没有把居中给加进去

3. 公式总结

至此,经过上面一大堆的分析,我们可以得出如下结论:

SolidLayer 最终经过两次矩阵变换而加载到 Canvas 上
1. ImageView 默认的 ScaleType 变换 FIT_CENTER
2. 图层自身的变换,包括 scale matrix + transform matrix

left = scale*posx + (canvasWidth-scaleWidth)/2
top = scale*posy + (canvasHeight-scaleHeight)/2
right = scale*(width+posx) + (canvasWidth-scaleWidth)/2
bottom = scale*(height+posy) + (canvasHeight-scaleHeight)/2

其中:
scale = Min(canvasWidth/compositionWidth, canvasHeight/compositionHeight)

scaleWidth = composition*scale
scaleHeight = composition*scale

composition 为 AE 中设置的画布大小
(posx, posy) 为 AE 中设置的 SolidLayer 的位移变换的位置
width、height 为 AE 中设置的 SolidLayer 的宽高

另外,最后还需要注意一点,这几个 composition、posx、posy、width、height 的数值大小在 AE 中(或说是在 json 中)和在 app 中是不太一样的,Lottie 框架在解析 json 得到这些数值后会再把它们乘上 density,这才是在 app 运行时进行计算的值。 所以在下面自己计算的时候,记得还要把他们乘上 density

相关文章

网友评论

    本文标题:Lottie伸缩规则分析

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