前言
在自定义 View 中,Canvas 和 Paint 配合使用,可以绘制各种 View。其中,Canvas 是画布的意思,Paint 是画笔的意思。本文主要介绍这两个类的一些基本 API,以及它们之间的配合使用。
预备知识
绘图坐标系和视图坐标系
- 绘图坐标系
在绘制图形的时候,需要确定一个落笔点,这个落笔点用坐标来表示。这个坐标所在的坐标系就是绘图坐标系。
初始状态下,Canvas 的左上角为坐标系的原点。在绘图过程中,绘图坐标是根据原点来确定的,这个原点是可以移动的。
- 视图坐标系
理论上 Canvas 是没有边界的,但是我们的手机屏幕是有界的。可以理解为我们是通过一个方形的洞(手机屏幕)来看一张巨画(Canvas)。
手机屏幕也有一个坐标系,原点位于屏幕左上角,向右和向下分别为 x 轴和 y 轴的正方向,称为视图坐标系。
在介绍绘图坐标系时所说的原点移动是指落笔点的移动,而画布本身是静止的。如果我们所绘制的图的范围超出了屏幕范围,那么就可以通过移动屏幕来查看超出范围的部分。移动屏幕可以使用 scrollTo 和 scrollBy。
对于手机屏幕和 Canvas 的关系,可以参考下图:(图片出处:https://juejin.im/post/5cc3d0686fb9a031f4160713#heading-3 )
Paint 的基本用法
Paint 的创建
Paint 的创建有三种方式:1
2
3
4
5Paint() // 默认的创建方式
Paint(int flags) // 可通过 flags 参数进行配置
Paint(Paint paint) // 相当于复制一个 paint
第一种方式是默认形式,没有配置任何 flag。
第二种方式不仅可以创建新画笔,还可以添加 flag。主要有这几种 flag:
flag | 简介 |
---|---|
ANTI_ALIAS_FLAG | 开启抗锯齿功能 |
FILTER_BITMAP_FLAG | 设置位图进行滤波处理 |
DITHER_FLAG | 在绘制时启用抖动 |
如果要添加多个 flag,中间可用’|’进行连接,例如:1
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
第三种方式根据已有的画笔复制一个新画笔,复制后的画笔是一个全新的画笔,对复制后的画笔进行改动不会影响被复制的画笔。
画笔颜色
我们经常要用到颜色,这里的颜色还包括透明度,在设置颜色时主要有这几个相关的方法:
方法 | 简介 |
---|---|
setColor(int color) | 设置颜色 |
setAlpha(int a) | 设置透明度 |
setARGB(int a, int r, int g, int b) | 设置透明度及颜色 |
在设置透明度时,其参数的取值范围为 0 - 255,即 0x00 - 0xFF。同理,setARGB 的另外 3 个参数的取值也是 0 - 255。例如:1
2
3
4
5mPaint.setARGB(0xFF, 0x03, 0xA9, 0xF4); // (1)
mPaint.setARGB(255, 3, 169, 244); // (2)
mPaint.setColor(0xFF03A9F4); // (3)
这三个式子是等价的,只不过第一个是用十六进制表示,而第二个用十进制表示,最后一个则是把几个参数合起来表示。
要注意的是,如果第一个参数传入 0,或者(3)传入的数为 0x03A9F4 ,即透明度为 0 时,颜色变为透明,你是看不见颜色的。
除了根据 ARGB 指定颜色外,还可以使用系统内置的一些颜色,例如:1
mPaint.setColor(Color.RED);
画笔模式(Paint.Style)
这里的画笔模式指在绘制一个图形的时候,是绘制图形轮廓还是填充图形内容,或者两者兼有。它有以下三种模式:
Style | 说明 |
---|---|
Paint.Style.FILL | 填充内容,默认模式 |
Paint.Style.STROKE | 描边,只绘制图形轮廓 |
Paint.Style.FILL_AND_STROKE | 填充 + 描边,同时填充内容和绘制轮廓 |
下面用一张图来说明各模式的区别:(图片出处:https://www.gcssloop.com/customview/paint-base )
使用方式如下,下面用三种模式各画了一个圆:1
2
3
4
5
6
7
8
9
10mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(50);
mPaint.setColor(Color.RED);
canvas.drawCircle(0f, 0f, 100f, mPaint);
mPaint.setStyle(Paint.Style.FILL);
canvas.drawCircle(0f, 300f, 100f, mPaint);
mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
canvas.drawCircle(0f, 600f, 100f, mPaint);
画笔线帽(Paint.Cap)
画笔线帽用于指定线段开始和结束时的效果。它有以下三种类型:
Cap | 说明 |
---|---|
Paint.Cap.BUTT | 无线帽,默认类型 |
Paint.Cap.ROUND | 以线条宽度为直径,在开头和结尾分别添加一个半圆 |
Paint.Cap.SQUARE | 以线条宽度为边长,在开头和结尾分别添加半个正方形 |
使用方式如下:1
2
3
4mPaint.setStrokeCap(Paint.Cap.ROUND);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(100);
canvas.drawLine(-200f, 0f, 200f, 0f, mPaint);
三种类型的区别如图:(图片出处:https://www.gcssloop.com/customview/paint-base )
其它基本方法
方法 | 说明 |
---|---|
setAntiAlias(boolean aa) | 是否抗锯齿,设置了抗锯齿后边界变模糊,锯齿减少 |
setDither(boolean dither) | 是否使用图像抖动,使用后绘制出来的图片更加平滑和饱满,更加清晰 |
setFilterBitmap(boolean filter) | 是否对位图进行滤波处理 |
Canvas 的基本用法
Canvas 的常用操作速查表
操作类型 | 相关 API | 说明 |
---|---|---|
绘制颜色 | drawColor, drawARGB, drawRGB | 使用单一颜色填充整个画布 |
绘制基本形状 | drawPoint, drawPoints, drawLine, drawLines, drawRect, drawRoundRect, drawOval, drawCircle, drawArc | 依次为画点、线、矩形、圆角矩形、椭圆、圆、圆弧 |
绘制图片 | drawBitmap, drawPicture | 绘制位图和图片 |
绘制文本 | drawText, drawTextOnPath | 绘制文字,根据路径绘制文字 |
绘制路径 | drawPath | 绘制路径 |
画布裁剪 | clipPath, clipRect | 设置画布的显示区域 |
画布快照 | save, restore, restoreToCount, getSaveCount | 依次为保存状态、回滚到上一次保存的状态、回滚到指定状态、获取保存次数 |
画布变换 | translate, scale, rotate, skew | 依次为平移、缩放、旋转、错切 |
Canvas 的绘制
绘制颜色
可以使用 drawColor, drawARGB, drawRGB 等方法给画布设置颜色,这些方法的使用和 Paint 中相关方法的使用相似,例如:1
2// 画布颜色变为红色
canvas.drawColor(Color.RED);
绘制点
有关的方法是 drawPoint 和 drawPoints,先看 drawPoint 方法,该方法绘制一个点。方法具体如下:1
drawPoint(float x, float y, Paint paint)
第一个和第二个参数是点的坐标(x, y),第三个参数是使用的画笔。下面看下该方法的使用:1
2
3
4
5
6mPaint.setColor(Color.RED); // 点的颜色
mPaint.setStrokeWidth(30f); // 宽度,不设置的话太小,可能看不见点
canvas.drawPoint(0f, 0f, mPaint); // 画出的点为正方形
mPaint.setStrokeCap(Paint.Cap.ROUND); // 设置为圆点
canvas.drawPoint(0f, 50f, mPaint); // 画出的点为圆形
再看另一个方法 drawPoints,该方法绘制一组点,方法具体如下:1
drawPoints(2) float[] pts, Paint paint) (multiple =
方法有两个参数,第一个参数是 float 数组,要求元素个数是 2 的倍数,每两个元素为一组(x, y 坐标)。第二个参数是使用的画笔。使用如下:1
2
3
4// 10 个元素,每两个元素为一组,即有 5 个点,分别为
// (0, 0), (0, 50), (0, -50), (-50, 0), (50, 0)
float[] points = {0f, 0f, 0f, 50f, 0f, -50f, -50f, 0f, 50f, 0f};
canvas.drawPoints(points, mPaint);
绘制直线
同样有两个方法绘制直线,分别是 drawLine 和 drawLines,先看 drawLine 方法,该方法绘制一条直线:1
2drawLine(float startX, float startY, float stopX, float stopY,
Paint paint)
方法有 5 个参数,第 1 和第 2 个参数为起点的坐标(x, y),第 3 和第 4 个参数为终点的坐标(x, y),第 5 个参数为画笔。使用如下:1
2
3mPaint.setColor(Color.RED);
mPaint.setStrokeWidth(30f); // 宽度
canvas.drawLine(-100f, 0f, 0f, 100f, mPaint);
另一方法 drawLines 可以绘制多条直线,具体如下:1
drawLines(@Size(multiple = 4) @NonNull float[] pts, @NonNull Paint paint)
和 drawPoints 类似,它的第一个参数也是一个 float 数组,不过这次要求元素个数为 4 的倍数,即每 4 个为一组。第二个参数是画笔。使用如下:1
2
3// 每四个为一组,这里有 8 个元素,即有两条直线
float[] lines = {-200f, -200f, 200f, 200f, -100f, 0f, 100f, 0f};
canvas.drawLines(lines, mPaint);
绘制形状
Canvas 可以画的形状有很多,例如矩形、圆角矩形、椭圆、圆等。下面依次看一下:
绘制矩形和圆角矩形
绘制矩形可以使用 drawRect 方法,该方法有三种形式:1
2
3drawRect( RectF rect, Paint paint)
drawRect( Rect r, Paint paint)
drawRect(float left, float top, float right, float bottom, Paint paint)
第一种和第二种类似,只是第一个参数不同,RectF 和 Rect 相似,只是前者使用 float 表示边长,而后者使用 int 表示边长。RectF 和 Rect 封装了一个矩形。
第三种重载方法的第 1 和第 2 个参数可以理解为左上角的坐标,第 3 和第 4 个参数可以理解为右下角的坐标。第 5 个参数为画笔。
下面来看下它们的使用:1
2
3
4
5
6 mPaint.setColor(Color.BLUE);
mPaint.setStyle(Paint.Style.FILL); // 填充内容
// 该矩形左上角的坐标为 (100, 200),长为 600-100 = 500,宽为 500-200 = 300
canvas.drawRect(100f, 200f, 600f, 500f, mPaint);
// // 等价于上一行
// canvas.drawRect(new RectF(100f, 200f, 600f, 500f), mPaint);
绘制圆角矩形和绘制矩形类似,使用 drawRoundRect 方法,它有两个重载方法:1
2
3drawRoundRect(float rx, float ry, Paint paint) RectF rect,
drawRoundRect(float left, float top, float right, float bottom, float rx, float ry,
Paint paint)
可以看到,比起 drawRect 方法,在 Paint 参数前,多了两个参数。分别表示在 x 轴和 y 轴方向上圆角的半径。使用如下:1
2// 可以看到左右两侧是一个半圆
canvas.drawRoundRect(100f, 200f, 600f, 500f, 100f, 150f, mPaint);
绘制椭圆和圆
绘制椭圆使用 drawOval 方法,它有两个重载方法:1
2drawOval( RectF oval, Paint paint)
drawOval(float left, float top, float right, float bottom, Paint paint)
参数跟上面的 drawRect 方法是一样的,可以说是在一个矩形框里绘制椭圆,该椭圆紧贴矩形边界。使用方式如下:1
canvas.drawOval(100f, 200f, 600f, 500f, mPaint);
绘制圆使用 drawCircle 方法,该方法如下:1
drawCircle(float cx, float cy, float radius, Paint paint)
第 1 和第 2 个参数是圆心的坐标(x, y),第 3 个参数是半径,第 4 个参数是画笔。使用方式如下:1
2// 以 (600, 500) 为圆心,100 为半径画圆
canvas.drawCircle(600f, 500f, 100f, mPaint);
绘制圆弧
绘制圆弧使用方法 drawArc,它有两个重载方法:1
2
3
4drawArc(float startAngle, float sweepAngle, boolean useCenter, RectF oval,
Paint paint)
drawArc(float left, float top, float right, float bottom, float startAngle,
float sweepAngle, boolean useCenter, Paint paint)
从参数可以看出,圆弧的绘制也是基于矩形框的。与矩形相关的参数前面已经说过了,新的参数有这三个:startAngle, float sweepAngle 和 useCenter。
startAngle 代表开始扫描的角度;sweepAngle 代表圆弧要扫描的范围,为正则顺时针扫,为负则逆时针扫;useCenter 表示是否使用中心点,为 ture 则扫描边缘两点与中心的连续区域,为 false 则只扫描边缘两点连续区域。
看个例子,还是用上面的矩形,从 0° 开始扫描,顺时针扫描 90°。分别看下 useCenter 为 true 和 为 false 的区别:1
2
3
4
5
6
7
8
9
10
11
12
13// 先画个蓝色的矩形
mPaint.setColor(Color.BLUE);
mPaint.setStyle(Paint.Style.FILL);
canvas.drawRect(100f, 200f, 600f, 500f, mPaint);
// 再基于上面的矩形画个红色的椭圆
mPaint.setColor(Color.RED);
canvas.drawOval(100f, 200f, 600f, 500f, mPaint);
// 先看下 useCenter 为 true 的情况
mPaint.setColor(Color.YELLOW);
canvas.drawArc(100f, 200f, 600f, 500f,
0f, 90f, true, mPaint);
为了方便比较,还在底下画了个蓝色的矩形和红色的椭圆,先看下经过中心点的运行结果:
然后将上面最后一行代码的倒数第二个参数改为 false,即不经过中心点:1
2canvas.drawArc(100f, 200f, 600f, 500f,
0f, 90f, false, mPaint);
运行结果如下:
对比之下,可以看出是否经过中心点的区别。另外,通过与矩形和椭圆的比较,还可以发现,圆弧就像是截取了椭圆的一部分,如果将上述的 sweepAngle 参数由 90° 改为 360° 的话,也就是扫描一圈,无论是否经过中心点,结果都是一个椭圆。1
2canvas.drawArc(100f, 200f, 600f, 500f,
0f, 360f, false, mPaint);
运行结果如下:
可以看到,黄色的圆弧(现在应该是椭圆了)覆盖了之前画的红色的椭圆。
绘制文字
绘制文字使用 drawText 方法,drawText 有几个重载方法:1
2
3
4
5
6
7drawText(char[] text, int index, int count, float x, float y,
Paint paint)
drawText(float x, float y, Paint paint) String text,
drawText(int start, int end, float x, float y, String text,
Paint paint)
drawText(int start, int end, float x, float y, CharSequence text,
Paint paint)
一般使用第二个方法,该重载方法最简单,只有四个参数:
第一个参数是要绘制的文字,第二、三个参数组成的坐标 (x, y) 对应要绘制文字的第一个字符的左下角,第四个参数是相应画笔。
使用示例如下:1
2
3
4mPaint.setTextSize(36);
mPaint.setColor(Color.RED);
// 第 2、3个参数 (x, y) 是绘制的第一个字符的左下角坐标
canvas.drawText("FengZH", 50f, 50f, mPaint);
画布变换
画布变换涉及到的方法有 translate, scale, rotate, skew,分别对应平移、缩放、旋转、错切。其中错切有点复杂,这里先不说了。
save & restore
画布变换往往要用到 save 和 restore 方法,而这两个方法又涉及到了图层的概念:
在 Canvas 中,每次的 save 都会将之前的状态保存起来,然后产生一个新的图层,在新图层的绘制不会影响已画好的图,最后用 restore >将这个图层合并到原图层中。
注意:可以多次 save 和 restore,每次 restore 回复到最后一次 save 的状态。
rotate
rotate 方法可以旋转画布,它有两个重载方法:1
2rotate(float degrees)
rotate(float degrees, float px, float py)
两个方法的第一个参数都指定了旋转角度,第一个重载方法是以原点为中心进行旋转,第二个方法是以第二、三个参数指定的坐标(x, y) 为中心进行选择(旋转时中心不会移动,其它点都会移动)。
下面以旋转矩形为例,顺便借此说明 save 和 restore 的作用:
假设有一个矩形,现在要以它的左上角为中心,旋转 45°。由于没有直接绘制旋转图形的 API,所以需要先旋转画布,然后再绘制矩形,这样就可以绘制一个旋转的矩形了。代码如下:1
2
3
4
5
6
7
8
9// 先绘制一个蓝色的矩形
mPaint.setColor(Color.BLUE);
mPaint.setStyle(Paint.Style.FILL);
canvas.drawRect(300f, 200f, 800f, 500f, mPaint);
// 以矩形的左上角为中心,旋转画布
canvas.rotate(45f, 300f, 200f);
// 在相同位置绘制一个红色的矩形
mPaint.setColor(Color.RED);
canvas.drawRect(300f, 200f, 800f, 500f, mPaint);
运行结果如下:
这时假设我们还要继续画一个没有旋转的矩形,如果不做处理直接绘制的话:1
2
3
4// ...
// 画一个没有旋转的黄色的矩形
mPaint.setColor(Color.YELLOW);
canvas.drawRect(300f, 700f, 800f, 1000f, mPaint);
绘制出来的结果是这样的:
显然,由于画布旋转了,所以现在不能绘制正常的矩形了。解决方法有两种:第一种就是再旋转一次画布,之前将画布顺时针旋转了 45°,那么现在就将画布逆时针旋转 45°,这样画布就正常了:1
2
3
4
5
6// ...
// 逆时针旋转画布,让画布恢复为一开始的状态
canvas.rotate(-45f, 300f, 200f);
// 画一个没有旋转的黄色的矩形
mPaint.setColor(Color.YELLOW);
canvas.drawRect(300f, 700f, 800f, 1000f, mPaint);
这时绘制的结果如下:
可以看到,现在可以正常绘制矩形了。现在说下另一种方法,另一种方法就是用到了 save 和 restore,我们可以在绘制旋转矩形前 save 画布,然后画完旋转矩形后,就可以 restore 回之前正常的状态,也就可以正常绘制矩形了。代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15mPaint.setColor(Color.BLUE);
mPaint.setStyle(Paint.Style.FILL);
canvas.drawRect(300f, 200f, 800f, 500f, mPaint);
// 旋转画布前先 save
canvas.save();
// 以矩形的左上角为中心,旋转画布
canvas.rotate(45f, 300f, 200f);
// 绘制旋转矩形
mPaint.setColor(Color.RED);
canvas.drawRect(300f, 200f, 800f, 500f, mPaint);
// 绘制完旋转矩形后,restore 回之前的正常状态
canvas.restore();
// 画一个没有旋转的绿色的矩形
mPaint.setColor(Color.GREEN);
canvas.drawRect(300f, 700f, 800f, 1000f, mPaint);
绘制结果如下:
可以看到,这种方法同样可以达到效果。并且比起第一种方法,利用 save 和 restore 更加优雅。
translate
translate 只有一个方法:1
translate(float dx, float dy)
它接收一个坐标(x, y),它的作用就是将绘图坐标系的原点(绘制点)移动到该坐标。例如:1
canvas.translate(mWidth/2, mHeight/2); // 画布的原点移动到中心
scale
scale 方法用于对画布进行缩放,它有两个重载方法:1
2scale(float sx, float sy)
scale(float sx, float sy, float px, float py)
类似于 rotate,第一个重载方法以原点为中心进行缩放,第二个重载方法可以利用第三、四个参数指定缩放的中心坐标(x, y)。
示例如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// 先画一个蓝色的矩形
mPaint.setColor(Color.BLUE);
mPaint.setStyle(Paint.Style.FILL);
canvas.drawRect(200f, 200f, 500f, 400f, mPaint);
canvas.save();
// 以矩形左上角为中心,画布扩大两倍,然后绘制一个同样的红色矩形
canvas.scale(2, 2, 200f, 200f);
mPaint.setColor(Color.RED);
canvas.drawRect(200f, 200f, 500f, 400f, mPaint);
canvas.restore();
// 绘制一个同样的绿色矩形,标志原来蓝色矩形的位置
mPaint.setColor(Color.GREEN);
canvas.drawRect(200f, 200f, 500f, 400f, mPaint);
绘制结果: