自定义View之Path

前言

本篇承上文 自定义View之Canvas.drawXXX()drawPath(@NonNull Path path, @NonNull Paint paint)方法而来,详细讲解其中的参数Path的作用。

简介

先上一下官方的注释

1
2
3
4
5
6
7
8
9
10
11
12
package android.graphics;

/**
* 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.
*/
public class Path {
……
}

可以看到Path类位于android.graphics包中。

注释的英文翻译过来大致如下:

Path类封装了由直线段、二次曲线和三次曲线组成的复合(多个轮廓)几何路径。

它可以用Canvas.drawPath(path,paint)绘制,无论是填充还是描边(取决于paint画笔的样式);

或者它可以用于剪裁或绘画路径上的文字。

用于裁截的方法有Canvas.clipPath(@NonNull Path path),另外还有几个重载方法。

用于绘画路径上的文字的方法我所知道的目前只有一个,drawTextOnPath(@NonNull char[] text, int index, int count, @NonNull Path path, float hOffset, float vOffset, @NonNull Paint paint),这个方法在上文自定义View之Canvas.drawXXX()中的drawText()一节中有简要说明。

Path 可以描述直线、二次曲线、三次曲线、圆、椭圆、弧形、矩形、圆角矩形。把这些图形结合起来,就可以描述出很多复杂的图形

这里主要围绕Canvas.drawPath(path,paint)展开说明。

Path类的具体使用

Path 主要有两类方法,一类是直接描述路径的,另一类是辅助的设置或计算。

Path 方法第一类:直接描述路径

Path.addXxx()系列

Path.addXX()系列是直接调用Path提供的方法添加子图形到Path中。

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

添加封闭的圆。

x, y, radius 这三个参数是圆的基本信息,最后一个参数 dir 是画圆的路径的方向。

路径方向有两种:顺时针 (CW clockwise) 和逆时针 (CCW counter-clockwise) 。对于描边(Paint.Style 为 STROKE )情况,这个参数填 CW 还是填 CCW 没有影响。它只是在需要填充图形 (Paint.Style 为 FILL 或 FILL_AND_STROKE) ,并且图形出现自相交时,用于判断填充范围的。比如下面这个图形:

path_circle_stoke

填充类型

path_fill_all

OR

path_fill_or

关于采取那个方式来操作,取决于Path.setFillType(),关于这方法,会在辅助的设置或计算中具体展开。

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

添加封闭的矩形。

left,top,right,bottom 四个参数确定矩形的四边。

重载方法:

  • addRect(RectF rect, Direction dir)

用RectF封装了float类型的四个变量: left,top,right,bottom

addRoundRect(RectF rect, float rx, float ry, Direction dir)

添加封闭的圆角矩形。

rx、ry分别与rect的四边形成四个矩形绘制椭圆,此椭圆的一角形成圆角矩形的圆角。

rx、ry的具体效果,可查看上篇 自定义View之Canvas.drawXXX()的drawRoundRect章节。

重载方法:

  • addRoundRect(float left, float top, float right, float bottom, float rx, float ry, Direction dir) 添加于api21
  • addRoundRect(RectF rect, float[] radii, Direction dir)
  • addRoundRect(float left, float top, float right, float bottom, float[] radii, Direction dir) 添加于api21

后两个重载方法中多了float[] radii这个参数。radii数组有8个值,组成4对(x,y),分别表示左上、右上,右下,左下,分别控制对应的圆角。

addArc(RectF oval, float startAngle, float sweepAngle)

添加弧形,该弧形是椭圆的一部分。
oval确定椭圆的四边界。可以看作是在oval决定的矩形中绘制了一个椭圆。
startAngle 为绘制椭圆弧形的起始角度(x 轴的正向,即正右的方向,是 0 度的位置;顺时针为正角度,逆时针为负角度);
sweepAngle 是弧形划过的角度;

重载方法:

  • addArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle) 添加于api21
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
    paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(Color.RED);
paint.setStyle(Paint.Style.STROKE);

mPath = new Path();
mDashPathEffect = new DashPathEffect(new float[]{4, 4}, 0); //设置虚线

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);

