一、概念
Canvas 虽然叫做画布,但其实是绘制的规则,内容实际是绘制在屏幕上。
二、获取对象
SurfaceView 里有一条线程是专门用于画图,所以该方式的画图性能最好,适用于高质量高刷新率的场景。onDraw() 的刷新频率低一些,但系统开销小节省资源。
2.1 通过空参构造创建
val canvas = Canvas()
2.2 通过 BItmap创建
val canvas = Canvas(bitmap)
2.3 在 onDraw() 中
verride fun onDraw(canvas: Canvas) {...}
2.4 在 SurfaceView 中
val surfaceView = SurfaceView(this)
val surfaceHolder = surfaceView.holder
val canvas = surfaceHolder.lockCanvas()
//进行一系列Canvas操作,结束后需要解锁并执行
surfaceHolder.unlockCanvasAndPost(canvas)
三、绘制方式
3.1 绘制基本图形
3.1.1 矩形 drawRect()
public void drawRect(float left, float top, float right, float bottom, @NonNull Paint paint) left、top、right、bottom 是四条边的坐标。 |
3.1.2 圆角矩形 drawRoundRect()
public void drawRoundRect(float left, float top, float right, float bottom, float rx, float ry, @NonNull Paint paint) left、top、right、bottom 是四条边的坐标,rx、ry是圆角的横向半径和纵向半径。 |
canvas.drawRoundRect(100, 100, 500, 300, 50, 50, paint)
3.1.3 线 drawLine()
public void drawLine(float startX, float startY, float stopX, float stopY, @NonNull Paint paint) 起点和终点坐标。 |
public void drawLines(@Size(multiple = 4) @NonNull float[] pts, @NonNull Paint paint) public void drawLines(@Size(multiple = 4) @NonNull float[] pts, int offset, int count, @NonNull Paint paint) 画多条线。pts数组是线的坐标(每4个元素表示一条线)、offset跳过数组中前几个元素再开始、count绘制几个元素(要填4的倍数)。 |
canvas.drawLine(200, 200, 800, 500, paint)
3.1.4 点 drawPoint()
public void drawPoint(float x, float y, @NonNull Paint paint) x、y 是点的坐标,点的大小可通过 paint.setStrokeWidth() 设置,点的形状可通过 paint.setStrokeCap() 设置(ROUND圆形的点、SQUARE或BUTT方形的点)。 |
public void drawPoints(@Size(multiple = 2) @NonNull float[] pts, @NonNull Paint paint) public void drawPoints(@Size(multiple = 2) float[] pts, int offset, int count, @NonNull Paint paint) 画多个点。pts数组是点的坐标(每2个元素表示一个点)、offset跳过数组中前几个元素再开始、count绘制几个元素(要填2的倍数)。 |
// 绘制四个点:(50, 50) (50, 100) (100, 50) (100, 100)
val points = floatArrayOf(0F, 0F, 50F, 50F, 50F, 100F, 100F, 50F, 100F, 100F, 150F, 50F, 150F, 100F)
canvas.drawPoints(points, 2, 8, paint)
3.1.5 圆 drawCircle()
public void drawCircle(float cx, float cy, float radius, @NonNull Paint paint) cx圆心横坐标、cy圆心纵坐标、radius圆的半径。 |
3.1.6 椭圆 drawOval()
public void drawOval(float left, float top, float right, float bottom, @NonNull Paint paint) left、top、right、bottom 是椭圆上下左右四个边界点的坐标。 |
paint.style = Paint.Style.STROKE
canvas.drawOval(50F, 50F, 350F, 200F, paint)
3.1.7 弧形扇形 drawArc()
public void drawArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean useCenter, @NonNull Paint paint) 使用一个椭圆来描述弧形,left、top、right、bottom 是椭圆上下左右四个边界点的坐标,startAngle是弧形起始角度(x轴正向为0度,顺时针正角度,逆时针负角度),sweepAngle弧形划过的角度,useCenter两端是否连接到圆心(不连接就是弧形,链接就是扇形)。 |
3.2 绘制颜色
用在绘制之前是设置底色,用在绘制之后是设置半透明蒙版。
public void drawColor(@ColorInt int color) public void drawRGB(int r, int g, int b) public void drawARGB(int a, int r, int g, int b) |
canvas.drawARGB(20,100,100,100)
canvas.drawColor(Color.RED)
3.3 绘制路径 drawPath()
public void drawPath(@NonNull Path path, @NonNull Paint paint) 通过描述路径的方式来绘制图形,Path是描述路径的对象。 |
添加子图形 path.adXXX()
dir是画图方向,分为Direction.CW顺时针和Direction .CCW逆时针,在需要填充图形(paint.style = FILL或FILL_AND_STROKE)并且图形出现相交的时候,用于判断填充部分,见下方2.2.4填充方式。。 | |
圆形 | public void addCircle(float x, float y, float radius, @NonNull Direction dir) |
矩形 | public void addRect(float left, float top, float right, float bottom, @NonNull Direction dir) |
椭圆 | public void addOval(float left, float top, float right, float bottom, @NonNull Direction dir) |
圆角矩形 | public void addRoundRect(float left, float top, float right, float bottom, float rx, float ry, @NonNull Direction dir) |
弧形 | public void addArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle) 调用下方arcTo()将forceMoveTo=true的版本。 |
添加另一个路径 | public void addPath(@NonNull Path src) |
添加线 path.xxxTo()
不带"r"开头的是绝对坐标(目标位置相对于原点) 带"r"开头的是相对坐标(目标位置相对于当前位置) 当前位置(最后一次调用path的位置,初始值为原点) | |
直线 | public void lineTo(float x, float y) public void rLineTo(float dx, float dy) 从当前位置向目标位置画一条直线。 |
二次贝塞尔曲线 | public void quadTo(float x1, float y1, float x2, float y2) public void rQuadTo(float dx1, float dy1, float dx2, float dy2) 起点是当前位置,x1和y1是控制点坐标,x2和y2是终点坐标。 |
三次贝塞尔曲线 | public void cubicTo(float x1, float y1, float x2, float y2, float x3, float y3) public void rCubicTo(float x1, float y1, float x2, float y2, float x3, float y3) 起点是当前位置,x1、y1、x2、y2是控制点坐标,x3和y3是终点坐标。 |
移动到目标位置 | public void moveTo(float x, float y) 改变起始点位置(上面的方法都是从当前位置开始画) |
弧线 | public void arcTo(float left, float top, float right, float bottom, float startAngle,float sweepAngle, boolean forceMoveTo) 使用一个椭圆来描述弧形,left、top、right、bottom 是椭圆上下左右四个边界点的坐标,startAngle是弧形起始角度(x轴正向为0度,顺时针正角度,逆时针负角度),sweepAngle弧形划过的角度,forceMoveTo是否抬笔移过去(当前位置和开始画弧的位置之间,移动过程中是否有痕迹,见下图) |
封闭子图形 path.close()
public void close() 从当前位置向子图形的绘制起点画一条直线使之封闭起来。当 paint.style = FILL 或 FILL_AND_STROKE 时 path 会自动封闭子图形。 |
设置填充方式 path.setFillType()
public void setFillType(@NonNull FillType ft) 有四种方式,以下是 EVEN_ODD 和 WINDING,其它两个是它们的取反色,INVERSE_EVEN_ODD 和 INVERSE_WINDING。 |
FillType.EVEN_ODD
even-odd rule 奇偶原则:对于平面中的任意一点,向任意方向射出一条射线(方向无所谓结果都是一样),这条射线和图形相交的次数(相切不算),如果是奇数,则这个点被认为在图形内部,是要被涂色的区域;如果是偶数,则这个点被认为在图形外部,是不被涂色的区域。
WINDING
non-zero winding rule 非零环绕数原则:会考虑到图形的绘制方向。从平面中的点向任意方向射出一条射线方向无所谓结果都是一样),以 0 为初始值,对于射线和图形的所有交点,遇到每个顺时针的交点(图形从射线的左侧向右侧穿过)把结果加 1,遇到每个逆时针的交点(图形从射线的右侧向左穿过)把结果减 1,最终把所有的交点都算上,得到的结果如果不是 0,则认为这个点在图形内部,是要被涂色的区域;如果是 0,则认为这个点在图形外部,是不被涂色的区域。
3.4 绘制图片
3.4.1 矢量图 drawPicture()
可以录制某个时间段内的 Canvas 操作,供后续重复使用。使用前需要关闭硬件加速以免引起不必要的问题。
三种绘制录制结果的区别 | 是否对 Canvas 状态有影响(clip、Matrix) | 对绘制结果可控程度 |
Picture.draw() | 有 | 低 |
Canvas.drawPicture() | 无 | 高 |
PictureDrawable.draw() | 无 | 高 |
override fun onDraw(canvas: Canvas) {
val picture = Picture()
val recordingCanvas = picture.beginRecording(width , height) //开始录制,会返回一个Canvas
...
recordingCanvas.drawXXX() //进行一系列的Canvas操作
...
picture.endRecording() //结束录制
//将 Picture 中的内容绘制出来
//方式一
picture.draw(canvas)
//方式二(如果设置的显示区域<绘制图形,会变形)
val rec = Rect(0, 0, picture.width, picture.height)
canvas.drawPicture(picture, rec)
//方式三(如果设置的显示区域<绘制图形,只显示部分)
val drawable = PictureDrawable(picture)
drawable.setBounds(0, 0, picture.width, picture.height)
drawable.draw(canvas)
}
3.4.2 位图 drawBitmap()
将已有的图片转换为 Bitmap 再绘制到 Canvas 上,因此选择通过 BitmapFactory 转换。
public void drawBitmap (Bitmap bitmap, float left, float top, Paint paint) 坐标是左上角偏移。 |
public void drawBitmap (Bitmap bitmap, Matrix matrix, Paint paint) 参数matrix, paint是在绘制时对图片进行一些改变 |
public void drawBitmap (Bitmap bitmap, Rect src, Rect dst, Paint paint) 参数是两个矩形区域,src指定需要绘制图片的区域(即要绘制图片的哪一部分),dst指定图片在屏幕上显示(绘制)的区域。如果src≠dst会被缩放。 |
drawBitmap(bitmap, 200, 100, paint)
3.5 绘制文字
文字的样式(大小,颜色,字体等)具体由画笔Paint控制。
通过 Paint.fontMetrics(获取Float数据)或 Paint.fontMetricInt(获取Int数据)对象,进一步获取以下六条线。 | |
top:顶点线 center:中心线 bottom:底部线 | 控件的顶部和底部,绘制文字不会超过这两条线。 |
ascent:建议顶点线 baseline:基线 descent:建议底部线 | 以基线这个y坐标为基础,向上都是负值,向下都是正值,一般建议不超过这两条线。 |
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
val fontMetrics = paint.fontMetrics
val x = width / 2 - paint.measureText(demoText) / 2
val y = height / 2 + (fontMetrics.bottom - fontMetrics.top) / 2 - fontMetrics.bottom
//绘制文本,参数(文本内容、x、基线y、画笔)
canvas?.drawText(demoText, x, y, paint)
}
3.5.1 指定偏移 drawText()
public void drawText(String text, float x, float y, Paint paint) public void drawText (String text, int start, int end, float x, float y, Paint paint) x 和 y是绘制点的偏移,start 和 end 是字符串的索引,可用来截取字符串只画部分。 |
3.5.2 指定位置 drawPosText()
已过时,不支持字形合成和分解,因此不能用于渲染复杂的脚本,还不能处理补充字符如emoji。
public void drawPosText (String text, float[] pos, Paint paint) 未指定位置的字符不会显示,在处理字符较多时会导致卡顿。 |
canvas.drawPosText("abc", floatArrayOf(
100, 100, //第一个字符的位置
200, 200, //第二个字符的位置
300, 300 //第三个字符的位置
), paint)
3.5.3 指定路径 drawTextOnPath()
public void drawTextOnPath(String text, Path path, float hOffset, float vOffset, Paint paint) |
val path = Path()
val paint = Paint()
path.cubicTo(540F, 750F, 640F, 450F, 840F, 600F)
canvas.drawPath(path, paint)
canvas.drawTextOnPath("在Path上写的字:Carson_Ho", path, 50F, 0F, paint)
四、画布操作
4.1 平移 translate
public void translate(float dx, float dy) |
4.2 缩放 scale
会先把形状先画到画布,然后再缩小/放大。所以当放大很多倍时会有明显锯齿。当缩放倍数为负数时,会先进行缩放,然后根据不同情况进行图形翻转。
- a<0,b>0:以px为轴翻转。
- a>0,b<0:以py为轴翻转。
- a<0,b<0:以旋转中心翻转
public void scale(float sx, float sy) 在x方向缩放sx倍,在y方向缩放sy倍,默认缩放中心是(0, 0) |
public final void scale(float sx, float sy, float px, float py) px、py用于控制缩放中心位置 |
val canvas = Canvas()
val rect = Rect(0, -200, 200, 0)
canvas.drawRect(rect, Paint().apply { color = Color.BLUE })
//原点缩放
canvas.scale(1.5F, 1.5F)
canvas.drawRect(rect, Paint().apply { color = Color.RED })
//指定缩放中心
canvas.scale(1.5F, 1.5F, 100F, 0F) //将画布放大到1.5倍,并将缩放中心移动到(100,0)
canvas.drawRect(rect, Paint().apply { color = Color.RED })
4.3 旋转 rotate
public void rotate(float degrees) 以原点为中心旋转 degrees 角度。 |
public final void rotate(float degrees, float px, float py) px、py用于控制旋转中心位置 |
4.4 裁剪 clip
从画布上裁剪一块区域,之后仅能编辑该区域。其余的区域只是不能编辑,并没有消失。
public boolean clipPath(Path path) 剪裁路径 | |
public boolean clipRect(int left, int top, int right, int bottom) 剪裁矩形 | |
public boolean clipRegion(Region region) 剪裁区域 | |
Region.Op | 在剪下多个区域下来的情况,当这些区域有重叠的时候,这个参数决定重叠部分该如何处理,多次裁剪之后究竟获得了哪个区域。 |
val canvas = Canvas()
val paint = Paint()
canvas.translate(300F, 500F)
//原来画布设置为灰色
canvas.drawColor(Color.GRAY)
//第一次裁剪
canvas.clipRect(0, 0, 600, 600)
//将第一次裁剪后的区域设置为红色
canvas.drawColor(Color.RED)
//第二次裁剪,并显示第一次裁剪与第二次裁剪不重叠的区域
canvas.clipRect(0F, 200F, 600F, 400F, Region.Op.DIFFERENCE)
//将第一次裁剪与第二次裁剪不重叠的区域设置为黑色
canvas.drawColor(Color.BLACK)
4.5 错切 skew
将画布在x方向倾斜a角度、在y方向倾斜b角度。
public void skew(float sx, float sy) sx = tan a ,sx>0时表示向X正方向倾斜(即向左)。 |
val canvas = Canvas()
val paint = Paint()
canvas.translate(300F, 500F)
canvas.drawRect(20F, 20F, 400F, 200F, paint)
//向X正方向倾斜45度
canvas.skew(1F, 0F)
canvas.drawRect(20F, 20F, 400F, 200F, paint)
//向X负方向倾斜45度
canvas.skew(-1F, 0F)
canvas.drawRect(20F, 20F, 400F, 200F, paint)
//向Y正方向倾斜45度
canvas.skew(0F, 1F)
canvas.drawRect(20F, 20F, 400F, 200F, paint)
//向Y负方向倾斜45度
canvas.skew(0F, -1F)
canvas.drawRect(20F, 20F, 400F, 200F, paint)
五、画布快照
画布状态 | 当前画布经过的一系列操作 |
状态栈 | 存放画布状态和图层的栈(后进先出) |
画布的构成 | 由多个图层构成: 1.在画布上操作 = 在图层上操作。 |
操作 | 作用 |
save() 保存当前画布状态 | 保存画布状态,即保存画布的一系列操作。每调用一次都会在栈顶添加一条状态信息(入栈)。(画布的操作是不可逆的,而且会影响后续的步骤,假如需要回到之前画布的状态去进行下一次操作,就需要对画布的状态进行保存和回滚) |
saveLayer() 保存某个图层状态 | 新建一个图层,并放入特定的栈中。(使用起来非常复杂,因为图层之间叠加会导致计算量成倍增长,应尽量避免使用。) |
restore() 回滚上一次保存的状态 | 恢复上一次保存的画布状态,即从栈顶取出一个状态进行恢复。 |
restoreToCount() 回滚指定保存的状态 | 恢复指定状态;将指定位置以及以上所有状态出栈。 |
getSaveCount() 获取保存的次数 | 获取保存过图层的次数,即获取状态栈中保存状态的数量。 |
对于画布状态的保存和回滚的套路,一般如下:
//步骤1:保存当前状态,把Canvas的当前状态信息入栈
save()
//步骤2:对画布进行各种操作(旋转平移等)
...
//步骤3:回滚到之前的画布状态,把栈里面的信息出栈,取代当前的Canvas信息
restore()
5.1 保存当前画布状态 save()
public int save () 保存全部状态 |
public int save (int saveFlags) 根据saveFlags参数保存一部分状态更加灵活 |
- ALL_SAVE_FLAG(默认):保存全部状态
- CLIP_SAVE_FLAG:保存剪辑区
- CLIP_TO_LAYER_SAVE_FLAG:剪裁区作为图层保存
- FULL_COLOR_LAYER_SAVE_FLAG:保存图层的全部色彩通道
- HAS_ALPHA_LAYER_SAVE_FLAG:保存图层的alpha(不透明度)通道
- MATRIX_SAVE_FLAG:保存Matrix信息(translate, rotate, scale, skew)
5.2 保存某个图层状态 saveLayer()
public int saveLayer (RectF bounds, Paint paint) 无图层alpha(不透明度)通道 |
public int saveLayerAlpha (RectF bounds, int alpha) 有图层alpha(不透明度)通道 |
5.3 回滚上一次保存的状态 restore()
public void restore() |
5.4 回滚指定保存的状态 restoreToCount()
public void restoreToCount(int saveCount) |
5.5 获取保存的次数 getSaveCount()
public int getSaveCount() |