Path 从懵逼到精通——基本操作

12,199 阅读15分钟

什么是Path?

我们先看看Android官方文档给出的定义:

The Path class encapsulates compound (multiple contour) geometric paths consisting of straight line segments, quadratic curves, and cubic curves. It can be drawn with canvas.drawPath(path, paint), either filled or stroked (based on the paint's Style), or it can be used for clipping or to draw text on a path.

这里大概翻译就是:
Path类封装了直线段,二次贝塞尔曲线和三次贝塞尔曲线的几何路径。
可以使用Canvas中drawPath方法将Path画出来。Path不仅可以使用Paint的填充模式和描边模式,也可以用画布裁剪和或者画文字。

总而言之,Path就是可以画出通过直线或者曲线的各种组合就可以做出很多很牛X的效果。

至于Path能做出多牛X的效果?上图给你们看看:






怎么使用Path?

要想用Path做出牛X的效果之前,就需要熟悉它的基本操作,这篇文章主要介绍的是Path的一些基本API,进阶的用法将会放在下一篇文章。

以下是Path的基本操作的方法:

第一类(直线与点的操作):lineTo,moveTo,setLastPoint,close

第二类(基本形状):

addXxx,arcTo

第三类(设置方法) :

set(),offset(),reset()

第四类(判断方法) :isConvex(),isEmpty(),isRect(RectF rect)

在说这些方法之前都要做一个画笔的初始化,代码如下:

private void initPaint() {
  mPaint = new Paint();       // 创建画笔
  mPaint.setColor(Color.BLACK);  // 画笔颜色 - 黑色
  mPaint.setStyle(Paint.Style.STROKE);  // 填充模式 - 描边
  mPaint.setStrokeWidth(10);  
}

第一类(直线与点的操作):

1.1 lineTo:

方法预览:
public void lineTo (float x, float y)
有什么用:

顾名思义,这个方法就是画一条直线的。确定一条直线需要两个点,但是这个方法里只提供了一个点的坐标啊?那另一个点的坐标是什么呢?这个点其实就是Path对象上次调用的最后一个点的坐标,如果在调用lineTo()方法前,并没有调用过任何Path的操作,那这个点就默认为坐标原点。

怎么用:

画出直线:

    Path path = new Path(); //创建Path对象
    path.lineTo(300, 300); //创建一条从原点到坐标(300,300)的直线
    canvas.drawPath(path, mPaint);//画出路径

效果如下:


path.lineTo(300, 300);

这个时候我在path.lintTo(300,300),后面再加一句 path.lineTo(100, 200); 看看效果如何?

    Path path = new Path(); //创建Path对象
    path.lineTo(300, 300); //创建一条从原点到坐标(300,300)的直线
    path.lineTo(100, 200); //创建从(300,300)到(100,200)的一条直线 
    canvas.drawPath(path, mPaint);//画出路径

效果如下:


path.lineTo(100, 200);

可以看到第二段线段的是从(300,300)到(100,200)的,那就可以知道lineTo方法的连接的起点是由lineTo方法上一个Path操作决定的。

1.2 moveTo:

方法预览:
public void moveTo(float x, float y)
有什么用:

这个方法的作用就是将下次画路径起点移动到(x,y)

怎么用:

还是用上面的代码:

    Path path = new Path(); //创建Path对象
    path.lineTo(300, 300); //创建一条从原点到坐标(300,300)的直线
    path.moveTo(0,0);  //将下一次操作路径的起点坐标移到(0,0)
    path.lineTo(100, 200); //创建从(0,0)到(100,200)的一条直线 
    canvas.drawPath(path, mPaint);//画出路径

效果如下:


path.moveTo(0,0);

可以看到在path.lineTo(100, 200);之前调用了path.moveTo(0, 0);方法,那就将lineTo的操作起始点移动到(0,0)。

1.3 setLastPoint:

方法预览:
public void setLastPoint(float dx, float dy)
有什么用:

改变上一次操作路径的结束坐标点

怎么用:
    Path path = new Path(); //创建Path对象
    path.lineTo(300, 300); //创建一条从原点到坐标(300,300)的直线
    path.setLastPoint(500,500);  //将上一次的操作路径的终点移动到(500,500)
    path.lineTo(100, 200); //创建从(0,0)到(100,200)的一条直线 
    canvas.drawPath(path, mPaint);//画出路径
效果如下:

path.setLastPoint(500,500);

可以知道在执行lineTo(100, 100)时坐标点是(100,100),使用setLastPoint(500, 500)后就变成(500,500),并且也会影响上一次操作路径的终点。

在这里我们就可以总结:

方法 作用
moveTo 会影响下次操作,不会影响上一次操作
setLastPoint 会影响下次操作,也会影响上一次操作

1.4 close:

方法预览:
public void close()
有什么用:

封闭当前路径,如果当前的点不等于路径的起始点,就会在整个操作的最后的点与起始点之间添加线段。

怎么用:
    Path path = new Path(); //创建Path对象
    path.lineTo(300, 300); //创建一条从原点到坐标(300,300)的直线
    path.lineTo(100, 200); //创建从(100,200)到(100,200)的一条直线 
    path.close(); //封闭路径
    canvas.drawPath(path, mPaint);//画出路径
效果如下:

path.close();

可以看到在执行close方法之后,在(100,200)与(0,0)之间添加了一条直线

第二类(基本形状):

2.1 addXxx,arcTo

方法预览:
//矩形             
public void addRect(RectF rect, Direction dir)

public void addRect(float left, float top, float right, float bottom, Direction dir)

//圆形
public void addCircle(float x, float y, float radius, Direction dir)


//圆角矩形
public void addRoundRect(RectF rect, float[] radii, Direction dir)

public void addRoundRect (float left,float top,float right,float bottom,float rx,float ry,Path.Direction dir)

public void addRoundRect (RectF rect,float[] radii,Path.Direction dir)

public void addRoundRect (float left,float top,float right,float bottom,float[] radii,Path.Direction dir)

//椭圆
public void addOval(RectF oval, Direction dir)

public void addOval (float left,float top,float right,float bottom,Path.Direction dir)

//圆弧
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)

// 添加Path
public void addPath (Path src)
public void addPath (Path src, float dx, float dy)
public void addPath (Path src, Matrix matrix)
2.1.1 addRect(矩形):
方法预览:
public void addRect(RectF rect, Direction dir)

public void addRect(float left, float top, float right, float bottom, Direction dir)
有什么用:

画出一个矩形

怎么用:
Path path = new Path();  //创建Path对象
RectF rect = new RectF(0, 0, 400, 400);
path.addRect(rect,Path.Direction.CW);
//path.addRect(0, 0, 400, 400, Path.Direction.CW);
//这个方法与上一句是同样的效果
canvas.drawPath(path, mPaint);
效果图:

path.addRect
解释:

addRect两个方法当中前面的所有参数其实都是确定一个矩形,这里就不说矩形的原理了,现在重点来说一下addRect的最后一个参数:Path.Direction dir。

这个参数是什么意思呢?这个参数就是确定当画这个矩形的时候究竟是顺时针方向画呢?还是用逆时针方向画。

Path.Direction.CW代表顺时针,Path.Direction.CCW代表逆时针。那这个方法究竟从哪个点开始画呢?我们来验证一下

Path path = new Path();
path.addRect(0, 0, 400, 400, Path.Direction.CW);
path.setLastPoint(0, 300);
canvas.drawPath(path, mPaint);

我们在addRect之后增加setLastPoint方法,重新设置最后一个点的坐标。

效果如下:

path.addRect(0, 0, 400, 400, Path.Direction.CW);

如果这个时候我们将矩形的方向换成逆时针方向,看看效果如何:

Path path = new Path();
path.addRect(0, 0, 400, 400, Path.Direction.CCW);
path.setLastPoint(300,0);
canvas.drawPath(path, mPaint);
效果如下:

path.addRect(0, 0, 400, 400, Path.Direction.CCW);

从以上两个效果就知道,addRect方向是从左上上角开始算起的。所以顺时针和逆时针的方向是会影响到绘制效果的。

2.1.2 addCircle(圆形):
方法预览:

