您的位置:首页 > 其它

自定义View学习笔记09—Path之Bezier

2017-12-29 15:46 169 查看
一、贝赛尔曲线来源

在数学的数值分析领域中,贝赛尔曲线(Bézier曲线)是电脑图形学中相当重要的参数曲线。更高维度的广泛化贝塞尔曲线就称作贝塞尔曲面,其中贝塞尔三角是一种特殊的实例。

关于贝赛尔曲线公式及退到,由于这部分难度太高,也讲不清楚,这里不细说了,有兴趣的可以自己看这里:

http://blog.csdn.net/harvic880925/article/details/50995587

http://www.gcssloop.com/customview/Path_Bezier

这两篇文章是目前为止,我发现的讲解的最清楚的了,可以观摩学习。

二、Android中贝赛尔曲线概要

1、一阶贝赛尔曲线:

原理:没有控制点,仅有两个数据点(A 和 B),最终效果就是一条线段;其实就是前面讲解过的lineTo。

2、二阶贝赛尔曲线:

原理:由两个数据点(确定曲线的起始和结束位置),一个控制点(确定曲线的弯曲程度)来描述曲线状态;对应的方法:

public void quadTo(float x1, float y1, float x2, float y2);
public void rQuadTo(float dx1, float dy1, float dx2, float dy2);


3、三阶贝赛尔曲线:

原理:由两个数据点(确定曲线的起始和结束位置),两个控制点(确定曲线的弯曲程度和状态)来描述曲线状态;对应的方法:

public void cubicTo(float x1,float y1,float x2,float y2,float x3,float y3);
public void rCubicTo(float x1,float y1,float x2,float y2,float x3,float y3);


这里,关于数据点和控制点的说明如下:



三阶曲线相比于二阶曲线可以制作更加复杂的形状,但是对于高阶的曲线,用低阶的曲线组合也可达到相同的效果,就是传说中的降阶。因此我们对贝塞尔曲线的封装方法一般最高只到三阶曲线。

关于降阶和升阶的说明:



三、二阶贝塞尔曲线的使用

1、二阶贝塞尔曲线quadTo的使用:

public void quadTo(float x1, float y1, float x2, float y2);


参数中(x1,y1)是控制点坐标,(x2,y2)是终点坐标 。疑问:有控制点和终点坐标,那起始点是多少呢?

解答:

整条线的起始点是通过Path.moveTo(x,y)来指定的,而如果我们连续调用quadTo(),前一个quadTo()的终点,就是下一个quadTo()函数的起点;如果初始没有调用Path.moveTo(x,y)来指定起始点,则默认以控件左上角(0,0)为起始点;(moveTo(x, y)一个点,再加上quadTo(x1,y1,x2,y2)两个点,一共三个点.)

2、代码示例:

public class Bezier extends View {
private Paint mPaint;
private int centerX, centerY;
private PointF start, end, control;

public Bezier(Context context) {
super(context);
mPaint = new Paint();
mPaint.setColor(Color.BLACK);
mPaint.setStrokeWidth(8);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setTextSize(60);

start = new PointF(0,0);
end = new PointF(0,0);
control = new PointF(0,0);
}

public Bezier(Context context,AttributeSet attrs) {
super(context, attrs);
}

@Override
protected void onSizeChanged(int w,int h,int oldw,int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
centerX = w/2;
centerY = h/2;
// 初始化数据点和控制点的位置
start.x = centerX - 200;
start.y = centerY;
end.x = centerX + 200;
end.y = centerY;
control.x = centerX;
control.y = centerY-100;
}

@Override
public boolean onTouchEvent(MotionEvent event) {
// 根据触摸位置更新控制点,并提示重绘
control.x = event.getX();
control.y = event.getY();
invalidate();
return true;
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 绘制数据点和控制点
mPaint.setColor(Color.GRAY);
mPaint.setStrokeWidth(20);
//绘制三个点:start,end ,control
canvas.drawPoint(start.x,start.y,mPaint);
canvas.drawPoint(end.x,end.y,mPaint);
canvas.drawPoint(control.x,control.y,mPaint);

// 绘制辅助线
mPaint.setStrokeWidth(4);
canvas.drawLine(start.x,start.y,control.x,control.y,mPaint);
canvas.drawLine(end.x,end.y,control.x,control.y,mPaint);
// 绘制贝塞尔曲线
mPaint.setColor(Color.RED);
mPaint.setStrokeWidth(8);
Path path = new Path();
//path.moveTo以start点开始,对应的path.quadTo以end点结束;相反的;
//如果以end点开始,对应的path.quadTo以start结束,否则,无法绘制曲线

//参数中(control.x,control.y)是控制点坐标,(end.x,end.y)是终点坐标
//整条线的起始点是通过Path.moveTo(start.x,start.y)来指定的
path.moveTo(start.x,start.y);
path.quadTo(control.x,control.y,end.x,end.y);
canvas.drawPath(path, mPaint);
}
}


