介绍:

ScrollPhysics一般是经过回来不同的simulation来控制ScrollView上的履行的动画的,其作用便是用来描绘一个算式,它会接收一些比较通俗的参数,如:速度、方位、鸿沟,然后经过solution办法把这些参数转化为算式中真正运用的参数,记录下来,过一段时刻后运用记录下的参数算出该时刻点的position值;当然,比较简略的算式一般是不必走_solution办法来额定核算参数,比较杂乱的会_solution一下。

举个比方

假如方程比较简略,例如:你想描绘一段匀速运动,一般需求传两个参数,初始方位 100,速度 10,那么这个simulation就会直接保存 originPosition = 100, originVelocity = 10,过了一段时刻后(一般是下一个vsync),当需求更新偏移量时,这个simulation会被调用,传进来一个时刻段t,是间隔这个simulation初始化的时刻间隔,然后simulation就会给出当时的方位newPosition=originPosition+velocity∗tnewPosition = originPosition + velocity * t, newVelocity=originVelocitynewVelocity = originVelocity,翻滚视图会拿着这个newPosition去更新偏移量,然后ScrollPhysics创立的一个新的这个simulation(也或许换成别的类型的Simulation,在ScrollPhysics可重写办法回来你需求的simulation),把上面的newPosition和newVelocity传到新的这个simulation的originPosition和originVelocity上,这个simulation等下一个vsync生效,然后再创立个新的simulation,一直这样进行下去。

假如方程比较杂乱,在比方:你想描绘一段Spring动画,但可传的参数一般是阻尼系数、劲度系数、质量这种,它们不能直接写在核算公式里边,所以需求额定进行一步solution,判别阻尼类型(阻尼系数和劲度系数的配比会影响阻尼的类型,它们对应微分方程不同解的方式),并把这些参数转成能直接写到算式里边的参数保存下来,其余流程和上面的匀速相同。

与列表相关的Simulation一般有这几个:

  • SpringSimulation/ScrollSpringSimulation:绷簧模仿器;后者是前者的子类,实践用到的是后者
  • FrictionSimulation:冲突(减速)模仿器;iOS中的decelerate作用。
  • BouncingScrollSimulation:一个复合的模仿器,里边包含了Spring和Friction;逾越鸿沟相当于Spring,未逾越鸿沟相当于Friction,这个便是终究给列表运用的。
  • ClampedSimulation: 给恣意一个simulation约束上下限,你能够传入恣意子simulation进去,回来值会被ClampedSimulation约束上下限。
  • GravitySimulation: 模仿重力。

咱们主要研讨前三种,由于Flutter中的Bouncing是由别的两个复合的,所以实践只是Spring和Friction两种;这两种作用,别离对应于iOS中的Bounces和Decelerate。

Friction

Flutter中称之为“冲突”,iOS称之为”减速”,两个是相同的作用,它们的公式都是 v=v0e−2tv = v_{0} e^{-2t},意思是速度随时刻呈指数衰减:速度越大冲突力越大,这些内容在一篇文章中进行过详细的验证。有趣的是:当你读Flutter或者iOS的代码是看不到这个参数 −2-2 的,你能在iOS中看到的参数是 0.9980.998

v6kJYR.png

在Flutter中看到的参数是 0.1350.135

v6kYf1.png

而在LNCustomScrollView中咱们用到的参数是 −2-2 本身,这三个数字表达的是同一个意思,感兴趣的同学能够验证一下:0.9981000≈0.1350.998^{1000} \approx 0.135 , 而 0.135 =1e20.135 ~= \frac{1}{e^2}。iOS的参数0.9980.998的意思是:减速时速度每毫秒衰减为本来0.9980.998倍,Flutter的参数0.1350.135表达的意思是:减速时速度每秒衰减为本来的0.1350.135倍,由于1s=1000ms1s = 1000ms,所以每秒钟会进行1000次0.998的衰减,就有了第一个联系:0.9981000=0.1350.998^{1000} = 0.135;至于0.1350.135e−2e^{-2}便是纯数值相等,也便是说iOS/Flutter找了个参数0.9980.998让每秒的衰减率刚好等于e−2e^{-2},所以这三个数表达的意思相同。

解释完这个参数,咱们回到Flutter的代码中,这个FrictionSimulation类

正常接收的初始参数有四个:

v6kB0e.png

  • Drag:这个便是衰减率,支撑自定义,列表默认用的0.1350.135
  • position:初始方位
  • velocity: 初始速度
  • rolerance:阈值的调集,举个比方,指数衰减收敛在0但永远不会等于0,能够一直衰减,这个时分你需求给出一个十分小的速度,让体系知道,当速度减到这个阈值的时分你能够把它视为静止,这个十分小的速度便是速度阈值;当然这个阈值的调集也能够包含时刻、间隔的阈值,在iOS中UIScrollView的速度阈值量大概是13pt/s,当减速到这个速度时,UIScrollView就中止翻滚了,防止翻滚很久带来一些比较差的交互体会。
_DragFor函数

v6k21P.png

实践的应用时,有些场景期望是这样的:我只想让列表从方位A减速到方位B,中心怎么减,衰减率是多少我不想关怀,例如:一个ViewPager,当手势落在两页之间时,我只希望他减速到上一页或者下一页,这个时分需求FrictionSimulation自行挑选适宜的衰减率,来满意“滚到特定方位”的需求;所以额定提供了一个Factory来协助运用者核算一个适宜的衰减率,在传入上述四个参数后through会调用_dragFor办法协助用户核算衰减率,仍是以ViewPager为例,当用户松手时速度比较大,算出来这个衰减率会比较接近于0,衰减得比较快,由于需求很强的衰减才能让翻滚中止下来;反之速度比较小,这个衰减率就会比较接近于1,由于减太快的话还没翻滚到指定方位就停了。

详细的核算能够拆成两步看:第一步是求出v=v0eatv = v_0e^{at} 的这个参数a(也便是咱们那个−2-2),代码中的式子直接运用了v1−v2y1−y2\frac{v_1 – v_2}{y_1 – y_2}这种方式,这个式子是这么来的:

由于:v=v0eat v = v_0e^{at},

两头积分(速度积分是位移):y=v0eata+Cy = \frac{v_0e^{at}}{a}+C ,C是个常数

取恣意两个时刻点t1t_1,t2t_2对应的位移y1y_1 ,y2y_2, 和速度v1v_1,v2v_2则有

y1=v0eat1a+Cy_1 = \frac{v_0e^{at_1}}{a} + C

y2=v0eat2a+Cy_2 = \frac{v_0e^{at_2}}{a} + C

v1=v0eat1v_1 = v_0e^{at_1}

v2=v0eat2v2 = v_0e^{at_2}

把上面四个式子带入到v1−v2y1−y2\frac{v_1 – v_2}{y_1 – y_2}中 ,两个C消掉了,两个减式子除了底下有个a,其他一模相同,所以

a=v1−v2y1−y2a = \frac{v_1 – v_2}{y_1 – y_2},由于t1t_1,t2t_2是恣意取的,所以初末方位的速度、位移也符合这个式子。

这样算出a今后,再取个eae^a便是一秒钟的衰减率。

FrictionSimulation履行solution后存的数便是这个drag,1秒的衰减率;还有一个额定dragLog 是对 drag取了个e为底的对数,Emmm,所以其实便是a。

x函数

v6khnS.png

此处代码顶用的都是x,我习惯用的y,都是位移的意思,这儿由于代码中写的是x,所以这部分都用x了

这个x函数是依据输入时刻t,算出当时方位xtx_t;上面我

把两个xxtt的表达式替代一下便是:

xt=x0+(v0eata+C)−(v0eat0a+C)x_t = x_0 + (\frac{v_0e^{at}}{a} + C) – (\frac{v_0e^{at_0}}{a} + C)

由于咱们规定初始的时刻是0, 也便是t0=0t_0 = 0,t_2是当时的时刻t, 把这两个带入之后简化一下便是:

xt=x0+v0∗eat−1ax_t = x_0 + v_0*\frac{e^{at} – 1}{a}

把这个eate^{at}换成 dragFortdragFor^t 就行,由于咱们之前说过 dragFor=eadragFor = e ^ a

这个便是x函数的回来值

dx函数

v6kbpq.png

这个dx函数,表明的便是某个时刻点的速度,xtx_t求导即可,初始速度按照dragFor的衰减率进行衰减:

v=v0(dragFor)tv = v_0 (dragFor)^t

finalX函数:

v6kOXT.png

这个finalX函数,意思是减速运动完毕地点的方位,能够用来核算减速的极限方位是否能触达鸿沟,假如触达鸿沟会持续履行弹性,不会触达就只要减速;还记得咱们上面提到过:a=v1−v2y1−y2a = \frac{v_1 – v_2}{y_1 – y_2}t1t_1t2t_2恣意取,把这个式子交流一下:y1−y2=v1−v2ay_1 – y_2 = \frac{v_1 – v_2}{a};这个y1y_1便是现在的finalX,y2y_2当作初始方位 x0x_0 , 末速度是0,所以v1v_1是0,初速度v2v_2便是 v0v_0, 这个式子就变成了 finalX=x0−v0afinalX = x_0 – \frac {v_0}{a}

