Bézier curve(贝塞尔曲线)是应用于二维图形应用程序的数学曲线。 曲线定义:起始点、终止点(也称锚点)、控制点。通过调整控制点,贝塞尔曲线的形状会发生化。 1962年,法国数学家Pierre Bézier第一个研究了这种矢量绘制曲线的方法,并给出了详细的计算公式,因此按照这样的公式绘制出来的曲线就用他的姓氏来命名,称为贝塞尔曲线。
线性公式
给定点p0、p1,线性贝塞尔曲线只是一条两点之间的直线,公式如下:
二次方公式
二次方贝塞尔曲线的路径由给定点p0、p1、p2的函数B(t),公式如下:
三次方公式
p0、p1、p2、p3四个点在平面或在三维空间定义了三次贝塞尔曲线。曲线起始于p0走向p1,并从p2的方向来到p3.一般不会经过p1或者p2;这两点只是在那里提供了方向资讯。p0和p1之间的间距,决定了曲线在转而趋进p3之前,走向p2方向的“长度有多长”,公式如下:
上面这段是摘自百度百科,由上面的动态图可以看出,一阶贝塞尔曲线是由两点控制的一条直线,二阶贝塞尔曲线是由一个控制点控制的曲线,三阶贝塞尔曲线是由两个控制点控制的曲线,至于三阶以上的不做研究。
下面看一下二阶贝塞尔曲线运行的效果图:
设置二阶贝塞尔曲线的方法如下
moveTo(float x, float y) 其中x、y坐标代表图中曲线靠左边起点的坐标位置
quadTo(float x1, float y1, float x2, float y2) 其中x1、y1坐标代表图中移动点的坐标,也就是我们所说的二阶贝塞尔曲线的控制点坐标;x2、y2坐标代表图中曲线靠右边终点的坐标位置
首先我们要重写view的onTouchEvent的事件,并对该事件进行拦截,也就是返回值为true,代码如下:
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
int moveX = (int) (event.getX());
int moveY = (int) (event.getY());
mControlPoint.x = moveX;
mControlPoint.y = moveY;
invalidate();
break;
}
return true;
}
在move事件中,获取到控制点的坐标,并在onDraw方法中进行路径的绘制,代码如下:
初始化起始点:
mPaint = new Paint();
mPaint.setStyle(Paint.Style.STROKE);
DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
mWidth = displayMetrics.widthPixels;
mHeight = displayMetrics.heightPixels;
mStartPoint.set(100, mHeight / 2);
mEndPoint.set(mWidth - 100, mHeight / 2);
mControlPoint.set(mWidth / 2, 100);
进行绘制:
private void drawQuadraticBezier(Canvas canvas) {
mPaint.setColor(Color.RED);
mPaint.setStrokeWidth(20);
mPaint.setStyle(Paint.Style.STROKE);
canvas.drawCircle(mControlPoint.x, mControlPoint.y, 10, mPaint);
mPaint.setStrokeWidth(10);
mPaint.setStyle(Paint.Style.FILL);
float[] lines = {mStartPoint.x, mStartPoint.y, mControlPoint.x, mControlPoint.y,
mControlPoint.x, mControlPoint.y, mEndPoint.x, mEndPoint.y,
mEndPoint.x, mEndPoint.y, mStartPoint.x, mStartPoint.y};
canvas.drawLines(lines, mPaint);
mPaint.setColor(Color.GREEN);
mPaint.setStyle(Paint.Style.STROKE);
Path path = new Path();
path.moveTo(mStartPoint.x, mStartPoint.y);
path.quadTo(mControlPoint.x, mControlPoint.y, mEndPoint.x, mEndPoint.y);
canvas.drawPath(path, mPaint);
}
二阶贝塞尔曲线到这里已经介绍完了,接下来介绍下三阶贝塞尔曲线,先看下效果图:
设置二阶贝塞尔曲线的方法如下
moveTo(float x, float y) 其中x、y坐标代表图中在圆周上靠左边起点的坐标位置
cubicTo(float x1, float y1, float x2, float y2, float x3, float y3) 其中x1、y1坐标代表图中左上角移动点的坐标,x2、y2坐标代表图中右上角移动点的坐标,x1、y1和x2、y2也就是我们所说的三阶贝塞尔曲线的控制点坐标;x3、y3坐标代表图中在圆周上靠右边终点的坐标位置
首先我们要重写view的onTouchEvent的事件,并对该事件进行拦截,也就是返回值为true,代码如下:
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
int moveX = (int) (event.getX());
int moveY = (int) (event.getY());
int distanceX = Math.abs(mControlPoint.x - moveX);
int distanceY = Math.abs(mControlPoint.y - moveY);
int distanceX1 = Math.abs(mControlPoint1.x - moveX);
int distanceY1 = Math.abs(mControlPoint1.y - moveY);
if (distanceX < 50 && distanceY < 50) {
mControlPoint.x = moveX;
mControlPoint.y = moveY;
} else if (distanceX1 < 50 && distanceY1 < 50) {
mControlPoint1.x = moveX;
mControlPoint1.y = moveY;
}
invalidate();
break;
}
return true;
}
在move事件中,判断当前触摸的是哪个控制点,并对该控制点进行赋值,绘制代码如下:初始化数据:
mBloomCenterPoint.set(mWidth / 2, mHeight / 2);
mStartPoint.set(mWidth / 2, mHeight / 2);
mEndPoint.set(mWidth / 2, mHeight / 2);
mControlPoint.set(mWidth / 2 - 200, 100);
mControlPoint1.set(mWidth / 2 + 200, 100);
开始绘制:
private void drawCubicBezier(Canvas canvas) {
Point topPoint = new Point(mBloomCenterPoint.x, mBloomCenterPoint.y - mRadius);
float angle1 = (mBloomCenterPoint.x - mControlPoint.x) * 1.0f / (mBloomCenterPoint.y - mControlPoint.y);
float angle2 = (mBloomCenterPoint.x - mControlPoint1.x) * 1.0f / (mBloomCenterPoint.y - mControlPoint1.y);
boolean isBig1 = false;
boolean isBig2 = false;
if (mControlPoint.y > mBloomCenterPoint.y) {
isBig1 = true;
}
if (mControlPoint1.y > mBloomCenterPoint.y) {
isBig2 = true;
}
//获取三阶贝塞尔曲线的起始点的值
mStartPoint = getFixPoint(topPoint, angle1, isBig1);
mEndPoint = getFixPoint(topPoint, angle2, isBig2);
mPaint.setColor(Color.RED);
mPaint.setStrokeWidth(1);
mPaint.setStyle(Paint.Style.STROKE);
canvas.drawCircle(mControlPoint.x, mControlPoint.y, 10, mPaint);
canvas.drawCircle(mControlPoint1.x, mControlPoint1.y, 10, mPaint);
canvas.drawCircle(mBloomCenterPoint.x, mBloomCenterPoint.y, mRadius, mPaint);
mPaint.setStrokeWidth(10);
mPaint.setStyle(Paint.Style.FILL);
float[] lines = {mStartPoint.x, mStartPoint.y, mControlPoint.x, mControlPoint.y,
mControlPoint.x, mControlPoint.y, mControlPoint1.x, mControlPoint1.y,
mControlPoint1.x, mControlPoint1.y, mEndPoint.x, mEndPoint.y,
mEndPoint.x, mEndPoint.y, mStartPoint.x, mStartPoint.y};
canvas.drawLines(lines, mPaint);
mPaint.setStrokeWidth(10);
mPaint.setColor(Color.GREEN);
mPaint.setStyle(Paint.Style.FILL);
Path path = new Path();
path.moveTo(mStartPoint.x, mStartPoint.y);
path.cubicTo(mControlPoint.x, mControlPoint.y, mControlPoint1.x, mControlPoint1.y, mEndPoint.x, mEndPoint.y);
canvas.drawPath(path, mPaint);
}
private Point getFixPoint(Point topPoint, float angle, boolean isBig) {
double radian = Math.atan(angle);
if (isBig) {
radian += Math.PI;
}
double sin = Math.sin(radian);
double cos = Math.cos(radian);
int x = (int) (topPoint.x - mRadius * sin);
int y = (int) (topPoint.y + mRadius * (1 - cos));
Point point = new Point(x, y);
return point;
}
高级进阶像360安全卫士清理内存的动态效果大家应该都不陌生吧,我们现在用二阶贝塞尔曲线实现这样的效果,先上效果图:
首先我们初始化数据,代码如下:
private void init() {
DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
mScreenWidth = displayMetrics.widthPixels;
mScreenHeight = displayMetrics.heightPixels;
int height = mScreenHeight * 7 / 10;
mStartPoint.set(mScreenWidth / 10, height);
mEndPoint.set(mScreenWidth * 9 / 10, height);
mRadius = 100;
}
然后重写onTouchEvent事件,不断的重绘红色的球和绿色的曲线,当只有在球与线接触时,才进行二阶贝塞尔曲线的绘制,touch事件的代码如下:
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
int moveX = (int) (event.getX());
int moveY = (int) (event.getY());
mControlPoint.x = moveX;
mControlPoint.y = moveY;
invalidate();
break;
case MotionEvent.ACTION_UP:
int x = mControlPoint.x;
int y = mControlPoint.y;
if (y > mStartPoint.y && x > mScreenWidth * 2 / 5
&& x < mScreenWidth * 3 / 5) {
startAnim();
}
break;
}
return true;
}
当执行ACTION_UP事件时,判断此时控制点是否进行了二阶变换,如果是,则进行动画的绘制,动画效果的代码如下:
private void startAnim() {
ValueAnimator valueAnimator = ValueAnimator.ofInt(mControlPoint.y, -10);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mControlPoint.y = (int) animation.getAnimatedValue();
invalidate();
}
});
valueAnimator.setDuration(1000);
valueAnimator.start();
}
下面看下球跟线接触时,视图是怎么绘制的,代码如下:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Paint paint = new Paint();
paint.setStrokeWidth(10);
paint.setStyle(Paint.Style.STROKE);
paint.setColor(Color.GREEN);
int x = mControlPoint.x;
int y = mControlPoint.y;
int height = mStartPoint.y;
if (y > mStartPoint.y && x > mScreenWidth * 2 / 5
&& x < mScreenWidth * 3 / 5) {
height = y + y - mStartPoint.y;
}
Path path = new Path();
path.moveTo(mStartPoint.x, mStartPoint.y);
path.quadTo(mScreenWidth / 2, height, mEndPoint.x, mEndPoint.y);
canvas.drawPath(path, paint);
paint.setStyle(Paint.Style.FILL);
paint.setColor(Color.RED);
canvas.drawCircle(x, y - mRadius, mRadius, paint);
}
代码中控制点高度的计算,是通过二阶变换公式相减得到的,到目前为止,该过程的绘制代码已全部列出。 在进行三阶贝塞尔曲线变换的时候,绿色部分有点像个花瓣,下面我们用三阶贝塞尔曲线,绘制一朵花,效果图如下:
我们先用进行下数据的初始化操作,定义些常量,代码如下:
public interface BloomOption {
//用于控制产生随机花瓣个数范围
int minPetalCount = 8;
int maxPetalCount = 12;
//用于控制产生延长线倍数范围
float minPetalStretch = 2f;
float maxPetalStretch = 3.5f;
//用于控制产生花朵半径随机数范围
int minBloomRadius = 100;
int maxBloomRadius = 300;
}
并进行数据的一些初始化操作:
private void init() {
DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
int screenWidth = displayMetrics.widthPixels;
int screenHeight = displayMetrics.heightPixels;
mBloomCenterPoint.set(screenWidth / 2, screenHeight / 2 - 200);
petals = new ArrayList<>();
initPetalData();
}
private void initPetalData() {
int petalCount = RandomUtil.randomInt(minPetalCount, maxPetalCount);
//每个花瓣应占用的角度
float angle = 360f / petalCount;
int startAngle = RandomUtil.randomInt(0, 90);
for (int i = 0; i < petalCount; i++) {
//随机产生第一个控制点的拉伸倍数
float stretchA = RandomUtil.random(minPetalStretch, maxPetalStretch);
//随机产生第二个控制地的拉伸倍数
float stretchB = RandomUtil.random(minPetalStretch, maxPetalStretch);
//计算每个花瓣的起始角度
int beginAngle = startAngle + (int) (i * angle);
PetalView petal = new PetalView(stretchA, stretchB, beginAngle, angle);
petals.add(petal);
}
}
下面进行绿色线条的绘制,代码如下:
private void drawStem(Canvas canvas) {
Paint paint = new Paint();
paint.setStrokeWidth(10);
paint.setColor(Color.GREEN);
paint.setStyle(Paint.Style.STROKE);
Path path = new Path();
path.moveTo(mBloomCenterPoint.x, mBloomCenterPoint.y);
path.quadTo(mBloomCenterPoint.x + 50, mBloomCenterPoint.y + 200, mBloomCenterPoint.x - 50, mBloomCenterPoint.y + 600);
canvas.drawPath(path, paint);
}
下面进行花的绘制,代码如下:onDraw方法:
int radius = RandomUtil.randomInt(minBloomRadius, maxBloomRadius);
int size = petals.size();
MyPoint point = new MyPoint(mBloomCenterPoint.x, mBloomCenterPoint.y);
for (int i = 0; i < size; i++) {
PetalView petal = petals.get(i);
if (petal != null) {
petal.render(point, radius, canvas);
}
}
PetalView.java:
public class PetalView {
private static final String TAG = "PetalView";
private float stretchA;//第一个控制点延长线倍数
private float stretchB;//第二个控制点延长线倍数
private float startAngle;//起始旋转角,用于确定第一个端点
private float angle;//两条线之间夹角,由起始旋转角和夹角可以确定第二个端点
private int radius = 100;//花芯的半径
private Path path = new Path();//用于保存三次贝塞尔曲线
private Paint paint = new Paint();
public PetalView(float stretchA, float stretchB, float startAngle, float angle) {
this.stretchA = stretchA;
this.stretchB = stretchB;
this.startAngle = startAngle;
this.angle = angle;
paint.setColor(Color.RED);
}
public void render(MyPoint p, int radius, Canvas canvas) {
if (this.radius <= radius) {
this.radius += 25;
}
draw(p, canvas);
}
private void draw(MyPoint p, Canvas canvas) {
path = new Path();
//将向量(0,radius)旋转起始角度,第一个控制点根据这个旋转后的向量计算
MyPoint t = new MyPoint(0, this.radius).rotate(RandomUtil.degrad(this.startAngle));
//第一个端点,为了保证圆心不会随着radius增大而变大这里固定为3
MyPoint v1 = new MyPoint(0, 3).rotate(RandomUtil.degrad(this.startAngle));
//第二个端点
MyPoint v2 = t.clone().rotate(RandomUtil.degrad(this.angle));
//延长线,分别确定两个控制点
MyPoint v3 = t.clone().mult(this.stretchA);
MyPoint v4 = v2.clone().mult(this.stretchB);
//由于圆心在p点,因此,每个点要加圆心坐标点
v1.add(p);
v2.add(p);
v3.add(p);
v4.add(p);
path.moveTo(v1.x, v1.y);
//参数分别是:第一个控制点,第二个控制点,终点
path.cubicTo(v3.x, v3.y, v4.x, v4.y, v2.x, v2.y);
canvas.drawPath(path, paint);
}
}
MyPoint.java:
public class MyPoint {
public int x;
public int y;
public MyPoint() {
}
public MyPoint(int x, int y) {
this.x = x;
this.y = y;
}
//旋转
public MyPoint rotate(float theta) {
int x = this.x;
int y = this.y;
this.x = (int) (Math.cos(theta) * x - Math.sin(theta) * y);
this.y = (int) (Math.sin(theta) * x + Math.cos(theta) * y);
return this;
}
//乘以一个常数
public MyPoint mult(float f) {
this.x *= f;
this.y *= f;
return this;
}
//复制
public MyPoint clone() {
return new MyPoint(this.x, this.y);
}
//向量相减
public MyPoint subtract(MyPoint p) {
this.x -= p.x;
this.y -= p.y;
return this;
}
//向量相加
public MyPoint add(MyPoint p) {
this.x += p.x;
this.y += p.y;
return this;
}
public MyPoint set(int x, int y) {
this.x = x;
this.y = y;
return this;
}
@Override
public String toString() {
return "MyPoint{" +
"x=" + x +
", y=" + y +
'}';
}
}
github地址参考文章:http://www.html5tricks.com/demo/jiaoben1892/index.html