android 自定义View之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)函数绘制,使用Paint.Style填充或描边,或它可以用于裁剪canvas.clipPath()或绘制文本路径canvas.drawTextOnPath()
根据android doc中的介绍我们可以看到,path是用来描述绘制几何图形路径的.其中包括我们经常见到的圆,多边形,直线,贝塞尔曲线…
我们先看一下Path中的几个枚举类型
Path.Direction
Specifies how closed shapes (e.g. rects, ovals) are oriented when they are added to a path
原文的意思是在绘制封闭的几何图形(例如矩形,椭圆…)时,使用的path是根据什么方向来的.这个方向指的是钟表的走向,顺时针:CW,逆时针:CCW.
具体用途,我们在下文的函数中在做详细介绍
Path.FillType
Enum for the ways a path may be filled.
用于表示填充路径的方式
- EVEN_ODD 奇偶规则
- WINDING 非零环绕数规则
- INVERSE_EVEN_ODD 反-奇偶规则
- INVERSE_WINDING 反-非零环绕数规则
我们使用Paint.Style.Fill给几何图形填充色值时,填充的集合图形的内部。一般简单的圆,矩阵……我们可以一样就看出它的内部和外部.但是对于一些自相交的多边形,有时候眼睛看到的和实际情况会出现差别,这时我们可与使用FillType中提到的两种方法来判断:
规则 | 描述 | 在内 | 在外 |
---|---|---|---|
奇-偶规则(Odd-even Rule) | 从任意点P构造一条射线(一条直线),从P向无穷大的任意方向,用这条射线找到C的所有交点,若为奇数,则P在多边形内部,否则在外部 | 相交点数为奇数 | 相交点数为偶数 |
非零环绕数规则(Nonzero Winding Number Rule) | 从任意点P构造一条射线(一条直线),从P向无穷大的任意方向。用这条射线找到与给定图形C上所有的焦点,判断焦点处的图形线段的方向:每一次顺时针交点(从左到右通过光线的曲线,从P点开始)减1;对于每一个逆时针交点(从右到左的曲线,从P点看)加1。如果总数为零,P在C外;否则,它是内部的。 | 不为零 | 为零 |
图示:
1.奇-偶规则(Odd-even Rule)
2.非零环绕数规则(Nonzero Winding Number Rule)
mPaint.style = Paint.Style.FILL
mPaint.color = Color.RED
canvas?.translate((width / 2).toFloat(), (height / 2).toFloat())
val path = Path()
// 叠加两个不同半径圆形组成自相交图形用来展示不同Direction下的非零环绕规则
path.addCircle(-250f, 0f, 100f, Path.Direction.CW);
path.addCircle(-250f, 0f, 200f, Path.Direction.CCW)
path.addCircle(250f, 0f, 100f, Path.Direction.CCW);
path.addCircle(250f, 0f, 200f, Path.Direction.CCW)
path.fillType = Path.FillType.WINDING
canvas?.drawPath(path, mPaint)
最后两个就不介绍了,这是一张Android APIDemo的示例图:
showPath(canvas, 0, 0, Path.FillType.WINDING, paint);
showPath(canvas, 160, 0, Path.FillType.EVEN_ODD, paint);
showPath(canvas, 0, 160, Path.FillType.INVERSE_WINDING, paint);
showPath(canvas, 160, 160, Path.FillType.INVERSE_EVEN_ODD, paint);
Path.Op
The logical operations that can be performed when combining two paths.
我们在canvas中介绍过这么一个枚举Region.Op,他们的作用差不多,只是比Canvas中的少一个属性而已,我们看看他的属性:
我们举个例子:使用path生成A,B两个几何图形,他们有一部分重叠在一起
- DIFFERENCE: 是A形状中不同于B的部分显示出来
- INTERSECT: 显示A,B相交的部分
- REVERSE_DIFFERENCE: 是B形状中不同于A的部分显示出来,与DIFFERENCE相反
- UNION: 显示A,B全部
- XOR: 显示全集部分减去相交部分
这个就不写例子了,如果有兴趣的话,可以根据Canvas中的Region.Op的例子,自己去敲一遍
Path的函数
构造函数
- Path():创建一个空的Path对象
- Path(Path src) :创建一个空的Path对象,并将指定path对象的内容复制到该空path对象中
path函数基本可以分为这么几类:
add几何图形
我们在绘制封闭几何图形的时候要注意:
在设置path.setFillType()的时候,我们要注意,不同方向的自相交的多边形填充时,会出现不一样的效果.
- addArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle)
- addArc(RectF oval, float startAngle, float sweepAngle)
弧线:
paint.style = Paint.Style.STROKE
paint.color = Color.RED
paint.strokeWidth = 2f
canvas?.translate(10f, 10f)
path.addArc(RectF(0f, 0f, 200f, 200f), 0f, 120f)
canvas?.drawPath(path, paint)
项目中最常用的地方就是写自定义加载进度条(用在下载和上传的地方:根据百分比自动画出我们需要的进度)
- addCircle(float x, float y, float radius, Path.Direction dir)
这个是我们最常见的,根据中心点和半径画圆,PathDirection:这个上文中已经介绍过了,表示方向
- addOval(RectF oval, Path.Direction dir)
- addOval(float left, float top, float right, float bottom, Path.Direction dir)
椭圆
- addRect(RectF rect, Path.Direction dir)
- addRect(float left, float top, float right, float bottom, Path.Direction dir)
矩形
- addRoundRect(RectF rect, float rx, float ry, Path.Direction dir)
- addRoundRect(float left, float top, float right, float bottom, float rx, float ry, Path.Direction dir)
- addRoundRect(RectF rect, float[] radii, Path.Direction dir)
- addRoundRect(float left, float top, float right, float bottom, float[] radii, Path.Direction dir)
这个是绘制带弧度的矩形,我们日常用到最多的带弧度的矩形图片和圆形图片了
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
paint.style = Paint.Style.FILL_AND_STROKE
paint.strokeWidth = 2f
paint.shader = BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
canvas?.translate(10f, 10f)
path.addRoundRect(RectF(0f, 0f, 200f, 150f), 20f, 20f, Path.Direction.CCW)
canvas?.drawPath(path, paint)
paint.style = Paint.Style.STROKE
path.addRoundRect(RectF(220f, 0f, 420f, 150f), 30f, 30f, Path.Direction.CCW)
canvas?.drawPath(path, paint)
path.addRoundRect(RectF(0f, 200f, 200f, 350f), floatArrayOf(20f, 20f, 0f, 0f, 0f, 0f, 0f, 0f), Path.Direction.CCW)
canvas?.drawPath(path, paint)
path.addRoundRect(RectF(220f, 200f, 420f, 350f), floatArrayOf(20f, 20f, 20f, 20f, 0f, 0f, 0f, 0f), Path.Direction.CCW)
canvas?.drawPath(path, paint)
path.addRoundRect(RectF(440f, 200f, 640f, 350f), floatArrayOf(20f, 20f, 20f, 20f,20f, 20f, 0f, 0f), Path.Direction.CCW)
canvas?.drawPath(path, paint)
}
我们可以根据上面的代码,把addRoundRect换成其他形状的函数,实现其他类型的自定义ImageView
注意:上述代码中,只提供了一个思路,实际开发中还有需要优化的地方:(1)获取view的高宽;(2)获取bitmap的高宽;(3)对bitmap进行缩放;(4)执行绘制操作后,回收bitmap
- addPath(Path src, Matrix matrix)
- addPath(Path src)
- addPath(Path src, float dx, float dy)
canvas?.translate(width / 2.toFloat(), height / 2.toFloat())
paint.style = Paint.Style.STROKE
paint.strokeWidth = 2f
var path1 = Path();
var path2 = Path();
path1.addCircle(0f, 0f, 100f, Path.Direction.CCW)
path2.addCircle(100f, 100f, 50f, Path.Direction.CCW)
path.addPath(path1)
path.addPath(path2)
canvas?.drawPath(path, paint)
合并两个path
添加Matrix:
var mMatrix: Matrix = Matrix()
mMatrix.setTranslate(-100f, -100f)
path.addPath(path1)
path.addPath(path2, mMatrix)
canvas?.drawPath(path, paint)
使用矩阵移动后,小圆移动到大圆的中心了.
使用addPath(Path src, float dx, float dy)对srcPath的x,y坐标进行位置偏移
var path1 = Path();
var path2 = Path();
path1.addCircle(0f, 0f, 100f, Path.Direction.CCW)
path2.addCircle(100f, 100f, 50f, Path.Direction.CCW)
path.addPath(path1)
path.addPath(path2,-100f,-100f)
canvas?.drawPath(path, paint)
根据第一张效果图和该效果图比较,可以知道dx,dy就是srcPath上x,y坐标上的偏移量
- arcTo(RectF oval, float startAngle, float sweepAngle)
- arcTo(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean forceMoveTo)
- arcTo(RectF oval, float startAngle, float sweepAngle, boolean forceMoveTo)
注意: sweepAngle取值范围是 [-360, 360),如果有兴趣的话,可以亲自写一下,把sweepAngle设置成360看看什么效果???
我试过,结果空白了,没有弧线了.然后设置成359就可以……
绘制弧线:可以附加到特定的path中
path.moveTo(0f, 0f)
path.lineTo(200f, 0f)
path.arcTo(RectF(300f, 0f, 500f, 200f), 0f, 270f)
canvas?.drawPath(path, paint)
path.moveTo(0f, 0f)
path.lineTo(200f, 0f)
path.arcTo(RectF(300f, 0f, 500f, 200f), 0f, 270f)
path.close()
canvas?.drawPath(path, paint)
- close():将当前path的结束点和开始点连接在一起
path.moveTo(0f, 0f)
path.lineTo(200f, 0f)
path.arcTo(RectF(300f, 0f, 500f, 200f), 0f, 270f,true)
canvas?.drawPath(path, paint)
注意: forceMoveTo是否将最后一个点移动到圆弧起点,true即不连接最后一个点与圆弧起点,如下图;false连接,如上图;
直线和弧线中间没有连接在一起
- computeBounds(RectF bounds, boolean exact)
- reset():清除path中的内容.但是会保留FillType.
- rewind():清除path中的内容,FillType也会清除.
计算path的边界,并把内容写到RectF中.
复用上面的代码:
mPaint.style = Paint.Style.STROKE
mPaint.color = Color.RED
canvas?.translate((width / 2).toFloat(), (height / 2).toFloat())
val path = Path()
// 叠加两个不同半径圆形组成自相交图形用来展示不同Direction下的非零环绕规则
path.addCircle(-250f, 0f, 100f, Path.Direction.CW);
path.addCircle(-250f, 0f, 200f, Path.Direction.CCW)
path.addCircle(250f, 0f, 100f, Path.Direction.CCW);
path.addCircle(250f, 0f, 200f, Path.Direction.CCW)
path.fillType = Path.FillType.WINDING
canvas?.drawPath(path, mPaint)
var rectF: RectF = RectF()
path.computeBounds(rectF, true)
mPaint.color = Color.BLUE
canvas?.drawRect(rectF, mPaint)
path.reset()
mPaint.color = Color.BLACK
mPaint.strokeWidth = 4f
path.moveTo(0f, 0f)
path.lineTo(200f, 200f)
path.lineTo(-100f, 0f)
canvas?.drawPath(path, mPaint)
mPaint.strokeWidth = 2f
var rectF1: RectF = RectF()
path.computeBounds(rectF1, true)
mPaint.color = Color.GREEN
canvas?.drawRect(rectF1, mPaint)
- setFillType(Path.FillType ft)
- getFillType()
- isInverseFillType,()
-
toggleInverseFillType()切换填充模式
填充类型,这个在上面介绍FillType的时候已经介绍过了,在这里就不再重复了;
-
incReserve(extraPtCount)
提示路径准备加入更多的点。这可以让道路更有效地分配存储
set(@NonNull Path src)将当前path中的内容替换成指定srcpath的中的内容
- offset(float dx, float dy):将当前的path进行偏移,偏移量为dx,dy
- offset(float dx, float dy, @Nullable Path dst)
public void offset(float dx, float dy, @Nullable Path dst) {
if (dst != null) {
dst.set(this);
} else {
dst = this;
}
dst.offset(dx, dy);
}
根据源码可以看出,如果目标path不为空的话,将指定dstPath中的内容替换成当前path中的内容,让后对指定path进行path偏移,如果偏移的话,直接将当前path赋值给指定path,然后对他进行偏移操作.
- moveTo(float x, float y):设置图形的起始点坐标
- lineTo(float x, float y):由上一个点画一条线到指定的x,y;如果前边没有使用moveTo()指定起始点的话,我们在第一次调用该函数的时候,起始点为坐标(0,0)点上;
mPath.lineTo(100f, 100f);
mPath.lineTo(0f, 200f);
mPath.moveTo(200f,200f)
mPath.lineTo(400f,200f)
mPath.lineTo(200f,400f)
canvas?.drawPath(mPath, mPaint)
- isEmpty():判断是否为空
- isRect(RectF rectF):判断path是否是一个矩形
- rLineTo(float dx, float dy)
- rMoveTo(float dx, float dy)
paint.color = Color.RED
path.rMoveTo(0f, 50f)
path.rLineTo(200f, 50f)
canvas?.drawPath(path, paint)
paint.color = Color.GREEN
path.reset()
path.moveTo(0f, 50f)
path.lineTo(200f, 50f)
canvas?.drawPath(path, paint)
根据图示,我们可以看出lineTo和moveTo函数都是根据最初始的坐标系来决定线段的点的,
而rLineTo和rMoveTo则不同,根据图第三个坐标系,我们可以看到path.rMoveTo(0f, 50f)先将起始点定位到原坐标系的(0,50),然后以该点为原点建立新的坐标系,rLineTo(200f, 50f)则是在新坐标系中从(0,0)到(200,50)连接而成的线则是我们需要的结果.
注意:所有带r和不带r的函数的区别就是我们提到坐标系的不同.
- setLastPoint(float dx, float dy)
设置最后一点的坐标
paint.color = Color.RED
path.moveTo(0f, 50f)
path.lineTo(200f, 50f)
path.lineTo(200f, 100f)
path.setLastPoint(100f, 100f)
canvas?.drawPath(path, paint)
我们可以看到,setLastPoint(100f, 100f)取代了他上面的结尾点点lineTo(200f, 100f)生成了新的线段.
- transform(Matrix matrix) 通过矩阵变换path上的点
- transform(Matrix matrix, Path dst) 通过矩阵变换path上的点,让后将结果写入到dstPath中
类似我们前边介绍的Bitmap.concat(@Nullable Matrix matrix)通过矩阵来进行移动,缩放,旋转,错切…变换
paint.color = Color.RED
path.addCircle(200f,200f,80f,Path.Direction.CCW)
canvas?.drawPath(path,paint)
paint.color = Color.GREEN
var matrix:Matrix = Matrix()
matrix.setScale(0.5f,0.5f)
path.transform(matrix)
canvas?.drawPath(path,paint)
Bézier curve 贝塞尔曲线
* quadTo(float x1, float y1, float x2, float y2):二阶贝塞尔曲线:有一个起点和一个终点,还有一个控制点,moveTo()为起点,(x1,y1)控制点,(x2,y2)终点
* cubicTo(float x1, float y1, float x2, float y2, float x3, float y3)三阶贝塞尔曲线,控制点和数据点类似上面,
这个由于本人水平也仅仅是会用,所以就不介绍了,如果想要了解的话可以去一下地址查看:
*:原理效果图等等….
一个叫GcsSloop的作者写的,非常全面