//绘制下半红色椭圆
mPath.addArc(100, 100, 600, 400, 0, 180);
canvas.drawPath(mPath, paint);
//绘制上半蓝色椭圆
mPath.reset();
paint.setColor(Color.BLUE);
mPath.addArc(100, 100, 600, 400, 0, -180);
canvas.drawPath(mPath, paint);
//绘制灰色虚线矩形
mPath.reset();
mPath.addRect(100, 100, 600, 400, Path.Direction.CCW);
paint.setColor(Color.GRAY);
paint.setPathEffect(mDashPathEffect); //设置虚线
canvas.drawPath(mPath, paint);
}

addArc

addOval(RectF oval, Direction dir)

添加椭圆。

重载方法:

  • addOval(float left, float top, float right, float bottom, Direction dir) 添加于api21

addPath(Path src)

将一个path数据添加到当前的path中。

重载方法:

  • addPath(Path src, float dx, float dy)
  • addPath(Path src, Matrix matrix)

addPath(Path src, float dx, float dy) 将坐标系平移到(dx,dy)后添加src内容。也可以说是给src内的每个坐标点都添加(dx,dy)大小。
addPath(Path src, Matrix matrix)addPath(Path src, float dx, float dy)类似,不过matrix能做的事比dx,dy多得多了,addPath(Path src, float dx, float dy)中的dx,dy相当于matrix.postTranslate(dx,dy)。通过matrix几何变换后再添加src。

Path.XxxTo系列

添加线条。

这一组和第一组 addXxx() 方法的区别在于,第一组是添加的完整封闭图形(除了 addPath() ),而这一组添加的只是一条线。

lineTo(float x, float y)、rLineTo(float dx, float dy)

画直线。
比如当前画笔停留的坐标为(x0,y0),则
lineTo是从(x0,y0)画直线到(x,y)。
rLineTo是从(x0,y0)画直线到(x0+dx,y0+dy)。

1
2
3
paint.setStyle(Style.STROKE);
path.lineTo(100, 100); // 由当前位置 (0, 0) 向 (100, 100) 画一条直线
path.rLineTo(100, 0); // 由当前位置 (100, 100) 向正右方 100 像素的位置画一条直线

lineToAndrLineTo

moveTo (float x, float y)、rMoveTo (float dx, float dy)

将当前画笔的位置移动到指定位置。此过程不绘制任何内容。
好比是写字时,你将笔抬起后移动到下一行这个过程,之后再继续书写。

moveTo 是直接将画笔移动到(x,y)处。
rMoveTo 是将画笔在当前位置基础上x轴移动dx,y轴移动dy。

arcTo(RectF oval, float startAngle, float sweepAngle)、arcTo(RectF oval, float startAngle, float sweepAngle, boolean forceMoveTo)、arcTo(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean forceMoveTo)

添加弧线。

addArc类似,arcTo多了参数forceMoveTo,用于控制是否连接上一点与弧线起点。true:不连接,直接开始画弧线;false:连接后再开始画弧线。

由此来看,addArc相当于是forceMoveTo=true的arcTo

arcTo(RectF oval, float startAngle, float sweepAngle)内部默认将forceMoveTo设置为false。

1
2
3
public void arcTo(RectF oval, float startAngle, float sweepAngle) {
arcTo(oval.left, oval.top, oval.right, oval.bottom, startAngle, sweepAngle, false);
}

arcTo(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean forceMoveTo) 添加于api21

1
2
3
paint.setStyle(Style.STROKE);
path.lineTo(100, 100);
path.arcTo(100, 100, 300, 300, -90, 90, true); // 强制移动到弧形起点(无痕迹)

arcTo_false

1
2
3
paint.setStyle(Style.STROKE);
path.lineTo(100, 100);
path.arcTo(100, 100, 300, 300, -90, 90, false); // 直接连线连到弧形起点(有痕迹)

arcTo_false

1
2
3
paint.setStyle(Style.STROKE);
path.lineTo(100, 100);
path.addArc(100, 100, 300, 300, -90, 90);

drawArc

quadTo(float x1, float y1, float x2, float y2)、rQuadTo(float dx1, float dy1, float dx2, float dy2)

