贝塞尔曲线
贝塞尔原理
贝塞尔曲线(Bézier curve),又称贝兹曲线,是应用于二维图形应用程序的数学曲线,贝塞尔曲线的运用十分广泛,可以说贝塞尔曲线奠定了计算机绘图的基础(因为它可以将任何复杂的图形用精确的数学语言进行描述),接下来以二阶贝塞尔曲线为例介绍贝塞尔曲线的定义、性质。
二阶曲线由两个数据点(P0 和 P2),一个控制点(P1)来描述曲线状态,如图所示红色曲线就是二阶贝塞尔曲线
推导过程如下
- 在P0P1、P1P2上分别取点A、B满足
- P0A/P0P1 = P1B/P1P2 = t
- 在AB上取点C满足
- AC/AB = t
C为贝塞尔曲线上一个点,当t从0-1变化时,所有满足条件的点C构成完整的贝塞尔曲线,公式为
二阶贝塞尔曲线有两个特性:
- 以P0P2为底边,二阶贝塞尔曲线最高点的坐标为P1P3的中点,其中P3为P0P2的中点。
- 贝塞尔曲线与P0P1、P1P2分别相切于P0、P2点
三阶贝塞尔曲线如下如所示,构造过程和二阶类似

应用一:实现粘性效果
使用贝塞尔曲线可以实现很多复杂的动画效果,这里介绍一种较为简单的小球粘性下拉动画,效果如下
首先我们定义小球可以下拉的最大高度为MAX_HEIGHT,当小球随手势向下拉动的时候通过当前的高度/MAX_HEIGHT计算出当前的进度progress,后面就通过progress来确定绘制所需要的所有点
如图所示在圆上取点e1,e1与圆心连城的直线与垂线夹角为θ。过e1做圆的切线交基准线L于c1点,取基准线上s1点和e1为数据点,c1为控制点做二阶贝塞尔曲线(右侧类似)即可实现粘性效果。其中随着progress从0到1变化过程中
- θ从0到105度变化
- s1从最左侧到距离O点120距离移动
以下是添加辅助点后的效果
应用二:平滑拟合曲线
在一些绘图应用上会提供用户手动绘制的功能,绘制的曲线其实是一个一个点连接而成的,这样就会导致曲线可能并不光滑,出现很多折角,这种情况就可以用贝塞尔曲线进行拟合,拟合方法如下:
假设绘制路径上有三个连续的点a、b、c,则分别取ab、bc中点A、B,以AB为数据点,b为控制点绘制二阶贝塞尔曲线。
使用如上方法就可以实现平滑曲线绘制,部分代码如下
1 | /** |
效果如下
应用三:小说仿真翻页效果
目前市面上的小说阅读app基本都提供仿真翻页阅读的功能,本节将详细介绍实现的细节,demo效果如下
原理解析
本节着重讲解实现翻页效果的理论知识,下面这张图是整个翻页效果的精髓所在,接下来以从右下角开始翻页为例进行讲解。


如图2所示,黄色区域为当前页面的背面,蓝色区域为下一页的内容,绿色为当前页面,我们第一步要做的就是确定三块区域对应的Path,这样后面才可以在Canvas上绘制对应的内容。
如图3所示,三角形aeh与三角形feh关于eh对称,aeh也就是我们翻页的部分,但是如果就按照这样来绘制显得不够逼真,没有真实翻页的效果,所以我们需要给翻页部分做一个平滑过渡的效果。经过ag中点做直线cj牌型与eh,交与三角形aeh于b、k点,且c、j为边界上的点。以cb为二阶贝塞尔曲线数据点,e为控制点做贝塞尔曲线,由贝塞尔性质可知该曲线与ce、ab相切,可以达到平滑过渡的效果。同理以ja为二阶贝塞尔曲线数据点,h为控制点做贝塞尔曲线平滑多度ih、ak。分别取两条贝塞尔曲线的定点d、i连接,则d、b、a、k、i5个点连接的区域为图2黄色区域,也就是当前页背页的区域。有了黄色区域的范围,就可以很简单的求得蓝色区域和绿色区域的范围了。
代码实现
实现整体的效果非常复杂,本节重点讲解几个比较关键的点,首先将图3中的点命名
- a -> mTouch
- f -> mCorner
- b/c/d/e -> mBzEnd1/mBzStart1/mBzVertex1/mBzControl1
- h/i/j/k -> mBzControl2/mBzVertex2/mBzStart2/mBzEnd2
1.mTouch点的确定
由于在实际触摸中会出现控制点超出屏幕的情况,所以这时候不能将触摸点当作mTouch的坐标,而是应该进行转换。转换原理是将超出屏幕的控制点缩小到屏幕的临界点(刚好不超出屏幕),计算该缩放的比例,将触摸点按该比例向mCorner点缩小。
2.获得黄色区域Path
由于d->b / i->k的曲线为贝塞尔曲线的一部分,我们无法直接通过Path来构造,只能通别的方式来获得到。第一步我们先获取黄色和蓝色区域的并集Path1,这一块没有什么难度,其中quadTo是连接二阶贝塞尔曲线
1 | val path1 = Path().apply { |
第二步将dbaki通过线段连接得到Path2
1 | val path2 = Path().apply { |
由于Path2使用的是线段相连,所以比我们想要获得的黄色区域要多出一部分,而恰好Path2只有这部分不在Path1中,所以我们可以通过取Path1和Path2的并集求得黄色区域的范围
1 | val matrix = getSymmetricalMatrix(mCalcData.mBzControl1, mCalcData.mBzControl2) |
这里在绘制背页的时候需要注意两个点
- 绘制文字的时候需要做关于eh的对称变化,这样才有背页的效果,我们需要做的是构造一个矩阵来实现,若点A1(X1,Y1)关于直线y=kx+b成轴对称,则对应点A2(X2,Y2)的坐标为


那么就可以求出对应的矩阵
1 | /** |
- 由于背页使用了贝塞尔曲线进行平滑扩展,所以绘制的区域是比实际区域要大,在绘制背页的时候不能带背景图片,只能用纯色填充,不然会出现空白区域