3、使用Path.lineTo()所存在问题:

当用Path.lineTo()绘制图形,尤其是有圆角弧度的图形的时候,在转角或者圆弧出,会出现明显的马赛克样的锯齿之类的东西,一点也不平滑,看起来不自在,尤其是图形比较大的时候。这就需要优化,实现线与线之间的平滑过渡;优化有两个方案:

一是设置mPaint.setAntiAlias(true);//防锯齿;
二是利用二阶贝赛尔曲线的Path.quadTo函数来重新实现移动轨迹效果。


两个方案可以结合起来使用,效果更佳。

public class FingerPath extends View {
private Paint mPaint;
private Path mPath;
private float mPreX,mPreY;
//true:调用LineTo方法;false:调用QuadTo方法
private boolean lineOrQuad = true;
public FingerPath(Context context) {
super(context);
if(mPaint == null){
mPaint = new Paint();
mPaint.setStrokeWidth(20);
mPaint.setAntiAlias(true);//防锯齿
mPaint.setColor(Color.GREEN);
mPaint.setStyle(Paint.Style.STROKE);
mPath = new Path();
}
}

@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN: {
if(lineOrQuad){
mPath.moveTo(event.getX(), event.getY());
}else {
mPath.moveTo(event.getX(),event.getY());
//mPreX,mPreY表示手指的前一个点
mPreX = event.getX();
mPreY = event.getY();
}
return true;
}
case MotionEvent.ACTION_MOVE:
if(lineOrQuad){
mPath.lineTo(event.getX(), event.getY());
}else {
//除以2的原因:mPreX/mPreY其实是很接近event.getX()/
event.getY()的,除以2取平均值,让每个点之间的波动更小。
float endX = (mPreX + event.getX())/2;
float endY = (mPreY + event.getY())/2;
mPath.quadTo(mPreX, mPreY, endX, endY);
//mPreX,mPreY表示手指的前一个点
mPreX = event.getX();
mPreY = event.getY();
}
//注意:这里用的是postInvalidate(),不是Invalidate();关乎到线程安全,
//Invalidate()一定要在UI线程执行,否则就会报错; postInvalidate()则没有那么
//多讲究,它可以在任何线程中执行,而不必一定要是主线程,其实postInvalidate()是
//利用handler给主线程发送刷新界面的消息来实现的,所以它是可以在任何线程中执行,由
//此带来的影响是在UI界面刷新的时候,postInvalidate()没有Invalidate()快。
invalidate();
break;
default:
break;
}
return super.onTouchEvent(event);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(Color.GRAY);
canvas.drawPath(mPath, mPaint);
}
public void reset(){
mPath.reset();
postInvalidate();
}
public void setQuadOrLine(boolean quadOrLine){
this.lineOrQuad = quadOrLine;
}
}


在ACTION_DOWN的时候,利用 mPath.moveTo(event.getX(),event.getY())将Path的初始位置设置到手指的触点处,如果不调用mPath.moveTo的话,会默认是从(0,0)开始的。然后我们定义两个变量mPreX,mPreY来表示手指的前一个点。我们通过上面的分析知道,这个点是用来做控制点的。最后return true让ACTION_MOVE,ACTION_UP事件继续向这个控件传递。

