Canvas绘制自定义图形

Path操作速查表

Canvas绘制自定义图形

前面写的就是Canvas 所有的简单图形的绘制。除了简单图形的绘制, Canvas 还可以使用 drawPath(Path path) 来绘制自定义图形。

drawPath(Path path, Paint paint) 画自定义图形

前面的那些方法,都是绘制某个给定的图形,而 drawPath() 可以绘制自定义图形。当你要绘制的图形比较特殊,使用前面的那些方法做不到的时候,就可以使用 drawPath() 来绘制。

drawPath(path) 这个方法是通过描述路径的方式来绘制图形的,它的 path 参数就是用来描述图形路径的对象。path 的类型是 Path ,使用方法大概像下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class MyView : View {

constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)

val paint = Paint()
val path = Path()


override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
paint.color = Color.RED
paint.style = Paint.Style.FILL

path.addArc(200f, 200f, 400f, 400f, -225f, 225f);
path.arcTo(400f, 200f, 600f, 400f, -180f, 225f, false);
path.lineTo(400f, 542f);
canvas.drawPath(path,paint)
}
}

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

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

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

第一组: addXxx() ——添加子图形

addCircle(float x, float y, float radius, Direction dir) 添加圆

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

路径方向有两种:顺时针 (CW clockwise) 和逆时针 (CCW counter-clockwise) 。对于普通情况,这个参数填 CW 还是填 CCW 没有影响。它只是在需要填充图形 (Paint.StyleFILLFILL_AND_STROKE) ,并且图形出现自相交时,用于判断填充范围的。下面再讨论dir这个参数

在用 addCircle()Path 中新增一个圆之后,调用 canvas.drawPath(path, paint) ,就能画一个圆出来

1
2
path.addCircle(300f,300f,200f,Path.Direction.CW)
canvas.drawPath(path,paint)

其他的 Path.add-() 方法和这类似,例如:

addOval(float left, float top, float right, float bottom, Direction dir) / addOval(RectF oval, Direction dir) 添加椭圆

addRect(float left, float top, float right, float bottom, Direction dir) / addRect(RectF rect, Direction dir) 添加矩形

addRoundRect(RectF rect, float rx, float ry, Direction dir) / addRoundRect(float left, float top, float right, float bottom, float rx, float ry, Direction dir) / addRoundRect(RectF rect, float[] radii, Direction dir) / addRoundRect(float left, float top, float right, float bottom, float[] radii, Direction dir) 添加圆角矩形

addPath(Path path) 添加另一个 Path

和前面addCircle()的使用都差不多,不再做过多介绍

第二组:xxxTo() ——画线(直线或曲线)

lineTo(float x, float y) / rLineTo(float x, float y) 画直线

当前位置向目标位置画一条直线, xy 是目标位置的坐标。这两个方法的区别是,lineTo(x, y) 的参数是绝对坐标,而 rLineTo(x, y) 的参数是相对当前位置的相对坐标 (前缀 r 指的就是 relatively 「相对地」)。

当前位置:所谓当前位置,即最后一次调用画 Path 的方法的终点位置。初始值为原点 (0, 0)。

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

quadTo(float x1, float y1, float x2, float y2) / rQuadTo(float dx1, float dy1, float dx2, float dy2) 画二次贝塞尔曲线

这条二次贝塞尔曲线的起点就是当前位置,而参数中的 x1, y1x2, y2 则分别是控制点和终点的坐标。和 rLineTo(x, y) 同理,rQuadTo(dx1, dy1, dx2, 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) 画三次贝塞尔曲线

moveTo(float x, float y) / rMoveTo(float x, float y) 移动到目标位置

不论是直线还是贝塞尔曲线,都是以当前位置作为起点,而不能指定起点。但你可以通过 moveTo(x, y)rMoveTo() 来改变当前位置,从而间接地设置这些方法的起点。

1
2
3
path.lineTo(300f,300f) //画斜线
path.moveTo(600f,300f) //移动点
path.lineTo(600f,0f) //画直线

moveTo(x, y) 虽然不添加图形,但它会设置图形的起点,所以它是非常重要的一个辅助方法。

第二组还有两个特殊的方法: arcTo()addArc()。它们也是用来画线的,但并不使用当前位置作为弧线的起点。

arcTo(RectF oval, float startAngle, float sweepAngle, boolean forceMoveTo)

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

arcTo(RectF oval, float startAngle, float sweepAngle) 画弧形

这个方法和 Canvas.drawArc() 比起来,少了一个参数 useCenter,而多了一个参数 forceMoveTo

