自定义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、二阶贝赛尔曲线:
原理:由两个数据点(确定曲线的起始和结束位置),一个控制点(确定曲线的弯曲程度)来描述曲线状态;对应的方法:
3、三阶贝赛尔曲线:
原理:由两个数据点(确定曲线的起始和结束位置),两个控制点(确定曲线的弯曲程度和状态)来描述曲线状态;对应的方法:
这里,关于数据点和控制点的说明如下:
三阶曲线相比于二阶曲线可以制作更加复杂的形状,但是对于高阶的曲线,用低阶的曲线组合也可达到相同的效果,就是传说中的降阶。因此我们对贝塞尔曲线的封装方法一般最高只到三阶曲线。
关于降阶和升阶的说明:
三、二阶贝塞尔曲线的使用
1、二阶贝塞尔曲线quadTo的使用:
参数中(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、代码示例:
3、使用Path.lineTo()所存在问题:
当用Path.lineTo()绘制图形,尤其是有圆角弧度的图形的时候,在转角或者圆弧出,会出现明显的马赛克样的锯齿之类的东西,一点也不平滑,看起来不自在,尤其是图形比较大的时候。这就需要优化,实现线与线之间的平滑过渡;优化有两个方案:
两个方案可以结合起来使用,效果更佳。
在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提供的方法预览:
前面我们说过方法签的单独的r其实就是relaytiveLayout的缩写,明白了这点,就会明白该方法也是一个利用相对位置来自定义View的。
其中:
这四个参数都是传递的都是相对值,相对上一个终点的位移值。
四、三阶贝赛尔
三阶贝赛尔曲线的使用与二阶贝塞尔曲线类似,不同的是需要两个控制点,
后面有更多的使用体会再来细说这个,在此先行略过了。
在数学的数值分析领域中,贝赛尔曲线(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);
后面有更多的使用体会再来细说这个,在此先行略过了。
相关文章推荐
- 自定义View学习笔记08—Path基本操作
- 自定义View学习笔记(一)
- View自定义学习摘要笔记(二)
- Android学习个人笔记1-Layout界面布局-xml,java混合模式-自定义view
- Android 学习笔记 初学自定义viewgroup
- android自定义view学习笔记1
- 自定义View学习笔记01—基础理论
- 自定义View学习笔记02—View的几个重要方法
- 学习鸿洋大神的自定义View(一)的笔记
- Android自定义view学习笔记02
- ASP.NET MVC学习笔记-----使用自定义的View Engine
- 自定义View的onMeasure、onDraw、BitmapShader等等笔记__学习笔记
- Qt学习笔记-----Model/View架构之自定义Model
- android 学习笔记(1) ExpandableListActivity 自定义view
- 3自定义View系列课程学习笔记——之Measure测量
- iOS学习笔记-054.自定义View02——小黄人
- iOS学习笔记01-自定义简单弹出pickerView
- Android自定义View学习笔记04
- iPhone开发学习笔记005——使用XIB自定义一个UIView,然后将这个view添加到controller的view