View 自定义 - 画布 Canvas

参考文章

一、概念

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)
public void drawBitmap (Bitmap bitmap, Rect src, RectF 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 clipPath(Path path, Region.Op op)

剪裁路径

public boolean clipRect(int left, int top, int right, int bottom)
public boolean clipRect(float left, float top, float right, float bottom)
public boolean clipRect(float left, float top, float right, float bottom, Region.Op op) 

剪裁矩形

public boolean clipRegion(Region region)
public boolean clipRegion(Region region, Region.Op op)

剪裁区域

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正方向倾斜(即向左)。
sy = tan b ,sy>0时表示向Y正方向倾斜(即向下)。

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.在画布上操作 = 在图层上操作。
2.如无设置,绘制操作和画布操作是默认在默认图层上进行。
3.在通常情况下,使用默认图层就可满足需求;若需要绘制复杂的内容(如地图),则需使用更多的图层。
4.最终显示的结果 = 所有图层叠在一起的效果。

操作作用

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)
public int saveLayer (RectF bounds, Paint paint, int saveFlags)
public int saveLayer (float left, float top, float right, float bottom, Paint paint)
public int saveLayer (float left, float top, float right, float bottom, Paint paint, int saveFlags)

无图层alpha(不透明度)通道

public int saveLayerAlpha (RectF bounds, int alpha)
public int saveLayerAlpha (RectF bounds, int alpha, int saveFlags)
public int saveLayerAlpha (float left, float top, float right, float bottom, int alpha)
public int saveLayerAlpha (float left, float top, float right, float bottom, int alpha, int saveFlags)

有图层alpha(不透明度)通道

5.3 回滚上一次保存的状态 restore()

public void restore()

5.4 回滚指定保存的状态 restoreToCount()

public void restoreToCount(int saveCount)

5.5 获取保存的次数 getSaveCount()

public int getSaveCount()
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值