画二次贝塞尔曲线。

起点为画笔的当前位置,(x1,y1)为控制点坐标,(x2,y2)为终点坐标。

假设画笔的当前位置为(x0,y0),则rQuadTo中的控制点坐标为(x0+dx1,y0+dy1),终点坐标为(x0+dx2,y0+dy2)。

cubicTo(float x1, float y1, float x2, float y2,float x3, float y3)、rCubicTo(float x1, float y1, float x2, float y2,float x3, float y3)

画三次贝塞尔曲线。
与二次贝塞尔曲线方法类似,不过三次贝塞尔曲线控制点有两个,其中(x1,y1),(x2,y2)为控制点,(x3,y3)为终点。

Path 方法第二类:辅助的设置或计算

setFillType(FillType ft)

先上官方注释

1
2
3
4
5
6
7
8
/**
* Set the path's fill type. This defines how "inside" is computed.
*
* @param ft The new fill type for this path
*/
public void setFillType(FillType ft) {
nSetFillType(mNativePath, ft.nativeInt);
}

设置path的填充类型。处理内容重叠曲的显示方式。

前面在说 dir 参数的时候提到, Path.setFillType(fillType) 是用来设置图形自相交时的填充算法的:

fillType

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* Enum for the ways a path may be filled.
*/
public enum FillType {
// these must match the values in SkPath.h
/**
* Specifies that "inside" is computed by a non-zero sum of signed
* edge crossings.
*/
WINDING (0),
/**
* Specifies that "inside" is computed by an odd number of edge
* crossings.
*/
EVEN_ODD (1),
/**
* Same as {@link #WINDING}, but draws outside of the path, rather than inside.
*/
INVERSE_WINDING (2),
/**
* Same as {@link #EVEN_ODD}, but draws outside of the path, rather than inside.
*/
INVERSE_EVEN_ODD(3);

FillType(int ni) {
nativeInt = ni;
}

final int nativeInt;
}

系统提供了4种填充类型:

  • WINDING
  • EVEN_ODD
  • INVERSE_WINDING
  • INVERSE_EVEN_ODD

INVERSE_WINDING和INVERSE_EVEN_ODD从字面上就可以知道与WINDING和EVEN_ODD刚好反了一下。

所以只要理了WINDING和EVEN_ODD,剩下的两种自然也就理解了。

EVEN_ODD

EVEN:偶数的 ODD:奇数的

即奇偶原则:对于平面中的任意一点,向任意方向射出一条射线,这条射线和图形相交的次数(相交才算,相切不算哦)如果是奇数,则这个点被认为在图形内部,是要被涂色的区域;如果是偶数,则这个点被认为在图形外部,是不被涂色的区域。还以左右相交的双圆为例:

EVEN_ODD

简单来讲:线方向无关,射线交点(奇数画,偶数不画)

射线的方向无所谓,同一个点射向任何方向的射线,结果都是一样的

WINDING

即 non-zero winding rule (非零环绕数原则)

首先,它需要你图形中的所有线条都是有绘制方向的:

WINDING_direction

然后,同样是从平面中的点向任意方向射出一条射线,但计算规则不一样:以 0 为初始值,对于射线和图形的所有交点,遇到每个顺时针的交点(图形从射线的左边向右穿过)把结果加 1,遇到每个逆时针的交点(图形从射线的右边向左穿过)把结果减 1,最终把所有的交点都算上,得到的结果如果不是 0,则认为这个点在图形内部,是要被涂色的区域;如果是 0,则认为这个点在图形外部,是不被涂色的区域。

WINDING_non_zero

简单来讲:线方向相关,射线交点(顺时针+1,逆时针-1),非0画,0不画

图形的方向:

对于添加子图形类方法(如 Path.addCircle() Path.addRect())的方向,由方法的 dir 参数来控制,这个在前面已经讲过了;

而对于画线类的方法(如 Path.lineTo()Path.arcTo()),线的方向就是图形的方向。

1
2
3
4
5
6
7
8
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(Color.RED);
mPath = new Path();
mPath.setFillType(Path.FillType.INVERSE_EVEN_ODD);

