Flutter滑动体验对齐原生-滑动曲线篇

2022年3月29日 274点热度 0人点赞 0条评论
自从使用了Flutter以来,闲鱼在享受着跨端带来的提效的同时,流畅度一直是困扰了我们许久的问题,也是被外界吐槽得比较多的地方。所以我们在过去半年,重新牵起了流畅度优化这件事情,目标只有一个,那就是拉平Flutter和Native的滑动体验
我们把这个目标拆分为了两个:
  • 滑动曲线优化,拉齐手感
  • 渲染性能优化,减少掉帧
本文主要跟大家介绍一下我们在优化滑动曲线手感方面的一些经验。和优化渲染单纯的想办法提升指标不同,滑动曲线感觉是否舒服因人而异,那么我们该如何去评判一个滑动曲线是否优秀呢?说实话我们一开始也没有很好的答案。既然这样,那我们的目标首先是需要把Flutter和Native的滑动曲线进行对齐,先找回习惯的感觉再考虑下一步的优化。

滑动对比

我们先使用platform_tests对比一下Flutter的滑动和Native的差异

(视频的左侧列表是Native,右侧列表是Flutter)

Android

iOS

从视频的表现中,我们可以看出来iOS的脱手滑动略有差异但还是基本符合预期的,Android的脱手滑动就比较糟糕了看起来明显阻尼比Native大很多。

Android滑动曲线阻尼问题

找滑动曲线代码的过程就不在这边过多赘述了,如果在创建滑动容器(ListView/GridView/...)时没有传入特定的physics,则Flutter会根据平台使用相应的ScrollPhysics(Android对应ClampingScrollPhysics,iOS对应BouncingScrollPhysics)。在发生Fling的时候会调用ScrollPhysics.createBallisticSimulation()构建一个Simulation,这个Simulation(ClampingScrollSimulation/BouncingScrollSimulation)就是最终控制脱手滑动动画参数的类。

问题分析

在ClampingScrollSimulation构造函数中,会根据传入的初速度,并结合一些小学二年级就学过的运动学公式计算出最终的距离以及滑动的总时间(因为没找到确定性的资料,只能根据参数推测,这边先不展开讨论)

图片

计算出总时长和总距离后,就可以根据当前时长的比例(动画运行的时间/总时长),带入公式计算出当前应该位移的距离,最后再加上初始位置即可得到最终Viewport的偏移量。

图片

这个公式是对Android的滑动曲线进行拟合产生的,拟合过程可以看注释,但是这个拟合并不完美。我们对这个d/t函数进行求导(即速度和时间的关系)可以发现,它的导数在0-1的区间内并不是单调递减的,这意味着在滑动接近结束的时候会有一个速度增加的情况,体感上在Clamping曲线滑动动画的末尾感觉上会有一个轻微的吸附感也说明了这一点。所以我们需要把Clamping的实现改一下,把Android的Scroller中计算滑动距离的代码弄过来。

图片

但是我们在上面的例子中,并没有发现这个吸附的感觉,这是不是说明了这条曲线根本就没能完整地跑完呢?我们可以进一步加一些Log看一下。我们在double x(double time)方法中把时间和position打出来,发现了一个问题,time一直在清零,position也一直在变。这说明了这个动画一直在被重新启动,我们简单改一下代码,强行让动画不要重启,使用同样的ADB指令进行滑动,通过Log对比可以发现这确实会导致滑动速度更快地衰减。
图片
那么我们就把问题拆分成了两个:
  • 解决发生Layout时的动画重启问题
  • Clamping的实现方式对齐Android的Scroller

问题解决

  • 动画重启问题

我们在ClampingScrollSimulation的构造函数中断点,可以发现当Fling过程中如果Item高度发生改变,则会触发RenderViewport.performLayout(),从而触发ScrollPositionWithSingleContext.goBallistic(),这个方法会以当前的状态为起点重新启动fling动画,这显然是不合理的,特别是当前滑动和边界无关的时候没必要因为布局改变而重启动画,这会导致一次Fling动画无法完整做完,移动的轨迹自然也就无法贴合预期中设计好的d/t曲线。