在ACTION_MOVE的时候,我们先找到结束点,我们说了结束点是这个线段的中间位置,所以很容易求出它的坐标endX,endY;控制点是上一个手指位置即mPreX,mPreY;那有些同学可能会问了,那起始点是哪啊。在开篇讲quadTo()函数时,就已经说过,第一个起始点是Path.moveTo(x,y)定义的,其它部分,一个quadTo的终点,是下一个quadTo的起始点。 所以这里的起始点,就是上一个线段的中间点。把各个线段的中间点做为起始点和终点,把终点前一个手指位置做为控制点。

4、Path.rQuadTo():

API提供的方法预览:

public void rQuadTo(float dx1, float dy1, float dx2, float dy2);


前面我们说过方法签的单独的r其实就是relaytiveLayout的缩写,明白了这点,就会明白该方法也是一个利用相对位置来自定义View的。

其中:

dx1:控制点X坐标,表示相对上一个终点X坐标的位移坐标,正值表示相加,负值表示相减;
dy1:控制点Y坐标,表示相对上一个终点Y坐标的位移坐标,正值表示相加,负值表示相减;
dx2:终点X坐标,表示相对上一个终点X坐标的位移值,正值表示相加,负值表示相减;
dy2:终点Y坐标,表示相对上一个终点Y坐标的位移值。正值表示相加,负值表示相减;


这四个参数都是传递的都是相对值,相对上一个终点的位移值。

public class WaveView extends View {
private Paint mPaint;
private Path mPath;
private Path mPath2;
private int mItemWaveLength = 400;//波长
private int mItemWaveLength2 = 900;//波长
private int dx = 0;
public WaveView(Context context) {
super(context);
mPaint = new Paint();
mPaint.setStrokeWidth(5);
mPaint.setAntiAlias(true);
mPaint.setStyle(Paint.Style.FILL);
mPath = new Path();
mPath2 = new Path();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPath.reset();
mPath2.reset();
int originY = 500;//波峰距离控件顶部的距离
int halfWaveLen = mItemWaveLength / 2;
float Height = 150;//波浪的高度的一半(波峰与波谷的垂直距离)
//将mPath的起始位置向左移一个波长:
mPath.moveTo(-mItemWaveLength + dx, originY);
mPath2.moveTo(-mItemWaveLength2 * 1.15f + dx, originY);
//for循环画出当前屏幕中可能容得下的所有波(因将mPath的起始位置向左移一个波长,同理也向右移一个波长,故乘2)
for (int i=-mItemWaveLength;i <= getWidth() + mItemWaveLength * 2; i += mItemWaveLength){
//画的是一个波长中的前半个波;halfWaveLen表示波的高度
mPath.rQuadTo(halfWaveLen / 2, Height / 2, halfWaveLen, 0);
mPath2.rQuadTo(halfWaveLen / 2, Height / 2, halfWaveLen, 0);
//画的是一个波长中的后半个波,波的深度,与高度合起来就是波高
mPath.rQuadTo(halfWaveLen / 2, -Height / 2, halfWaveLen, 0);
mPath2.rQuadTo(halfWaveLen / 2, -Height / 2, halfWaveLen, 0);
}
mPath.lineTo(getWidth(), getHeight());
mPath.lineTo(0, getHeight());
mPath.close();
mPath2.lineTo(getWidth(), getHeight());
mPath2.lineTo(0, getHeight());
mPath2.close();
mPaint.setColor(Color.parseColor("#428ddd"));
canvas.drawPath(mPath,mPaint);
mPaint.setAlpha(20);
mPaint.setColor(Color.parseColor("#43c5dd"));
canvas.drawPath(mPath2,mPaint);
}
public void startAnim(){
ValueAnimator animator = ValueAnimator.ofInt(0, mItemWaveLength);
animator.setDuration(500);//动画时间
animator.setRepeatCount(ValueAnimator.INFINITE);
animator.setInterpolator(new LinearInterpolator());
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
dx = (int)animation.getAnimatedValue();
postInvalidate();
}
});
animator.start();
}
}


四、三阶贝赛尔

三阶贝赛尔曲线的使用与二阶贝塞尔曲线类似,不同的是需要两个控制点,

public void cubicTo(float x1,float y1,float x2,float y2,float x3,float y3);
public void rCubicTo(float x1,float y1,float x2,float y2,float x3,float y3);


后面有更多的使用体会再来细说这个,在此先行略过了。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: