自定义View学习笔记08—Path基本操作

从本篇笔记开始,我们进入上一篇笔记里面提到的大杀器Path的世界。
同样的,我们依循惯例先来看看Path的常用方法(API21以下的):

Path的常用方法

同样的,使用到Path,也要关闭硬件加速。方法同前。

Path作用
之所以称Path是一个大杀器,并单独来学习,就是因为Path不仅能够绘制简单图形,也可以绘制这些比较复杂的图形。另外,根据路径绘制文本和剪裁画布都会用到Path。在2D绘图中基本离不开Path。

Path含义
Path封装了由直线和曲线(二次,三次贝塞尔曲线)构成的几何路径。你能用Canvas中的drawPath来把这条路径画出来(同样支持Paint的不同绘制模式),也可以用于剪裁画布和根据路径绘制文字。我们有时会用Path来描述一个图像的轮廓,所以也会称为轮廓线(轮廓线仅是Path的一种使用方法,两者并不等价)。
另外路径有开放和封闭的区别。开放路径:没有首位相接形成封闭区域;封闭路径:首尾相接形成了一个封闭区域。

Path使用详解
第1组: lineTo、moveTo、 setLastPoint和 close

lineTo:是指从某个点到参数坐标点之间连一条线,这里的某个点就是上次操作结束的点,如果没有进行过操作则默认点为坐标原点。

依照惯例我们先看看API提供的方法:

public void lineTo (float x, float y)

就是指指从某个点到参数坐标点之间连一条线,这里的“某个点”指的是默认的点(如坐标原点或者上一次操作划线的点),如下:

Path mPath = new Path(); 
Path.lineTo(100,24); 
Path.lineTo(520,550); 

这里我们调用了两次lineTo,第一次由于之前没有过操作,所以默认点就是坐标原点左上角(这里是View的左上角),结果就是坐标原点到A(100,24)之间连直线。
第二次lineTo,由于我们已经操作过一次了,所以这次的起点就是上一次操作过的终点(100,24),其结果就是绘制了一条从坐标(100,24)到(520,550)的点:

lineTo

这里我们以常见的图表为例做进一步的学习:

private void drawLineTo(Canvas canvas, Paint mPaint){
    //默认起始点为上次操作结束的点或者坐标原点(屏幕或者控件的左上角)。
    //如下,做了canvas.translate(10, hei-10)之后,默认点为(10, hei-10)处。
    canvas.drawColor(Color.parseColor("#dfdfdf"));
    canvas.translate(10, hei-10);
    //准备数据
    float[] x = {100, 150, 200, 250, 300, 350, 400, 450, 500, 550, 600};
    float[] y = {24, 150, 380, 73, 400, 220, 245, 280, 350, 465, 500};
    List<PathCanvasBean> pathList = new ArrayList<>();
    for (int i = 0; i < x.length; i++) {
        pathList.add(new PathCanvasBean(x[i], y[i]*(-1)));
    }

    //绘制折线
    mPaint.setStrokeWidth(4);
    //线冒样式(相当于给原来的直线末端加上一个帽子):圆形,方块,无;
    //http://blog.csdn.net/harvic880925/article/details/51010839
    mPaint.setStrokeCap(Paint.Cap.SQUARE);
    //设置线段连接处过渡样式:锐角过渡(尖锐的意思),圆弧过渡,直线过渡;
    mPaint.setStrokeJoin(Paint.Join.ROUND);
    mPaint.setColor(Color.parseColor("#93A5F4"));
    Path mPath = new Path();
    //设置折线的起点位置,否则折线起点是系统默认的左上角或者View的左上角。
    mPath.moveTo(pathList.get(0).getX(), pathList.get(0).getY());
    for (int i = 0; i < x.length; i++) {
        mPath.lineTo(pathList.get(i).getX(), pathList.get(i).getY());
        canvas.drawPath(mPath, mPaint);
    }

    float textSize = 12;
    mPaint.setAntiAlias(true);
    mPaint.setColor(Color.BLACK);
    mPaint.setTextSize(textSize);
    mPaint.setStrokeWidth(1);

    //cirPaint用于画每段线段之间的点
    Paint cirPaint = new Paint();
    cirPaint.setAntiAlias(true);
    cirPaint.setStyle(Paint.Style.FILL_AND_STROKE);
    cirPaint.setColor(Color.WHITE);
    for (int i = 0; i < x.length; i++) {
        //绘制折线转折出的点
        int x = pathList.get(i).getX();
        int y = pathList.get(i).getY();
        canvas.drawCircle(x, y, 5, cirPaint);
        String coordinate = "(" + x + ",  " + y*(-1) + ")";
        //绘制每个点对应的坐标值,至于为什么这里要分别加10和textSize/2,
        //掌握了上一篇笔记“自定义View学习笔记07—Canvas绘制文字”的都应该知道
        canvas.drawText(coordinate, x + 10, y + textSize/2, mPaint);
    }
}

在onDraw()方法里面调用上述方法,运行结果如下:
Path之lineTo

moveTo 和 setLastPoint
依照惯例,我们先看下API提供的方法:

public void moveTo (float x, float y);
public void setLastPoint (float dx, float dy);

这两个方法是完全不相同的
moveTo和setLastPoint的区别

现在用代码来区别:

//moveTo的用法
private void drawMoveTo(Canvas canvas, Paint mPaint){
    Path mPath = new Path();
    mPath.moveTo(50, 50);
    mPath.lineTo(500,60);
    mPath.moveTo(180,160);
    mPath.lineTo(520,550);
    canvas.drawPath(mPath, mPaint);
    //moveTo:移动起点到:x, y;
    //含义解说:mPath.moveTo(50, 50):折现起点/原点,
    // mPath.lineTo(500,60):从起点/原点划线到(500,60);
    //然后将绘图起点移到【mPath.moveTo(180,160)】(180,160),
    //再从(180,160)划线到(520,550)【mPath.lineTo(520,550)】;
}

//setLastPoint的用法
private void drawLastPoint(Canvas canvas, Paint mPaint){
    Path mPath = new Path();
    mPath.moveTo(50,50);
    mPath.lineTo(100,24);
    mPath.setLastPoint(200,320);
    mPath.lineTo(500,580);
    canvas.drawPath(mPath, mPaint);
    //setLastPoint是重置上一次操作的最后一个点,在执行完第一次的lineTo的时候,
    // 最后一个点是A(100,24),而setLastPoint更改最后一个点为C(300,220),
    // 所以在实际执行的时候,第一次的lineTo就不是从原点O到A(100,24)的连线了,
    // 而变成了从原点(50,50)到C(200,320)之间的连线。然后再从(200,320)划线到(500,580)
}

在onDraw()方法里面分别调用上述方法,二者运行效果如下:
moveTo运行效果:
moveTo运行效果

setLastPoint运行效果
setLastPoint运行效果

close:
依照惯例,我们先看下API提供的方法:

public void close ()

主要作用是:用于连接当前最后一个点和最初的一个点(如果两个点不重合的话),形成一个封闭的图形。

private void drawClose(Canvas canvas, Paint mPaint){
    Path closePath = new Path();
    closePath.moveTo(50,50);
    closePath.lineTo(120, 260);
    closePath.lineTo(300,400);
    closePath.lineTo(500,300);
    closePath.lineTo(400,500);
    closePath.close();
    canvas.drawPath(closePath, mPaint);
}

在onDraw()方法里面调用上述方法,运行效果如下:
close的效果

注意:
close的作用是封闭路径,与连接当前最后一个点和第一个点,形成封闭图形。如果连接了最后一个点和第一个点仍然无法形成封闭图形,则close什么 也不做。

第2组: addXxx、arcTo
主要作用是在Path中添加基本图形,重点区分addArc与arcTo。

第一类addXxx(基本形状)方法预览:

// 第一类(基本形状)
public void addCircle(float x,float y,float radius,Path.Direction dir) // 圆形
public void addOval(RectF oval, Path.Direction dir)//椭圆
public void addRect(float left,float top,float right,float bottom,
									Path.Direction dir)//矩形
public void addRect(RectF rect, Path.Direction dir)//矩形
public void addRoundRect(RectF rect,float[]radii,Path.Direction dir)//圆角矩形
public void addRoundRect(RectF rect,float rx,float ry,Path.Direction dir)//圆角矩形

这一类就是在path中添加一个基本形状,基本形状部分和前面所讲的绘制基本形状并无太大差别,使用上都是通过path.addXxx的方式调用,最后再通过canvas.drawPath()的方法绘制图形,如下:

private void drawRect(Canvas canvas, Paint mPaint){
    canvas.translate(wid / 2, hei / 2);
    Path rectPath = new Path();

    rectPath.addCircle(0, 0, 100, Path.Direction.CCW);//圆

    RectF rect = new RectF(-200,-200, 200, 200);//矩形
    rectPath.addRect(rect, Path.Direction.CW);

    RectF rect1 = new RectF(-200,-100, 200, 100);//椭圆
    rectPath.addOval(rect1, Path.Direction.CW);

    RectF rect2 = new RectF(-250,-200, 250, 200);//圆角矩形
    rectPath.addRoundRect(rect2, 50, 50, Path.Direction.CCW);
    canvas.drawPath(rectPath, mPaint);
}

在onDraw方法中调用上方法,运行效果如下:
addXxx系列方法

方法中的参数含义都应该能理解到,这里重点讲解Path.Direction,Direction的意思是方向、趋势,包含两个枚举常量:
Path.Direction释疑
至于对闭合顺序到底有啥影响,图形的渲染等问题等请慢慢看下去。
a、先研究确定CW的闭合顺序的问题,添加一个矩形试试看:

private void addRxxCW(){
	canvas.translate(wid / 2, hei / 2);
	Path rectPath = new Path();
	RectF rect = new RectF(-200,-200, 200, 200);
	rectPath.addRect(rect, Path.Direction.CW);
	canvas.drawPath(rectPath, mPaint);
}

在onDraw方法中调用上方法,运行效果如下:
CW的闭合顺序

b、再确定CCW的闭合顺序的问题,添加一个矩形试试看:

private void addRxxCCW(){
	canvas.translate(wid / 2, hei / 2);
	Path rectPath = new Path();
	RectF rect = new RectF(-200,-200, 200, 200);
	rectPath.addRect(rect, Path.Direction.CCW);
	canvas.drawPath(rectPath, mPaint);
}

