(1)定义
Path顾名思义就是路径的意思,也可以说是轨迹的意思,Path可以帮助view完成一些复杂的动画效果。
(2)基本方法
作用 | 相关方法 | 备注 |
---|---|---|
移动起点 | moveTo | 移动下一次操作的起点位置 |
设置终点 | setLastPoint | 重置当前path中最后一个点位置,如果在绘制之前调用,效果和moveTo相同 |
连接直线 | lineTo | 添加上一个点到当前点之间的直线到Path |
闭合路径 | close | 连接第一个点连接到最后一个点,形成一个闭合区域 |
添加内容 | addRect, addRoundRect, addOval, addCircle, addPath, addArc, arcTo | 添加(矩形, 圆角矩形, 椭圆, 圆, 路径, 圆弧) 到当前Path (注意addArc和arcTo的区别) |
是否为空 | isEmpty | 判断Path是否为空 |
是否为矩形 | isRect | 判断path是否是一个矩形 |
替换路径 | set | 用新的路径替换到当前路径所有内容 |
偏移路径 | offset | 对当前路径之前的操作进行偏移(不会影响之后的操作) |
贝塞尔曲线 | quadTo, cubicTo | 分别为二次和三次贝塞尔曲线的方法 |
rXxx方法 | rMoveTo, rLineTo, rQuadTo, rCubicTo | 不带r的方法是基于原点的坐标系(偏移量), rXxx方法是基于当前点坐标系(偏移量) |
填充模式 | setFillType, getFillType, isInverseFillType, toggleInverseFillType | 设置,获取,判断和切换填充模式 |
提示方法 | incReserve | 提示Path还有多少个点等待加入(这个方法貌似会让Path优化存储结构) |
布尔操作(API19) | op | 对两个Path进行布尔运算(即取交集、并集等操作) |
计算边界 | computeBounds | 计算Path的边界 |
重置路径 | reset, rewind | 清除Path中的内容,reset不保留内部数据结构,但会保留FillType。rewind会保留内部的数据结构,但不保留FillType |
矩阵操作 | transform | 矩阵变换 |
(3)Paint配置
private void init(Context mContext){
mPaint = new Paint();
mPaint.setColor(Color.BLACK);
mPaint.setStrokeWidth(10);
mPaint.setStrokeCap(Paint.Cap.ROUND);
mPaint.setStyle(Paint.Style.STROKE);
}
这里需要注意的是:
- 如果绘制非闭合图形,务必将画笔设置成
Paint.Style.STROKE
描边模式,否则绘制无效。 - 如果绘制闭合图形,可以使用三种模式:
Paint.Style.STROKE
、Paint.Style.FILL
、Paint.Style.FILL_AND_STROKE
,分别是描边模式、填充模式、描边并填充模式。
(4)moveTo和lineTo
lineTo(x, y)
:绘制一条执行。
mPaint.setColor(Color.BLUE);
Path path = new Path();
path.lineTo(200, 200);
canvas.drawPath(path, mPaint);
lineTo
的两个传参是确定某一点的位置,那么两点确定一条直线
,这里还有一个点是什么呢?
如下图所示,
我们的画笔默认位置是(0, 0),如上图所示,(0, 0)的位置就在红色矩形区域里面的小红点位置,也就是说,画笔从(0, 0)到(200, 200)绘制直线,这样就满足了两点确定一条直线的理念
,绘制之后的效果图如下:
Path
的moveTo
方法可以指定画笔的位置,也就是下次绘制的开始位置,下面我们结合moveTo
画直线。我们现在画一个假直角坐标系,将画笔位置移动到原点
,并绘制直线。
mPaint.setColor(Color.BLACK);
//绘制一个假直角坐标系
canvas.drawLine(0, 800, canvas.getWidth(), 800, mPaint);
canvas.drawLine(canvas.getWidth() / 2, 0, canvas.getWidth() / 2, canvas.getHeight(),mPaint);
mPaint.setColor(Color.BLUE);
Path path = new Path();
path.moveTo(canvas.getWidth() / 2, 800);
path.lineTo(200, 200);
canvas.drawPath(path, mPaint);
(5)moveTo
和setLastPoint
绘制两条直线
mPaint.setColor(Color.BLUE);
Path path = new Path();
path.moveTo(canvas.getWidth() / 2, 800);
path.lineTo(200, 200);
path.moveTo(50, 200);
path.lineTo(100,600);
canvas.drawPath(path, mPaint);
代码分析:
- 期初画笔位置是(0, 0),执行
path.moveTo(canvas.getWidth() / 2, 800)
之后画笔位置变成了(canvas.getWidth() / 2, 800); -
lineTo(200, 200):
绘制直线,画笔将从(canvas.getWidth() / 2, 800)开始画直线,直到(200, 200)停下; -
moveTo(50, 200):
画笔将从(200, 200)移动到(50, 200); -
lineTo(100,600):
绘制直线,画笔将从(50, 200)开始画直线,直到(100,600)停下; -
drawPath:
开始绘制,这一步才开始绘制,前面的只是设置轨迹
而已。
效果图:
下面开始解释下setLastPoint
,setLastPoint
的意思就是重置最近一次画笔位置。
mPaint.setColor(Color.BLUE);
Path path = new Path();
path.moveTo(canvas.getWidth() / 2, 800);
path.lineTo(200, 200);
path.setLastPoint(50, 200);
path.lineTo(100,600);
canvas.drawPath(path, mPaint);
代码分析:
- 期初画笔位置是(0, 0),执行
path.moveTo(canvas.getWidth() / 2, 800)
之后画笔位置变成了(canvas.getWidth() / 2, 800); -
lineTo(200, 200):
绘制直线,画笔将从(canvas.getWidth() / 2, 800)开始画直线,直到(200, 200)停下; -
setLastPoint(50, 200):
此时画笔的位置是(200, 200),setLastPoint
将重置画笔的位置,使得上一次画笔的位置变成了(50, 200); -
lineTo(100,600):
绘制直线,画笔将从(50, 200)开始画直线,直到(100,600)停下; -
drawPath:
开始绘制,这一步才开始绘制,前面的只是设置轨迹
而已。
效果图如下:
(6)close
Path有个close
方法,可以将第一个点和第二个点相连,形成闭合区域。
mPaint.setColor(Color.BLUE);
Path path = new Path();
path.moveTo(canvas.getWidth() / 2, 800);
path.lineTo(200, 200);
path.lineTo(100,600);
path.close();
canvas.drawPath(path, mPaint);
如图所示
如果是Paint
的样式修改成Paint.Style.FILL
或者Paint.Style.FILL_AND_STROKE
,那么可以不需要执行close()
也可以达到闭合效果。
(7)addXXX
系列
这些方法大致都是添加路劲(弧路径
、圆路径
、椭圆路径
、矩形路径
、圆角矩形路径
等等)
这里唯一需要说明的是Path.Direction.CW
、Path.Direction.CCW
。
一些方法中有个参数:
Path.Direction.CW:
顺时针
Path.Direction.CCW:
逆时针
我们来画一个圆
顺时针:
mPaint.setColor(Color.BLUE);
Path path = new Path();
path.addCircle(0, 0, 200, Path.Direction.CW);
canvas.drawPath(path, mPaint);
如图:
画笔起始点是(200, 0),结束点是(0, -200)。
那么, 我们可以利用setLastPoint
来画一个桃子
path.setLastPoint(200, -200);
逆时针:
path.addCircle(0, 0, 200, Path.Direction.CCW);
起始点是(200, 0),结束点是(0, 200),桃子在下面
path.setLastPoint(200, 200);
其它的路径就不举例了,总之,如果是闭合区域,我们首先需要确定的是顺时针还是逆时针,进而推敲出路径的起始点和结束点。
(8)addArc
与arcTo
addArc:
直接添加一个圆弧到path中
arcTo:
直接添加一个圆弧到path中,并且将当前路径的起始点和上一个路径的结束点连接。
arcTo(RectF oval, float startAngle, float sweepAngle, boolean forceMoveTo)
arcTo(RectF oval, float startAngle, float sweepAngle)
arcTo(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean forceMoveTo)
addArc
与arcTo
都是圆弧,这里主要展示一下它们的区别?
使用lineTo和addArc绘制直线和圆弧
path.lineTo(100, 100);
RectF rectF = new RectF();
rectF.left = 150;
rectF.top = 100;
rectF.right = 300;
rectF.bottom = 200;
path.addArc(rectF, 30,60);
canvas.drawPath(path, mPaint);
我们发现直线和圆弧互不相连。现在将addArc
替换成arcTo
path.lineTo(100, 100);
RectF rectF = new RectF();
rectF.left = 150;
rectF.top = 100;
rectF.right = 300;
rectF.bottom = 200;
path.arcTo(rectF, 30, 60);
canvas.drawPath(path, mPaint);
我们发现,当前圆弧路径的起始点和上一个路径的终点相连了。
在方法里面有个参数forceMoveTo
,
true: 不相连,相当于addArc
false: 当前圆弧路径的起始点和上一个路径的终点相连;
(9) computeBounds
计算path的边界。
computeBounds(RectF bounds, boolean exact)
bounds:矩形边界
exact:这个参数已经没用了
mPaint.setColor(Color.BLUE);
Path path = new Path();
path.lineTo(200, 200);
path.moveTo(-200, -200);
path.lineTo(-300,100);
RectF rectF = new RectF();
path.computeBounds(rectF, false);
path.addRect(rectF, Path.Direction.CW);
canvas.drawPath(path, mPaint);
效果图如下:
代码中画了两条线段,然后画了一个矩形,但是奇怪的是,这个矩形没有设置任何边界大小,computeBounds
是一个很有意思的方法,它可以自动计算两条线段所在的边界范围,所以当绘制这个矩形的时候其边界不是(0,0,0,0),当Path的轨迹所占点
数量为0或者1时,绘制这个矩形的时候其边界是(0,0,0,0)。
(10)incReserve(int extraPtCount)
提示路径以准备添加更多点。这可以允许更有效地分配其存储的路径。
extraPtCount:
可以添加到这个的额外点数
(11)isEmpty()
判断Path的路径是否为空,如果Path没有路径,则说明Path的路径是空的。
(12)isRect
判断Path是否是矩形路径。
Path path = new Path();
path.lineTo(0, 0);
path.lineTo(200, 0);
path.lineTo(200, 200);
path.lineTo(0,200);
boolean isRect = path.isRect(rectF);
//path.addRect(rectF, Path.Direction.CW);
canvas.drawPath(path, mPaint);
和 computeBounds
有点类似,都是计算当前路径的边界,但是又和 computeBounds
不同:
-
isRect
只是判断当前Path的路径是否是矩形; -
isRect
传递一个rectF参数,如果返回true,则被计算之后的rectF和computeBounds
效果一样,添加path.addRect(rectF, Path.Direction.CW)
同样可以绘制出矩形边界; -
isRect
传递一个rectF参数,如果返回false(Path的路径非矩形),rectF大小就是(0,0,0,0),此时rectF将被忽略,如果这时再添加path.addRect(rectF, Path.Direction.CW)
,rectF将不再被忽略,rectF将被绘制出来,由于rectF的大小是(0,0,0,0),所以之前的非矩形路径随之被隐藏。
(13)isConvex()
判断曲线是否具有凸性。
首先我们绘制两条直线,两条直线的结束点和起始点相连,如图所示
其中(0,0)我们称之为曲线的拐点
,下面我们设置一下Path效果,让这个曲线更像一个曲线吧
mPaint.setPathEffect(new CornerPathEffect(200));
定义:
如果曲线上任意两点都在曲线的上
方,则这个曲线具有上凸
特性。
我们再画一个曲线,如下图:
定义:
如果曲线上任意两点都在曲线的下
方,则这个曲线具有下凸
特性。
再画一个曲线,如下图:
像这样的曲线既不满足上凸
的特性,也不满足下凸
的特性,所以该曲线没有凸性
;
再画一个,如下图:
RectF rect = new RectF(0,0,400,400);
path.addRect(rect, Path.Direction.CCW);
那么这个矩形是否符合凸性
呢?
想要搞清楚这个问题,必须搞清楚凸性的起点和终点,我们可以通过setLastPoint
方法来找出凸性的起点和终点。
第一次实验:
RectF rect = new RectF(0,0,400,400);
path.addRect(rect, Path.Direction.CCW);
path.setLastPoint(100, 200);
由于矩形是按照逆时针
的方式绘制,并且setLastPoint
之后图形变成上图的样子,那么可以证明,图形的起始点(0,0),原来终点是(400,0),现在终点是(100,200),由于矩形是闭合区域
所以起点
和终点
相连之后形成了闭合图形
,现在我们不让它闭合
,擦除多余的部分:
此时,该曲线完全符合上凸
特性,我们称之为,当矩形按照逆时针
绘制后的图形,具有凸性
。
第二次试验:
RectF rect = new RectF(0,0,400,400);
path.addRect(rect, Path.Direction.CW);
path.setLastPoint(200, 100);
由于矩形是按照顺时针
的方式绘制,并且setLastPoint
之后图形变成上图的样子,那么可以证明,图形的起始点(0,0),原来终点是(0,400),现在终点是(200,100),由于矩形是闭合区域
所以起点
和终点
相连之后形成了闭合图形
,现在我们不让它闭合
,擦除多余的部分:
此时,该曲线完全符合下凸
特性,我们称之为,当矩形按照顺时针
绘制后的图形,具有凸性
。
isConvex
总结:
-
isConvex
是API 21新增的接口,其使用量也相对较少; - 判断一个图形是否具有
凸性
,并不是靠猜
,而是有方法的; - 需要对数学几何的
上凸
和下凸
的特性具有一定的了解; - 需要找到图形的
起点
和终点
,如果是封闭区域,需要擦除起点
和终点
连接(path.close())
的区域,让图形变成不再闭合
,最终判断曲线是否符合凸性
; - 可以结合
setLastPoint
方法寻找图形的起点
和终点
; - 上面判断图形是否具有
凸性
的方法写的很明白了,其它图形(比如:圆)
也可通过这个方法判断是否具有凸性
。
(14)setFillType
和isInverseFillType
setFillType:
设置Path的填充类型,指定内部的计算方式;
其填充类型有:
FillType .WINDING:
填充每一个封闭路径。
FillType .EVEN_ODD:
填充每个封闭路径不重合的地方。
FillType .INVERSE_WINDING:
与WINDING
相反,WINDING
填充每个封闭路径的内部
,而INVERSE_WINDING
填充每个封闭路径的外部空间。
FillType .INVERSE_EVEN_ODD:
与EVEN_ODD
相反,它填充封闭路径之外的空间和路径和路径相交
的空间。
演示这些填充类型之前,需要将Paint的样式改为填充模式
mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
或者
mPaint.setStyle(Paint.Style.FILL);
FillType.WINDING
-
FillType.EVEN_ODD
FillType.INVERSE_WINDING
FillType.INVERSE_EVEN_ODD
isInverseFillType:
判断是否为反转填充类型。
反转填充类型有两种FillType.INVERSE_WINDING
、FillType.INVERSE_EVEN_ODD
,我们看一下源码
/**
* Returns true if the filltype is one of the INVERSE variants
*
* @return true if the filltype is one of the INVERSE variants
*/
public boolean isInverseFillType() {
final int ft = nGetFillType(mNativePath);
return (ft & FillType.INVERSE_WINDING.nativeInt) != 0;
}
核心语句是(ft & FillType.INVERSE_WINDING.nativeInt) != 0;
,核心算法是按位与计算取值范围
,按位与(&)
使数字分组:
第一组:
取值范围是 20
第二组:
取值范围是 [21,22)
第三组:
取值范围是 [22,23)
第四组:
取值范围是 [23,24)
依次类推...
算法特性:
同一范围内的两数的&
运算,等于当前范围的最小数(比如5&6=4)
,不同范围的&
运算结果为0;
根据这个算法特性,我们再来看下代码;
分析:
- 填充类型取值分别是:0,1,2,3
-
FillType.INVERSE_WINDING
的取值是2,FillType.INVERSE_EVEN_ODD
的取值是3,这两个反转填充类型的取值正好都在第二组范围。(我想接下来不需要我解释了吧)
(15)set(Path src)
将原有Path,替换为src。
(16)offset
将Path平移到指定点。
offset(float dx, float dy)
offset(float dx, float dy, @Nullable Path dst)
offset
有两个方法。
方法一:
Path path = new Path();
path.addCircle(0,0,200, Path.Direction.CW);
path.offset(100, 100);
canvas.drawPath(path, mPaint);
效果如下:
方法二:
Path path = new Path();
path.addCircle(0,0,200, Path.Direction.CW);
Path path1 = new Path();
path.offset(100, 100, path1);
canvas.drawPath(path, mPaint);
canvas.drawPath(path1, mPaint);
效果如下:
(17)Op
op(Path path, Op op)
op(Path path1, Path path2, Op op)
组合两条路径时可以执行的逻辑操作。
逻辑操作有:
Op.DIFFERENCE:
从第一条路径中减去第二条路径。
Path path = new Path();
path.addCircle(-100,-100,200, Path.Direction.CW);
Path newPath = new Path();
newPath.addCircle(100,100,200, Path.Direction.CW);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
path.op(newPath, Path.Op.DIFFERENCE);
}
canvas.drawPath(path, mPaint);
Op.INTERSECT:
两条路径相交。
Path path = new Path();
path.addCircle(-100,-100,200, Path.Direction.CW);
Path newPath = new Path();
newPath.addCircle(100,100,200, Path.Direction.CW);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
path.op(newPath, Path.Op.INTERSECT);
}
canvas.drawPath(path, mPaint);
Op.UNION:
把这两条路联合起来。
Path path = new Path();
path.addCircle(-100,-100,200, Path.Direction.CW);
Path newPath = new Path();
newPath.addCircle(100,100,200, Path.Direction.CW);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
path.op(newPath, Path.Op.UNION);
}
canvas.drawPath(path, mPaint);
Op.XOR:
排他或两条路。
Path path = new Path();
path.addCircle(-100,-100,200, Path.Direction.CW);
Path newPath = new Path();
newPath.addCircle(100,100,200, Path.Direction.CW);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
path.op(newPath, Path.Op.XOR);
}
canvas.drawPath(path, mPaint);
Op.REVERSE_DIFFERENCE:
从第二条路径中减去第一条路径。
Path path = new Path();
path.addCircle(-100,-100,200, Path.Direction.CW);
Path newPath = new Path();
newPath.addCircle(100,100,200, Path.Direction.CW);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
path.op(newPath, Path.Op.REVERSE_DIFFERENCE);
}
canvas.drawPath(path, mPaint);
(18)reset()
和rewind()
将Path清空。
reset:
不保留内部数据结构,但会保留FillType。
rewind:
会保留内部的数据结构,但不保留FillType。
一般使用reset
。
(19)toggleInverseFillType
切换填充规则(即原有规则与反向规则之间相互切换)
FillType .WINDING
与FillType .INVERSE_WINDING
相互切换。
FillType .EVEN_ODD
与FillType .INVERSE_EVEN_ODD
相互切换。
(20)transform
transform(Matrix matrix)
transform(Matrix matrix, Path dst)
对Path进行矩阵操作,我们就拿矩阵的旋转操作来演示,代码如下:
mPaint.setColor(Color.BLUE);
Path path = new Path();
path.addCircle(100,100,200, Path.Direction.CW);
path.addCircle(-100,-100,200, Path.Direction.CW);
Matrix matrix = new Matrix();
matrix.setRotate(degrees);
path.transform(matrix);
canvas.drawPath(path, mPaint);
degrees = degrees + 2;
invalidate();
另外,第二个方法有个参数dst,意思就是:Path在矩阵操作之后,在Path保存到dst对象。
(21)rMoveTo
和rLineTo
-
moveTo
和rMoveTo
的区别?
moveTo:
移动的是画笔的位置;
rMoveTo:
不仅移动画笔的位置,而且直角坐标系也随之移动,此时画笔的位置相当于没有变化。
-
lineTo
和rLineTo
的区别?
moveTo:
移动的是画笔的位置;
rMoveTo:
不仅移动画笔的位置,而且直角坐标系也随之移动,此时画笔的位置相当于没有变化。
(22)quadTo、cubicTo、rQuadTo、rCubicTo
贝赛尔曲线是Path的一个非常重要的知识点,我给它单独整理了一篇文章,如下:
[本章完...]