这个问题比较明确,所以解决问题的思路也很明确,就是在Fling的过程中不要重启动画了,而是去更新一些相关的变量,使得动画能够合理的继续完成。一开始我是希望更新Simulation中和边界有关的的相关参数,但是因为这个方案有个始终绕不过去的类型检查(https://github.com/flutter/flutter/pull/96512)Flutter团队的同学认为不能通过。所以诞生了下面的新方案(https://github.com/flutter/flutter/pull/100133)

首先解决更新时机的问题,当RenderViewport.performLayout()被调用的时候,会回调ScrollActivity.applyNewDimensions在惯性滑动的过程中的ScrollActivity是BallisticScrollActivity

图片
之前提到的ScrollPositionWithSingleContext.goBallistic()也就是在这个方法中被调用,所以我们在这边做一下修改,让其不再调用goBallistic(),而是调用updateBallisticAnimation()生成一个新的Simulation,并将其更新到AnimationController中。
图片
updateBallisticAnimation()中,我们还是使用createBallisticSimulation()来创建Simulation。这里重要的一点是,因为我们动画时间没有清零,所以我们创建Simulation的时候一定要用初始的ScrollMetrics和Velocity来创建。因为布局变化有可能会带来相应的边界变化,所以这里只将相应的ScrollExtent(也就是边界值)更新为最新的值即可。
图片

Android滑动曲线对齐

做完上述的操作后,我们看到了熟悉的吸附的效果,但这并不是我们想要的。为了彻底对齐与Native的体验,拟合曲线肯定是满足不了我们的,下一步我们需要将Clamping的公式彻底换成Android的Scroller.java中的实现。这时候有的小朋友可能要问了:“都要换掉它了,那我们刚才费这劲去修复它干嘛呢?”
图片
翻译Scroller.java的代码到dart这个工作并不难,并且这部分工作其实有一位Google的同学已经做了(https://github.com/flutter/flutter/pull/77497),但是这个PR在合入后不久就被Revert了,被Revert的原因是在某些场景下会导致滑动停不下来。是的,就是因为上面提到的动画重启的问题,导致了这个滑动无法停止。所以我会在动画重启的PR被合入后,Reland这个Android曲线的PR。现在我们先来看看最后的效果,不能说是十分相似只能说是一摸一样了。
图片

滑动速度引发的问题

我们把曲线修改完成后开开心心地在搜索场景上线了,但是没过多久就传来噩耗,产品和交互同学一致觉得我们的滑动比以前快了很多,会影响到用户体验。
图片
但是让我把曲线调回到那滑不动的样子,我是拒绝的。难道就没有一个方法能让用户在用的时候想仔细看的时候慢想滑走的时候快吗?算了,我们先想想看怎么让他慢下来吧。

如何合理的给曲线减速

Android和iOS两条曲线,这么多的参数,到底该把哪个调低在能在不影响滑动曲线的整体形态的同时降低体感速度呢?在很久以前Flutter其实是给iOS做过一个减速的,在BouncingScrollPhysics创建BouncingScrollSimulation时,给初速度乘了0.91。我们最初使用的曲线也是这个减速版本的,所以切换到正常的曲线后才会显得比较快。这个0.91在一次PR中被删除了(https://github.com/flutter/flutter/pull/59623)原因是导致了iOS曲线阻尼过大,但是实际上背后的原因,其实是上面提到的动画重启问题,因为动画重启导致速度多次乘了0.91,才使得滑动速度加速衰减。那么现在问题解决了,我们是不是也可以尝试通过这种方式对曲线进行减速。iOS的滑动公式比较简单,我们就通过iOS来分析,Android也是同样的道理。
图片先把初速度衰减后的曲线画出来看看。可以发现其实整体曲线的形态是没有发生变化的,只是滑动的距离减少了,这样就显得比较慢了。所以给初速度增加衰减值这个方案,是比较符合我们的预期的。

如何让滑动能快能慢

减速的问题解决了,接下来该思考一下想快的时候该怎么快起来。那么用户怎么样的一个行为才表示他现在想快点滑呢?我认为滑动速度其实一定程度上反映了用户的意愿,当用户滑动速度快的时候,天然就表示了他希望能快点滑走。顺着这个思路,如果我们给初速度的衰减值再增加一个衰减值(套起来了)让他在速度慢的时候衰减值大,速度快的时候衰减值小,就可以解决这个问题了。
图片
这时候,灵光一现,突然想到了一个小学二年级就学过的抛物线y=ax^2+bx+c,中间低两边高恰好满足我的诉求,当用户滑动得很慢或很快的时候不增加衰减,当用户在中速滑动的时候给出最大衰减。为了方便在线上做实验以及数据分析,我们尽量把参数缩减到一个,我选择了对称的图形(强迫症),所以必过两个点(0,1)和(1,1),并且顶点的x坐标也确定为了0.5,剩下一个顶点y坐标,初中学的顶点式就不多说了吧,直接上代码。

图片

最终效果

最终滑动的效果符合我们的预期,上线进行了分桶实验,我们得出了一个最佳实践的衰减顶点值在0.7左右,效果大概如视频所示。调整曲线的按钮是临时增加的,切换不同曲线并且使用同一个adb命令进行滑动,可以看出来使用新的曲线在快速滑动的情况下会比之前的曲线多滑动一到两屏。

总结和展望

本文给大家详细介绍了闲鱼优化滑动曲线手感的方案,虽然过程中遇到了不少问题,我们还算圆满地完成了任务,将Flutter的滑动手感对齐到了原生,并且根据业务场景进行了曲线速度优化。如果大家需要对曲线进行优化可以提前合入以下两个PR,上文提到的减速方案也可以根据业务情况酌情使用。
  • 修复动画重启问题:https://github.com/flutter/flutter/pull/100133
  • Android曲线复刻:https://github.com/flutter/flutter/pull/77497
后续我们会在此基础上继续进行优化,包括但不限于以下几点:
  1. 推进本文提到的几个PR的合入
  2. 结合业务数据并配合交互设计师,对滑动手感进行更精细的控制
  3. 在滑动的过程中根据速度的不同,结合图片库进行图片加载控制,以提升滑动流畅度
当然了最后一点更偏向于性能优化,下一篇文章也会给大家介绍一下这段时间所做的流畅度相关的性能优化,敬请期待(一定不咕)。
图片

49630Flutter滑动体验对齐原生-滑动曲线篇

这个人很懒,什么都没留下

文章评论