mPath.addCircle(200,200,100, Path.Direction.CW);
mPath.addCircle(350,200,100, Path.Direction.CW);
canvas.drawPath(mPath,paint);

INVERSE_EVEN_ODD

1
2
3
4
5
6
7
8
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(Color.RED);
mPath = new Path();
mPath.setFillType(Path.FillType.INVERSE_WINDING);

mPath.addCircle(200,200,100, Path.Direction.CW);
mPath.addCircle(350,200,100, Path.Direction.CW);
canvas.drawPath(mPath,paint);

INVERSE_WINDING 同方向

1
2
3
4
5
6
7
8
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(Color.RED);
mPath = new Path();
mPath.setFillType(Path.FillType.INVERSE_WINDING);

mPath.addCircle(200,200,100, Path.Direction.CW);
mPath.addCircle(350,200,100, Path.Direction.CCW);
canvas.drawPath(mPath,paint);

INVERSE_WINDING 反方向

EVEN_ODD 和 WINDING 的效果应该是这样的:

EVEN_ODD和WINDING效果演示

Path.Op

类似于Paint中的xfermode属性,可以用来组合两个Path。

系统提供了如下几种:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* The logical operations that can be performed when combining two paths.
*
* @see #op(Path, android.graphics.Path.Op)
* @see #op(Path, Path, android.graphics.Path.Op)
*/
public enum Op {
/**
* Subtract the second path from the first path.
*/
DIFFERENCE,
/**
* Intersect the two paths.
*/
INTERSECT,
/**
* Union (inclusive-or) the two paths.
*/
UNION,
/**
* Exclusive-or the two paths.
*/
XOR,
/**
* Subtract the first path from the second path.
*/
REVERSE_DIFFERENCE
}

不添加OP属性示例

1
2
3
4
5
6
7
8
9
10
11
12
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(Color.RED);
paint.setStyle(Paint.Style.STROKE);
mPath = new Path();
mPathSec = new Path();

mPath.addCircle(200, 200, 100, Path.Direction.CW);
mPathSec.addRect(200, 200, 300, 300, Path.Direction.CW);
canvas.drawPath(mPath, paint);

paint.setColor(Color.GRAY);
canvas.drawPath(mPathSec, paint);

为了区分圆形和矩形,设置了不同颜色。

分开绘制

DIFFERENCE

从当前path中去除另一个path,即显示当前path中不相交的部分。

1
2
3
4
5
6
7
8
9
10
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(Color.RED);
paint.setStyle(Paint.Style.STROKE);
mPath = new Path();
mPathSec = new Path();

mPath.addCircle(200, 200, 100, Path.Direction.CW);
mPathSec.addRect(200, 200, 300, 300, Path.Direction.CW);
mPath.op(mPathSec, Path.Op.DIFFERENCE);
canvas.drawPath(mPath, paint);

Path.Op.DIFFERENCE

INTERSECT

取相交的部分显示出来

1
2
3
4
5
6
7
8
9
10
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(Color.RED);
paint.setStyle(Paint.Style.STROKE);
mPath = new Path();
mPathSec = new Path();

mPath.addCircle(200, 200, 100, Path.Direction.CW);
mPathSec.addRect(200, 200, 300, 300, Path.Direction.CW);
mPath.op(mPathSec, Path.Op.INTERSECT);
canvas.drawPath(mPath, paint);

Path.Op.INTERSECT

UNION

inclusive-or,两个path组合在一起(STORKE的样式下,重叠部分的线不绘制)。

1
2
3
4
5
6
7
8
9
10
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(Color.RED);
paint.setStyle(Paint.Style.STROKE);
mPath = new Path();
mPathSec = new Path();

mPath.addCircle(200, 200, 100, Path.Direction.CW);
mPathSec.addRect(250, 250, 300, 300, Path.Direction.CW);
mPath.op(mPathSec, Path.Op.UNION);
canvas.drawPath(mPath, paint);

Path.Op.UNION

XOR

取两个path不相交部分。

当画笔在描边模式下时,与 不添加OP属性示例一致。

1
2
3
4
5
6
7
8
9
10
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(Color.RED);
paint.setStyle(Paint.Style.STROKE);
mPath = new Path();
mPathSec = new Path();

