贝塞尔曲线入门
2017-12-01 本文已影响0人
黄烨1121
本章目录
- Part One:一阶贝塞尔曲线
- Part Two:二阶贝塞尔曲线
- Part Three:三阶贝塞尔曲线
贝塞尔曲线由法国工程师皮埃尔.贝塞尔所广泛发表,他运用贝济埃曲线来为汽车的主体进行设计,现在成为计算机图形学中相当重要的参数曲线。
贝塞尔曲线的原理就是利用多个点的位置来确定出一条曲线。这多个点就是起点,终点,控制点。控制点可以没有,也可以有多个。由这条曲线来确定View的移动轨迹,从而绘制出一些酷炫的动画。
贝塞尔曲线可以有很多阶,一般来说,我们用二阶和三阶比较多。相应的,Android官方也提供了1到3阶贝塞尔曲线的api。
四阶以上的比较少见,如果确实有需要的,可以去网上搜相应的算法。
首先,我们先从最简单的一阶贝塞尔曲线开始看
Part One:一阶贝塞尔曲线
一阶贝塞尔曲线也叫线性曲线,该函数中的t会经过由P0至P1的B(t)所描述的曲线。例如当t=0.25时,B(t)即一条由点P0至P1路径的四分之一处。就像由0至1的连续t,B(t)描述一条由P0至P1的直线。
注:图片来源于维基百科
它的公式是
image.png
具体代码为
package com.terana.mycustomview.customview;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Point;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;
public class HeartView extends View {
private Paint linePaint;
private Paint moveLinePaint;
private Path linePath;
private Path moveLinePath;
private Point startPoint;
private Point endPoint;
public HeartView(Context context) {
this(context, null);
}
public HeartView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public HeartView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public HeartView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
initVariables();
}
private void initVariables() {
//静止曲线画笔
linePaint = new Paint();
linePaint.setAntiAlias(true);//抗锯齿
linePaint.setDither(true);//防抖动
linePaint.setStyle(Paint.Style.STROKE);//绘制样式
linePaint.setStrokeWidth(5);//线条粗细
linePaint.setColor(Color.GRAY);//灰色线条
// 运动曲线画笔
moveLinePaint = new Paint();
moveLinePaint.setAntiAlias(true);//抗锯齿
moveLinePaint.setDither(true);//防抖动
moveLinePaint.setStyle(Paint.Style.STROKE);//绘制样式
moveLinePaint.setStrokeWidth(5);//线条粗细
moveLinePaint.setColor(Color.RED);//红色线条
//静止线条路径
linePath = new Path();
//运动线条路径
moveLinePath = new Path();
//起始点坐标
startPoint = new Point();
startPoint.set(100, 100);
//终止点坐标
endPoint = new Point();
endPoint.set(400, 400);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
linePath.moveTo(startPoint.x, startPoint.y);
linePath.lineTo(endPoint.x, endPoint.y);//一阶贝塞尔曲线线条
canvas.drawPath(linePath, linePaint);
drawMovePath(canvas);
}
private float moveX = 100f, moveY = 100f;
private long delay = 20;
private long period = 1000;
private void drawMovePath(Canvas canvas) {
moveLinePath.moveTo(startPoint.x, startPoint.y);
moveLinePath.lineTo(moveX, moveY);
canvas.drawPath(moveLinePath, moveLinePaint);
getHandler().postDelayed(runnable, delay);
if (i == (period / delay + 1)){
getHandler().removeCallbacks(runnable);
}
}
private int i = 0;
private Runnable runnable = new Runnable() {
@Override
public void run() {
moveX = calculateDimension(true, i);
moveY = calculateDimension(false, i);
invalidate();
i++;
}
};
private float calculateDimension(boolean flag, int index) {
float result;
float t = delay * index * 1.0f / period;
if (flag) {
result = (1 - t) * startPoint.x + t * endPoint.x;
} else {
result = (1 - t) * startPoint.y + t * endPoint.y;
}
return result;
}
}
Part Two:二阶贝塞尔曲线
二阶贝塞尔曲线的路径由给定点P0、P1、P2的函数B(t)追踪:
image.png
二阶贝萨尔曲线.gif
自定义View布局部分
package com.terana.mycustomview.customview;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Point;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class HeartView extends View {
private Paint linePaint;
private Paint moveLinePaint;
private Path linePath;
private Path moveLinePath;
private Point startPoint;
private Point endPoint;
private Point controlPointOne;
public HeartView(Context context) {
this(context, null);
}
public HeartView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public HeartView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public HeartView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
initVariables();
}
private void initVariables() {
//静止曲线画笔
linePaint = new Paint();
linePaint.setAntiAlias(true);//抗锯齿
linePaint.setDither(true);//防抖动
linePaint.setStyle(Paint.Style.STROKE);//绘制样式
linePaint.setStrokeWidth(5);//线条粗细
linePaint.setColor(Color.GRAY);//灰色线条
// 运动曲线画笔
moveLinePaint = new Paint();
moveLinePaint.setAntiAlias(true);//抗锯齿
moveLinePaint.setDither(true);//防抖动
moveLinePaint.setStyle(Paint.Style.STROKE);//绘制样式
moveLinePaint.setStrokeWidth(5);//线条粗细
moveLinePaint.setColor(Color.RED);//红色线条
//静止线条路径
linePath = new Path();
//运动线条路径
moveLinePath = new Path();
//起始点坐标
startPoint = new Point();
startPoint.set(100, 100);
//终止点坐标
endPoint = new Point();
endPoint.set(400, 400);
//控制点1坐标
controlPointOne = new Point();
controlPointOne.set(600, 100);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
linePath.moveTo(startPoint.x, startPoint.y);
//linePath.lineTo(endPoint.x, endPoint.y);//一阶贝塞尔曲线线条
linePath.quadTo(controlPointOne.x, controlPointOne.y, endPoint.x, endPoint.y);//二阶贝塞尔曲线
canvas.drawPath(linePath, linePaint);
drawMovePath(canvas);
}
private List<Map<String, Float>> pointList = new ArrayList<>();
private static final String XPOSITION = "xPosition";
private static final String YPOSITION = "yPosition";
private void drawMovePath(final Canvas canvas) {
moveLinePath.moveTo(startPoint.x, startPoint.y);
for (int index = 0; index < pointList.size(); index++){
if (index > 0){
canvas.drawLine(pointList.get(index - 1).get(XPOSITION), pointList.get(index - 1).get(YPOSITION),
pointList.get(index).get(XPOSITION), pointList.get(index).get(YPOSITION), moveLinePaint);
}
}
}
public Point getStartPoint() {
return startPoint;
}
public Point getEndPoint() {
return endPoint;
}
public Point getControlPointOne() {
return controlPointOne;
}
public void setPointList(float currentX, float currentY){
Map<String, Float> temp = new HashMap<>();
temp.put(XPOSITION, currentX);
temp.put(YPOSITION, currentY);
pointList.add(temp);
invalidate();
}
}
Activity部分:
package com.terana.mycustomview.activities;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import com.terana.mycustomview.R;
import com.terana.mycustomview.customview.HeartView;
public class HeartActivity extends BaseActivity {
private static Handler handler;
private HeartView heartView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
removeStatusBar();
setContentView(R.layout.activity_heart);
initViews();
updatePosition();
}
float xPosition = 100f;
float yPosition = 100f;
private void updatePosition() {
new Thread() {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
float t = i * 1.0f / 1000;
Message message = new Message();
message.what = 100;
Bundle bundle = new Bundle();
bundle.putFloat("x", xPosition);
bundle.putFloat("y", yPosition);
message.setData(bundle);
handler.sendMessage(message);
try {
sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
xPosition = (1 - t) * (1 - t) * heartView.getStartPoint().x + 2 * t * (1 - t) * heartView.getControlPointOne().x
+ t * t * heartView.getEndPoint().x;
yPosition = (1 - t) * (1 - t) * heartView.getStartPoint().y + 2 * t * (1 - t) * heartView.getControlPointOne().y
+ t * t * heartView.getEndPoint().y;
}
}
}.start();
}
private void initViews() {
heartView = findViewById(R.id.heartView_heart);
handler = new MyHandler();
}
private class MyHandler extends Handler {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case 100:
Bundle bundle = msg.getData();
heartView.setPointList(bundle.getFloat("x"), bundle.getFloat("y"));
default:
break;
}
super.handleMessage(msg);
}
}
}
Part Three:三阶贝塞尔曲线
P0、P1、P2、P3四个点在平面或在三维空间中定义了三次方贝塞尔曲线。曲线起始于P0走向P1,并从P2的方向来到P3。一般不会经过P1或P2;这两个点只是在那里提供方向资讯。P0和P1之间的间距,决定了曲线在转而趋进P2之前,走向P1方向的“长度有多长”。
曲线的参数形式为:
image.png
三阶贝萨尔曲线.gif
自定义View部分:
package com.terana.mycustomview.customview;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Point;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class HeartView extends View {
private Paint linePaint;
private Paint moveLinePaint;
private Path linePath;
private Path moveLinePath;
private Point startPoint;
private Point endPoint;
private Point controlPointOne;
private Point controlPointTwo;
public HeartView(Context context) {
this(context, null);
}
public HeartView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public HeartView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public HeartView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
initVariables();
}
private void initVariables() {
//静止曲线画笔
linePaint = new Paint();
linePaint.setAntiAlias(true);//抗锯齿
linePaint.setDither(true);//防抖动
linePaint.setStyle(Paint.Style.STROKE);//绘制样式
linePaint.setStrokeWidth(5);//线条粗细
linePaint.setColor(Color.GRAY);//灰色线条
// 运动曲线画笔
moveLinePaint = new Paint();
moveLinePaint.setAntiAlias(true);//抗锯齿
moveLinePaint.setDither(true);//防抖动
moveLinePaint.setStyle(Paint.Style.STROKE);//绘制样式
moveLinePaint.setStrokeWidth(5);//线条粗细
moveLinePaint.setColor(Color.RED);//红色线条
//静止线条路径
linePath = new Path();
//运动线条路径
moveLinePath = new Path();
//起始点坐标
startPoint = new Point();
startPoint.set(100, 100);
//终止点坐标
endPoint = new Point();
endPoint.set(400, 400);
//控制点1坐标
controlPointOne = new Point();
controlPointOne.set(600, 100);
//控制点2坐标;
controlPointTwo = new Point();
controlPointTwo.set(100, 300);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
linePath.moveTo(startPoint.x, startPoint.y);
//linePath.lineTo(endPoint.x, endPoint.y);//一阶贝塞尔曲线线条
linePath.cubicTo(controlPointOne.x, controlPointOne.y, controlPointTwo.x, controlPointTwo.y, endPoint.x, endPoint.y);//二阶贝塞尔曲线
canvas.drawPath(linePath, linePaint);
drawMovePath(canvas);
}
private List<Map<String, Float>> pointList = new ArrayList<>();
private static final String XPOSITION = "xPosition";
private static final String YPOSITION = "yPosition";
private void drawMovePath(final Canvas canvas) {
moveLinePath.moveTo(startPoint.x, startPoint.y);
for (int index = 0; index < pointList.size(); index++) {
if (index > 0) {
canvas.drawLine(pointList.get(index - 1).get(XPOSITION), pointList.get(index - 1).get(YPOSITION),
pointList.get(index).get(XPOSITION), pointList.get(index).get(YPOSITION), moveLinePaint);
}
}
}
public Point getStartPoint() {
return startPoint;
}
public Point getEndPoint() {
return endPoint;
}
public Point getControlPointOne() {
return controlPointOne;
}
public Point getControlPointTwo() {
return controlPointTwo;
}
public void setPointList(float currentX, float currentY) {
Map<String, Float> temp = new HashMap<>();
temp.put(XPOSITION, currentX);
temp.put(YPOSITION, currentY);
pointList.add(temp);
invalidate();
}
}
Avtivity部分:
package com.terana.mycustomview.activities;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import com.terana.mycustomview.R;
import com.terana.mycustomview.customview.HeartView;
public class HeartActivity extends BaseActivity {
private static Handler handler;
private HeartView heartView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
removeStatusBar();
setContentView(R.layout.activity_heart);
initViews();
updatePosition();
}
float xPosition = 100f;
float yPosition = 100f;
private void updatePosition() {
new Thread() {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
float t = i * 1.0f / 1000;
Message message = new Message();
message.what = 100;
Bundle bundle = new Bundle();
bundle.putFloat("x", xPosition);
bundle.putFloat("y", yPosition);
message.setData(bundle);
handler.sendMessage(message);
try {
sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
xPosition = (float) (Math.pow(1 - t, 3) * heartView.getStartPoint().x
+ 3 * heartView.getControlPointOne().x * t * Math.pow(1 - t, 2)
+ 3 * heartView.getControlPointTwo().x * Math.pow(t, 2) * (1 - t)
+ Math.pow(t, 3) * heartView.getEndPoint().x);
yPosition = (float) (Math.pow(1 - t, 3) * heartView.getStartPoint().y
+ 3 * heartView.getControlPointOne().y * t * Math.pow(1 - t, 2)
+ 3 * heartView.getControlPointTwo().y * Math.pow(t, 2) * (1 - t)
+ Math.pow(t, 3) * heartView.getEndPoint().y);
}
}
}.start();
}
private void initViews() {
heartView = findViewById(R.id.heartView_heart);
handler = new MyHandler();
}
private class MyHandler extends Handler {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case 100:
Bundle bundle = msg.getData();
heartView.setPointList(bundle.getFloat("x"), bundle.getFloat("y"));
default:
break;
}
super.handleMessage(msg);
}
}
}
最终效果为:
三阶贝塞尔曲线效果图.gif
其实三阶的会了,高阶的也差不多会了,就是控制点多了点,数学算法更复杂了一点,网上有模型,把公式复制过来即可,就不再多说了。