少了 useCenter ,是因为 arcTo() 只用来画弧形而不画扇形,所以不再需要 useCenter 参数;而多出来的这个 forceMoveTo 参数的意思是,绘制是要「抬一下笔移动过去」,还是「直接拖着笔过去」,区别在于是否留下移动的痕迹

1
2
3
path.lineTo(100f,100f)
path.arcTo(100f,100f,300f,300f,-90f,90f,true) //前面4个参数代表所在椭圆的上下左右4个顶点距离父容器左边和上边的距离,第5个参数表示从-90度位置开始画,第6个参数表示划过90弧度,第7个参数表示直接从前面(100,100)点的位置移过来,不留痕迹
canvas.drawPath(path,paint)

1
2
3
path.lineTo(100f,100f)
path.arcTo(100f,100f,300f,300f,-90f,90f,false) //这次留下移动轨迹,直接连线连到弧形起点
canvas.drawPath(path,paint)

addArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle) / addArc(RectF oval, float startAngle, float sweepAngle)

又是一个弧形的方法。一个叫 arcTo ,一个叫 addArc(),都是弧形,区别在哪里?其实很简单: addArc() 只是一个直接使用了 forceMoveTo = true 的简化版 arcTo() ,其只有6个参数

close() 封闭当前子图形

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

1
2
3
4
path.moveTo(300f,300f)
path.lineTo(600f,300f)
path.lineTo(450f,450f)
canvas.drawPath(path,paint) //未使用close

1
2
3
4
5
path.moveTo(300f,300f)
path.lineTo(600f,300f)
path.lineTo(450f,450f)
path.close() //调用close来封闭图形 path.lineTo(300f, 300f)
canvas.drawPath(path,paint)

close()lineTo(起点坐标) 是完全等价的。

不是所有的子图形都需要使用 close() 来封闭。当需要填充图形时(即 Paint.StyleFILLFILL_AND_STROKEPath 会自动封闭子图形

1
2
3
4
5
paint.style = Paint.Style.FILL
path.moveTo(300f,300f)
path.lineTo(600f,300f)
path.lineTo(450f,450f)
canvas.drawPath(path,paint)

以上就是 Path 的第一类方法:直接描述路径的。

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

这类方法的使用场景比较少,理解其中一个方法: setFillType(FillType fillType)

Path.setFillType(Path.FillType ft) 设置填充方式

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

方法中填入不同的 FillType 值,就会有不同的填充效果。FillType 的取值有四个:

  • EVEN_ODD
  • WINDING (默认值)
  • INVERSE_EVEN_ODD
  • INVERSE_WINDING

其中后面的两个带有 INVERSE_ 前缀的,只是前两个的反色版本,所以只要把前两个,即 EVEN_ODDWINDING,搞明白就可以了。

先来看一下通常情形下的效果

接下来了解一下填充的原理

EVEN_ODD

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

WINDING

即 non-zero winding rule (非零环绕数原则):首先,它需要你图形中的所有线条都是有绘制方向的:

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

关于图形的方向

图形的方向:对于添加子图形类方法(如 Path.addCircle() Path.addRect())的方向,由方法的 dir 参数来控制,这个在前面已经讲过了;而对于画线类的方法(如 Path.lineTo() Path.arcTo())就更简单了,线的方向就是图形的方向。

所以,完整版的 EVEN_ODDWINDING 的效果应该是这样的:

Canvas绘制Bitmap和文字

drawBitmap(Bitmap bitmap, float left, float top, Paint paint) 画 Bitmap

绘制 Bitmap 对象,也就是把这个 Bitmap 中的像素内容贴过来。其中 lefttop 是要把 bitmap 绘制到的位置坐标。它的使用非常简单。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class MyView : View {

constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)

val paint = Paint()
val bitmap = BitmapFactory.decodeResource(context.resources,R.drawable.test_006)

override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
paint.color = Color.RED
paint.style = Paint.Style.FILL
paint.strokeWidth = 10f
canvas.drawBitmap(bitmap,300f,300f,paint)
}
}

它还有一些重载方法,不列举了

drawText(String text, float x, float y, Paint paint) 绘制文字

界面里所有的显示内容,都是绘制出来的,包括文字。 drawText() 这个方法就是用来绘制文字的。参数 text 是用来绘制的字符串,xy 是绘制的起点坐标。

Paint.setTextSize(float textSize)

1
2
3
paint.setTextSize(18);
canvas.drawText(text, 100, 25, paint);
paint.setTextSize(36);
重置路径

重置Path有两个方法,分别是reset和rewind,两者区别主要有一下两点:

这个两个方法应该何时选择呢?

选择权重: FillType > 数据结构

因为“FillType”影响的是显示效果,而“数据结构”影响的是重建速度。

0%