在上一篇文章的末尾提到了Canvas绘制文本的第二种实现方式,是使用了Canvas.drawTextOnPath(...)这个方法,里面会要求传一个Path对象,那么本篇文章主要学习Path相关的内容
Android自定义控件探索之旅一7
Android自定义控件探索之旅一6
Android自定义控件探索之旅一5
Android自定义控件探索之旅一4
Android自定义控件探索之旅一3
Android自定义控件探索之旅一2
Android自定义控件探索之旅一1
Path
首先看下系统源码关于Path的介绍:
/**
* 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进行绘制。绘制路径(路径,绘制),填充或描边(基于绘制的风格),也可以用于裁剪或绘制路径上的文本。
简单点理解就是:使用Path不仅能够绘制简单图形,也可以绘制这些比较复杂的图形。
为了兼容性,下面的这张截图去除了部分API21(即安卓版本5.0)以上才添加的方法:
值得一提的是,路径有开放和封闭的区别:
为了对具体的API体验和使用有原滋原味的体验,下面会把涉及到的API进行源码分析,分析内容含系统源码对方法的解释说明以及参数分析
moveTo、 setLastPoint、 lineTo 和 close
- lineTo:
/**
* Add a line from the last point to the specified point (x,y).
* If no moveTo() call has been made for this contour, the first point is
* automatically set to (0,0).
*
* @param x The x-coordinate of the end of a line
* @param y The y-coordinate of the end of a line
*/
public void lineTo(float x, float y) {
isSimplePath = false;
nLineTo(mNativePath, x, y);
}
这个方法的注释一目了然:从最后的点到指定点(x,y)增加一条线。如果没有对此轮廓线进行 moveTo() 调用,则第一个点将自动设置为(0,0)。由于Path可以用来描述一个图像的轮廓,而图像的轮廓通常都是用一条线构成的。因此这里的某个点就是上次操作结束的点,如果没有进行过操作则默认点为坐标原点。
下面是参考示例代码:
//创建画笔
Paint mPaint = new Paint();
//画笔颜色 - 黑色
mPaint.setColor(Color.BLACK);
//画笔颜色 - 黑色
mPaint.setStyle(Paint.Style.STROKE);
//边框宽度 - 10
mPaint.setStrokeWidth(10);
//移动坐标系到屏幕中心(宽高数据在onSizeChanged中获取)
canvas.translate(mWidth / 2, mHeight / 2);
//创建Path
Path path = new Path();
//lineTo
path.lineTo(200, 200);
path.lineTo(200,0);
//绘制Path
canvas.drawPath(path, mPaint);
运行之后的效果图:
代码分析:由于第一次调用lineTo( )之前没有操作,所以默认点就是坐标原点O,结果就是坐标原点O到A(200,200)之间连直线(用蓝色圈1标注);接着第二次调用lineTo,由于上次的结束位置是A(200,200),所以就是A(200,200)到B(200,0)之间的连线(用蓝色圈2标注)。
- moveTo、 setLastPoint
/**
* Set the beginning of the next contour to the point (x,y).
*
* @param x The x-coordinate of the start of a new contour
* @param y The y-coordinate of the start of a new contour
*/
public void moveTo(float x, float y) {
nMoveTo(mNativePath, x, y);
}
/**
* Sets the last point of the path.
*
* @param dx The new X coordinate for the last point
* @param dy The new Y coordinate for the last point
*/
public void setLastPoint(float dx, float dy) {
isSimplePath = false;
nSetLastPoint(mNativePath, dx, dy);
}
由于moveTo以及setLastPoint这2个函数的内部最终调用了Native层的函数,这里就不继续点进源码。首先看下moveTo这个函数,系统源码对其方法的说明:
moveTo(float x, float y):
将下一个轮廓的起始点设置为(x,y),其中x:新轮廓线起始的x坐标,y:新轮廓线起始的y坐标
setLastPoint(float x, float y):
设置路径的最后一点。其中x:最后一点的X坐标,y:最后一点的X坐标
这两个方法虽然在作用上貌似有相似之处(都是设置路径的点),但实际上却是完全不同的作用,具体参照下表:
方法 | 简介 | 是否影响之前的操作 | 是否影响之后操作 |
---|---|---|---|
moveTo | 移动下一次操作的起点位置 | 否 | 是 |
setLastPoint | 设置之前操作的最后一个点位置 | 是 | 是 |
下面是moveTo(float x, float y)参考示例代码:
Paint mPaint = new Paint(); // 创建画笔
mPaint.setColor(Color.BLACK); // 画笔颜色 - 黑色
mPaint.setStyle(Paint.Style.STROKE); // 填充模式 - 描边
mPaint.setStrokeWidth(10); // 边框宽度 - 10
Canvas canvas = new Canvas();
// 移动坐标系到屏幕中心 mWidth 是屏幕的最大宽度,mHeight 是屏幕的最大高度,
canvas.translate(mWidth / 2 , mHeight / 2);
// 创建Path
Path path = new Path();
// lineTo
path.lineTo(200, 200);
// setLastPoint
path.moveTo(200,100);
// lineTo
path.lineTo(200,0);
// 绘制Path
canvas.drawPath(path, mPaint);
moveTo(float x, float y)运行之后的效果图:
运行效果及代码逻辑分析:
执行完第一次LineTo的时候,现在的默认点位置变成了A(200,200),但是moveTo将其变成为了C(200,100),因此在第二次调用lineTo的时候就是连接C(200,100) 到 B(200,0) 之间的直线(用蓝色圈2标注)。
下面是setLastPoint(float x, float y)参考示例代码:
Paint mPaint = new Paint(); // 创建画笔
mPaint.setColor(Color.BLACK); // 画笔颜色 - 黑色
mPaint.setStyle(Paint.Style.STROKE); // 填充模式 - 描边
mPaint.setStrokeWidth(10); // 边框宽度 - 10
Canvas canvas = new Canvas();
// 移动坐标系到屏幕中心 mWidth 是屏幕的最大宽度,mHeight 是屏幕的最大高度,
canvas.translate(mWidth / 2 , mHeight / 2);
// 创建Path
Path path = new Path();
// lineTo
path.lineTo(200, 200);
// setLastPoint
path.setLastPoint(200,100);
// lineTo
path.lineTo(200,0);
// 绘制Path
canvas.drawPath(path, mPaint);
setLastPoint(float x, float y)运行之后的效果图:
运行效果及代码逻辑分析:
setLastPoint是重置上一次操作的最后一个点,在执行完第一次的lineTo的时候,最后一个点是A(200,200),而setLastPoint更改最后一个点为C(200,100),所以在实际执行的时候,第一次的lineTo就不是从原点O到A(200,200)的连线了,而变成了从原点O到C(200,100)之间的连线了。
在执行完第一次lineTo和setLastPoint后,最后一个点的位置是C(200,100),所以在第二次调用lineTo的时候就是C(200,100) 到 B(200,0) 之间的连线(用蓝色圈2标注)。
- close
/**
* Close the current contour. If the current point is not equal to the
* first point of the contour, a line segment is automatically added.
*/
public void close() {
isSimplePath = false;
nClose(mNativePath);
}
这个方法的英文注释翻译过来就是:关闭当前轮廓线。如果当前点不等于轮廓的第一个点,则自动添加线段。
下面是close参考示例代码:
Paint mPaint = new Paint(); // 创建画笔
mPaint.setColor(Color.BLACK); // 画笔颜色 - 黑色
mPaint.setStyle(Paint.Style.STROKE); // 填充模式 - 描边
mPaint.setStrokeWidth(10); // 边框宽度 - 10
Canvas canvas = new Canvas();
// 移动坐标系到屏幕中心 mWidth 是屏幕的最大宽度,mHeight 是屏幕的最大高度,
canvas.translate(mWidth / 2 , mHeight / 2);
// 创建Path
Path path = new Path();
// lineTo
path.lineTo(200,0);
// lineTo
path.lineTo(200, 200);
// lineTo
path.lineTo(200,0);
// close
path.close();
// 绘制Path
canvas.drawPath(path, mPaint);
close运行之后的效果图:
运行效果及代码逻辑分析:
两个lineTo分别代表第1和第2条线,而close在此处的作用就算连接了B(200,0)点和原点O之间的第3条线,使之形成一个封闭的图形。需要注意的是,close的作用是封闭路径,但是与连接当前最后一个点和第一个点并不等价。如果连接了最后一个点和第一个点仍然无法形成封闭图形,则close什么 也不做(这样也就失去了意义)
addXxx与arcTo
Path中的addXxx主要是在Path中添加基本图形,既然是基本图形,那也可以分为几类:
第一类(基本形状)
// 第一类(基本形状)
// 圆形
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)
第一类(基本形状)里面具体的API,方法中最后都有一个 Path.Direction,这个Path.Direction按照老规矩首先看下源码:
/**
* Specifies how closed shapes (e.g. rects, ovals) are oriented when they
* are added to a path.
*/
public enum Direction {
/** clockwise */
CW (0), // must match enum in SkPath.h
/** counter-clockwise */
CCW (1); // must match enum in SkPath.h
Direction(int ni) {
nativeInt = ni;
}
final int nativeInt;
}
这个方法的注释翻译过来就是:指定添加到路径时封闭形状(例如矩形、椭圆)的方向。这里的clockwise ,是顺时针的意思,也就是 CW (0);counter-clockwise是逆时针的意思,也就是 CW (1)
第二类(Path)
/**
* Add a copy of src to the path
*
* @param src The path that is appended to the current path
*/
public void addPath(Path src) {
isSimplePath = false;
nAddPath(mNativePath, src.mNativePath);
}
/**
* Add a copy of src to the path, transformed by matrix
*
* @param src The path to add as a new contour
*/
public void addPath(Path src, Matrix matrix) {
if (!src.isSimplePath) isSimplePath = false;
nAddPath(mNativePath, src.mNativePath, matrix.native_instance);
}
/**
* Add a copy of src to the path, offset by (dx,dy)
*
* @param src The path to add as a new contour
* @param dx The amount to translate the path in X as it is added
*/
public void addPath(Path src, float dx, float dy) {
isSimplePath = false;
nAddPath(mNativePath, src.mNativePath, dx, dy);
}
可以看到这是一个方法重载,这个方法的注释翻译过来就是(对应顺序):
- 将src的副本添加到路径中
- 将src的副本添加到路径中,由Matrix(矩阵)变换
- 将src的副本添加到路径中,偏移量为(dx,dy)
总之,这类方式就是将两个Path合并成为一个。然后可以设置矩阵、设置偏移量来进行自定义数据的设置。
下面是drawPath()参考示例代码:
Paint mPaint = new Paint(); // 创建画笔
mPaint.setColor(Color.BLACK); // 画笔颜色 - 黑色
mPaint.setStyle(Paint.Style.STROKE); // 填充模式 - 描边
mPaint.setStrokeWidth(10); // 边框宽度 - 10
Canvas canvas = new Canvas();
// 移动坐标系到屏幕中心 mWidth 是屏幕的最大宽度,mHeight 是屏幕的最大高度,
canvas.translate(mWidth / 2, mHeight / 2);
// <-- 注意 翻转y坐标轴
canvas.scale(1,-1);
Path path = new Path();
Path src = new Path();
//画矩形
path.addRect(-200,-200,200,200, Path.Direction.CW);
//画圆形
src.addCircle(0,0,100, Path.Direction.CW);
path.addPath(src,0,200);
// 绘制合并后的路径
mPaint.setColor(Color.BLACK);
canvas.drawPath(path,mPaint);
addPath()运行之后的效果图:
运行效果及代码逻辑分析:
两个Path(矩形和圆形)中心都是坐标原点,在将包含圆形的path添加到包含矩形的path之前将其进行移动了一段距离,最终绘制出来的效果就如上面所示
第三类(addArc与arcTo)
老规矩,先看下官方系统的源码
/**
* Add the specified arc to the path as a new contour.
*
* @param oval The bounds of oval defining the shape and size of the arc
* @param startAngle Starting angle (in degrees) where the arc begins
* @param sweepAngle Sweep angle (in degrees) measured clockwise
*/
public void addArc(RectF oval, float startAngle, float sweepAngle) {
addArc(oval.left, oval.top, oval.right, oval.bottom, startAngle, sweepAngle);
}
/**
* Add the specified arc to the path as a new contour.
*
* @param startAngle Starting angle (in degrees) where the arc begins
* @param sweepAngle Sweep angle (in degrees) measured clockwise
*/
public void addArc(float left, float top, float right, float bottom, float startAngle,
float sweepAngle) {
isSimplePath = false;
nAddArc(mNativePath, left, top, right, bottom, startAngle, sweepAngle);
}
/**
* Append the specified arc to the path as a new contour. If the start of
* the path is different from the path's current last point, then an
* automatic lineTo() is added to connect the current contour to the
* start of the arc. However, if the path is empty, then we call moveTo()
* with the first point of the arc.
*
* @param oval The bounds of oval defining shape and size of the arc
* @param startAngle Starting angle (in degrees) where the arc begins
* @param sweepAngle Sweep angle (in degrees) measured clockwise
*/
public void arcTo(RectF oval, float startAngle, float sweepAngle) {
arcTo(oval.left, oval.top, oval.right, oval.bottom, startAngle, sweepAngle, false);
}
/**
* Append the specified arc to the path as a new contour. If the start of
* the path is different from the path's current last point, then an
* automatic lineTo() is added to connect the current contour to the
* start of the arc. However, if the path is empty, then we call moveTo()
* with the first point of the arc.
*
* @param oval The bounds of oval defining shape and size of the arc
* @param startAngle Starting angle (in degrees) where the arc begins
* @param sweepAngle Sweep angle (in degrees) measured clockwise, treated
* mod 360.
* @param forceMoveTo If true, always begin a new contour with the arc
*/
public void arcTo(RectF oval, float startAngle, float sweepAngle,
boolean forceMoveTo) {
arcTo(oval.left, oval.top, oval.right, oval.bottom, startAngle, sweepAngle, forceMoveTo);
}
/**
* Append the specified arc to the path as a new contour. If the start of
* the path is different from the path's current last point, then an
* automatic lineTo() is added to connect the current contour to the
* start of the arc. However, if the path is empty, then we call moveTo()
* with the first point of the arc.
*
* @param startAngle Starting angle (in degrees) where the arc begins
* @param sweepAngle Sweep angle (in degrees) measured clockwise, treated
* mod 360.
* @param forceMoveTo If true, always begin a new contour with the arc
*/
public void arcTo(float left, float top, float right, float bottom, float startAngle,
float sweepAngle, boolean forceMoveTo) {
isSimplePath = false;
nArcTo(mNativePath, left, top, right, bottom, startAngle, sweepAngle, forceMoveTo);
}
addArc,arcTo这2个方法都是方法重载,下面就逐一分析:
addArc 这个方法的注释翻译过来就是:将指定的弧作为新的轮廓添加到路径中。
- 参数 RectF oval :椭圆的边界,定义弧的形状和大小
- 参数 float startAngle:圆弧开始的起始角(度)
- 参数 float sweepAngle:顺时针测量的角度
arcTo 这个方法的注释翻译过来就是:将指定的弧作为新轮廓追加到路径。如果路径的起始点与当前路径的最后一点不同,则添加一个自动lineTo()将当前轮廓线连接到圆弧的起始点。但是,如果路径是空的,则使用弧线的第一点调用moveTo()
- 参数 RectF oval :确定圆弧形状和大小的椭圆边界
- 参数 float startAngle :圆弧开始的起始角(度)
- 参数 float sweepAngle :顺时针测量的角度
- 参数 boolean forceMoveTo :如果为True,总是以圆弧开始新的轮廓线;如果为False,则不移动,而是连接最后一个点与圆弧起点
值得一提的是:sweepAngle取值范围是 [-360, 360),不包括360。当 >= 360 或者 < -360 时将不会绘制任何内容, 对于360,你可以用一个接近的值替代,例如: 359.99。
isEmpty(返回值 boolean)
isEmpty()这个方法主要是判断path中是否包含内容,如果没有内容,则为true,有内容则为false
isRect(返回值 boolean)
isRect()这个方法顾名思义,判断path是否是一个矩形,如果是一个矩形的话,会将矩形的信息存放进参数rect中
set(返回值 void)
set(Path path),这个方法是将新的path赋值到现有path中
offset
offset(float dx, float dy)这个方法就是对path进行一段平移,它和Canvas中的translate作用很像,但Canvas作用于整个画布,而path的offset只作用于当前path。
如果这篇文章对您有开发or学习上的些许帮助,希望各位看官留下宝贵的star,谢谢。
Ps:著作权归作者所有,转载请注明作者, 商业转载请联系作者获得授权,非商业转载请注明出处(开头或结尾请添加转载出处,添加原文url地址),文章请勿滥用,也希望大家尊重笔者的劳动成果