Android 实现贝塞尔曲线

转自:http://www.2cto.com/kf/201604/497130.html

Android 自定义View高级特效,神奇的贝塞尔曲线


效果图

效果图

效果图中我们实现了一个简单的随手指滑动的二阶贝塞尔曲线,还有一个复杂点的,穿越所有已知点的贝塞尔曲线。学会使用贝塞尔曲线后可以实现例如QQ红点滑动删除啦,360动态球啦,bulabulabula~

什么是贝塞尔曲线?

贝赛尔曲线(Bézier曲线)是电脑图形学中相当重要的参数曲线。更高维度的广泛化贝塞尔曲线就称作贝塞尔曲面,其中贝塞尔三角是一种特殊的实例。贝塞尔曲线于1962年,由法国工程师皮埃尔·贝塞尔(Pierre Bézier)所广泛发表,他运用贝塞尔曲线来为汽车的主体进行设计。贝塞尔曲线最初由Paul de Casteljau于1959年运用de Casteljau算法开发,以稳定数值的方法求出贝塞尔曲线。

读完上述贝塞尔曲线简介我还是一头雾水,来个示例呗。

示例

线性贝塞尔曲线

给定点P0、P1,线性贝塞尔曲线只是一条两点之间的直线。这条线由下式给出:
1
1

二次方贝塞尔曲线

二次方贝塞尔曲线的路径由给定点P0、P1、P2的函数B(t)追踪:
2
22

三次方贝塞尔曲线

P0、P1、P2、P3四个点在平面或在三维空间中定义了三次方贝塞尔曲线。曲线起始于P0走向P1,并从P2的方向来到P3。一般不会经过P1或P2;公式如下:
3
33

N次方贝塞尔曲线

身为三维生物超出三维我很方,这里只给示例图。想具体了解的同学请左转度娘。
44

就当没看过上面

Android在API=1的时候就提供了贝塞尔曲线的画法,只是隐藏在Path#quadTo()和Path#cubicTo()方法中,一个是二阶贝塞尔曲线,一个是三阶贝塞尔曲线。当然,如果你想自己写个方法,依照上面贝塞尔的表达式也是可以的。不过一般没有必要,因为Android已经在native层为我们封装好了二阶和三阶的函数。

从一个二阶贝塞尔开始

自定义一个BezierView

初始化各个参数,花3s扫一下即可。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<code class = "hljs java" >    private Paint mPaint;
     private Path mPath;
     private Point startPoint;
     private Point endPoint;
     // 辅助点
     private Point assistPoint;
         public BezierView(Context context) {
         this (context, null );
     }
 
     public BezierView(Context context, AttributeSet attrs) {
         this (context, attrs, 0 );
     }
 
     public BezierView(Context context, AttributeSet attrs, int defStyleAttr) {
         super (context, attrs, defStyleAttr);
         init(context);
     }
 
     private void init(Context context) {
         mPaint = new Paint();
         mPath = new Path();
         startPoint = new Point( 300 , 600 );
         endPoint = new Point( 900 , 600 );
         assistPoint = new Point( 600 , 900 );
         // 抗锯齿
         mPaint.setAntiAlias( true );
         // 防抖动
         mPaint.setDither( true );
     }</code>

在onDraw中画二阶贝塞尔

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<code class = "hljs avrasm" >        // 画笔颜色
         mPaint.setColor(Color.BLACK);
         // 笔宽
         mPaint.setStrokeWidth(POINTWIDTH);
         // 空心
         mPaint.setStyle(Paint.Style.STROKE);
         // 重置路径
         mPath.reset();
         // 起点
         mPath.moveTo(startPoint.x, startPoint.y);
         // 重要的就是这句
         mPath.quadTo(assistPoint.x, assistPoint.y, endPoint.x, endPoint.y);
         // 画路径
         canvas.drawPath(mPath, mPaint);
         // 画辅助点
         canvas.drawPoint(assistPoint.x, assistPoint.y, mPaint);</code>