public void addCircle(float x, float y, float radius, Direction dir)

有什么用:

画出一个圆形

怎么用:
    Path path = new Path(); 
    path.addCircle(200, 200, 100, Direction.CW); //创建一个圆心坐标为(200,200),半径为100的圆
    canvas.drawPath(path, mPaint);
效果如下:

path.addCircle(200, 200, 100, Direction.CW);
2.1.3 addRoundRect(圆角矩形):
方法预览:
public void addRoundRect(RectF rect, float rx, float ry, Direction dir)

public void addRoundRect (float left,float top,float right,float bottom,float rx,float ry,Path.Direction dir)

public void addRoundRect (RectF rect,float[] radii,Path.Direction dir)

public void addRoundRect (float left,float top,float right,float bottom,float[] radii,Path.Direction dir)
有什么用:

画出一个圆角矩形

怎么用:

在说这个方法怎么用之前,要先说一下圆角矩形的构成原理。
请看下面这幅图:


圆角矩形.jpg

圆角矩形的圆角其实就是一段圆弧,圆弧需要什么才能确定它的位置和大小呢?答案就是圆心和半径,那为什么上面的方法会出现两个半径呢?其实这个并不是正圆的半径,而是椭圆的半径。

    Path path = new Path();
    RectF rect = new RectF(100,100,800,500);
    path.addRoundRect(rect, 150, 100, Direction.CW); //创建一个圆角矩形
    canvas.drawPath(path, mPaint);
效果如下:

path.addRoundRect

现在我们看一下,圆角矩形后面的那两个方法,这两个方法都有一个参数: float[] radii 。这个参数的意思就是控制圆角的四个角的半径。
这个数组至少要有8个值,如果少于8个值就会报异常。这8个值分成4组,每组的第一和第二个值分别代表圆角的x半径和y半径。
每组数据也会作用于圆角矩形的不同位置,总结如下

值的位置 作用圆角矩形哪个角
0,1 左上角
2,3 右上角
4,5 右下角
6,7 左下角
2.1.4 addOval(椭圆):
方法预览:
//椭圆
public void addOval(RectF oval, Direction dir)

public void addOval (float left,float top,float right,float bottom,Path.Direction dir)
有什么用:

画一个椭圆

怎么用:

为了便于观察,我将椭圆中的参数的矩形用不同颜色画出来。

  Path path = new Path();
  RectF rect = new RectF(100,100,800,500);
  mPaint.setColor(Color.GRAY);
  mPaint.setStyle(Style.FILL);
  canvas.drawRect(rect, mPaint);
  mPaint.setColor(Color.BLACK);
  path.addOval(rect, Direction.CW);
  canvas.drawPath(path, mPaint);
效果如下:

path.addOval(rect, Direction.CW);

从效果图就可以知道,这个就是矩形的内切圆。那如果想用这个方法画出正圆应该怎么画呢?没错,就是将这个矩形变成正方形,画出来的圆就是正圆了。现在验证一下:

  Path path = new Path();
  RectF rect = new RectF(100,100,800,800); //将矩形变成正方形
  mPaint.setColor(Color.GRAY);
  mPaint.setStyle(Style.FILL);
  canvas.drawRect(rect, mPaint);
  mPaint.setColor(Color.BLACK);
  path.addOval(rect, Direction.CW);
  canvas.drawPath(path, mPaint);
效果如下:

用addOval画出正圆
2.1.5 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)

先说一下 startAngle sweepAngle 这两个参数的意思。

参数 意思
startAngle 开始的角度
sweepAngle 扫过的角度

startAngle 是代表开始的角度,那么Android中矩形的0°是从哪里开始呢?其实矩形的0°是在矩形的右边的中点,按顺时针方向逐渐增大。

如图:


开始角度

sweepAngle 扫过的角度就是从起点角度开始扫过的角度,并不是指终点的角度。例如如果你的startAngle是90°,sweepAngle是180°。那么这个圆弧的终点应该在270°,而不是在180°。

现在验证一下看看:

  Path path = new Path();
  RectF rect = new RectF(300,300,1000,800);
  mPaint.setColor(Color.GRAY);
  mPaint.setStyle(Style.FILL);
  canvas.drawRect(rect, mPaint);
  mPaint.setColor(Color.BLACK);
  path.addArc(rect, 90, 180);
  canvas.drawPath(path, mPaint);

效果如下:


path.addArc(rect, 90, 180);

知道了addArc的用法之后,我们来看一下arcTo这个方法,这个方法也是用来画圆弧的,但是与addArc有些不同,总结如下:

方法 作用
addArc 直接添加一段圆弧
arcTo 添加一段圆弧,如果圆弧的起点与上一次Path操作的终点不一样的话,就会在这两个点连成一条直线

举个例子:

    Path path = new Path();
    RectF rect = new RectF(300,300,1000,800);
    mPaint.setColor(Color.GRAY);
    mPaint.setStyle(Style.FILL);
    canvas.drawRect(rect, mPaint);
    mPaint.setColor(Color.BLACK);
    mPaint.setStyle(Style.STROKE);
    path.lineTo(100, 100); //用path画一条从(0,0)到(100,100)的直线
    path.arcTo(rect, 90, 180); //用arcTo方法画一段圆弧
    canvas.drawPath(path, mPaint); //直线终点(100,100)与圆弧起点会连成一条直线

效果如下:


path.arcTo

如果你不想这两个点连线的话,arcTo在一个方法中有forceMoveTo的参数,这个参数如果设为true就说明将上一次操作的点设为圆弧的起点,也就是说不会将圆弧的起点与上一次操作的点连接起来。如果设为false就会连接。

来验证一下:

    Path path = new Path();
    RectF rect = new RectF(300,300,1000,800);
    mPaint.setColor(Color.GRAY);
    mPaint.setStyle(Style.FILL);
    canvas.drawRect(rect, mPaint);
    mPaint.setColor(Color.BLACK);
    mPaint.setStyle(Style.STROKE);
    path.lineTo(100, 100); //用path画一条从(0,0)到(100,100)的直线
    path.arcTo(rect, 90, 180,true); //用arcTo方法画一段圆弧
    canvas.drawPath(path, mPaint); //直线终点(100,100)与圆弧起点不会连成一条直线

效果如下:


path.arcTo(rect, 90, 180,true);
2.1.6 addPath(添加Path):
方法预览:
    //添加Path:
    public void addPath (Path src)
    public void addPath (Path src, float dx, float dy)
    public void addPath (Path src, Matrix matrix)
有什么用:

将两个Path合并在一起

怎么用:

这里先讲addPath的前两个方法,最后那个方法等写到Matrix才细讲。

  Path path = new Path();
  Path src = new Path();
  path.addRect(0, 0, 400, 400, Path.Direction.CW); //宽高为400的矩形
  src.addCircle(200, 200, 100, Path.Direction.CW); //圆心为(200,200)半径为100的正圆
  path.addPath(src);
  canvas.drawPath(path, mPaint);
效果如下:

path.addPath(src);

addPath的第二个方法的 dx dy 两个参数是什么意思呢?
其实它们是代表添加path后的位移值。
例如,上面这个例子,如果我将path.addPath(src);改成path.addPath(src,200,0);会出现什么现象呢?这时候src画的圆的圆心的坐标会移动到(400,200)。

让我们来验证一下:

  Path path = new Path();
  Path src = new Path();
  path.addRect(0, 0, 400, 400, Path.Direction.CW); //宽高为400的矩形
  src.addCircle(200, 200, 100, Path.Direction.CW); //圆心为(200,200)半径为100的正圆
  //path.addPath(src);
  path.addPath(src,200,0);
  canvas.drawPath(path, mPaint);

效果如下:


path.addPath(src,200,0);

path画出宽高为400的矩形,src画出一个圆心为(0,0),半径为100的圆。path.addPath将src合并到一起,并将src的中心设置为(200,200)。

第三类(设置方法):

3.1 set()

方法预览:
 public void set(Path src)
有什么用:

将新的path赋值到现有的path

怎么用:
  Path path = new Path();
  Path src = new Path();
  path.addRect(0, 0, 400, 400, Path.Direction.CW);
  src.addCircle(200, 200, 100, Path.Direction.CW);
  path.set(src); // 相当于 path = src;
  canvas.drawPath(path, mPaint);

效果如下:


path.set(src);

这个方法就是将path之前的矩形变成圆形。

3.2 offset()

方法预览:
 public void offset (float dx, float dy)
 public void offset (float dx, float dy, Path dst)
有什么用:

将path进行平移

怎么用:
  Path path = new Path();
  path.addRect(0, 0, 400, 400, Path.Direction.CW);
  canvas.drawPath(path, mPaint);
  mPaint.setColor(Color.RED); //将画笔变成红色
  path.offset(100,0);  //将path向右平移
  canvas.drawPath(path, mPaint);
效果如下:

path.offset(100,0);

offset的第二个方法的第三个参数的意思就是将平移后的path存储到dst参数中。
如果传入dst不为空,将平移后的状态存储到dst中,不影响当前path。dst为空,平移作用当前的path,相当于第一个方法。
现在验证一下:

  Path path = new Path();
  Path dst = new Path();
  path.addRect(0, 0, 400, 400, Direction.CW); //path添加矩形
  dst.addCircle(100,100, 100, Direction.CW); //dst添加圆形
  path.offset(100,0,dst); //将平移后的path存储到dst
  canvas.drawPath(dst, mPaint);

效果如下:


path.offset(100,0,dst);

3.3 reset()

方法预览:
public void reset()
有什么用:

这个方法很简单,就是将path的所有操作都清空掉。

第四类(判断方法) :

4.1 isConvex()(这个方法在API21之后才有)

方法预览:
public boolean isConvex ()
有什么用:

判断path是否为凸多边形,如果是就为true,反之为false。

要理解这个方法首先,我们要知道什么是凸多边形。
凸多边形的概念:
1.每个内角小于180度
2.任何两个顶点间的线段位于多边形的内部或边界上。

也就是说矩形,三角形,直线都是凸多边形,但是五角星那种形状就不是。现在我们用代码验证一下:

代码如下:

        Path path = new Path();
        path.moveTo(600,600);
        path.lineTo(500,700);
        path.lineTo(380,700);
        path.lineTo(500,780);
        path.close();
        Log.e("Path", "===============path.isConvex() " + path.isConvex());
        canvas.drawPath(path,mPaint);

效果如下:


path.isConvex()

打印的结果为:

E Path    : ===============path.isConvex() false

因为该图形并不是凸多边形,所以返回false。

但这里有个坑,如果我直接使用addRect方法,然后用setLastPoint来将这个矩形变成凹多边形。

代码如下:

  Path path = new Path();
  RectF rect = new RectF(0,0,400,400);
  path.addRect(rect, Direction.CCW);
  path.setLastPoint(100, 300);
  Log.e("Path", "===============path.isConvex() " + path.isConvex());
  canvas.drawPath(path,mPaint);

效果如下:


path.isConvex()

可以看出图形是一个凹多边形,但是打印的信息却是:

E Path    : ===============path.isConvex() true

这个地方我一直也想不明白,为什么会返回true。等我以后有思路了再来解决这个问题吧。

4.2 isEmpty:

方法预览:
  public boolean isEmpty ()
有什么用:

判断path中是否包含内容:

怎么用:
  Path path = new Path();
  Log.e("path.isEmpty()","==============path.isEmpty()1: " + path.isEmpty());

  path.lineTo(100,100);
  Log.e("path.isEmpty()","==============path.isEmpty()2: " + path.isEmpty());

Log输出的结果:

E path.isEmpty(): ==============path.isEmpty()1: true
E path.isEmpty(): ==============path.isEmpty()2: false

4.3 isRect:

方法预览:
public boolean isRect (RectF rect)
有什么用:

判断path是否是一个矩形,如果是一个矩形的话,将矩形的信息存到参数rect中。

怎么用:
   path.lineTo(0,400);
   path.lineTo(400,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输出的结果:

E Rect    : ======isRect:true| left:0.0| top:0.0| right:400.0| bottom:400.0

参考资料: