大家好,我是深红骑士,爱开玩笑,技术一渣渣,热爱钻研,这篇文章是今年的最后一篇了,首先祝大家在新的一年里心想事成,诸事顺利。今天来学习贝塞尔曲线,之前一直想学,可惜没时间。什么是贝塞尔曲线呢?一开始我也是不懂的,当查了很多资料,现在还是不够了解,其推导公式还是不能深入了解。对发布这曲线的法国工程师由衷敬佩,贝塞尔曲线,又称贝兹曲线或者贝济埃曲线,是应用于应用程序的数学曲线.1962年,运用贝塞尔曲线为汽车的主体进行设计,贝塞尔曲线最初由Paul de Casteljau于1959年运用算法开发,以稳定的述职方法求出贝塞尔曲线。其实贝塞尔曲线就在我们日常生活中,如一些成熟的位图软件中:PhotoSHop,Flash5等。在前端开发中,贝塞尔曲线也是无处不在:前端2D或者3D图形图标库都会使用贝塞尔曲线;它可以用来绘制曲线,在svg和canvas中,原生提供的曲线绘制都是用贝塞尔曲线实现的;在css的属性,可以使用贝塞尔曲线来描述过渡的缓动计算。
上面这个动画是不是很炫,它就是用贝塞尔曲线来实现的。
贝塞尔曲线是用一系列点来控制曲线状态的,我将这一系列点分为三个点:,,。通过改变这些点,贝塞尔曲线就会发生变化。
- 起点:确定曲线的起点
- 终点:确定曲线的终点
- 控制点:确定曲线的控制点
一阶曲线原理
一阶曲线就是一条直线,只有两个点,就是和,也就是最终效果就是一条线段。还是直接上图比较直观:
一阶公式如下:
那么上面的公式是怎么来的呢?为了方便,我就在纸上写了,字有点丑,见谅了:
二阶曲线原理
二阶曲线由两个数据点(起始点和终点),一个控制点来描述曲线状态,如下图,下面A点是起始点,C是终点,B是控制点。
红线AC是怎么生成的呢?继续上图:
简单来看连接AB,BC两条线段,在AB,BC分别取D,E两点,连接DE。如下图:
D在AB线段上从A往B做一阶曲线运动,E在BC线段上从B往C做一阶曲线运动,而F在DE上做一阶曲线运动,那么F这点就是贝塞尔曲线上的一个点,动态图如下:
再简单理解就是:二阶贝塞尔曲线就是 和
不断变化的一阶贝塞尔曲线。二阶公式如下:
那么上面这个公司怎么推导出来的呢?同样为了方便,我就在纸上写了:
三阶曲线原理
三阶曲线其实就是由两个数据点(起始点和终点),两个控制点来描述曲线的状态,如下图,下面A是起始点,D是终点,B和C是控制点。
动态图如下:
可以这么理解,两个数据点和控制点不断变化的二阶贝塞尔曲线,即拆分为p0p1p2和p1p2p3两个二阶贝塞尔曲线。三阶公式如下:
那么上面这个公式是怎么推导出来的呢?直接上图:
四阶,五阶的效果图和推导公式就不上了,原理是一样的。通过上面一阶,二阶,三阶的推导可以发现这样一个规律:没N阶贝塞尔曲线都可以拆分为两个N-1阶,和高数中二项式展开一样,就是阶数越高,控制点之间就会越近,绘制的曲线就会更加丝滑。通用公式如下:
把贝塞尔曲线原理弄懂了,下面就可以用来做实际性的东西了。
在Android中,Path类中有四个方法与贝塞尔曲线相关的,也就是已经封装了关于贝塞尔曲线的函数,开发者直接调用即可:
上面的四个函数中,quadTo、rQuadTo是二阶贝塞尔曲线,cubicTo、rCubicTo是三阶贝塞尔曲线。因为三阶贝塞尔曲线使用方法和二阶贝塞尔曲线相似,用处也很少,就不细说了。下面就针对二阶的贝塞尔曲线quadTo、rQuadTo为详细说明。
quadTo原理
先看看quadTo函数的定义:
看上面的注释可以知道:(x1,y1)是控制点,(x2,y2)是终点坐标,怎么没有起点的坐标呢?作为Android开发者都知道,一条线段的起始点都是通过Path.move(x,y)来指定的。如果连续调用quadTo函数,那么前一个quadTo的终点就是下一个quadTo函数的起始点,如果初始化没有调用Path.moveTo(x,y)来指定起始点,那么控件视图就会以左上角(0,0)为起始点,还是直接上例子描述。 下面实现绘制下面以下效果图:
下面先通过PhotoShop来模拟画出上面这条轨迹的辅助控制点的位置:
下面通过草图分析确定起始点,终点,控制点的位置, 注意,下面的分析图位置不是很准备,只是为了确定控制点的位置。
先看p0-p1这条路径,是以p0为起始点,p2为终点,p1为控制点。起始的坐标设置为(200,400),终点的坐标设置(400,400),控制点是在p0,p1的上方,因此纵坐标y的值比两点都要小,横坐标的位置是在p0和p2的中间。那么p1坐标设定为(300,300);同理,在p2-p4的这条二阶贝塞尔曲线上,控制点p3的坐标位置应该是(500,500),因为p0-p2,p2-p4这两条贝塞尔曲线是对称的。
示例代码
布局文件如下:
效果图如下图所示:
下面把 注释,再看看效果:
通过上面的简单例子,可以得出以下两点:
- 当连续调用quadTo函数时,前一个quadTo函数的终点就是调用下一个quadTo函数的起始点。
- 贝塞尔曲线的起点是通过Path.moveTo(x,y)来指定的,如果一开始没有调用Path.move(x,y),则会取控件的左上角(0,0)作为起点。
Path.lineTo和Path.quadTo的区别
下面来看看Path.lineTo和Path.quadTo的区别,Path.lineTo是连接直线,是连接上一个点到当前点的之间的直线,下面来实现绘制手指在屏幕上所走的路径,也不难就在上面的基础上增加方法即可,代码如下:
直接上效果图:
当用户点击屏幕时,首先触发的是 这个条件,然后调用
,当用户移动手指时,就用
将各个点连接起来,然后调用
重新绘制。这里简单说一下在
为什么要返回
。
表示当前的控件已经消费了按下事件,剩下的
和
都会被执行;如果在
下返回
,后续的
,
都不会被接收到,因为没有消费
,系统就会认为
没有发生过,所以
和
就不能捕获,下面把图放大仔细看:
把C放大后,很明显看出,这个C字不是很平滑,像是很多一折一折的线段构成,出现这样的原因也很简单分析出来,因为这个C字是由各个不同点之间连线构成的,之间就没有平滑过渡,如果横纵坐标变化剧烈时,更加突出有折痕效果。如果要解决这问题,这时候二阶曲线的作用体现出来了。
上图中,有三个黑点连成两条直线,从两个线段可以看出,如果使用Path.lineTo的时候,是直接把触摸点p0,p1,p2连接起来,那么现在要实现这个三个点之间的流畅过渡,我想到的就是把这 两条线的中点分别作为起点和终点,把连接这两条线段的点(p1)作为
控制点,那这样就能解决上面折痕的问题,直接上效果图:
但是上面会有这样一个问题:当你绘制二阶曲线的时候,结束的时候,最开始那段线段的前半部分也就是p0-p3,最后那段线段的后半部分也就是p4-p2不会绘制出来。其实这两端距离可以忽略不计,因为手指滑动的时候,所产生的点与点之间的距离是很小的,因此p0-p3,p4-p2的距离可以忽略不算了。每个图形中,肯定有很多点共同连接线段,而现在就是将两个线段的中间做为二阶曲线的起点和终点,把线段与线段之间的转折点做为控制点,这样来组成平滑的连线。理论上应该可以,那就下面之间敲代码:
这里简单说明一下,在的时候,先调用设置曲线的初始位置就是手指触屏的位置,上面也解释了如果不调用的话,那么绘制点就会从控件的(0,0)开始。用和记录手指移动的前一个横纵坐标,而这个点是做控制点,最后返回为了让和向本控件传递。下面说说在方法的逻辑处理,首先是确定结束点,上面也说了结束点是线段的中间位置,所以用了两条公式来和求这个中间位置的横纵坐标,而控制点就是上个手指触摸屏幕的位置,后面就是更新前一个手指坐标。这里注意一下,上面也说了当连续调用的时候,第一个起始点是来设置的,其他部分,前面调用的终点是下一个的起点,这里所说的起始点就是上一个线段的中间点。上面的逻辑用一句话表示:把各个线段的中间点作为起始点和终点,把前一个手指位置作为控制点,最终效果如下:
可以看到通过 实现的曲线会更顺滑。
Path.rQuadTo原理
直接看这个函数的说明:
- x1:控制点的X坐标,表示相对于上一个终点X坐标的位移值,可以为负值,正值表示相加,负值表示相减
- x2:控制点的Y坐标,表示相对于上一个终点Y坐标的位移值,可以为负值,正值表示相加,负值表示相减
- x2:终点的X坐标,表示相对于上一个终点X坐标的位移值,可以为负值,正值表示相加,负值表示相减
- y2:终点的Y坐标,表示相对一上一个终点Y坐标的位移值,可以为负值,正值表示相加,负值表示相减 这么说可能不理解,下面还是直接举例子: 如果上一个终点坐标是(100,200),如果这时候调用rQuardTo(100,-100,200,200),得到的控制点坐标是(100 + 100,200 - 100 )就是(200,100),得到的终点坐标是(100 + 200,200 + 200)就是(300,400),下面两段是相等的:
在上面中,用实现了一个波浪线,下图:
下面是上面用 实现的代码:
下面就用来实现这个波浪线,先上分析图:
代码如下:
第一行:path.rQuadTo(100,-100,200,0);这个一行代码是基于(200,400)这个点来计算曲线p0-p2的控制点和终点坐标。
- 控制点X坐标 = 上一个终点的X坐标 + 控制点X位移值 = 200 + 100 = 300;
- 控制点Y坐标 = 上一个终点的Y坐标 + 控制点Y位移值 = 400 - 100 = 300;
- 终点X坐标 = 上一个终点的X坐标 + 终点X的位移值 = 200 + 200 = 400;
- 终点Y坐标 = 上一个终点的Y坐标 + 终点Y的位移值 = 400 + 0 = 400; 这句和path.quadTo(300,300,400,400)是等价的。
那么第一条曲线就容易绘制出来了,并且第一条曲线的终点也知道了是(400,400),那么第二句path.rQuadTo(100,100,200,0)是基于这个终点(400,400)来计算第二条曲线的控制点和终点。
- 控制点X坐标 = 上一个终点的X坐标 + 控制点X位移值 = 400 + 100 = 500;
- 控制点Y坐标 = 上一个终点的Y坐标 + 控制点Y位移值 = 400 + 100 = 500
- 终点X坐标 = 上一个终点的X坐标 + 终点X的位移值 = 400 + 200 = 600;
- 终点Y坐标 = 上一个终点的Y坐标 + 终点Y的位移值 = 400 + 0 = 400;
其实这句是和相等的,实际运行的效果图也和用方法绘制的一样,通过这个例子,可以知道这个方法的参数都是实际结果的坐标,而这个方法的参数是以上一个终点位置为基准来做位移的。
实现封闭波浪
下面要实现以下效果:
实现静态封闭波浪
对应代码如下:
下面一行一行分析:
首先将起始位置向左移一个波长,为了就是后面实现的位移动画,然后利用循环来画出屏幕所容下的所有波浪:
这里我简单说一下,循环里的第一行画的是一个波长的前半部分,下面把数值放进去就很容易理解了,因为是400,所以就是200,而就是,而就是,上面说过的用法了,就不再叙述,下面直接上分析图,下面只是分析最左边的第一个波浪起始点,控制点的坐标,其余波浪只是通过循环绘制,就不分析了:
因为需要连续调用两次 方法才能绘制出一个完整的波浪,所以上面分析需要确定五个点的位置。这里注意,上面图左右有一条线段连接底部,形成封闭图形,因为要填充内部,所以要封闭绘制
。当波浪绘制完成时,
点会在A点,然后用
连接A,B点,再调用
连接B,C点,最后调用
连接初始点就是连接C和起始点,这样满横屏的波浪就绘制完成了。
实现位移动画的波浪
下面实现左右上下位移动画,这就会有一点点进度条的感觉,我的做法很简单,因为一开始在View的左边多画了一个波浪,也就是说,将起始点向右边移动,并且要移动一个波浪的长度就可以让波纹重合,然后不断循环即可,简单来讲就是,动画移动的距离是一个波浪的长度,当移动到最大的距离时设置不断循环,就会重新绘制波浪的初始状态。
动画的位移距离是一个波浪的长度,并将位移的距离保存到中,然后开始的时候,在加上这个距离,就可以了,完整代码如下:
效果如下:
上面只是添加横向移动,下面添加垂直的移动,我这边为了方便,垂直移动距离跟横向距离一样,很简单,把初始纵坐标同样减去移动距离,因为是向上移动,所以是要减 ,最后调用以下代码:
效果如上上上图。经过上面,自己对贝塞尔曲线由初步的了解,下面就实现波浪形进度条。
学到了上面的基本知识,那下面就实现一个小例子,就是圆形波浪进度条,最终效果在文章最底部,惯例下面就一步一步来实现。
绘制一段波浪
先绘制一段满屏的波浪线,绘制原理就不详细讲了,直接上代码:
xml布局文件:
实际效果如下:
下面绘制封闭波浪,效果分析图如下:
绘制封闭静态波浪
因为圆形进度框中的波浪是随着进度的增加而不断上升的,所以波浪是填充物,先绘制波浪,然后用和来连接封闭起来,构成一个填充图形,分析如下图:
绘制顺序是p0-p1-p2-p3,代码如下:
测量自适应View的宽高
在上面中,发现一个问题,就是宽和高都在初始化方法中定死了,一般来讲视图View的宽高都是在文件中定义或者类文件中定义的,那么就要重写View的方法:
上面就很简单了,就是增加了一个View的实际宽高变量,让代码扩展性更强和精确到更高。
绘制波浪上升
下面实现波浪高度随着进度变化而变化,当进度增加时,波浪高度增高,当进度减少时,波浪高度减少,其实很简单,也就是p0-p3,p1-p2的高度根据进度变化而变化,并增加动画,代码增加如下:
最后在Activity调用一些代码:
最终效果如下图:
绘制波浪左右平移
上面实现了波浪直线上升的动画,下面实现波浪平移的动画,添加左移的效果,这里想到前面也实现了平移的效果,但是下面实现方式和上面有点出入,简单来讲就是移动p0坐标,但是如果移动p0坐标会出现波浪不铺满整个View的情况,这里运用到一种很常见的循环处理办法。在飞机大战的背景滚动图,是两张背景图拼接起来,当飞机从第一个背景图片最底端出发,向上移动了第一个背景图片高度的距离时,将角色重新放回到第一个背景图片的最底端,这样就能实现背景图片循环的效果。也就是一开始绘制两端p0-p1,然后随着进度变化,p0会左移,一开始不在View中的波浪会从右边往左边移动出现,当滑动最大距离时,又重新绘制最开始状态,这样就达到循环了。还是先上分析图:
View的初始状态是蓝色区域,然后经过动画位移慢慢变成红色区域,代码实现如下:
最后的效果如下图:
绘制圆形外框背景
这里要用到的知识,其实也不难,先上各种模式的效果图:
这张图看起来很正常,但这张图其实给 开发者造成很大的误区,看下面这篇博文并且自己动手实践一下
android PorterDuffXferMode真正的效果测试集合(对比官方demo)。 下面用到了
,因为先绘制圆形背景,再绘制波浪线,而
模式在两者相交的地方绘制源图像,并且绘制的效果会受到目标图像对应地方透明度的影响,看上图就知道了,代码如下:
实际效果如下图:
简单的进度条终于出来了~下面继续完善,到这里你可能发现,颜色,大小什么的都在类中定死了,但实际中有很多属性都要在布局文件中设置的,同一个View在不同场景下状态可能是不一样的,所以为了提高扩展性,在 文件下添加
文件,给
添加自定义属性,如下:
在自定义View为属性值赋值:
下面就可以在布局文件自定义设置波浪颜色,高度,宽度以及圆形背景颜色:
效果图就不贴出来了。
绘制文字进度效果
下面要实现文字显示进度,进度条肯定缺不了具体数值的显示,最简单就是直接在中实现绘制文字的操作,这种是很简单的,我之前实现自定义View都是将逻辑放在里面,这样就显得View很臃肿和扩展性不高,因为你想,假如我现在要改变字体位置和样式,那就需要在这个View去改去大动干戈。假如这个View能开放出处理文字接口的话,也就是后面修改文字样式只通过这个接口就可以了,这样就实现了文字和进度条这个View的解耦。
然后在文件实现接口,进行逻辑处理:
布局文件增加一个:
最终效果如下图:
绘制双波浪效果
要实现第二层波浪平移方向和第一层波浪平移方向相反,要改一下绘制顺序,。下图:
要从p1开始绘制,因为第二层是要右移,所以右边要绘制多一次波浪,像绘制第一层波浪一样,只不过这次是左边,代码如下:
attrs文件增加第二层波浪的颜色:
类文件:
在方法增加:
在方法增加:
最后在Activty文件设置:
最终效果如下图:
经过贝塞尔公式的推导和小例子实现,有了更深刻的印象。有很多东西看起来并不是那么触达,就好像当自己拿到一个开发需求时,技术评估发现会用到自己没有之前没有用到的技术,这时候就要多去参照别人实现的思路和方法,或者厚着脸皮问技术牛的人,学到了就是自己的,多付出就努力变得容易。