timeAtX函数

v6kx74.png

这个函数用来核算翻滚到某个方位对应的时刻点,其间会呈现double.infinity这种极限值,由于减速运动或许永远也无法达到某个十分远的方位,上面所说的finalX也用在了此处,假如方位超过了finalX,咱们认为永远也无法到达那个方位。判别条件这儿就不赘述了,这个回来成果便是依据上面那个x函数里边的公式,把t当作变量,x当作已知量,左右移一移动,搞一搞,就出来了。

isDone函数

假如当时的速度(dx)小于必定的阈值就视为中止。

以上便是FrictionSimulation中一切函数中的核算办法。

SpringSimulation

这个类的初始换参数有五个:

v6A9hR.png

  • spring: 描绘spring参数的类,主要参数有三个:质量(mass),刚度(stiffness了解成绷簧的劲度系数k),阻尼(damp),这儿边质量能够先暂时忽略,当作1就行。
  • start:初始方位
  • end:完毕方位,也便是绷簧的平衡点
  • velocity:初速度
  • tolerance :阈值的调集

拿到这五个参数,Simulation会先履行一个_SpringSolution办法:

v6Ai1x.png

这个函数叫“求解”:来把传入的数值解为能够用到公式中的参数,Emmm…这些参数便是二阶齐次线性常系数微分方程里边的c/r/a/b之类的,c一般是指数的系数,r一般是指数的幂系数,a、b一般是△<0时分,复数根的实部和虚部。整个springSimulation的核算根本都是围绕这个微分方程展开:

这个方程是背诵题,假如现已忘记了这个方程的解公式,能够温习一下:二阶常系数线性齐次微分方程

大致分三步:

1.找特征根方程(一元二次方程)

2.解特征根方程

3.依据解方式写答案

这儿边_SpringSolution的三个子类: _CriticalSolution/ _OverdampedSolution / _UnderdampedSolution 便是依据特征根方程解的方式评论终究方程的方式,判别条件:

v6AEnO.png

实践上便是特征根方程的△(b方减4ac):

  • △ > 0 ,俩实根,对应_OverdampedSolution过阻尼;
  • △ = 0,一个实根,对应 _CriticalSolution,临界阻尼;
  • △ < 0,俩复根,对应 _UnderdampedSolution 欠阻尼,这种情况会产生震动,由于阻尼太小了。(这个cmk不知道是什么含义,它的成果便是:”delta”)

上面便是整体思路,之后详细到每个参数:

评论一个物体一起遭到两个弹力和阻力的作用,弹力与到平衡点的偏移量呈正比,阻力总是与瞬时速度呈反比,质量为m,受力方程:

−ky−cv=ma-ky-cv = ma

对应到Flutter的代码里:

c 便是 springDescription.damping,阻尼系数

k 便是springDescrition.stiffness,劲度系数

m 便是springDescription.mass,质量

y 是间隔绷簧平衡点的位移

v 是速度

a 是加速度

这么看就能当作微分方程:a 是 y” , v 是 y’ , y是y, t是变量:my′′+cy′+ky=0my” + cy’ + ky = 0

那个判别条件cmk,便是 c2−4mkc^2 – 4mk, 哦!!!本来这个cmk是这个意思,名字起的有水平!

判别好条件就只需求用c、m、k依据需求算出三种解方式里边的c1、c2、r1、r2、a、b(这个r1、r2一般在教材里写的是:λ1、λ2)

△ > 0 时:

两个根便是二次函数解的公式,这公式是这么背的:“2a分之负b加减根号下b方减4ac”,这个算出的两个值别离做r1和r2。然后算c1、c2的时分需求依据实践情况: t = 0 时,位移是初始传入的distance,速度是初始传入的velocity ,然后对 y=c1er1t+c2er2ty = c_1e^{r_1t} + c_2 e^{r_2t} 求个导数,v=c1r1er1t+c2r2er2tv = c_1r_1e^{r_1t} + c_2r_2e^{r_2t}, 把t = 0带到上面两个式子你能得到:

distance=c1+c2distance = c_1 + c_2 (distance便是Simulation里边传的initialPosition,t = 0时的初方位)

velocity=c1r1+c2r2velocity = c_1r_1 + c_2r_2

r1r_1r2r_2上面解出来了,用这两个式子解出来c1 c_1c2c_2就能够了:

v6VR6x.png

△ = 0的时分略微简略一些:

