《Android自定义控件入门到精通》文章索引 ☞ https://blog.csdn.net/Jhone_csdn/article/details/118146683
《Android自定义控件入门到精通》所有源码 ☞ https://gitee.com/zengjiangwen/Code
文章目录
Path
在几何图形的绘制中,我们发现并没有画三角形等多边形的函数,以及如何画任意形状的图形?
Path用来描述一个路径,通过Path可以构建各种形状
三角形
@Override
protected void onDraw(Canvas canvas) {
mPaint.setStyle(Paint.Style.FILL);
Path path=new Path();
path.moveTo(120,20);
path.lineTo(160,80);
path.lineTo(80,80);
path.lineTo(120,20);
canvas.drawPath(path,mPaint);
mPaint.setStyle(Paint.Style.STROKE);
Path path2=new Path();
path2.moveTo(240,20);
path2.lineTo(280,80);
path2.lineTo(200,80);
path2.close();
canvas.drawPath(path2,mPaint);
}
跟我们画画一样,通过path.moveTo(x,y)把笔头移动到一个点开始下笔,通过lineTo(x,y),在纸上画线段,就可以画出自己想要的图形啦,是不是很形象。
可以看到path是通过lineTo(x,y)回到原点闭合路径,path2是通过path2.close()闭合路径,所以起点和终点的闭合可以有这两种方式。
开头我们说过,Path是用来表示路径的,任何图形,都有它的路径,所以,通过Path,我们可以画任意图形
几何图形
addOval(RectF oval,Direction dir)//椭圆
addRect(RectF rect,Direction dir)//矩形
addCircle(float x, float y, float radius,Direction dir)//圆
addArc(RectF oval, float startAngle, float sweepAngle)//弧
addRoundRect(RectF rect, float[] radii, Direction dir)//圆角矩形,radii的size=8,为各个圆角的两个半径
@Override
protected void onDraw(Canvas canvas) {
mPaint.setStyle(Paint.Style.STROKE);
Path path = new Path();
path.addOval(new RectF(20, 20, 40, 80), Path.Direction.CW);
path.addRect(new RectF(80, 20, 120, 80), Path.Direction.CCW);
path.addCircle(180, 60, 40, Path.Direction.CW);
path.addArc(new RectF(20, 160, 40, 240), 30, 160);
path.addRoundRect(new RectF(100, 160, 140, 240), 6, 6, Path.Direction.CCW);
canvas.drawPath(path, mPaint);
}
可以看到,除了path.addArc没有Direction参数外,其它方法都有
-
Direction.CW : clockwise 顺时针
-
Direction.CCW :counter-clockwise 逆时针
在上面的代码中,我们画椭圆和圆是顺时针,画矩形是逆时针,没什么区别啊!?别急,后面我们学Text绘制的时候再来细说
Path的方法
moveTo(float x, float y)//将画笔起点移动到指定位置
rMoveTo(float dx, float dy)//将画笔的起点(x,y)移动到(x+dx,y+dy)
lineTo(float x, float y)//从当前终点位置画直线到(x,y)
rLineTo(float dx, float dy)//从当前终点位置(x,y)画直线到(x+dx,y+dy)
arcTo(RectF oval, float startAngle, float sweepAngle,boolean forceMoveTo)//从当前终点画弧线
quadTo(float x1, float y1, float x2, float y2)//二阶贝塞尔曲线,(x1,y1)为控制点,(x2,y2)为终点
rQuadTo(float dx1, float dy1, float dx2, float dy2)//同理,代表点偏移的距离
cubicTo(float x1, float y1, float x2, float y2,float x3, float y3)//三阶贝塞尔曲线,(x1,y1)、(x2,y2)为控制点,(x3,y3)为终点
rCubicTo(float dx1, float dy1, float dx2, float dy2,float dx3, float dy3)//同理,代表点偏移的距离
arcTo(RectF oval, float startAngle, float sweepAngle,boolean forceMoveTo)
oval:弧度所在的矩形
startAngle:起始的角度
sweepAngle:起点到终点的角度
forceMoveTo:是否将画笔移动到弧的起点
@Override
protected void onDraw(Canvas canvas) {
mPaint.setStyle(Paint.Style.STROKE);
Path path = new Path();
path.moveTo(20, 20);
path.lineTo(80, 20);
path.arcTo(80, 80, 160, 140, 30, 150, false);//见下图,左边为false 右边为true
canvas.drawPath(path, mPaint);
}
贝塞尔曲线
贝塞尔曲线是一个很有意思的东西,深入学习的可以看下这篇博客:贝塞尔曲线
二阶贝塞尔曲线quadratic bezier
在二阶贝塞尔曲线中,已知三点恒定(P0,P1,P2),设定在P0 P1中的点为Pa,在P1 P2中的点为Pb,Pt在Pa Pb上的点,这三点都在相同时间t内做匀速运动,Pt滑过的路径,就是曲线的路径。
也就是说,P0和P2为曲线的起点和终点,通过P1来控制曲线的路径,说着有点像Ps中的钢笔工具啊
二阶贝塞尔曲线:quadTo(float x1, float y1, float x2, float y2)
我们定义P0(50,150),P2(150,150),控制点P1(100,100)
@Override
protected void onDraw(Canvas canvas) {
mPaint.setStyle(Paint.Style.STROKE);
Path path = new Path();
path.moveTo(50, 150);
path.quadTo(100,100,150,150);
canvas.drawPath(path, mPaint);
}
二阶贝塞尔曲线应用:如何实现绘制手势轨迹?
首先我们用lineTo()来模拟手势轨迹效果
private Path mPath;
private void init() {
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setColor(Color.parseColor("#ff0000"));
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(2);
mPath = new Path();
}
@Override
protected void onDraw(Canvas canvas) {
canvas.drawPath(mPath, mPaint);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mPath.moveTo(event.getX(), event.getY());
break;
case MotionEvent.ACTION_MOVE:
mPath.lineTo(event.getX(),event.getY());
invalidate();
break;
}
return super.onTouchEvent(event);
}
可以看到,用lineTo()的方式画出的轨迹并不圆滑,因为点与点之间是通过线段(line)连接的
那我们可以通过二阶贝塞尔曲线实现更加圆滑的手势轨迹,起点和终点好说,这个控制点该怎么确定呢?我们不防用Ps来模拟一下。
整个曲线过程为以下四步完成:
- path.moveTo(P0)
- path.quadTo(P0,(P0+P2)/2)
- path.quadTo(P2,(P2+P3)/2)
- path.quadTo(P3,(P3+P4)/2)
最后P3-P4中点到P4直接省略不画,不影响最终的结果(手势点之间的距离很密的,不可能看出来)
private Path mPath;
private void init() {
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setColor(Color.parseColor("#ff0000"));
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(2);
mPath = new Path();
}
@Override
protected void onDraw(Canvas canvas) {
canvas.drawPath(mPath, mPaint);
}
private float startX,startY;
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
startX=event.getX();
startY=event.getY();
mPath.moveTo(startX,startY);
break;
case MotionEvent.ACTION_MOVE:
mPath.quadTo(startX,startY,(startX+event.getX())/2f,(startY+event.getY())/2f);
startX=event.getX();
startY=event.getY();
invalidate();
break;
}
return super.onTouchEvent(event);
}
三阶贝塞尔曲线cubic bezier
三阶贝塞尔曲线有两个控制点(P1,P2),也可以看作是二阶贝塞尔曲线,不过控制点是在P1-P2上动态变化的
三阶贝塞尔曲线:cubicTo(float x1, float y1, float x2, float y2,float x3, float y3)
前面我们一直用Ps钢笔工具演示了曲线,其实从整个曲线来看是用的三阶贝塞尔曲线特性,因为有两个控点,只是正好两个控点是相对的,且我们观察的某一小段曲线,用二阶贝塞尔曲线是几乎不可能画出连续圆滑的一条曲线的,如下图:
而现实需求是给出几个峰值和谷值,画出走势图
如绿色曲线段,影响它的控点为P1和P2,x坐标为始终点的中点
我们用cubicTo()来模拟实现一条曲线
@Override
protected void onDraw(Canvas canvas) {
ArrayList<Point> list = new ArrayList<>();
list.add(new Point(0, 0));
list.add(new Point(30, 60));
list.add(new Point(60, 30));
list.add(new Point(120, 100));
list.add(new Point(180, 80));
list.add(new Point(260, 160));
list.add(new Point(400, 60));
Path path = new Path();
Point P1, P2;
for (int i = 0; i < list.size(); i++) {
if (i == 0) {
path.moveTo(list.get(i).x, list.get(i).y);
} else{
P1 = new Point((list.get(i - 1).x + list.get(i).x) / 2, list.get(i-1).y);
P2 = new Point((list.get(i - 1).x + list.get(i).x) / 2, list.get(i).y);
path.cubicTo(P1.x,P1.y,P2.x,P2.y,list.get(i).x,list.get(i).y);
}
}
canvas.drawPath(path, mPaint);
}
这个例子中呢,我们的控杆是平行X轴的,所以保证了给出的点一定为波峰和波谷。
在工作中,我们常常为了使两段曲线完美衔接,采用另外一种确定控制点的方式
通过这种方式可以很好的、圆滑过度的连接两条曲线。