学习贝塞尔曲线

画一个简单的心。

效果图如下:

心图

四个控制点的贝塞尔曲线。定义四个点

1
2
3
4
5
6
7
int gridSpace=80;
float pointRadius=20;
//四个自定义点 假设位置
BPoint p0=new BPoint(7*gridSpace,6*gridSpace);
BPoint p1=new BPoint(11*gridSpace,5*gridSpace);
BPoint p2=new BPoint(11*gridSpace,11*gridSpace);
BPoint p3=new BPoint(7*gridSpace,12*gridSpace);

BPoint

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.widget.bezier;

/**
* Created by yeqinfu on 2017/11/23.
* 贝塞尔坐标点
*/

public class BPoint {
public float x;
public
float y;

public BPoint() {
}

public BPoint(float x, float y) {
this.x = x;
this.y = y;
}
}

主要绘画代码

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
@Override
protected void onDraw(Canvas canvas) {
//背景网格
drawBackground(canvas);
//画四个控制点
drawControllerPoint(canvas);
//三阶贝塞尔曲线
Path path2=new Path();
BPoint start2=Utils_Berzier.calculateBerzierThree(p0,p1,p2,p3,0);
path2.moveTo(start2.x,start2.y);
for (float i=0.01f;i<1f;i+=0.01f){
BPoint point=Utils_Berzier.calculateBerzierThree(p0,p1,p2,p3,i);
path2.lineTo(point.x,point.y);
}
// canvas.drawPath(path2,getPathPaint());

/**计算p1 p2镜像点位置*/
BPoint p4=calculataImagePoint(p0,p3,p1);
BPoint p5=calculataImagePoint(p0,p3,p2);
//镜像三阶贝塞尔曲线
Path path3=new Path();
BPoint start3=Utils_Berzier.calculateBerzierThree(p0,p4,p5,p3,0);
path3.moveTo(start3.x,start3.y);
for (float i=0.01f;i<1f;i+=0.01f){
BPoint point=Utils_Berzier.calculateBerzierThree(p0,p4,p5,p3,i);
path3.lineTo(point.x,point.y);
}
path3.addPath(path2);
canvas.drawPath(path3,getPathPaint());
}

网格背景

1
2
3
4
5
6
7
8
9
private void drawBackground(Canvas canvas) {
for (int i=0;i<getHeight();i+=gridSpace){
canvas.drawLine(0,i,getWidth(),i,getBackgroundPaint());
}
for (int i=0;i<getWidth();i+=gridSpace){
canvas.drawLine(i,0,i,getHeight(),getBackgroundPaint());
}

}

画四个控制点,并连线

1
2
3
4
5
6
7
8
9
private void drawControllerPoint(Canvas canvas) {
drawPoint(p0,canvas);
drawPoint(p1,canvas);
drawPoint(p2,canvas);
drawPoint(p3,canvas);
linePoint(p0,p1,canvas);
linePoint(p1,p2,canvas);
linePoint(p2,p3,canvas);
}

画点代码

1
2
3
private void drawPoint(BPoint p0, Canvas canvas) {
canvas.drawCircle( p0.x, p0.y,pointRadius,getPointPaint());
}

连线代码

1
2
3
private void linePoint(BPoint p0, BPoint p1, Canvas canvas) {
canvas.drawLine(p0.x,p0.y,p1.x,p1.y,getPointPaint());
}

根据这四个点就可以画出三阶曲线了,画出来的曲线存在path2中

1
2
3
4
5
6
7
8
//三阶贝塞尔曲线
Path path2=new Path();
BPoint start2=Utils_Berzier.calculateBerzierThree(p0,p1,p2,p3,0);
path2.moveTo(start2.x,start2.y);
for (float i=0.01f;i<1f;i+=0.01f){
BPoint point=Utils_Berzier.calculateBerzierThree(p0,p1,p2,p3,i);
path2.lineTo(point.x,point.y);
}

查看calculateBerzierThree方法。把这条曲线细分成一百个点,然后连线。这边并没有用android自带的path2.quadTo();path2.cubicTo();这几个方法去实现贝塞尔曲线。而是自己用公式计算,并细分细节。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 三阶贝塞尔曲线
* @param p0
* @param p1
* @param p2
* @param p3
* @param t
* @return
*/
public static BPoint calculateBerzierThree(BPoint p0,BPoint p1,BPoint p2,BPoint p3,float t){
BPoint bPoint=new BPoint();
bPoint.x=getBerzierThree(p0.x,p1.x,p2.x,p3.x,t);
bPoint.y=getBerzierThree(p0.y,p1.y,p2.y,p3.y,t);
return bPoint;
}

返回是一个不同细节时候的点。在上一层被连接。接着看getBerzierThree代码

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 三阶贝塞尔曲线
* @param p0
* @param p1
* @param p2
* @param p3
* @param t
* @return
*/
private static float getBerzierThree(float p0,float p1,float p2,float p3,float t){
return p0*(1-t)*(1-t)*(1-t)+3*p1*t*(1-t)*(1-t)+3*p2*t*t*(1-t)+p3*t*t*t;
}

直接就是三阶曲线方程式

然后返回ondraw方法.因为这样只画出了一半的心形线

1
2
3
4
5
6
7
8
9
10
11
/**计算p1 p2镜像点位置*/
BPoint p4=calculataImagePoint(p0,p3,p1);
BPoint p5=calculataImagePoint(p0,p3,p2);
//镜像三阶贝塞尔曲线
Path path3=new Path();
BPoint start3=Utils_Berzier.calculateBerzierThree(p0,p4,p5,p3,0);
path3.moveTo(start3.x,start3.y);
for (float i=0.01f;i<1f;i+=0.01f){
BPoint point=Utils_Berzier.calculateBerzierThree(p0,p4,p5,p3,i);
path3.lineTo(point.x,point.y);
}

p4,p5是分别关于p0,p3直线函数的镜像点。

查看镜像计算方法calculataImagePoint

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
*
* @param p0 直线点1
* @param p3 直线点2
* @param p1 镜像点
* @return p1的镜像点
*/
private BPoint calculataImagePoint(BPoint p0, BPoint p3, BPoint p1) {
//计算p0p3直线公式y=ax+b 的ab值
float a=(p0.y-p3.y)/(p0.x-p3.x);
float b=p0.y-p0.x*a;
//根据中点公式,斜率相乘为-1得到一个代数式
BPoint result=new BPoint();
result.x=(2*p1.y*a-2*b*a-a*a*p1.x+p1.x)/(1+a*a);
result.y=p1.y-(result.x-p1.x)/a;
return result;
}

如图注解,就是一个代数式的总结而已。得到的镜像path

1
2
path3.addPath(path2);
canvas.drawPath(path3,getPathPaint());

合并并描绘

再来看控制点触摸监听

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Override
public boolean onTouchEvent(MotionEvent event) {

if (event.getAction()==MotionEvent.ACTION_DOWN){//按下事件
BPoint point=calculatePointArea(event);
if (point!=null){//说明按在控制点上
move=point;
return true;
}
}else if (event.getAction()==MotionEvent.ACTION_MOVE){//移动事件
if (move!=null){
move.x=event.getX();
move.y=event.getY();
invalidate();
return true;
}
}else if (event.getAction()==MotionEvent.ACTION_UP){//松手
move=null;
return true;
}
return super.onTouchEvent(event);
}

按下事件计算是否按下四个控制点的其中一个查看calculatePointArea方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 计算按下的点是否在控制点范围内
* @param event
* @return 如果在,返回该控制点,不在返回null
*/
private BPoint calculatePointArea(MotionEvent event) {
float distance=calculatePointDistance(event,p0);
if (distance<=pointRadius){
return p0;
}
distance=calculatePointDistance(event,p1);
if (distance<=pointRadius){
return p1;
}
distance=calculatePointDistance(event,p2);
if (distance<=pointRadius){
return p2;
}
distance=calculatePointDistance(event,p3);
if (distance<=pointRadius){
return p3;
}
return null;
}

两点之间距离计算calculatePointDistance

1
2
3
4
private float calculatePointDistance(MotionEvent event, BPoint p0) {
float result= (float) Math.sqrt((event.getX()-p0.x)*(event.getX()-p0.x)+(event.getY()-p0.y)*(event.getY()-p0.y));
return result;
}

然后move事件不断重新更新被按下的控制点的坐标重绘整个界面

1
2
3
4
5
6
if (move!=null){
move.x=event.getX();
move.y=event.getY();
invalidate();
return true;
}

完结。