mPath.addCircle(200, 200, 100, Path.Direction.CW);
mPathSec.addRect(250, 250, 300, 300, Path.Direction.CW);
mPath.op(mPathSec, Path.Op.XOR);
canvas.drawPath(mPath, paint);

Path.Op.XOR_STROKE

1
2
3
4
5
6
7
8
9
10
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(Color.RED);
paint.setStyle(Paint.Style.FILL);//或者FILL_OR_STROKE
mPath = new Path();
mPathSec = new Path();

mPath.addCircle(200, 200, 100, Path.Direction.CW);
mPathSec.addRect(250, 250, 300, 300, Path.Direction.CW);
mPath.op(mPathSec, Path.Op.XOR);
canvas.drawPath(mPath, paint);

Path.Op.XOR_FILL

REVERSE_DIFFERENCE

DIFFERENCE是从当前path中去除另一个path;

而REVERSE_DIFFERENCE是从另一个path中去除当前path部分,即显示另一个path中不相交的部分。

1
2
3
4
5
6
7
8
9
10
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(Color.RED);
paint.setStyle(Paint.Style.STROKE);
mPath = new Path();
mPathSec = new Path();

mPath.addCircle(200, 200, 100, Path.Direction.CW);
mPathSec.addRect(250, 250, 300, 300, Path.Direction.CW);
mPath.op(mPathSec, Path.Op.REVERSE_DIFFERENCE);
canvas.drawPath(mPath, paint);

Path.Op.REVERSE_DIFFERENCE

close()

封闭当前子图形,即由当前位置向当前子图形的起点绘制一条直线。

相当于lineTo(起点坐标) 。

当Paint画笔为填充类型时(即 Paint.Style 为 FILL 或 FILL_AND_STROKE),会自动封闭子图形。

1
2
3
4
paint.setStyle(Style.STROKE);
path.moveTo(100, 100);
path.lineTo(200, 100);
path.lineTo(150, 150);

not_close

1
2
3
4
5
paint.setStyle(Style.STROKE);
path.moveTo(100, 100);
path.lineTo(200, 100);
path.lineTo(150, 150);
path.close(); // 使用 close() 封闭子图形。等价于 path.lineTo(100, 100)

close

1
2
3
4
paint.setStyle(Style.FILL);//或者Style.FILL_AND_STROKE
path.moveTo(100, 100);
path.lineTo(200, 100);
path.lineTo(150, 150);

fill_auto_close

reset()

重置path,清空内容,但是不会改变fillType。

set (@NonNull Path src)

用src替换当前path内的内容。

setLastPoint(float dx, float dy)

设置path的最后一个点坐标。

moveTo()的比较

类型 是否影响起点 是否影响之前的操作
moveTo()
setLastPoint()

如瞎下面画矩形addRect(200, 400, 400, 500, Path.Direction.CW)

确定矩形的四个顶点坐标为(200,400),(400,400),(200,500),(400,500)。

但设置setLastPoint(100, 450)后,四个点的坐标变为(200,400),(400,400),(100,450),(400,500)。

这个方法在不同形状的子图形上有不同的显示效果,可以自己去试试。

1
2
3
4
5
6
7
8
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(Color.RED);
paint.setStyle(Paint.Style.STROKE);
mPath = new Path();

mPath.addRect(200, 400, 400, 500, Path.Direction.CW);
mPath.setLastPoint(100, 450);
canvas.drawPath(mPath, paint);

setLastPoint

isConvex ()

判断path是否为凸多边形。

offset(float dx, float dy)、offset(float dx, float dy, @Nullable Path dst)

offset(float dx, float dy):将当前path平移(dx,dy)

offset(float dx, float dy, @Nullable Path dst):dst为null时,与offset(float dx, float dy)效果一致;dst不为null时,将当前path平移后的状态存入dst中,不会影响当前path。

1
2
3
4
5
6
7
8
public void offset(float dx, float dy, @Nullable Path dst) {
if (dst != null) {
dst.set(this);
} else {
dst = this;
}
dst.offset(dx, dy);
}
坚持原创技术分享,您的支持是对我最大的鼓励!