Canvas,称之为画布,在Android自定义view中十分重要,几乎所有的view都是在Canvas上进行绘制的。
画布与图层的概念
- 图层:每次调用canvas的drawXXX系列函数时,都会生成一个透明图层来专门进行绘制
- 画布:其实每一个画布都是一个Bitmap,所有的绘制都是在Bitmap上进行的。所以这里说的画布并不是我们经常说的Canvas。上面说了每调用一次drawXXX都会生成一个新的图层。所以如果连续调用5个draw系列函数,就会生成5个透明图层,然后在透明图层上绘制。绘制完了之后会依次覆盖在画布,也就是Bitmap上来显示。画布有两种,一种时onDraw()函数传过来的,称为原始画布。另外一种称之为人造画布,是通过new Canvas(bitmap)或者saveLayer()函数来手动创建的。尤其是通过saveLayer()创建的画布,以后所有的绘制都是在这个画布上进行的,只有调用了restore或者restoreToCount才会返回到原始画布上绘制
- Canvas:Canvas是一个类,它只不过有绘制的功能,是画布的一种代码上的表现形式。无论是原始画布还是人造画布,最后都是通过Canvas画到Bitmap上的。可以理解成绘制的工具
Canvas的获取方式
1 重写onDraw()或dispatchDraw()
在自定义view时一般都需要重写onDraw()或者dispatchDraw()方法。在这两个方法参数中都有一个Canvas对象。这个Canvas对象是view中的Canvas对象,利用这个Canvas对象绘图,效果会直接反应在view中。
2 利用Bitmap创建
直接只用构造函数
public Canvas(Bitmap bitmap) { }
或者
Canvas canvas = new Canvas();
canvas.setBitmap(bitmap);
最后一种则是调用SurfaceHolder.lockCanvas()函数来获得Canvas对象。
drawXXX
canvas有很多绘制的函数,都是以draw开头,并且有很多功能都是一样的,只不过传的参数不同罢了。不过可以大致分为以下几种:
- drawText() 用于绘制文字
- drawBitmap() 用于绘制位图
- drawColor() 用于绘制颜色
- drawCirlce() 用于绘制圆形 与之类似的还有drawRect(),drawLine(),drawOval()等都是绘制一些图形
- drawPath() 用于绘制路径
Canvas变换
除了上面的绘制函数之外,还可以对画布进行一些操作,如平移,旋转,缩放,裁剪等
平移 translate
要想对Canvas进行平移,需要使用其中的translate()函数。既然要进行平移,肯定要有一个原点作为参考点。默认的左上角为原点(0 , 0)。x轴向右为正方向,y轴向下为正方向。
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val paint1 = Paint()
paint1.apply {
color = Color.GREEN
style = Paint.Style.STROKE
strokeWidth = 8f
}
canvas.drawRect(0f, 0f, 400f, 300f, paint1)
canvas.translate(100f, 100f)
val paint2 = Paint()
paint2.apply {
color = Color.RED
style = Paint.Style.STROKE
strokeWidth = 8f
}
canvas.drawRect(0f, 0f, 400f, 300f, paint2)
}
上面代码中显示构建了一个绿色的画笔,然后画了一个矩形。接着把画布的原点平移到了(100,100),然后构建一个红色的画笔,并绘制了一个一样大小的矩形。根据绘制出来的图形我们可以得到的结果有:
- 每次调用draw系列函数的时候确实会创建一个新的图层来进行绘制
- 每次在调用draw系列函数之前对画布进行的平移旋转等都是不可逆的,之后的绘制都将在这个画布上显示
旋转 rotate
val paint1 = Paint()
paint1.apply {
color = Color.GREEN
style = Paint.Style.STROKE
strokeWidth = 8f
}
canvas.drawRect(300f, 200f, 700f, 500f, paint1)
canvas.rotate(20f)
val paint2 = Paint()
paint2.apply {
color = Color.RED
style = Paint.Style.STROKE
strokeWidth = 8f
}
canvas.drawRect(300f, 200f, 700f, 500f, paint2)
需要注意的是,如果不指定旋转中心点的话,默认还是原点。
缩放 scale
val paint1 = Paint()
paint1.apply {
color = Color.GREEN
style = Paint.Style.STROKE
strokeWidth = 8f
}
canvas.drawRect(300f, 200f, 700f, 500f, paint1)
canvas.scale(1f,0.5f)
val paint2 = Paint()
paint2.apply {
color = Color.RED
style = Paint.Style.STROKE
strokeWidth = 8f
}
canvas.drawRect(300f, 200f, 700f, 500f, paint2)
x轴方向保持不变,y轴上缩放百分之50
扭曲 skew
val paint1 = Paint()
paint1.apply {
color = Color.GREEN
style = Paint.Style.STROKE
strokeWidth = 8f
}
canvas.drawRect(300f, 200f, 700f, 500f, paint1)
canvas.skew(0.6f,0f)
val paint2 = Paint()
paint2.apply {
color = Color.RED
style = Paint.Style.STROKE
strokeWidth = 8f
}
canvas.drawRect(300f, 200f, 700f, 500f, paint2)
skew()里的两个参数分别为角度的正切值。例如在一个角为30度的直角三角形中,30度角对应的直角边长度为斜边的一半。假设30度对应的直角边长度为1,那么斜边就为2,根据勾股定理则可以求出另外一条边长的平方为3,那么开方之后则约为1.732。那么对应的三十度的正切值就为对边1比上邻边1.732,约等于0.57。上面代码中则直接给了0.6,相当于把画布在x轴方向上倾斜了30度左右。
image.png
图层与画布
public int saveLayer(RectF bounds, Paint paint) { }
public int saveLayer(float left, float top, float right, float bottom, Paint paint, int saveFlags) { }
这两个函数的功能是一样的,都是为了保存指定矩形区域的Canvas的内容,只不过参数不同。其中saveFlags有很多种,包括:
- ALL_SAVE _FLAG 保存所有内容
- MATRIX_SAVE_FLAG 仅保存Canvas的matix数组
- CLIP_SAVE_ FLAG 仅保存Canvas的当前大小
- HAS_ALPHA_LAYER_SAVE_FLAG 标识新建的bitmap具有透明图,在与上层画布结合时,透明位置显示上层图像。与FULL_COLOR_LAYER_SAVE_FLAG冲突,若二者同时存在,以HAS_ALPHA_LAYER_SAVE_FLAG为主
- FULL_COLOR_LAYER_SAVE_FLAG 标识新建的bitmap颜色完全独立,与上层画布结合时,显清空上层画布再覆盖上去
- CLIP_TO_LAYER_SAVE_FLAG 在保存图层之前先把当前画布根据bounds裁剪。与CLIP_SAVE_FLAG冲突。若同时存在,以CLIP_SAVE_FLAG为主
这6个flag中,saveLayer()函数都可以使用,而save()函数只能使用前三个。
在调用saveLayer()函数时,会生成一个全新的画布,画布的大小就是参数中指定的大小,并且新生成的画布是全透明的。那么在调用savaLayer()函数后的所有绘制都是在这个指定的透明画布上。
先来看一个例子:
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// 先把整个画布涂成绿色
canvas.drawColor(Color.GREEN)
// 生成一个全新的画布,并在上面绘制
val layerId =
canvas.saveLayer(0f, 0f, width.toFloat(), height.toFloat(), null, Canvas.ALL_SAVE_FLAG)
// 绘制目标图像 目标图像为黄色的圆
canvas.drawBitmap(dstBitmap, 0f, 0f, paint)
paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)
// 绘制源图像 源图像是一个正方形 原点为目标图像的圆心
canvas.drawBitmap(srcBitmap, 400f / 2, 400f / 2, paint)
paint.xfermode = null
// 恢复画布
canvas.restoreToCount(layerId)
}
下面把canvas.saveLayer()和canvas.restoreToCount()两行注释掉,再来看一下效果:
这其中的区别就是因为调用saveLayer()之后会创建一个新的透明画布,并在这个画布上绘制。于是根据代码来看,紧接着就会绘制一个黄色的圆在这个透明的画布上。(由于绿色是在saveLayer()之前绘制的,所以这个透明画布上只有一个黄色的圆)。下面使用了Xfermode来绘制源图像。由于Xfermode的存在,会把之前画布上的所有内容作为源图像。而之前的画布正是调用saveLayer()生成的透明画布,此时上面有一个黄色的圆。而每次调用canvas的drawXXX()系列函数的时候,都会创建一个新的图层进行绘制,绘制完了之后又会叠加到最近的画布上。因为Xfermode使用了SRC_IN的算法(SRC_IN算法是以显示源图像为主,当源图像和目标图像相交时,利用目标图像的透明度来改变源图像的透明度。如果目标图像透明度为0时,原图像就完全不显时),而除了圆形之外都是透明的,所以源图像正方形除了和圆相交的部分都变成透明的了。整个流程大概是这样的:
不使用saveLayer()的大概过程是这样的:
与saveLayer()类似的还有一个saveLayerAlpha()函数。区别就是后者带有透明度。
未完待续。。。
网友评论