Android 动画里的贝塞尔曲线
对贝塞尔曲线的听闻,大概是 Photoshop 里的钢笔工具,用来画曲线的,但一直不明白这个曲线有什么用,接触学习到 Android 动画,又发现了贝塞尔曲线的身影,这玩意不是在绘图软件里画曲线的吗,怎么和动画扯上关系了,好吧,今天高低得来了解一下。
插值
在数学里面的插值(Interpolation),是一种通过已知的、离散的数据点,在范围内推求新数据点的过程或方法。
下面这个表给出了某个未知函数 <math xmlns="http://www.w3.org/1998/Math/MathML"> f f </math>f 的值
x | f(x) |
---|---|
0 | 0 |
1 | 0.08415 |
2 | 0.9093 |
3 | 0.1411 |
4 | −0.7568 |
5 | −0.9589 |
6 | −0.2794 |
通过插值就可以估算中间点函数,如得到 <math xmlns="http://www.w3.org/1998/Math/MathML"> x = 2.5 x=2.5 </math>x=2.5 时的值。
插值方法有许多如片段插值、线性插值、多项式插值等等
线性插值
假设我们已知坐标 <math xmlns="http://www.w3.org/1998/Math/MathML"> ( x 0 , y 0 ) \left ( {{x}{0},\, {y}{0}} \right ) </math>(x0,y0) 与 <math xmlns="http://www.w3.org/1998/Math/MathML"> ( x 1 , y 1 ) \left ( {{x}{1},\, {y}{1}} \right ) </math>(x1,y1),求: <math xmlns="http://www.w3.org/1998/Math/MathML"> [ x 0 , x 1 ] \left [ {{x}{0},\, {x}{1}} \right ] </math>[x0,x1] 区间内某一位置 <math xmlns="http://www.w3.org/1998/Math/MathML"> x x </math>x 在所对应的 <math xmlns="http://www.w3.org/1998/Math/MathML"> y y </math>y 值。
因为 <math xmlns="http://www.w3.org/1998/Math/MathML"> ( x 0 , y 0 ) \left ( {{x}{0},\, {y}{0}} \right ) </math>(x0,y0) 和 <math xmlns="http://www.w3.org/1998/Math/MathML"> ( x , y ) \left ( x,\, y \right ) </math>(x,y) 之间的斜率,与 <math xmlns="http://www.w3.org/1998/Math/MathML"> ( x 0 , y 0 ) \left ( {{x}{0},\, {y}{0}} \right ) </math>(x0,y0) 和 <math xmlns="http://www.w3.org/1998/Math/MathML"> ( x 1 , y 1 ) \left ( {{x}{1},\, {y}{1}} \right ) </math>(x1,y1) 之间的斜率相同,所以:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> y − y 0 x − x 0 = y 1 − y 0 x 1 − x 0 {\frac {y-{y}{0}} {x-{x}{0}}=\frac {{y}{1}-{y}{0}} {{x}{1}-{x}{0}}\, } </math>x−x0y−y0=x1−x0y1−y0
其中 <math xmlns="http://www.w3.org/1998/Math/MathML"> x 0 {x}{0} </math>x0、 <math xmlns="http://www.w3.org/1998/Math/MathML"> y 0 {y}{0} </math>y0、 <math xmlns="http://www.w3.org/1998/Math/MathML"> x 1 {x}{1} </math>x1、 <math xmlns="http://www.w3.org/1998/Math/MathML"> y 1 {y}{1} </math>y1、 <math xmlns="http://www.w3.org/1998/Math/MathML"> x x </math>x 都已知,那么:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> y = y 1 − y 0 x 1 − x 0 ⋅ ( x − x 0 ) + y 0 = y 0 + ( y 1 − y 0 ) ⋅ x − x 0 x 1 − x 0 y=\frac {{y}{1}-{y}{0}} {{x}{1}-{x}{0}}·\left ( {x-{x}{0}} \right )+{y}{0}={y}{0}+\left ( {{y}{1}-{y}{0}} \right )·\frac {x-{x}{0}} {{x}{1}-{x}{0}} </math>y=x1−x0y1−y0⋅(x−x0)+y0=y0+(y1−y0)⋅x1−x0x−x0
整个过程,和上面的例子,线性插值估算未知函数 <math xmlns="http://www.w3.org/1998/Math/MathML"> f ( 2.5 ) f(2.5) </math>f(2.5) 的值是不是一样?
贝塞尔曲线
线性贝塞尔曲线
线性贝塞尔曲线是一条两点之间的直线,给定点 <math xmlns="http://www.w3.org/1998/Math/MathML"> P 0 {P}{0} </math>P0、 <math xmlns="http://www.w3.org/1998/Math/MathML"> P 1 {P}{1} </math>P1,这条线由下式给出:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> B ( t ) = P 0 + ( P 1 − P 0 ) ⋅ t , t ∈ [ 0 , 1 ] B\left ( {t} \right )={P}{0}+\left ( {{P}{1}-{P}_{0}} \right )·t,\, t\in \left [ {0,\, 1} \right ] </math>B(t)=P0+(P1−P0)⋅t,t∈[0,1]
这不就是线性插值嘛,线性插值的结果也在一条两点之间的直线上。
二次方贝塞尔曲线
既然两个点线性插值可以表示一条两点之间的直线,那...3个点线性插值的结果,几何表示会是什么样?
点 <math xmlns="http://www.w3.org/1998/Math/MathML"> P 0 {P}{0} </math>P0、 <math xmlns="http://www.w3.org/1998/Math/MathML"> P 1 {P}{1} </math>P1 插值得到连续点 <math xmlns="http://www.w3.org/1998/Math/MathML"> Q 0 {Q}{0} </math>Q0,描述线段 <math xmlns="http://www.w3.org/1998/Math/MathML"> P 0 P 1 {P}{0}{P}_{1} </math>P0P1,是一条线性贝塞尔曲线;
点 <math xmlns="http://www.w3.org/1998/Math/MathML"> P 1 P 2 {P}{1}{P}{2} </math>P1P2 插值得到连续点 <math xmlns="http://www.w3.org/1998/Math/MathML"> Q 1 {Q}{1} </math>Q1,描述线段 <math xmlns="http://www.w3.org/1998/Math/MathML"> P 1 P 2 {P}{1}{P}_{2} </math>P1P2,是一条线性贝塞尔曲线;
点 <math xmlns="http://www.w3.org/1998/Math/MathML"> Q 0 Q 1 {Q}{0}{Q}{1} </math>Q0Q1 再插值,也就是对图中绿色线段进行插值,得到连续点 <math xmlns="http://www.w3.org/1998/Math/MathML"> B B </math>B,描述曲线 <math xmlns="http://www.w3.org/1998/Math/MathML"> P 0 P 2 {P}{0}{P}{2} </math>P0P2,也就是图中红色曲线,是一条二次贝塞尔曲线。
原来,两个及以上的点 线性插值函数 就是 贝塞尔曲线函数,我们可以简单地不断循坏迭代两点线性插值来得到最终结果。
动画速度曲线 与 三次方贝塞尔曲线
无论是网页设计里的 CSS 还是 Android 开发,它们里面的动画速度曲线其实是三次方贝塞尔曲线,由 4 个点不断两两插值得到。
我们自定义自己的动画曲线(三次方贝塞尔曲线)时,里面包含 4 个点的信息,其中第一个和最后一个点的坐标是 <math xmlns="http://www.w3.org/1998/Math/MathML"> ( 0 , 0 ) (0, 0) </math>(0,0) 和 <math xmlns="http://www.w3.org/1998/Math/MathML"> ( 1 , 1 ) (1, 1) </math>(1,1),我们还需要提供中间两个点的坐标。老实说...当初看到自定义动画曲线要填入 4 个数字,真是百思不得其解......
另外,你可以用网址 cubic-bezier 快速定制自己的动画曲线。
Jetpack Compose------Easing
现实生活中极少存在线性匀速运动的场景,汽车启动、停下、自由落体运动等等都包含加速、减速,人的脑子里已经潜移默化地习惯了这种加速减速地运动。动画也是一种运动,设计动画的时候应该遵循现实世界的物理模型,让动画看起来更加自然,符合直觉。
缓动 Ease,表示缓慢地移动(缓动),在 CSS 过渡动画里面,我们可以选择动画的缓动(Easing)类型,其中一些关键字有:
linear
ease-in
ease-out
ease-in-out
在经典动画中,开始阶段缓慢,然后加速的动作称为 "slow in";开始阶段运动较快,然后减速的动作称为 "slow out"。网络上面分别叫 "ease in" 和 "ease out",这里的 in/out 可以理解成一个动画里的一开始(start)或者最后(end)
slow in (ease in)
比较适合出场动画,因为开始阶段比较慢,容易让人注意到哪个元素要开始移动,然后加速飞到视线之外。
好比你送朋友,看到朋友上了车,车子缓缓启动,然后加速驶去。
slow out (ease out)
比较适合进场动画,因为结束阶段比较缓慢,能让人清楚看到是哪个元素飞了进来。
就像你站在公交车站,看到一辆公交车远远飞速驶来,减速停下。
ease in out
那 ease in out 又是啥呢?ease 是缓和的意思,而 in/out 前面说过可以看作是一次动画里面的开始或结束阶段。ease in out 自然就代表:在一次动画里的开始阶段和结束阶段,动作都是缓和的,仅中间阶段是加速的,能够将用户注意力集中在过渡的末端。这也是 Material Design 的标准缓动,由于现实世界中的物体不会立即开始或停止移动,这种缓动类型可以让动画更有质感。
这种动画曲线比较适合转换动画,也就是说一个元素运动过程中,没有涉及入场与离场,它始终位于屏幕内,只是由一种形态变换为另一种形态。
Jetpack Compose 里面,表示动画速度曲线的接口是 Easing,Compose 提供了 4 中常见的速度曲线:
kotlin
/**
* Elements that begin and end at rest use this standard easing. They speed up quickly
* and slow down gradually, in order to emphasize the end of the transition.
*
* Standard easing puts subtle attention at the end of an animation, by giving more
* time to deceleration than acceleration. It is the most common form of easing.
*
* This is equivalent to the Android `FastOutSlowInInterpolator`
*/
val FastOutSlowInEasing: Easing = CubicBezierEasing(0.4f, 0.0f, 0.2f, 1.0f)
/**
* Incoming elements are animated using deceleration easing, which starts a transition
* at peak velocity (the fastest point of an element's movement) and ends at rest.
*
* This is equivalent to the Android `LinearOutSlowInInterpolator`
*/
val LinearOutSlowInEasing: Easing = CubicBezierEasing(0.0f, 0.0f, 0.2f, 1.0f)
/**
* Elements exiting a screen use acceleration easing, where they start at rest and
* end at peak velocity.
*
* This is equivalent to the Android `FastOutLinearInInterpolator`
*/
val FastOutLinearInEasing: Easing = CubicBezierEasing(0.4f, 0.0f, 1.0f, 1.0f)
/**
* It returns fraction unmodified. This is useful as a default value for
* cases where a [Easing] is required but no actual easing is desired.
*/
val LinearEasing: Easing = Easing { fraction -> fraction }
LinearEasing 是匀速运动,最好理解了,另外 3 个和前面提到的 CSS 里的 ease-in-out、ease-out、ease-in 其实都是对应的
- ease-in-out => FastOutSlowIn
- ease-out => LinearOutSlowIn
- ease-in => FastOutLinearIn
...真是想不明白 Compose 官方这个命名是从哪个角度理解的