目录
2.2、moveTo、rMoveTo、setLastPoint
前言
在之前介绍过Canvas如何绘制一些简单的几何图形(如:圆、圆弧、矩形等),那么如果需要绘制一些复杂的图形(心形、五角星等)呢?
Path不仅能绘制简单的图形,也可以绘制比较复杂的图形。另外,根据路径绘制文本和剪裁画布都会用到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中的drawPath方法根据不同的绘制模式(填充或描边)来绘制,也可以用于剪裁画布和根据路径绘制文字。
另外路径有开放和封闭的区别。如下:
图像 | 名称 | 备注 |
---|---|---|
![]() | 封闭路径 | 首尾相接形成了一个封闭区域 |
![]() | 开放路径 | 没有首尾相接形成封闭区域 |
接下来,看下如何使用Path。
二、Path的使用
1、构造方法
Path有两个构造函数:
Path() // 空的构造函数
Path(Path src) //创建一个新的路径,并且从src路径里赋值内容
2、常用方法
Path常用方法表:
方法类别 | 相关API | 说明 |
---|---|---|
线操作 | lineTo、rLineTo | 绘制线 |
点操作 | moveTo、rMoveTo: setLastPoint: | 改变后面操作的起始点位置 改变前面操作中最后点的位置 |
绘制常规图形 | addRect: addRoundRect: addCircle: addOval: addArc、arcTo: | 绘制矩形 绘制圆角矩形 绘制圆 绘制椭圆 绘制圆弧 |
闭合path | close | 如果连接Path起点和终点能形成一个闭合图形,则会将起点和终点连接起来形成一个闭合图形 |
贝塞尔曲线 | quadTo、rQuadTo、cubicTo、rCubicTo | 绘制贝塞尔曲线 |
2.1、lineTo、rLineTo
- lineTo(float x, float y) :添加当前点(x',y')到目标点(x,y)构成的直线路径
- rLineTo(float dx, float dy):添加当前点(x',y')到目标点(x'+dx,y'+dy)构成的直线路径
ps:如果之前没有过操作,那么默认的起始点就是坐标原点(0,0)。另外为了方便好观察坐标的变化,我在屏幕上画出了网格,每块网格的宽高都是100
下面看下代码:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//画网格
drawFrame(canvas);
//设置Path
Path path = new Path();
//起始点(0,0)到(200,400)画一条直线
path.lineTo(200, 200);
//以(200,200)为起始点,目标点为(200+300,200+100)画一条直线,
path.rLineTo(300, 100);
canvas.drawPath(path, mPaint);
}
效果图:
2.2、moveTo、rMoveTo、setLastPoint
- moveTo(float x, float y) :改变下面操作的起点位置为(x,y)
- rMoveTo(float dx, float dy) :改变下面操作的起点位置为(x+dx,y+dy)
- setLastPoint(float dx, float dy) :改变前一步操作最后一个点的位置为(dx,dy),同时改变下一步操作的起始点的位置为(dx,dy)。
先看moveTo和rMoveTo的区别,示例:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//画网格
drawFrame(canvas);
Path path = new Path();
//移动起始点为(100,100)
path.moveTo(100, 100);
//起始点(100,100)到(200,200)画一条直线
path.lineTo(200, 200);
//移动起始点为(200+100,200+0)
path.rMoveTo(100, 0);
//起始点(200+100,200+0)到(400,300)画一条直线
path.lineTo(400, 300);
canvas.drawPath(path, mPaint);
}
效果图:
接下来看下,moveTo和setLastPoint的区别,示例:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//画网格
drawFrame(canvas);
//设置Path
Path path = new Path();
//移动起始点为(100,100)
path.moveTo(100, 100);
//起始点(100,100)到(200,200)画一条直线
path.lineTo(200, 200);
//移动起始点为(200+100,200+0)
path.setLastPoint(300, 200);
//起始点(200+100,200+0)到(400,300)画一条直线
path.lineTo(400, 300);
canvas.drawPath(path, mPaint);
}
效果图:
根据上面两个示例的效果图,可以得出结论:moveTo影响的是下一步操作的起点位置,不会影响之前的操作;而 setLastPoint改变下一步操作的起点位置的同时改变了前一步操作最后一个点的位置的,不仅影响前一步操作,同时也会影响后一步操作!
2.3、绘制常规图形
//绘制圆
addCircle(float x, float y, float radius, Direction dir)
//绘制椭圆
addOval(RectF oval, Direction dir)
//要求API21及以上
addOval(float left, float top, float right, float bottom, Direction dir)
//绘制矩形
addRect(RectF rect, Direction dir)
addRect(float left, float top, float right, float bottom, Direction dir)
//绘制圆角矩形
addRoundRect(RectF rect, float rx, float ry, Direction dir)
//要求API21及以上
addRoundRect(float left, float top, float right, float bottom, float rx, float ry,Direction dir)
addRoundRect(RectF rect, float[] radii, Direction dir)
//要求API21及以上
addRoundRect(float left, float top, float right, float bottom, float[] radii,Direction dir)
看下代码示例:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//画网格
drawFrame(canvas);
//设置Path
Path path = new Path();
//以(400,200)为圆心,半径为100绘制圆
path.addCircle(200, 200, 100, Path.Direction.CW);
//绘制椭圆
RectF rectF = new RectF(400, 100, 800, 300);
//第一种方法绘制椭圆
path.addOval(rectF, Path.Direction.CW);
//绘制矩形
RectF rect = new RectF(900, 100, 1200, 300);
//第一种方法绘制矩形
path.addRect(rect, Path.Direction.CW);
//第一种方法绘制矩形
path.addRect(1300, 100, 1600, 300, Path.Direction.CCW);
//绘制圆角矩形
RectF roundRect = new RectF(100, 400, 300, 600);
//第一种方法绘制圆角矩形
path.addRoundRect(roundRect, 20, 20, Path.Direction.CW);
path.addRoundRect(new RectF(400, 400, 600, 600),
10, 50, Path.Direction.CCW);
//float[] radii中有8个值,依次为左上角,右上角,右下角,左下角的rx,ry
RectF roundRectT = new RectF(700, 400, 900, 600);
path.addRoundRect(roundRectT, new float[]{50, 50, 50, 50, 50, 50, 0, 0}, Path.Direction.CCW);
path.addRoundRect(new RectF(1000, 400, 1200, 600),
new float[]{50, 50, 50, 50, 50, 50, 0, 0}, Path.Direction.CCW);
canvas.drawPath(path, mPaint);
}
效果图:
从上面的方法可以看出,所有方法里面都有一个共同的参数Direction :
Direction | 备注 |
---|---|
Path.Direction.CCW | counter-clockwise ,沿逆时针方向绘制 |
Path.Direction.CW | clockwise ,沿顺时针方向绘制 |
Direction 用法示例:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
String content="Path.Direction.CW顺时针绘制圆 Path.Direction.CCW逆时针绘制圆。";
//画网格
drawFrame(canvas);
//设置Path
Path path = new Path();
//以(400,400)为圆心,300为半径绘制圆
//Path.Direction.CW顺时针绘制圆 Path.Direction.CCW逆时针绘制圆
path.addCircle(400, 400, 300, Path.Direction.CW);
//沿path绘制文字
canvas.drawTextOnPath(content, path, 0, 0, mPaint);
canvas.drawPath(path, mPaint);
Path pathCCW = new Path();
pathCCW.addCircle(1100, 400, 300, Path.Direction.CCW);
canvas.drawTextOnPath(content, pathCCW, 0, 0, mPaint);
canvas.drawPath(pathCCW, mPaint);
}
效果图:
效果很明显,设置为Path.Direction.CW时,文字沿顺时针绘制;设置为Path.Direction.CCW时,文字沿逆时针绘制。
2.4、绘制圆弧
//绘制圆弧
addArc(RectF oval, float startAngle, float sweepAngle)
addArc(float left, float top, float right, float bottom, float startAngle,float sweepAngle)
//forceMoveTo:是否强制将path最后一个点移动到圆弧起点,
//true是强制移动,即为不连接两个点;false则连接两个点
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都是添加圆弧到path中,不过他们之间还是有区别的:addArc是直接添加圆弧到path中,而arcTo会判断要绘制圆弧的起点与绘制圆弧之前path中最后的点是否是同一个点,如果不是同一个点的话,就会连接两个点。
代码示例:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//画网格
drawFrame(canvas);
//设置Path
Path path = new Path();
//在(400, 200, 600, 400)区域内绘制一个300度的圆弧
RectF rectF = new RectF(100, 100, 300, 300);
path.addArc(rectF, 0, 300);
//在(400, 600, 600, 800)区域内绘制一个90度的圆弧,并且不连接两个点
RectF rectFTo = new RectF(400, 100, 600, 300);
path.arcTo(rectFTo, 0, 90, true);
//等价于path.addArc(rectFTo, 0, 90);
path.arcTo(new RectF(700, 100, 900, 300),
0, 90, false);
canvas.drawPath(path, mPaint);
}
效果图:
对比发现我们只是将arcTo最后一个参数变成了false,即连接绘制圆弧之前path的最后一个点和绘制圆弧的起点。
2.5、闭合Path
- path.close():如果path的终点和起始点不是同一个点的话,close()连接这两个点,形成一个封闭的图形。
示例:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//画网格
drawFrame(canvas);
//设置Path
Path path = new Path();
//在(400, 200, 600, 400)区域内绘制一个300度的圆弧
RectF rectF = new RectF(100, 100, 300, 300);
path.addArc(rectF, 0, 270);
//在(400, 600, 600, 800)区域内绘制一个90度的圆弧,并且不连接两个点
canvas.drawPath(path, mPaint);
Path pathClose = new Path();
pathClose.addArc(new RectF(400, 100, 600, 300),
0, 270);
pathClose.close();
canvas.drawPath(pathClose, mPaint);
}
效果图:
可以看到调用close()后,连接了path的起始点和终点形成了一个封闭图形!
三、PathEffect
之前介绍Paint的时候,有一个方法setPathEffect(PathEffect effect)没有详细说明,顾名思义这个方法肯定和Path有关。在Path类中,如果将Paint的Style设定为Stroke,则会看到由一条条实线组成图形。然而这些图形 看上去往往很单调,当我们需要绘制虚线,或者由一些小的图形来沿着一定的路径组成图形,API接口里其实提供了这个功能,使用PathEffect就可以完成这些比较特殊一些的效果。
PathEffec类本身没什么什么效果,主要实现是靠它的六个子类:
- CornerPathEffect
- PathDashPathEffect
- DashPathEffect
- DiscretePathEffect
- ComposePathEffect
- SumPathEffect
先看下没有设置任何效果的实现:
3.1、CornerPathEffect
CornerPathEffect的作用就是将Path的各个连接线段之间的夹角用一种更平滑的方式连接,类似于圆弧与切线的效果。
构造函数CornerPathEffect(float radius),其中radius为圆角的半径。效果如图:
3.2、PathDashPathEffect
PathDashPathEffect的作用是使用Path图形来填充当前的路径,可以用来显示虚线。构造函数:
PathDashPathEffect (Path shape, float advance, float phase,PathDashPathEffect.Stylestyle)。
- shape:则是指填充图形,
- advance:指每个图形间的间距,
- phase:为绘制时的偏移量,
- style:为该类自由的枚举值,有三种情况:Style.ROTATE、Style.MORPH和Style.TRANSLATE。其中ROTATE的情况下,线段连接处的图形转换以旋转到与下一段移动方向相一致的角度进行转转;MORPH时图形会以发生拉伸或压缩等变形的情况与下一段相连接;TRANSLATE时,图形会以位置平移的方式与下一段相连接。
效果如下:
3.3、DashPathEffect
DashPathEffect的作用就是将Path的线段虚线化。构造函数:
DashPathEffect(float[] intervals, float phase),其中
- intervals为虚线的ON和OFF数组,该数组的length必须大于等于2,下标为偶数的参数(注意数组下标是从0开始哦)定义了我们第一条实线的长度,而下标为奇数参数则表示第一条虚线的长度,如果此时数组后面不再有数据则重复第一个数以此往复循环
- phase为绘制时的偏移量。
效果如下:
3.4、DiscretePathEffect
DiscretePathEffect的效果就是把Path的线段切断,造成类似生锈的铁丝的效果。构造函数:
DiscretePathEffect(float segmentLength, float deviation),其中:
- segmentLength是指定切断的长度
- deviation为切断之后线段的偏移量,随机的,小于等于deviation。
效果如下:
3.5、ComposePathEffect
组合效果,ComposePathEffect类需要两个PathEffect参数来构造一个实例,构造函数ComposePathEffect (PathEffect outerpe,PathEffect innerpe),表现时,会首先将innerpe表现出来,然后再在innerpe的基础上去增加outerpe的效果。效果如下(PathDashPathEffect和DashPathEffect组合):
3.6、SumPathEffect
叠加效果,SumPathEffect类同样也需要两个PathEffect作为参数,构造函数:SumPathEffect(PathEffect first,PathEffect second),但与ComposePathEffect不同的是,在表现时,会分别对两个参数的效果各自独立进行表现,然后将两个效果简单的重叠在一起显示出来。如下(DashPathEffect和DiscretePathEffect叠加):
3.7、关于参数phase
在PathDashPathEffect和DashPathEffect中,如果phase参数的值不停发生改变,那么所绘制的图形也会随着偏移量而不断的发生变动,这个时候,看起来这条线就像动起来了一样。
下面用代码实现总的效果对下,代码:
public class CustomView extends View {
Paint paint;
float phase;
PathEffect[] effects = new PathEffect[7];
int[] colors;
Path path;
public CustomView(Context context) {
this(context,null);
}
public CustomView(Context context, AttributeSet attrs) {
this(context,attrs,0);
}
public CustomView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setLayerType(LAYER_TYPE_SOFTWARE,null);
// 初始化画笔
this.mContext=context;
initLinePint();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Log.i("TAG","onDraw");
effects[0] = null;
effects[1] = new CornerPathEffect(10);
Path p = new Path();
p.addRect(0,0,4,4, Path.Direction.CCW);
effects[2] = new PathDashPathEffect(p, 12, phase, PathDashPathEffect.Style.MORPH);
effects[3] = new DashPathEffect(new float[]{20, 10, 5, 10}, phase);
effects[4] = new DiscretePathEffect(3.0f, 6.0f);
effects[5] = new ComposePathEffect(effects[2], effects[3]);
effects[6] = new SumPathEffect(effects[3], effects[4]);
//将画布移动至(8,8)处开始绘制;
for(int i = 0; i < effects.length; i++ )
{
paint.setPathEffect(effects[i]);
paint.setColor(colors[i]);
canvas.drawPath(path, paint);
canvas.translate(0, 80);
}
phase += 1;//改变phase的值,形成动画效果
invalidate();
}
void initLinePint(){
paint = new Paint();
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(4);
path = new Path();
path.moveTo(10, 30);
for (int i = 1; i <= 39; i++)
{
path.lineTo(i * 20, (float)Math.random() * 60); ;
}
path.lineTo(800, 30);
path.close();
colors = new int[]{Color.BLACK, Color.BLUE, Color.CYAN, Color.GREEN, Color.MAGENTA,Color.RED, Color.YELLOW};
}
}
效果图:
总结:
以上是Path简单的用法,在Path类中还有一个重要的内容贝塞尔曲线,会后面再介绍。
祝:工作愉快!
参考资料:
https://www.jianshu.com/p/9ad3aaae0c63
http://www.gcssloop.com/customview/Path_Basic/
https://blog.youkuaiyun.com/lixin84915/article/details/8148920