(1)概念
将任意一条曲线转化为精确的数学公式。
(2)组成部分
数据点:
曲线的起点和终点。
控制点:
控制点决定了一条路径的弯曲轨迹,根据控制点的个数,贝塞尔曲线被分为一阶贝塞尔曲线(0个控制点)、二阶贝塞尔曲线(1个控制点)、三阶贝塞尔曲线(2个控制点)等等。
假设现在有n阶贝赛尔曲线,那么,该贝赛尔曲线的控制点数量为:n-1。
(3)一阶贝塞尔曲线
首先,说一说一阶贝赛尔曲线。
Path的lineTo
和rLineTo
可以构建一阶贝塞尔曲线,一阶贝赛尔曲线,其实就是一条直线。
假设现在有两点,分别是P0和P1,现在从P0开始向P1勾勒出一阶贝赛尔曲线)(其实就是一条直线),它的计算公式如下:
P(t) = P0 + (P1-P0)t = (1-t)P0 + tP1,t∈[0,1]。
那么,这个公式是怎么来的呢?
这个公式是经过德卡斯特里奥算法
推导得出,贝赛尔曲线是从P0到P1勾勒出的若干个点组成,假设P0和P1之间的点为P,那么
t = (P - P0)/ (P1 - P0),整理之后就是上面的公式了。
当参数t由0到1变化时,其过程如下
这个t值是曲线占总曲线长度的分量,你可以把总曲线长度看作1,当t为0.5时,图中的黑点就在这条曲线的中间(虽然一次贝塞尔曲线实际上就是条直线)
根据这个公式,我们也可以代码实现其变化,代码如下:
public class Bers extends View {
private Paint mPaint;
private float t = 0;
private float startX = 100;
private float endX = 1000;
private float startY = 500;
private float endY = 500;
public Bers(Context context) {
this(context, null);
}
public Bers(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public Bers(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init(){
mPaint = new Paint();
mPaint.setColor(Color.BLUE);
mPaint.setAntiAlias(true);
mPaint.setStrokeWidth(8);
mPaint.setTextSize(60);
mPaint.setStyle(Paint.Style.STROKE);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//关闭硬件加速
setLayerType(LAYER_TYPE_SOFTWARE, mPaint);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setColor(Color.GRAY);
mPaint.setAntiAlias(true);
mPaint.setStrokeWidth(8);
Path path = new Path();
path.moveTo(100, 500);
path.lineTo(1000, 500);
//绘制路径
canvas.drawPath(path, mPaint);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setStrokeWidth(25);
mPaint.setColor(Color.parseColor("#D81B60"));
//画起始点
canvas.drawPoint(100, 500, mPaint);
//画终点
canvas.drawPoint(1000, 500, mPaint);
mPaint.setStrokeWidth(8);
//这里直接套公式
//P(t) = P0 + (P1-P0)t = (1-t)P0 + tP1,t∈[0,1]
//P0(100,500) P1(1000,500)
canvas.drawLine(startX, startY, (1 - t) * startX + t * endX, (1 - t) * startY + t * endY, mPaint);
t = t >= 1 ? 0 : t + 0.002f;
invalidate();
}
}
效果如下:
(4)二阶贝塞尔曲线
给定点P0、P1,P2,其中P1是控制点,这条曲线由下式给出:
P(t) = (1 - t)2P0 + 2t(1-t)P1 + t2P2,t∈[0,1]。
这个公式是经过德卡斯特里奥算法
推导得出,贝赛尔曲线是从P0到P2勾勒出的若干个点组成,其中P1为控制点,它的推导过程我就在本子上体现了,如下:
当参数t由0到1变化时,其过程如下
Path的quadTo
和rQuadTo
可以构建二阶贝塞尔曲线。
quadTo(float x1, float y1, float x2, float y2)
x1:
控制点x坐标
y1:
控制点y坐标
x2:
终点x坐标
y2:
终点y坐标
三个点可以构成一个二阶贝塞尔曲线
,分别是起点
、控制点
、终点
起点:
一般而言(0, 0)是贝塞尔曲线的起点,可以使用Path的moveTo
来改变;
控制点:
由quadTo
方法的前两个参数决定;
终点:
由quadTo
方法的后两个参数决定;
其中,控制点的位置决定这曲线的曲率。
那么我们来动态画一个曲线,当点击屏幕的某个位置时,控制点随之移动到当前位置。
效果图如下:
代码如下:
public class TestView extends View {
private float mStartX = 300;//曲线开始位置X坐标
private float mStartY = 1500;//曲线开始位置Y坐标
private float mEndX = 1100;//曲线终点位置X坐标
private float mEndY = 1500;//曲线终点位置Y坐标
private float mMoveX = mStartX;//初始化移动位置X坐标
private float mMoveY = mStartY;//初始化移动位置Y坐标
private Paint mPaint;
public TestView(Context context) {
this(context, null);
}
public TestView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init(){
mPaint = new Paint();
mPaint.setColor(Color.BLUE);
mPaint.setAntiAlias(true);
mPaint.setStrokeWidth(8);
mPaint.setTextSize(60);
mPaint.setStyle(Paint.Style.STROKE);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//关闭硬件加速
setLayerType(LAYER_TYPE_SOFTWARE, mPaint);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setColor(Color.BLUE);
mPaint.setAntiAlias(true);
mPaint.setStrokeWidth(8);
Path path = new Path();
//确定贝塞尔曲线的起始点
path.moveTo(mStartX, mStartY);
//二阶贝塞尔曲线(确定贝塞尔曲线的控制点和终点)
path.quadTo(mMoveX, mMoveY, mEndX, mEndY);
path.lineTo(mMoveX, mMoveY);
path.lineTo(mStartX, mStartY);
//绘制路径
canvas.drawPath(path, mPaint);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setStrokeWidth(25);
mPaint.setColor(Color.parseColor("#D81B60"));
//画起始点
canvas.drawPoint(mStartX, mStartY, mPaint);
//画终点
canvas.drawPoint(mEndX, mEndY, mPaint);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(Color.BLACK);
mPaint.setTextAlign(Paint.Align.CENTER);
mPaint.setStrokeWidth(2);
Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();
float textHeight = fontMetrics.descent - fontMetrics.ascent;
canvas.drawText("起点("+mStartX+","+(mStartY+textHeight)+")", mStartX, mStartY+textHeight, mPaint);
canvas.drawText("终点("+mEndX+","+(mEndY+textHeight)+")", mEndX, mEndY+textHeight, mPaint);
if(mStartX != mMoveX || mStartY != mMoveY){
Log.d("aaa", "mStartX:"+mStartX);
Log.d("aaa", "mMoveX:"+mMoveX);
Log.d("aaa", "mStartY:"+mStartY);
Log.d("aaa", "mMoveY:"+mMoveY);
canvas.drawText("控制点("+mMoveX+","+(mMoveY-textHeight)+")", mMoveX, mMoveY-textHeight, mPaint);
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
mMoveX = x;
mMoveY = y;
Log.d("aaa", "mMoveX:"+mMoveX);
Log.d("aaa", "mMoveY:"+mMoveY);
invalidate();
break;
}
return true;
}
}
(5)三阶贝塞尔曲线
给定点P0、P1,P2,P3,其中P1和P2是控制点,这条曲线由下式给出:
P(t) = P0(1-t)3 + 3P1t(1-t)2 + 3P2t2(1-t) + P3t3,t∈[0,1]
其推导过程,我也写在了本子上,如下:
如图所示,一个3阶公式被我降成了3个二阶公式。
当参数t由0到1变化时,其过程如下
Path的cubicTo
和rCubicTo
可以构建三阶贝塞尔曲线。
cubicTo(float x1, float y1, float x2, float y2, float x3, float y3)
x1:
第一个控制点x坐标
y1:
第一个控制点y坐标
x2:
第二个控制点x坐标
y2:
第二个控制点y坐标
x2:
终点x坐标
y2:
终点y坐标
四个点可以构成一个三阶贝塞尔曲线,分别是起点、控制点(2个)、终点
起点: 一般而言(0, 0)是贝塞尔曲线的起点,可以使用Path的moveTo来改变;
控制点: 由cubicTo方法的前四个参数决定;
终点: 由cubicTo方法的后两个参数决定;
其中,控制点的位置决定这曲线的曲率。
(6)绘制手指轨迹
Path的lineTo
可以实现手指绘制功能,代码也比较简单
public class FingerView extends View {
private Paint mPaint;
private Path path;
public FingerView(Context context) {
this(context, null);
}
public FingerView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public FingerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init(){
mPaint = new Paint();
mPaint.setColor(Color.BLUE);
mPaint.setAntiAlias(true);
mPaint.setStrokeWidth(8);
mPaint.setTextSize(60);
mPaint.setStyle(Paint.Style.STROKE);
path = new Path();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//关闭硬件加速
setLayerType(LAYER_TYPE_SOFTWARE, mPaint);
canvas.drawPath(path, mPaint);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
path.moveTo(x, y);
break;
case MotionEvent.ACTION_MOVE:
path.lineTo(x, y);
invalidate();
break;
}
return true;
}
}
效果如下:
使用lineTo
虽然可以实现手指绘制功能,但是只是若干个线段连接起来的,必然会知道线条看起来不怎么平滑,使用二阶赛贝尔曲线可以让线条看起来更加平滑一下。
思路:
二阶贝塞尔曲线需要三个点:起点
、控制点
、终点
起点:
手指点击时的点;
终点:
手指移动时的点;
控制点:
计算控制点很重要,那么该怎么计算呢?
直接贴一下我的代码吧
public class FingerView extends View {
private Paint mPaint;
private Path path;
private float mStartX = 0;
private float mStartY = 0;
public FingerView(Context context) {
this(context, null);
}
public FingerView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public FingerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init(){
mPaint = new Paint();
mPaint.setColor(Color.BLUE);
mPaint.setAntiAlias(true);
mPaint.setStrokeWidth(8);
mPaint.setTextSize(60);
mPaint.setStyle(Paint.Style.STROKE);
path = new Path();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//关闭硬件加速
setLayerType(LAYER_TYPE_SOFTWARE, mPaint);
canvas.drawPath(path, mPaint);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
float eventX = event.getX();
float eventY = event.getY();
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
//设置贝塞尔曲线的起点
mStartX = eventX;
mStartY = eventY;
path.moveTo(mStartX, mStartY);
break;
case MotionEvent.ACTION_MOVE:
//eventX和eventY是贝塞尔曲线的终点
//现在需要做的是计算出贝塞尔曲线的控制点
float controlX = (eventX + mStartX) / 2;
float controlY = (eventY + mStartY) / 2;
path.quadTo(controlX, controlY, eventX, eventY);
Log.d("aaa", "("+mStartX+","+mStartY+")("+eventX+","+eventY+")("+controlX+","+controlY+")");
//将上一次绘制的结束点作为下一次绘制的起点
mStartX = eventX;
mStartY = eventY;
invalidate();
break;
}
return true;
}
}
核心的计算代码都在onTouchEvent
里。
演示如下:
(7)贝赛尔曲线实现水波纹
代码如下:
public class WaveView extends View {
private Paint mPaint;
private int mWidth;
private int mHeight;
private int mWaveHeight;
private int mWaveDx;
private int dx;
public WaveView(Context context) {
this(context, null);
}
public WaveView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public WaveView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setDither(true);
mPaint.setColor(Color.parseColor("#FF3891"));
mPaint.setStyle(Paint.Style.FILL);
//波长的的长度(这里设置为屏幕的宽度)
mWaveDx = getResources().getDisplayMetrics().widthPixels;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//控件的宽高
mWidth = MeasureUtils.measureView(widthMeasureSpec, mWaveDx);
mHeight = MeasureUtils.measureView(heightMeasureSpec, 300);
//水波的高度
mWaveHeight = DensityUtil.dip2px(getContext(), 16);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawWave(canvas);
}
private void drawWave(Canvas canvas) {
Path path = new Path();
path.reset();
path.moveTo(-mWaveDx + dx, mHeight / 2);
for (int i = -mWaveDx; i < getWidth() + mWaveDx; i += mWaveDx) {
path.rQuadTo(mWaveDx / 4, -mWaveHeight, mWaveDx / 2, 0);
path.rQuadTo(mWaveDx / 4, mWaveHeight, mWaveDx / 2, 0);
}
path.lineTo(mWidth, mHeight);
path.lineTo(0, mHeight);
//path.close() 绘制封闭的区域
path.close();
canvas.drawPath(path, mPaint);
}
public void startAnimation() {
ValueAnimator valueAnimator = ValueAnimator.ofInt(0, mWaveDx);
valueAnimator.setDuration(2000);
valueAnimator.setRepeatCount(ValueAnimator.INFINITE);
valueAnimator.setInterpolator(new LinearInterpolator());
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
//水平方向的偏移量
dx = ( int ) animation.getAnimatedValue();
invalidate();
}
});
valueAnimator.start();
}
}
DensityUtil.java
public class DensityUtil {
public static int dip2px(Context var0, float var1) {
float var2 = var0.getResources().getDisplayMetrics().density;
return ( int ) (var1 * var2 + 0.5F);
}
public static int dp2px(float value) {
final float scale = Resources.getSystem().getDisplayMetrics().densityDpi;
return ( int ) (value * (scale / 160) + 0.5f);
}
public static int px2dip(Context var0, float var1) {
float var2 = var0.getResources().getDisplayMetrics().density;
return ( int ) (var1 / var2 + 0.5F);
}
public static int sp2px(Context var0, float var1) {
float var2 = var0.getResources().getDisplayMetrics().scaledDensity;
return ( int ) (var1 * var2 + 0.5F);
}
public static int px2sp(Context var0, float var1) {
float var2 = var0.getResources().getDisplayMetrics().scaledDensity;
return ( int ) (var1 / var2 + 0.5F);
}
}
MeasureUtils.java
public class MeasureUtils {
/**
* 用于View的测量
*
* @param measureSpec 测量模式和大小
* @param defaultSize 默认的大小
* @return
*/
public static int measureView(int measureSpec, int defaultSize) {
int measureSize;
//获取用户指定的大小以及模式
int mode = View.MeasureSpec.getMode(measureSpec);
int size = View.MeasureSpec.getSize(measureSpec);
//根据模式去返回大小
if (mode == View.MeasureSpec.EXACTLY) {
//精确模式(指定大小以及match_parent)直接返回指定的大小
measureSize = size;
} else {
//UNSPECIFIED模式、AT_MOST模式(wrap_content)的话需要提供默认的大小
measureSize = defaultSize;
if (mode == View.MeasureSpec.AT_MOST) {
//AT_MOST(wrap_content)模式下,需要取测量值与默认值的最小值
measureSize = Math.min(measureSize, size);
}
}
return measureSize;
}
private int measureSize(int measureSpec) {
int result;
int specMode = View.MeasureSpec.getMode(measureSpec);
int specSize = View.MeasureSpec.getSize(measureSpec);
if (specMode == View.MeasureSpec.EXACTLY) {
result = specSize;
} else {
result = 300;
if (specMode == View.MeasureSpec.AT_MOST) {
result = Math.min(result, specSize);
}
}
return result;
}
}
动画效果如下:
[本章完...]