直接运用 “2a分之负b”,就能找到仅有的一个r,然后用这个解方式:y=(c1+c2∗t)∗erty = (c_1 + c_2 * t)*e^{rt} ,求个导数,用“左导乘右加右导乘左”:v=c2ert+(c1+c2t)rertv = c_2e^{rt} + (c_1+c_2t ) re^{rt},和前面条件相同,t = 0时:

distance=c1+0distance = c_1 + 0

velocity=c2+c1rvelocity = c_2 + c_1r

算出:

c1=distancec1 = distance

c2=velocity−c1rc_2 = velocity – c_1r

v6VhnK.png

Emmm,这个c2c_2,不知道为什么flutter里边写成了c2=velocityc1rc_2 = \frac{velocity}{c_1r},我验证了几次应该便是减号,这儿写的除号,感觉是不小心将减号写成了除号,就给Flutter提了个小问题:[github.com/flutter/flu…] ;假如这儿确实有问题,这个问题也不会经常呈现,由于临界阻尼呈现的条件十分严苛,大部分情况下应该都是过阻尼,只要damp和stiffness刚好满意了cmk = 0的情况下才走临界阻尼。

△<0 的时分略微杂乱一些:

咱们需求先求出共轭复根的实部和虚部,代码里边的w便是虚部:

数值等于根号下 △的绝对值 : 4ac−b22a\frac{\sqrt{4ac – b^2}}{2a},(由于b2−4acb^2-4ac 总是小于0的,所以绝对值直接写成了4ac−b24ac-b^2) , a代成m, b 代成damping , c 代成 stiffness , 求出w。

r便是实部:−b2a\frac{-b}{2a} , b代成damping,a代成m,求出r。

得到: y=ert(c1cos(wt)+c2sin(wt))y = e^{rt} (c_1cos(wt) + c_2sin(wt))

老法子,求个导数:

得到:v=rert(c1cos(wt)+c2sin(wt))+ert(−c1wsin(wt)+c2wcos(wt)) v = re^{rt}(c_1cos(wt) + c_2sin(wt)) +e^{rt}(-c_1wsin(wt)+c_2wcos(wt))

t = 0时:

y=c1+0y = c_1 + 0

v=rc1+wc2v =rc1+wc2

所以:

c1 = y

c2 = (v – r * c1) / w

v6V57D.png

以上便是三种运动公式一切参数的求解办法。

总结一下:springSimulation的整体思路便是在创立的时分,依据cmk相对0的巨细别离回来不同的阻尼子类,依据输入参数:初始位移、初始速度、绷簧刚度、阻尼系数、质量 ;核算出能表明运动方程的参数 c1、c2 、w、r 并保存下来,依据这些参数和自己对应的公式,在x()和dx()两个办法中给出一段时刻之后的y和v。

而ScrollSpringSimulation 是一个倒推的进口,经过你输入的当时方位、当时速度、完毕的方位、完毕的速度(这个参数省掉的,由于它总是0,所以没必要传),协助你定制一根绷簧,这根绷簧能刚好让你的这些条件都满意:速度等于0的时分刚好翻滚到完毕的方位;这种定制规则一般在页面做pageEnable切割的时分运用到,每次松手后都会定制一个刚好滚到目标点的绷簧;而在页面鸿沟处的四根绷簧则不会定制,它们是标准相同的。

BouncingScrollSimulation

到这儿,咱们现已研讨了两个基础的simulation,之后把这两种组合一下就能够凑成一个BouncingScrollSimulation, bouncing代表反弹也便是Spring,scroll代表翻滚+冲突力也便是frictionSimulation;所以这个Simulation描绘的运动包含两段,你给一个列表必定的初速度,在触摸鸿沟之前松手,这样他会先履行一个减速(1段),触摸鸿沟后履行一个回弹(2段);当然,它也或许触摸不到鸿沟,这样就只履行减速;也或许一开始就在鸿沟外,这样就只履行回弹,反正一切判别都是在这个simulation内部做的。

和其他的Simulation相同,先研讨下入参:

v6ZSAg.png

  • position: 初始方位
  • velocity:初始速度
  • leadingExtent: 左鸿沟/上鸿沟
  • trailingExtend: 右鸿沟/下鸿沟
  • spring: 对绷簧的描绘

收到参数后依据条件创立需求的simulation:

v6Zi3n.png

  • 当时方位小于左鸿沟的时分,创立一个收敛到左鸿沟的绷簧
  • 当时方位大于右鸿沟的时分,创立一个收敛到有鸿沟的绷簧
  • 在两个鸿沟的规模之内,创立一个能够自由翻滚Friction:这儿边参数0.135咱们之前评论过了,其实是 e−2e^{-2},剩下的核算都是关于Friction和Spring的临界点的核算。

速度正向和反向是对称的两个运算,所以这儿咱们只评论正向的:当速度大于0,且末方位大于右侧鸿沟,这个Friction的末方位咱们前面也评论过,在这个参数下便是 position + velocity/2。假如这个末方位超过了鸿沟,说明会撞击到右侧的绷簧上,咱们需求找到刚好触摸到鸿沟的那个时刻点,来对两段运动进行区分。此处用到的便是FrictionSimulation.timeAt()办法,传一个末方位,回来一个时刻;

算出这个切割时刻后,它会创立两个Simulation,前半段Friction,和后半段Spring;这两个会在 _simulation 这个属性的get办法中依据当时的条件:时刻在Friction完毕时刻点的前/后,别离回来。

这样就完成了一个依据运动情况组合起来的:BoucingScrollSimulation。

(一句题外话:这儿由于前半段是减速所以能算出这个时刻准确值,幻想别的一个场景:翻滚视图的鸿沟或许会把你可视区域弹出来,就像用弹弓发射一枚石子,发出去后会遭到空气阻力,假如是这样的话,前半段是Spring,后半段是Friction,Spring的完毕时刻很难求,举个比方: 10t=et10t = e ^ t 的交点,只能求数值解,一般用牛顿迭代法,只要确保精度到小数点后3位根本就能确保在屏幕上不出问题,所以Simulation基类只规定要提供x() 、dx(),没有规定必定要提供timeAt()办法,由于暂时用不到,也求不出准确解,假如这个simulation需求支撑恣意几个运动需求先后拼接在一起的话,我想它是需求支撑timeAt()办法的)

ClampingScrollSimulation

这个是安卓的,看起来用的是幂函数,我不认识这Penetration是什么意思,特意百度一下,分明是个动词,我不知道为什么要刻意加一个括号描绘是什么东西做了这个动作。这儿边有一些和iOS是共性的东西,flingVelocityPenentration函数是在描绘一段减速运动,运用的是一个三次函数,但事实上他运用的也是指数,假如你记得常用的特勒展开式,其间有一个:

v6ZVBT.png

你会觉得这个三次函数十分面善,代码里长这样:

y=3.065t−3.27t2+1.2t3y = 3.065t – 3.27t ^ 2 + 1.2t^3,(由于它限制了t总是0~1之间,越高次项对y的影响会越来越小,所以我倒过来写)

我再给出一个:

y=3.065te−1.13ty = 3.065te^{-1.13t}

把一式那个指数取泰勒展开的前三项,你会得到,在 t = 0.f附近:

y=3.065t(1−1.13t+0.57t2)=3.065t−3.46t2+1.75t3y = 3.065t(1-1.13t+0.57t^2) = 3.065t – 3.46t^2 + 1.75t ^ 3

虽然数值不是完全相同,但现已十分接近了;为什么会有误差,由于矫正后的参数的特性:

t=0t = 0t=1t = 1带入到安卓的方程:

y=0−0+0=0y = 0 – 0 + 0 = 0

y=3.065−3.27+1.2=0.995 =1y = 3.065 – 3.27 + 1.2 = 0.995 ~= 1

再别离带入到我随便给的那个方程:

y=0−0+0=0y = 0 – 0 + 0 = 0

y=3.065−3.46+1.75=1.35>1y = 3.065 – 3.46 + 1.75 = 1.35 > 1

所以,这个参数设置的意图应该便是让 [0,1] 的两头对齐(初末方位),这样直线就能够表明成曲线,运动末方位仍是相同的;所以这个函数的意图便是依据间隔和时刻,给出一段阻尼运动的曲线,让他刚好和初末方位,初末速度和条件相同的匀减速运动一致;由于两头都是0和1,只要中心改变的快慢产生了改变,从直线变成了曲线,同样这个函数的导数也能表明速度的改变率。所以其实是用一个三次函数表明了一个指数。

总结:

这个调研的起因是:用到了bilibili漫画的日漫模式,其间有多页合并的功用,我尝试用iOS的原生控件(UICollectionView/UICollectionViewLayout)完成相似的作用,不是冲突力大便是切割感弱,属于是鱼和熊掌不可兼得了;后来得知应该是运用Flutter完成,我从官网copy了个列表的Demo,完成了一个ScrollPhysics的子类传到了列表,分段回来不同的Simulation,完成了这种多页合并的作用,调研的时分搜到了许多关于ScrollPhysics的博客,大都是大略提了一下ScrollPhysics、Simulation、Solution几个类之的联系,没有描绘详细的核算过程所以这儿共享一下。

Flutter雀食好用,读源码,收成颇丰。