Android 动画里的贝塞尔曲线

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 官方这个命名是从哪个角度理解的

相关推荐
火柴就是我8 小时前
让我们实现一个更好看的内部阴影按钮
android·flutter
砖厂小工15 小时前
用 GLM + OpenClaw 打造你的 AI PR Review Agent — 让龙虾帮你审代码
android·github
张拭心15 小时前
春节后,有些公司明确要求 AI 经验了
android·前端·人工智能
张拭心16 小时前
Android 17 来了!新特性介绍与适配建议
android·前端
Kapaseker18 小时前
Compose 进阶—巧用 GraphicsLayer
android·kotlin
黄林晴18 小时前
Android17 为什么重写 MessageQueue
android
南山安1 天前
手写 Cursor 核心原理:从 Node.js 进程到智能 Agent
人工智能·agent·设计
JavaTalks2 天前
高并发保护实战:限流、熔断、降级如何配合落地
后端·架构·设计
阿巴斯甜2 天前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker2 天前
实战 Compose 中的 IntrinsicSize
android·kotlin