上面注释很清晰就不赘述了。示例中贝塞尔是可以跟着手指的滑动而变化,我一拍榴莲,肯定是复写了onTouchEvent()!

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<code class = "hljs cs" >    @Override
     public boolean onTouchEvent(MotionEvent event) {
         switch (event.getAction()) {
             case MotionEvent.ACTION_DOWN:
             case MotionEvent.ACTION_MOVE:
                 assistPoint.x = ( int ) event.getX();
                 assistPoint.y = ( int ) event.getY();
                 Log.i(TAG, "assistPoint.x = " + assistPoint.x);
                 Log.i(TAG, "assistPoint.Y = " + assistPoint.y);
                 invalidate();
                 break ;
         }
         return true ;
     }</code>

最后将我们自定义的BezierView添加到布局文件中。至此一个简单的二阶贝塞尔曲线就完成了。假设一下,在向下拉动的过程中,在曲线上增加一个“小超人”,360动态清理是不是就出来了呢?有兴趣的可以自己拓展下。

以一个三阶贝塞尔结束

天气预报曲线图示例

(图一)
DEMO1


(图二)
demo2

概述

要想得到上图的效果,需要二阶贝塞尔和三阶贝塞尔配合。具体表现为,第一段和最后一段曲线为二阶贝塞尔,中间N段都为三阶贝塞尔曲线。

思路

先根据相邻点(P1,P2, P3)计算出相邻点的中点(P4, P5),然后再计算相邻中点的中点(P6)。然后将(P4,P6, P5)组成的线段平移到经过P2的直线(P8,P2,P7)上。接着根据(P4,P6,P5,P2)的坐标计算出(P7,P8)的坐标。最后根据P7,P8等控制点画出三阶贝塞尔曲线。

点和线的解释

黑色点:要经过的点,例如温度 蓝色点:两个黑色点构成线段的中点 黄色点:两个蓝色点构成线段的中点 灰色点:贝塞尔曲线的控制点 红色线:黑色点的折线图 黑色线:黑色点的贝塞尔曲线,也是我们最终想要的效果

声明

为了方便讲解以及读者的理解。本篇以图一效果为例进行讲解。BezierView坐标都是根据屏幕动态生成的,想要图二的效果只需修改初始坐标,不用对代码做很大的修改即可实现。

那么,开始吧!

初始化参数

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<code class = "hljs java" >    private static final String TAG = "BIZIER" ;
     private static final int LINEWIDTH = 5 ;
     private static final int POINTWIDTH = 10 ;
 
     private Context mContext;
     /** 即将要穿越的点集合 */
     private List<point> mPoints = new ArrayList<>();
     /** 中点集合 */
     private List<point> mMidPoints = new ArrayList<>();
     /** 中点的中点集合 */
     private List<point> mMidMidPoints = new ArrayList<>();
     /** 移动后的点集合(控制点) */
     private List<point> mControlPoints = new ArrayList<>();
 
     private int mScreenWidth;
     private int mScreenHeight;
     private void init(Context context) {
         mPaint = new Paint();
         mPath = new Path();
         // 抗锯齿
         mPaint.setAntiAlias( true );
         // 防抖动
         mPaint.setDither( true );
 
         mContext = context;
         getScreenParams();
         initPoints();
         initMidPoints( this .mPoints);
         initMidMidPoints( this .mMidPoints);
         initControlPoints( this .mPoints, this .mMidPoints , this .mMidMidPoints);
 
     }</point></point></point></point></code>

第一个函数获取屏幕宽高就不说了。紧接着初始化了初始点、中点、中点的中点、控制点。我们一个个的跟进。首先是初始点。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<code class = "hljs java" >    /** 添加即将要穿越的点 */
     private void initPoints() {
         int pointWidthSpace = mScreenWidth / 5 ;
         int pointHeightSpace = 100 ;
         for ( int i = 0 ; i < 5 ; i++) {
             Point point;
             // 一高一低五个点
             if (i% 2 != 0 ) {
                 point = new Point(( int ) (pointWidthSpace*(i + 0.5 )), mScreenHeight/ 2 - pointHeightSpace);
             } else {
                 point = new Point(( int ) (pointWidthSpace*(i + 0.5 )), mScreenHeight/ 2 );
             }
             mPoints.add(point);
         }
     }</code>

这里循环创建了一高一低五个点,并添加到List mPoints中。上文说道图一到图二只需修改这里的初始点即可。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<code class = "hljs java" >    /** 初始化中点集合 */
     private void initMidPoints(List<point> points) {
         for ( int i = 0 ; i < points.size(); i++) {
             Point midPoint = null ;
             if (i == points.size()- 1 ){
                 return ;
             } else {
                 midPoint = new Point((points.get(i).x + points.get(i + 1 ).x)/ 2 , (points.get(i).y + points.get(i + 1 ).y)/ 2 );
             }
             mMidPoints.add(midPoint);
         }
     }
 
     /** 初始化中点的中点集合 */
     private void initMidMidPoints(List<point> midPoints){
         for ( int i = 0 ; i < midPoints.size(); i++) {
             Point midMidPoint = null ;
             if (i == midPoints.size()- 1 ){
                 return ;
             } else {
                 midMidPoint = new Point((midPoints.get(i).x + midPoints.get(i + 1 ).x)/ 2 , (midPoints.get(i).y + midPoints.get(i + 1 ).y)/ 2 );
             }
             mMidMidPoints.add(midMidPoint);
         }
     }</point></point></code>

这里算出中点集合以及中点的中点集合,小学数学题没什么好说的。唯一需要注意的是他们数量的差别。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<code class = "hljs avrasm" >    /** 初始化控制点集合 */
     private void initControlPoints(List<point> points, List<point> midPoints, List<point> midMidPoints){
         for ( int i = 0 ; i < points.size(); i ++){
             if (i == 0 || i == points.size()- 1 ){
                 continue ;
             } else {
                 Point before = new Point();
                 Point after = new Point();
                 before.x = points.get(i).x - midMidPoints.get(i - 1 ).x + midPoints.get(i - 1 ).x;
                 before.y = points.get(i).y - midMidPoints.get(i - 1 ).y + midPoints.get(i - 1 ).y;
                 after.x = points.get(i).x - midMidPoints.get(i - 1 ).x + midPoints.get(i).x;
                 after.y = points.get(i).y - midMidPoints.get(i - 1 ).y + midPoints.get(i).y;
                 mControlPoints.add(before);
                 mControlPoints.add(after);
             }
         }
     }</point></point></point></code>

大家需要注意下这个方法的计算过程。以图一(P2,P4, P6,P8)为例。现在P2、P4、P6的坐标是已知的。根据由于(P8, P2)线段由(P4, P6)线段平移而来,所以可得如下结论:P2 - P6 = P8 - P4 。即P8 = P2 - P6 + P4。其余同理。

画辅助点以及对比折线图

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<code class = "hljs mel" >    @Override
     protected void onDraw(Canvas canvas) {
         super .onDraw(canvas);
         // ***********************************************************
         // ************* 贝塞尔进阶--曲滑穿越已知点 **********************
         // ***********************************************************
 
         // 画原始点
         drawPoints(canvas);
         // 画穿越原始点的折线
         drawCrossPointsBrokenLine(canvas);
         // 画中间点
         drawMidPoints(canvas);
         // 画中间点的中间点
         drawMidMidPoints(canvas);
         // 画控制点
         drawControlPoints(canvas);
         // 画贝塞尔曲线
         drawBezier(canvas);
 
     }</code>

可以看到,在画贝塞尔曲线之前我们画了一系列的辅助点,还有和贝塞尔曲线作对比的折线图。效果如图一。辅助点的坐标全都得到了,基本的画画就比较简单了。有能力的可跳过下面这段,直接进入drawBezier(canvas)方法。基本的画画这里只贴代码,如有疑问可评论或者私信。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<code class = "hljs java" >    /** 画原始点 */
     private void drawPoints(Canvas canvas) {
         mPaint.setStrokeWidth(POINTWIDTH);
         for ( int i = 0 ; i < mPoints.size(); i++) {
             canvas.drawPoint(mPoints.get(i).x, mPoints.get(i).y, mPaint);
         }
     }
 
     /** 画穿越原始点的折线 */
     private void drawCrossPointsBrokenLine(Canvas canvas) {
         mPaint.setStrokeWidth(LINEWIDTH);
         mPaint.setColor(Color.RED);
         // 重置路径
         mPath.reset();
         // 画穿越原始点的折线
         mPath.moveTo(mPoints.get( 0 ).x, mPoints.get( 0 ).y);
         for ( int i = 0 ; i < mPoints.size(); i++) {
             mPath.lineTo(mPoints.get(i).x, mPoints.get(i).y);
         }
         canvas.drawPath(mPath, mPaint);
     }
 
     /** 画中间点 */
     private void drawMidPoints(Canvas canvas) {
         mPaint.setStrokeWidth(POINTWIDTH);
         mPaint.setColor(Color.BLUE);
         for ( int i = 0 ; i < mMidPoints.size(); i++) {
             canvas.drawPoint(mMidPoints.get(i).x, mMidPoints.get(i).y, mPaint);
         }
     }
 
     /** 画中间点的中间点 */
     private void drawMidMidPoints(Canvas canvas) {
         mPaint.setColor(Color.YELLOW);
         for ( int i = 0 ; i < mMidMidPoints.size(); i++) {
             canvas.drawPoint(mMidMidPoints.get(i).x, mMidMidPoints.get(i).y, mPaint);
         }
 
     }
 
     /** 画控制点 */
     private void drawControlPoints(Canvas canvas) {
         mPaint.setColor(Color.GRAY);
         // 画控制点
         for ( int i = 0 ; i < mControlPoints.size(); i++) {
             canvas.drawPoint(mControlPoints.get(i).x, mControlPoints.get(i).y, mPaint);
         }
     }
</code>

画贝塞尔曲线

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<code class = "hljs avrasm" >    /** 画贝塞尔曲线 */
     private void drawBezier(Canvas canvas) {
         mPaint.setStrokeWidth(LINEWIDTH);
         mPaint.setColor(Color.BLACK);
         // 重置路径
         mPath.reset();
         for ( int i = 0 ; i < mPoints.size(); i++){
             if (i == 0 ){ // 第一条为二阶贝塞尔
                 mPath.moveTo(mPoints.get(i).x, mPoints.get(i).y); // 起点
                 mPath.quadTo(mControlPoints.get(i).x, mControlPoints.get(i).y, // 控制点
                         mPoints.get(i + 1 ).x,mPoints.get(i + 1 ).y);
             } else if (i < mPoints.size() - 2 ){ // 三阶贝塞尔
                 mPath.cubicTo(mControlPoints.get( 2 *i- 1 ).x,mControlPoints.get( 2 *i- 1 ).y, // 控制点
                         mControlPoints.get( 2 *i).x,mControlPoints.get( 2 *i).y, // 控制点
                         mPoints.get(i+ 1 ).x,mPoints.get(i+ 1 ).y); // 终点
             } else if (i == mPoints.size() - 2 ){ // 最后一条为二阶贝塞尔
                 mPath.moveTo(mPoints.get(i).x, mPoints.get(i).y); // 起点
                 mPath.quadTo(mControlPoints.get(mControlPoints.size()- 1 ).x,mControlPoints.get(mControlPoints.size()- 1 ).y,
                         mPoints.get(i+ 1 ).x,mPoints.get(i+ 1 ).y); // 终点
             }
         }
         canvas.drawPath(mPath,mPaint);
     }
</code>

注释太详细,都没什么好写的了。不过这里需要注意判断里面的条件,对起点和终点的判断一定要理解。要不然很可能会送你一个ArrayIndexOutOfBoundsException。

结束

贝塞尔曲线可以实现很多绚丽的效果,难的不是贝塞尔,而是good idea。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值