在onDraw方法中调用上方法,运行效果如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4IyoB2Lf-1589615279337)(https://img-blog.csdn.net/20171229101334625?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvaGFveXVlZ29uZ3pp/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)]

运行结果一模一样,感觉很神奇,并且,你他妈的都一模一样还弄两个出来搞毛啊,消遣人么?其实这个东东是自带隐身技能的,想要让它现出原形,就要用到咱们刚刚学到的setLastPoint(重置当前最后一个点的位置)。

在addRxxCCW()方法中的canvas.drawPath(rectPath, mPaint);之前再加一行代码:rectPath.setLastPoint(-250, 250);运行结果如下,可以明显看到,图形发生了奇怪的变化
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-63nJhW8s-1589615279339)(https://img-blog.csdn.net/20171229101549426?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvaGFveXVlZ29uZ3pp/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)]

需要注意的是,交换坐标点的顺序可能就会影响到某些绘制内容哦,例如上面的例子,你可以尝试交换两个坐标点,或者指定另外两个点来作为参数,虽然指定的是同一个矩形,但实际绘制出来是不同的————参数中点的顺序很重要!参数中点的顺序很重要!参数中点的顺序很重要!一定要记得,重要的事情说三遍。

第二类(Path)方法预览:

// 第二类(Path)
public void addPath (Path src)
public void addPath (Path src, float dx, float dy)
public void addPath (Path src, Matrix matrix)

第一个方法相对比较简单,也很容易理解,就是将两个Path合并成为一个;
第二个方法比第一个方法多出来的两个参数是将src进行了位移之后再添加进当前path中;
第三个方法是将src添加到当前path之前先使用Matrix进行(矩阵)变换。难度一个个的增大。

下面我们用哪个代码来做演示:

private void addPathPaint(){
	canvas.translate(wid / 2, hei / 2);
	canvas.scale(1, -1);
	Path mergePath = new Path();
	Path src = new Path();
	mergePath.addRect(-100,-100,100,100, Path.Direction.CW);//矩形
	src.addCircle(0,100,80, Path.Direction.CW);//圆
	mergePath.addPath(src);//将圆添加到矩形path可以,反过来则出问题
	canvas.drawPath(mergePath, mPaint);
}

在onDraw方法中调用上方法,运行效果如下:

addPath演示

第三类(addArc与arcTo) 方法预览:

public void addArc(RectF oval,float startAngle,float sweepAngle);
public void arcTo(RectF oval,float startAngle,float sweepAngle); 
public void arcTo(RectF oval,float startAngle,float sweepAngle,boolean forceMoveTo);

addArc与arcTo的区别:
addArc与arcTo的区别

其中参数的含义如下:
addArc与arcTo参数解释

特别注意:
sweepAngle取值范围是 [-360, 360),当 >= 360 或者 < -360 时将不会绘制任何内容, 对于360,你可以用一个接近的值替代,例如: 359.99。

forceMoveTo作用: 是否使用moveTo强制将变量移动到圆弧的起点位移。

addArc ()示例:

private void drawAddArc(Canvas canvas, Paint mPaint){
    canvas.translate(wid / 2, hei / 2);// 移动坐标系到屏幕中心
    Path path = new Path();        
    path.lineTo(141.5f, 141.5f);//画直线       
    RectF oval = new RectF(-200, -200, 200, 200);//画圆弧
	//path.addArc(oval, 135, 270);
    path.arcTo(oval, 135, 270, true);//和上面一句作用等价
    canvas.drawPath(path, mPaint);
}

在onDraw方法中调用上方法,运行效果如下:
addArc ()

arcTo ()示例:

private void drawArcTo(Canvas canvas, Paint mPaint){
    canvas.translate(wid / 2, hei / 2);  // 移动坐标系到屏幕中心
    Path path = new Path();
    path.lineTo(141.5f, 141.5f);

    RectF oval = new RectF(-200, -200, 200, 200);
	//path.addArc(oval, 135, 270);
    //arcTo中Boolean值作用:是否形成封闭的图形
    path.arcTo(oval, 135, 270, false);//和上一行代码作用等价
    canvas.drawPath(path,mPaint);
}

在onDraw方法中调用上方法,运行效果如下:
arcTo ()

第3组:isEmpty、 isRect、isConvex、 set 、offset:
这一组比较简单,稍微说一下就可以了。

isEmpty()方法预览:

public boolean isEmpty ();作用是判断path中是否包含内容。	

Path path = new Path(); 
Log.e("1",path.isEmpty()+"");
path.lineTo(100,100);
Log.e("2",path.isEmpty()+"");

log输出结果: com.sloop.canvas E/1: true;;com.sloop.canvas E/2: false

isRect()方法预览:

public boolean isRect (RectF rect); 作用是判断path是否是一个矩形,如果是,会将矩形的信息存放进参数rect中

path.lineTo(0,400);	
path.lineTo(400,0);	path.lineTo(0,0);
RectF rect = new RectF();		
boolean b = path.isRect(rect);
Log.e("Rect","isRect:"+b+"| left:"+rect.left+"| top:"+rect.top+"| right:"+rect.right+"| bottom:"+rect.bottom);

log 输出结果:
com.sloop.canvas E/Rect: isRect:true| left:0.0| top:0.0| right:400.0| bottom:400.0

set()方法预览:

public void set (Path src); 将新的path赋值到现有path。

canvas.translate(mWidth / 2, mHeight / 2);  // 移动坐标系到屏幕中心
canvas.scale(1,-1);// <-- 注意 翻转y坐标轴
Path path = new Path();// path添加一个矩形
path.addRect(-200,-200,200,200, Path.Direction.CW);
Path src = new Path();// src添加一个圆
src.addCircle(0,0,100, Path.Direction.CW);
path.set(src);// 大致相当于 path = src;
canvas.drawPath(path,mPaint);

运行结果如下:
set()方法

offset()方法预览:

public void offset (float dx, float dy);
public void offset (float dx, float dy, Path dst);

这个的作用就是对path进行一段平移,它和Canvas中的translate作用很像,但Canvas作用于整个画布,而path的offset只作用于当前path。第二个方法的dst参数是存储平移后的path的: dst不为空, 将当前path平移后的状态存入dst中,不会影响当前path; dst为空(null),平移将作用于当前path,相当于第一种方法。

canvas.translate(mWidth / 2, mHeight / 2);// 移动坐标系到屏幕中心
canvas.scale(1,-1);// <-- 注意 翻转y坐标轴
Path path = new Path();// path中添加一个圆形(圆心在坐标原点)
path.addCircle(0,0,100, Path.Direction.CW);
Path dst = new Path();// dst中添加一个矩形
dst.addRect(-200,-200,200,200, Path.Direction.CW);
path.offset(300,0,dst);// 平移
canvas.drawPath(path,mPaint);// 绘制path
mPaint.setColor(Color.BLUE);// 更改画笔颜色
canvas.drawPath(dst,mPaint);// 绘制dst

运行结果如下:
offset()方法

第4组: rXxx方法,该方法整体上来讲就是利用相对坐标编程。
rXxx方法的坐标使用的是相对位置(基于当前点的位移),而之前方法的坐标是绝对位置(基于当前坐标系的坐标)。最前面的r就是RelativeLayout的缩写。

使用如下:

private void drawRxxx(Canvas canvas, Paint mPaint){
    Path path = new Path();
    path.lineTo(100, 200);
    canvas.drawPath(path, mPaint);
    Path path1 = new Path();
    path1.moveTo(100, 100);
    //Rxxx系列方法:相对坐标编程
    //相对于上一个点(100, 100);绝对坐标下的坐标为:(200, 300)
    path1.rLineTo(100, 200);
    canvas.drawPath(path1, mPaint);
    canvas.drawCircle(200, 300, 10, mPaint);
}

onDraw方法里面利用上述方法啊,运行结果如下:
rXxx方法

此处仅以 rLineTo 为例,只要理解 “绝对坐标” 和 “相对坐标” 的区别,其他方法类比即可。

布尔操作(API19):
布尔操作与我们中学所学的集合操作非常像,只要知道集合操作中的子集,交集,并集,差集,补集等操作,那么理解布尔操作也是很容易的。

布尔操作是两个Path之间的运算,主要作用是用一些简单的图形通过一些规则合成一些相对比较复杂,或难以直接得到的图形。

布尔运算的核心:布尔逻辑,它有五种逻辑,如下:
布尔操作的5种逻辑

在Path中的布尔运算有两个方法:

boolean op (Path path, Path.Op op);
boolean op (Path path1, Path path2, Path.Op op);

两个方法中的返回值用于判断布尔运算是否成功,它们使用方法如下:
对 path1 和 path2 执行布尔运算,运算方式由第二个参数指定,运算结果存入到path1中。

path1.op(path2, Path.Op.DIFFERENCE);
// 对 path1 和 path2 执行布尔运算,运算方式由第三个参数指定,运算结果存入到path3中。
path3.op(path1, path2, Path.Op.DIFFERENCE);

如太极中的阴阳鱼,如果用贝塞尔曲线制作的话,可能需要六段贝塞尔曲线才行,而在这里我们可以用四个Path通过布尔运算得到,而且会相对来说更容易理解一点。

这里我就用画阴阳鱼来演示布尔操作:

private void drawTaiJi(Canvas canvas, Paint mPaint){
    canvas.translate(wid / 2, hei / 2);
    mPaint.setStrokeWidth(5);
    mPaint.setColor(Color.BLACK);
    mPaint.setAntiAlias(true);
    mPaint.setStyle(Paint.Style.FILL);

    Path path1 = new Path();
    Path path2 = new Path();
    Path path3 = new Path();
    Path path4 = new Path();

    path1.addCircle(0, 0, 200, Path.Direction.CW);
    path2.addRect(0, -200, 200, 200, Path.Direction.CW);
    path3.addCircle(0, -100, 100, Path.Direction.CW);
    path4.addCircle(0, 100, 100, Path.Direction.CCW);

    path1.op(path2, Path.Op.DIFFERENCE);
    path1.op(path3, Path.Op.UNION);
    path1.op(path4, Path.Op.DIFFERENCE);
    canvas.drawPath(path1, mPaint);

    mPaint.setStyle(Paint.Style.STROKE);
    canvas.drawCircle(0, 0, 200, mPaint);

    mPaint.setStyle(Paint.Style.FILL);
    canvas.drawCircle(0, 100, 30, mPaint);
    mPaint.setColor(Color.WHITE);
    canvas.drawCircle(0, -100, 30, mPaint);
}

onDraw方法里面利用上述方法啊,运行结果如下:
用布尔操作绘制太极阴阳鱼

Path的基本操作到此结束。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值