1、自定义绘制概述
View 要在 Activity 中显示出来,要经过测量、布局和绘制三个步骤,分别对应三个方法:onMeasure()、onLayout() 和 onDraw()。由于本文的目的是明确阐述 View 的绘制,所以篇幅着重会针对第三个步骤〈绘制〉进行展开,测量和布局会在代码片段中提及,下表是三个方法对应的说明:
动作 | 方法 | 说明 |
---|---|---|
测量 | onMeasure() | 决定 View 的大小 |
布局 | onLayout() | 决定 View 在 ViewGroup 中的位置 |
绘制 | onDraw() | 决定绘制这个 View |
自定义绘制过程中核心的两个 API 是 Canvas 与 Paint,Canvas 类持有各种“绘制”方法,要绘制一些东西,需要具备 4 个基本组件:一个用来保存像素的 Bitmap 位图,一个执行绘制方法的 Canvas 画布(用于写入位图),一个用于绘制的图元(如 Rect,Path,text,Bitmap)和一个 Paint 画笔(用于描述绘制的颜色和样式);Paint 类持有关于绘制几何图形,文本和位图的样式和颜色信息。
2、Canvas 和 Paint 可以做什么
2.1、Canvas 可以做什么?
事实上查看 Canvas 的 API 你就会发现这个类主要用于绘制颜色、线、圆、椭圆、矩形、图像、文字等图元。通过各个图元的组合绘制,再配合 Paint 设置的颜色和样式,就能满足大部分的绘制需求了。此外,Canvas 还可以进行裁切和几何变换的操作。
2.2、Paint 可以做什么?
Paint 可以做的事,不只是设置颜色,还可以设置实心、空心、线条粗细、有无阴影等。
方法 | 说明 |
---|---|
Paint.setStyle(Style style) | 设置绘制模式 |
Paint.setColor(int color) | 设置颜色 |
Paint.setStrokeWidth(float width) | 设置线条宽度 |
Paint.setTextSize(float textSize) | 设置文字大小 |
Paint.setAntiAlias(boolean aa) | 设置抗锯齿开关 |
3、自定义绘制实践
自定义绘制的上手非常容易:提前创建好 Paint 对象,重写 onDraw(),把绘制代码写在 onDraw() 里面,就是自定义绘制最基本的实现。大概就像下面的代码片段,唯一需要注意的是别漏写了 super.onDraw(canvas):
1 | Paint paint = new Paint(); |
3.1 填充颜色
方法 | |
void drawColor (int color) 会在整个区域填充指定的颜色 | |
参数 | |
color | int: 绘制到画布上的颜色值 |
1 | canvas.drawColor(Color.BLACK); // 填充纯黑色 |
1 | canvas.drawColor(Color.parseColor("#88880000")); // 填充半透明红色 |
类似的方法还有 drawRGB(int r, int g, int b)
和 drawARGB(int a, int r, int g, int b)
,它们和 drawColor(color)
只是使用方式不同,作用都是一样的。
1 | canvas.drawRGB(100, 200, 100); |
3.2 画圆
方法 | |
void drawCircle(float cx, float cy, float radius, Paint paint) 使用指定的画笔绘制指定的圆 | |
参数 | |
cx | float: 要绘制的圆的中心 x 坐标 |
cy | float: 要绘制的圆的中心 y 坐标 |
radius | float: 要绘制的圆的半径 |
paint | Paint: 用来绘制画圆的画笔(不能为空) |
1 | package com.sunzn.paint.view; |
绘制圆的代码如上,编写一个类继承 View 类,在构造方法中初始化 Paint
并设置抗锯齿 paint.setAntiAlias(true)
,在 onMeasure(int widthMeasureSpec, int heightMeasureSpec)
方法中确定圆的宽高(取宽高的最小值作为圆的宽高),在 onDraw(Canvas canvas)
方法中确定圆心坐标,设置圆的填充色,最后绘制圆。
这里在确定圆心坐标的过程中涉及到了 Android 中 View 的坐标系。在 Android 里,每个 View 都有一个自己的坐标系,彼此之间是不影响的。这个坐标系的原点是 View 左上角的那个点;水平方向是 X 轴,右正左负;竖直方向是 Y 轴,下正上负。
结合 View 坐标系,一个 View 的坐标 (x, y) 处,指的就是相对它自身左上角 (0, 0) 点的水平 x 像素、竖直方向 y 像素的点。例如:(300, 300) 指的就是相对于 View 自身左上角 (0, 0) 点的向右 300、向下 300 像素的位置;(100, -50) 指的就是相对于 View 自身左上角 (0, 0) 点的向右 100、向上 50 像素的位置。所以,本例中不考虑 padding 因素,圆心的位置 (cx, cy) 应该为宽的一半(在 onMeasure 方法中将宽高的最小值设为 View 的宽高值),圆的半径 radius 也为宽的一半。所以canvas.drawCircle(cx, cy, getWidth() / 2, paint)
这行代码绘制出的圆,在 View 中的位置和尺寸应该如下图所示:
在本例中,会发现绘制圆的时候为 Paint 设置了颜色、开启了抗锯齿效果,其实 Paint 还有好多其他的设置方法,下面会将 Paint 的相关方法进行说明。
3.2.1 Paint 设置颜色
方法 | |
void setColor (int color) 设置画笔颜色 | |
参数 | |
color | int: 设置到画笔上的颜色值(包括 alpha)。 |
1 | paint.setColor(Color.BLUE); |
Paint.setColor(int color)
是 Paint 最常用的方法之一,用来设置绘制内容的颜色。你不仅可以用它绘制红色的圆,也可用它来绘制红色的矩形、五角星、文字等。
3.2.2 Paint 设置样式
方法 | |
void setStyle (Paint.Style style) 设置画笔样式,用于控制原始图形的几何解释(除了 drawBitmap,默认为填充)。 | |
参数 | |
style | Paint.Style : 设置到画笔的新样式。 |
如果你想绘制的不是实心圆,而是空心圆,可以使用 paint.setStyle(Paint.Style.STROKE)
把绘制模式改为画线模式。
1 | paint.setStyle(Paint.Style.STROKE); |
setStyle (Paint.Style style)
方法设置的是绘制的 Style。Style 具体来说有三种:FILL、STROKE 和 FILL_AND_STROKE。
样式 | 说明 |
---|---|
FILL | 填充模式,默认为填充模式 |
STROKE | 画线模式(即勾边模式) |
FILL_AND_STROKE | 既画线又填充 |
3.2.3 Paint 设置描边宽度
方法 | |
void setStrokeWidth (float width) 设置划线的宽度。 | |
参数 | |
width | float : 设置画笔的描边宽度,当画笔的样式为 Stroke 或 StrokeAndFill 时使用。 |
在 Stroke 或 StrokeAndFill 样式下,可以使用 paint.setStrokeWidth (float width)
来设置线条的宽度。本例中要注意的是,在设置圆的半径的时候需要减掉描边宽度的一半,否则绘制的圆会越界。
1 | paint.setStrokeWidth(20f); |
3.2.4 Paint 设置抗锯齿
方法 | |
void setAntiAlias (boolean aa) 设置或清除 ANTI_ALIAS_FLAG 位 AntiAliasing 以保证平滑的绘制边缘,但对形状的内部没有影响。 | |
参数 | |
aa | boolean : 为 true 时设置 antialias 标志位,false 将其清除。 |
在绘制的时候,往往需要开启抗锯齿来让图形和文字的边缘更加平滑。开启抗锯齿很简单,只要在 new Paint()
的时候加上一个 ANTI_ALIAS_FLAG
参数就行:
1 | Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); |
另外,你也可以使用 Paint.setAntiAlias(boolean aa)
来动态开关抗锯齿。
1 | paint.setAntiAlias(true); |
可以看出,没有开启抗锯齿的时候,图形会有毛边现象,所以一定记得要打开抗锯齿。
冷知识
好奇的人可能会问:抗锯齿既然这么有用,为什么不默认开启,或者干脆把这个开关取消,自动让所有绘制都开启抗锯齿?
短答案:因为抗锯齿并不一定适合所有场景。
长答案:所谓的毛边或者锯齿,发生的原因并不是很多人所想象的「绘制太粗糙」「像素计算能力不足」;同样,抗锯齿的原理也并不是选择了更精细的算法来算出了更平滑的图形边缘。
实质上,锯齿现象的发生,只是由于图形分辨率过低,导致人眼察觉出了画面中的像素颗粒而已。换句话说,就算不开启抗锯齿,图形的边缘也已经是最完美的了,而并不是一个粗略计算的粗糙版本。
那么,为什么抗锯齿开启之后的图形边缘会更加平滑呢?因为抗锯齿的原理是:修改图形边缘处的像素颜色,从而让图形在肉眼看来具有更加平滑的感觉。一图胜千言,上图:
上面这个是把前面那两个圆放大后的局部效果。看到没有?未开启抗锯齿的圆,所有像素都是同样的黑色,而开启了抗锯齿的圆,边缘的颜色被略微改变了。这种改变可以让人眼有边缘平滑的感觉,但从某种角度讲,它也造成了图形的颜色失真。 所以,抗锯齿好不好?好,大多数情况下它都应该是开启的;但在极少数的某些时候,你还真的需要把它关闭。「某些时候」是什么时候?到你用到的时候自然就知道了。
3.3 画矩形
方法 | |
void drawRect (float left, float top, float right, float bottom, Paint paint) 使用指定的画笔绘制指定的矩形 | |
参数 | |
left | float: 要绘制的矩形的左侧 |
top | float: 要绘制的矩形的顶端 |
right | float: 要绘制的矩形的右侧 |
bottom | float: 要绘制的矩形的底部 |
paint | Paint: 用来绘制矩形的画笔(不能为空) |
1 | paint.setStyle(Paint.Style.FILL); |
另外,它还有两个重载方法 drawRect(RectF rect, Paint paint)
和 drawRect(Rect rect, Paint paint)
,让你可以直接填写 RectF
或 Rect
对象来绘制矩形。
3.4 画单点
方法 | |
void drawPoint (float x, float y, Paint paint) 绘制单个点 | |
参数 | |
x | float: 绘制点的 x 坐标 |
y | float: 绘制点的 y 坐标 |
paint | Paint: 用来绘制点的画笔(不能为空) |
点的大小可以通过 paint.setStrokeWidth(width)
来设置;点的形状可以通过 paint.setStrokeCap(cap)
来设置:ROUND
画出来是圆形的点,SQUARE
或 BUTT
画出来是方形的点。
注:Paint.setStrokeCap(cap) 可以设置点的形状,但这个方法并不是专门用来设置点的形状的,而是一个设置线条端点形状的方法。端点有圆头 (ROUND)、平头 (BUTT) 和方头 (SQUARE) 三种。
1 | paint.setStrokeWidth(50); |
好像有点像 FILL 模式下的 drawCircle()
和 drawRect()
?事实上确实是这样的,它们和 drawPoint()
的绘制效果没有区别。各位在使用的时候按个人习惯和实际场景来吧,哪个方便和顺手用哪个。
3.5 画多点
方法一 | |
void drawPoints (float[] pts, Paint paint) 绘制多个点 | |
参数 | |
pts | float: 绘制点的坐标数组 |
paint | Paint: 用来绘制点的画笔(不能为空) |
方法二 | |
void drawPoints (float[] pts, int offset, int count, Paint paint) 绘制多个点 | |
参数 | |
pts | float: 绘制点阵列[x0 y0 x1 y1 x2 y2 ...] |
offset | int: 开始绘制前要跳过的值的个数 |
count | int: 在跳过偏移量之后要处理的值的数量 |
paint | Paint: 用来绘制点的画笔(不能为空) |
同样是画点,它和 drawPoint()
的区别是可以画多个点。pts
这个数组是点的坐标,每两个成一对;offset
表示跳过数组的前几个数再开始记坐标;count
表示一共要处理几个值,例如:要绘制 4 个点,一个点用 2 个值表示,那么这个 count 就等于 4 x 2 = 8。
1 | paint.setStrokeCap(Paint.Cap.ROUND); |
3.6 画椭圆
方法 | |
void drawOval (float left, float top, float right, float bottom, Paint paint) 使用指定的画笔绘制指定的椭圆 | |
参数 | |
left | float: 要绘制的椭圆的左侧 |
top | float: 要绘制的椭圆的顶端 |
right | float: 要绘制的椭圆的右侧 |
bottom | float: 要绘制的椭圆的底部 |
paint | Paint: 用来绘制矩形的画笔(不能为空) |
只能绘制横着的或者竖着的椭圆,不能绘制斜的(斜的倒是也可以,但不是直接使用 drawOval(),而是配合几何变换)。left,top,right,bottom 是这个椭圆的左、上、右、下四个边界点的坐标。
1 | paint.setStyle(Paint.Style.FILL); |
另外,它还有一个重载方法 drawOval(RectF rect, Paint paint)
,让你可以直接填写 RectF 来绘制椭圆。
3.7 画单条线
方法 | |
void drawLine (float startX, float startY, float stopX, float stopY, Paint paint) 使用指定的起止点绘制一条线 | |
参数 | |
startX | float: 线的起始点的 x 坐标 |
startY | float: 线的起始点的 y 坐标 |
stopX | float: 线的中止点的 x 坐标 |
stopY | float: 线的中止点的 y 坐标 |
paint | Paint: 用来绘制线的画笔(不能为空) |
1 | paint.setStrokeWidth(10); |
由于直线不是封闭图形,所以
setStyle(style)
对直线没有影响。
3.8 画多条线
方法一 | |
void drawLines (float[] pts, Paint paint) 绘制多条线 | |
参数 | |
pts | float: 绘制线的坐标数组 |
paint | Paint: 用来绘制线的画笔(不能为空) |
方法二 | |
void drawLines (float[] pts, int offset, int count, Paint paint) 绘制多条线 | |
参数 | |
pts | float: 绘制点阵列[x0 y0 x1 y1 x2 y2 ...](不能为空) |
offset | int: 开始绘制前要跳过的值的个数 |
count | int: 在跳过偏移量之后要处理的值的数量 |
paint | Paint: 用来绘制线的画笔(不能为空) |
1 | float[] points = {0, 5, 150, 5, 150 / 2, 0, 150 / 2, 150, 0, 150 - 5, 150, 150 - 5}; |
注意:这里在进行绘制的时候要考虑线的粗细,如果指定了宽高,而线条的绘制又是顶格绘制,顶部和底部线的起止点坐标要加减线条粗细度的一半进行调整,否则只有线条粗细度的一半会被绘制。
3.9 画圆角矩形
方法 | |
void drawRoundRect (float left, float top, float right, float bottom, float rx, float ry, Paint paint) 使用指定的画笔绘制指定的圆角矩形 | |
参数 | |
left | float: 要绘制的圆角矩形的左侧 |
top | float: 要绘制的圆角矩形的顶端 |
right | float: 要绘制的圆角矩形的右侧 |
bottom | float: 要绘制的圆角矩形的底部 |
rx | float: x 轴方向的圆角半径 |
ry | float: y 轴方向的圆角半径 |
paint | Paint: 用来绘制圆角矩形的画笔(不能为空) |
1 | paint.setStyle(Paint.Style.FILL); |
另外,它还有一个重载方法 drawRoundRect(RectF rect, float rx, float ry, Paint paint)
,让你可以直接填写 RectF
来绘制圆角矩形。
3.10 画弧形、扇形
方法 | |
drawArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean useCenter, Paint paint) 绘制指定的弧,其将被缩放以适合指定的椭圆。 | |
参数 | |
left | float: 要绘制的弧形、扇形的左侧 |
top | float: 要绘制的弧形、扇形的顶端 |
right | float: 要绘制的弧形、扇形的右侧 |
bottom | float: 要绘制的弧形、扇形的底部 |
startAngle | float: 弧开始的起始角度(x 轴的正向,即正右的方向,是 0 度的位置;顺时针为正角度,逆时针为负角度,以度为单位) |
sweepAngle | float: 弧形顺时针划过的角度(度) |
useCenter | boolean: 表示是否连接到圆心,如果不连接到圆心,就是弧形,如果连接到圆心,就是扇形。 |
paint | Paint: 用来绘制圆角弧形的画笔(不能为空) |
1 | paint.setStyle(Paint.Style.FILL); // 填充模式 |
到此为止,以上就是 Canvas 所有的简单图形的绘制。除了简单图形的绘制, Canvas 还可以使用 drawPath(Path path)
来绘制自定义图形。
3.11 画自定义图形
前面的这些方法,都是绘制某个给定的图形,而 drawPath()
可以绘制自定义图形。当你要绘制的图形比较特殊,使用前面的那些方法做不到的时候,就可以使用 drawPath()
来绘制。
方法 | |
drawPath(Path path, Paint paint) 使用指定的画笔绘制指定的路径。 | |
参数 | |
path | Path: 要绘制的路径(不能为空) |
paint | Paint: 用来绘制路径的画笔(不能为空) |
drawPath(path)
这个方法是通过描述路径的方式来绘制图形的,它的 path 参数就是用来描述图形路径的对象。path 的类型是 Path ,使用方法大概像下面这样:
1 | public class PathView extends View { |
Path 可以描述直线、二次曲线、三次曲线、圆、椭圆、弧形、矩形、圆角矩形。把这些图形结合起来,就可以描述出很多复杂的图形。下面我就说一下具体的怎么把这些图形描述出来。
Path 有两类方法,一类是直接描述路径的,另一类是辅助的设置或计算。
3.11.1 Path 方法第一类:直接描述路径
这一类方法还可以细分为两组:添加子图形和画线(直线或曲线)
第一组:addXxx() 添加子图形
(1)添加圆
方法 | |
addCircle(float x, float y, float radius, Path.Direction dir) 向路径添加一个封闭圆形。 | |
参数 | |
x | float: 要添加到路径的圆心 x 坐标 |
y | float: 要添加到路径的圆心 y 坐标 |
radius | float: 要添加到路径的圆的半径 |
dir | Path.Direction: 画圆的路径方向 |
路径方向有两种:顺时针(CW clockwise) 和逆时针(CCW counter-clockwise)。对于普通情况,这个参数填 CW 还是填 CCW 没有影响。它只是在需要填充图形 (Paint.Style 为 FILL 或 FILL_AND_STROKE) ,并且图形出现自相交时,用于判断填充范围的。比如下面这个图形:
是应该填充成这样呢?
还是应该填充成这样呢?
在用 addCircle()
为 Path 中新增一个圆之后,调用 canvas.drawPath(path, paint)
,就能画一个圆出来。就像这样:
1 | path.addCircle(300, 300, 100, Path.Direction.CW); |
可以看出,path.AddCircle(x, y, radius, dir)
+ canvas.drawPath(path, paint)
这种写法,和直接使用 canvas.drawCircle(x, y, radius, paint)
的效果是一样的,区别只是它的写法更复杂。所以如果只画一个圆,没必要用 Path,直接用 drawCircle()
就行了。drawPath()
一般是在绘制组合图形时才会用到的。
(2)添加其他图形
添加椭圆 | |
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) | |
添加另一个 Path | |
addPath(Path path) |
上面这几个方法的使用和 addCircle()
差不多,这里不做过多介绍。
第二组:xxxTo() 画线(直线或曲线)
这一组和第一组 addXxx() 方法的区别在于,第一组是添加的完整封闭图形(除了 addPath() ),而这一组添加的只是一条线。
(1)画直线
方法一 | |
lineTo(float x, float y) 从当前位置向目标位置画一条线,如果没有调用 moveTo(),第一个点自动设置为(0,0)。 | |
参数 | |
x | float: 目标位置的 x 坐标 |
y | float: 目标位置的 y 坐标 |
方法二 | |
rLineTo(float dx, float dy) 与 lineTo 相似,但是相对于当前位置的坐标。 | |
参数 | |
x | float: 相对于上一个点的 x 坐标 |
y | float: 相对于上一个点的 y 坐标 |
从当前位置向目标位置画一条直线, x 和 y 是目标位置的坐标。这两个方法的区别是,lineTo(x, y)
的参数是绝对坐标,而 rLineTo(x, y)
的参数是相对当前位置的相对坐标 (前缀 r 指的就是 relatively 「相对地」)。
当前位置:所谓当前位置,即最后一次调用画 Path 的方法的终点位置。初始值为原点 (0, 0)。
1 | paint.setStyle(Paint.Style.STROKE); |
(2)画二次贝塞尔曲线
方法一 | |
quadTo(float x1, float y1, float x2, float y2) 以当前位置为起点,接近控制点(x1,y1),并以(x2,y2)结束。 | |
参数 | |
x1 | float: 二次曲线上控制点的 x 坐标 |
y1 | float: 二次曲线上控制点的 y 坐标 |
x2 | float: 二次曲线终点的 x 坐标 |
y2 | float: 二次曲线终点的 y 坐标 |
方法二 | |
rQuadTo(float dx1, float dy1, float dx2, float dy2) 与 quadTo 相似,但是相对于当前位置的坐标。 | |
参数 | |
x1 | float: 相对于上一个点的 x 坐标,用于二次曲线的控制点 |
y1 | float: 相对于上一个点的 y 坐标,用于二次曲线的控制点 |
x2 | float: 相对于上一个点的 x 坐标,用于二次曲线的终点 |
y2 | float: 相对于上一个点的 y 坐标,用于二次曲线的终点 |
这条二次贝塞尔曲线的起点就是当前位置,而参数中的 x1
, y1
和 x2
, y2
则分别是控制点和终点的坐标。和 rLineTo(x, y)
同理,rQuadTo(dx1, dy1, dx2, dy2)
的参数也是相对坐标。
1 | path.quadTo(400, 400, 400, 200); |
为了便于理解控制点和终点在贝塞尔曲线绘制过程中的作用,我在绘制过程中将对应坐标点也画了出来。
贝塞尔曲线:贝塞尔曲线是几何上的一种曲线。它通过起点、控制点和终点来描述一条曲线,主要用于计算机图形学。概念总是说着容易听着难,总之使用它可以绘制很多圆润又好看的图形,但要把它熟练掌握、灵活使用却是不容易的。不过还好的是,一般情况下,贝塞尔曲线并没有什么用处,只在少数场景下绘制一些特殊图形的时候才会用到,所以如果你还没掌握自定义绘制,可以先把贝塞尔曲线放一放,稍后再学也完全没问题。
(3)画三次贝塞尔曲线
方法一 | |
cubicTo(float x1, float y1, float x2, float y2, float x3, float y3) 以当前位置为起点添加一个三次贝塞尔曲线,接近控制点(x1,y1)和(x2,y2),并以(x3,y3)结束。 | |
参数 | |
x1 | float: 三次曲线上第一个控制点的 x 坐标 |
y1 | float: 三次曲线上第一个控制点的 y 坐标 |
x2 | float: 三次曲线上第二个控制点的 x 坐标 |
y2 | float: 三次曲线上第二个控制点的 y 坐标 |
x3 | float: 三次曲线上终点的 x 坐标 |
y3 | float: 三次曲线上终点的 y 坐标 |
方法二 | |
rCubicTo(float x1, float y1, float x2, float y2, float x3, float y3) 与 cubicTo 相似,但是相对于当前位置的坐标。 | |
参数 | |
x1 | float: 三次曲线上第一个控制点的 x 坐标 |
y1 | float: 三次曲线上第一个控制点的 y 坐标 |
x2 | float: 三次曲线上第二个控制点的 x 坐标 |
y2 | float: 三次曲线上第二个控制点的 y 坐标 |
x3 | float: 三次曲线上终点的 x 坐标 |
y3 | float: 三次曲线上终点的 y 坐标 |
1 | protected void onDraw(Canvas canvas) { |
和上面的 quadTo()
rQuadTo()
的二次贝塞尔曲线同理,cubicTo()
和 rCubicTo()
是三次贝塞尔曲线,不再解释。
(4)移动到目标位置
方法一 | |
moveTo(float x, float y) 设置绘制图形的起点。 | |
参数 | |
x | float: 新起点的 x 坐标 |
y | float: 新起点的 y 坐标 |
方法二 | |
rMoveTo(float dx, float dy) 设置绘制图形相对于当前位置的新起点。 | |
参数 | |
x | float: 相对于上一个终点 x 坐标 |
y | float: 相对于上一个终点 y 坐标 |
1 | path.lineTo(300, 300); // 画斜线 |
(5) 画弧形
方法 | |
arcTo(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean forceMoveTo) 将指定的弧添加到路径,但并不使用当前位置作为弧线的起点。 | |
参数 | |
left | float |
top | float |
right | float |
bottom | float |
startAngle | float: 弧开始的起始角度(x 轴的正向,即正右的方向,是 0 度的位置;顺时针为正角度,逆时针为负角度,以度为单位) |
sweepAngle | float: 弧形顺时针划过的角度(度) |
forceMoveTo | boolean: 绘制是要「抬一下笔移动过去」,还是「直接拖着笔过去」,区别在于是否留下移动的痕迹 |
1 | protected void onDraw(Canvas canvas) { |
1 | path.lineTo(200, 200); // 画斜线 |
这个方法和 Canvas.drawArc()
比起来,少了一个参数 useCenter
,而多了一个参数 forceMoveTo
。少了 useCenter
,是因为 arcTo()
只用来画弧形而不画扇形,所以不再需要 useCenter
参数;而多出来的这个 forceMoveTo
参数的意思是,绘制是要「抬一下笔移动过去」,还是「直接拖着笔过去」,区别在于是否留下移动的痕迹。
另外,它还有两个重载方法 arcTo(RectF oval, float startAngle, float sweepAngle, boolean forceMoveTo)
、arcTo(RectF oval, float startAngle, float sweepAngle)
让你可以直接填写 RectF 来绘制弧形。
(6) 画弧形
方法 | |
addArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle) 将指定的弧添加到路径,但并不使用当前位置作为弧线的起点。 | |
参数 | |
left | float |
top | float |
right | float |
bottom | float |
startAngle | float: 弧开始的起始角度(x 轴的正向,即正右的方向,是 0 度的位置;顺时针为正角度,逆时针为负角度,以度为单位) |
sweepAngle | float: 弧形顺时针划过的角度(度) |
又是一个弧形的方法。一个叫 arcTo()
,一个叫 addArc()
,都是弧形,区别在哪里?其实很简单:addArc()
只是一个直接使用了 forceMoveTo = true 的简化版 arcTo()
。
1 | path.lineTo(200, 200); // 画斜线 |
(7) 封闭当前子图形
方法 | |
close() 它的作用是把当前的子图形封闭,即由当前位置向当前子图形的起点绘制一条直线。 |
1 | path.moveTo(200, 200); |
1 | path.moveTo(200, 200); |
子图形:官方文档里叫做 contour 。但由于在这个场景下我找不到这个词合适的中文翻译(直译的话叫做「轮廓」),所以我换了个便于中国人理解的词:「子图形」。前面说到,第一组方法是「添加子图形」,所谓「子图形」,指的就是一次不间断的连线。一个 Path 可以包含多个子图形。当使用第一组方法,即
addCircle()
addRect()
等方法的时候,每一次方法调用都是新增了一个独立的子图形;而如果使用第二组方法,即lineTo()
arcTo()
等方法的时候,则是每一次断线(即每一次「抬笔」),都标志着一个子图形的结束,以及一个新的子图形的开始。
另外,不是所有的子图形都需要使用close()
来封闭。当需要填充图形时(即 Paint.Style 为 FILL 或 FILL_AND_STROKE),Path 会自动封闭子图形。
1 | paint.setStyle(Paint.Style.FILL); |
3.11.2 Path 方法第二类:辅助的设置或计算
这类方法的使用场景比较少,在这里就不多讲了,只讲其中一个方法: setFillType(FillType fillType)
。前面在说 dir 参数的时候提到, Path.setFillType(fillType)
是用来设置图形自相交时的填充算法的:
方法中填入不同的 FillType 值,就会有不同的填充效果。FillType 的取值有四个:
- EVEN_ODD
- WINDING (默认值)
- INVERSE_EVEN_ODD
- INVERSE_WINDING
其中后面的两个带有 INVERSE_ 前缀的,只是前两个的反色版本,所以只要把前两个,即 EVEN_ODD 和 WINDING,搞明白就可以了。
EVEN_ODD
和 WINDING
的原理有点复杂,直接讲出来的话信息量太大,所以先给一个简单粗暴版的总结感受一下: WINDING
是「全填充」,而 EVEN_ODD
是「交叉填充」:
之所以叫「简单粗暴版」,是因为这些只是通常情形下的效果;而如果要准确了解它们在所有情况下的效果,就得先知道它们的原理,即它们的具体算法。
EVEN_ODD 和 WINDING 的原理
EVEN_ODD
即 even-odd rule (奇偶原则):对于平面中的任意一点,向任意方向射出一条射线,这条射线和图形相交的次数(相交才算,相切不算)如果是奇数,则这个点被认为在图形内部,是要被涂色的区域;如果是偶数,则这个点被认为在图形外部,是不被涂色的区域。还以左右相交的双圆为例:
射线的方向无所谓,同一个点射向任何方向的射线,结果都是一样的,从上图可以看出,射线每穿过图形中的一条线,内外状态就发生一次切换,这就是为什么 EVEN_ODD 是一个「交叉填充」的模式。
WINDING
即 non-zero winding rule (非零环绕数原则):首先,它需要你图形中的所有线条都是有绘制方向的。
然后,同样是从平面中的点向任意方向射出一条射线,但计算规则不一样:以 0 为初始值,对于射线和图形的所有交点,遇到每个顺时针的交点(图形从射线的左边向右穿过)把结果加 1,遇到每个逆时针的交点(图形从射线的右边向左穿过)把结果减 1,最终把所有的交点都算上,得到的结果如果不是 0,则认为这个点在图形内部,是要被涂色的区域;如果是 0,则认为这个点在图形外部,是不被涂色的区域。
和 EVEN_ODD 相同,射线的方向并不影响结果。所以,前面的那个「简单粗暴」的总结,对于 WINDING 来说并不完全正确:如果你所有的图形都用相同的方向来绘制,那么 WINDING 确实是一个「全填充」的规则;但如果使用不同的方向来绘制图形,结果就不一样了。
所以,完整版的 EVEN_ODD 和 WINDING 的效果应该是这样的:
而 INVERSE_EVEN_ODD 和 INVERSE_WINDING ,只是把这两种效果进行反转而已,你懂了 EVEN_ODD 和 WINDING ,自然也就懂 INVERSE_EVEN_ODD 和 INVERSE_WINDING 了,这里就不讲了。
好,花了好长的篇幅来讲 drawPath(path) 和 Path,终于讲完了。同时,Canvas 对图形的绘制就也讲完了。图形简单时,使用 drawCircle() drawRect() 等方法来直接绘制;图形复杂时,使用 drawPath() 来绘制自定义图形。
3.12 画 Bitmap
方法 | |
drawBitmap(Bitmap bitmap, float left, float top, Paint paint) 绘制指定的位图 | |
参数 | |
bitmap | Bitmap: 要绘制的位图(不能为空) |
left | float: 绘制位图左侧的位置 |
top | float: 绘制位图的顶端的位置 |
paint | Paint: 用来绘制位图的画笔(不能为空) |
1 | canvas.drawBitmap(bitmap, 100, 150, paint); |
绘制 Bitmap 对象,也就是把这个 Bitmap 中的像素内容贴过来。其中 left 和 top 是绘制 bitmap 的左上角位置坐标。它的重载方法:
- drawBitmap(Bitmap bitmap, Rect src, Rect dst, Paint paint)
- drawBitmap(Bitmap bitmap, Rect src, RectF dst, Paint paint)
- drawBitmap(Bitmap bitmap, Matrix matrix, Paint paint)
drawBitmap 还有一个兄弟方法 drawBitmapMesh(),可以绘制具有网格拉伸效果的 Bitmap。 drawBitmapMesh() 的使用场景较少,所以不讲了,如果有兴趣你可以自己研究一下。
3.13 绘制文字
方法 | |
drawText(String text, float x, float y, Paint paint) 绘制文本 | |
参数 | |
bitmap | Bitmap: 要绘制的文字(不能为空) |
x | float: 绘制的文本的原点的 x 坐标 |
y | float: 绘制的文本的基线的 y 坐标 |
paint | Paint: 用来绘制文字的画笔(不能为空) |
1 | paint.setTextSize(120); |
通过 Paint.setTextSize(float textSize),可以设置文字的大小。
1 | paint.setStrokeWidth(1f); |
设置文字的位置和尺寸,这些只是绘制文字最基本的操作。文字的绘制具有极高的定制性,不过由于它的定制性实在太高了,所以会在后面专门用一期来讲文字的绘制。
绘制部分第一节, Canvas 的 drawXXX() 系列方法和 Paint 的基本使用,就到这里。