参考与内容来自
- https://blog.csdn.net/harvic880925/article/details/51317746.
- https://blog.csdn.net/harvic880925/article/details/51332494
周末去天坛走了一圈,门票好像降了,如果没记错,2年前,好像是50的联票,现在是30多了;或许这是在京最后一次去天坛了,如果不是有亲友来北京玩,需要当导游的话;
关于图层这块,或是画布,一直很晕,前面我们也通过原博客,了解到一些,如save、restore,在xfermode的时候,用到saveLayer,那他们到底是什么,这是我们需要学习的;
关于saveLayer传递flag这块,因为标记为过时了,如想了解,可参加原博客;
好的,继续学习;
如何获得Canvas对象
- 自定义View时,重写onDraw()、dispathDraw()方法时,会有一个Canvas对象传过来;这个Canvas是View的Canvas对象,利用这个canvas对象绘图,效果会直接反应在View中;
- 使用Bitmap绘制,前面都有用到过
val c = Canvas(srcBmp)
图层与画布
前面我们使用过 save
与 restore
函数,简单回顾一下;
save用来保存当前状态,restore用来恢复之前的画布状态;除了save与restore还有其他函数也可以实现功能;
saveLayer()
相关方法
从上图,可以看到,saveLayer函数一些标记为过时了;
saveLayer绘制流程
saveLayer会生成一个全新的bitmap
,此bitmap
大小就是指定的保存区域的大小,新生成的bitmap
是全透明的,在调用saveLayer后所有的绘图操作都是在这个bitmap
上进行的。
根据对layer翻译理解的不同,这个bitmap也可理解成 图层
,layer 层的意思;
save方法是保存画布状态,还原点;
val layerID = canvas.saveLayer(0f, 0f, wid * 2.toFloat(), hei * 2.toFloat(), paint)
canvas.drawBitmap(dstBmp, 0f, 0f, paint)
paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)
canvas.drawBitmap(srcBmp, wid / 2.toFloat(), hei / 2.toFloat(), paint)
paint.xfermode = null
canvas.restoreToCount(layerID)
在画源图像时,会把之前画布上
的所有内容当做 dst
;
而在saveLayer新生成的bitmap
上,只有dst
对应的圆形,所以除了与圆形相交之外的位置都是空像素
。
在画图完成之后,会把saveLayer所生成的bitmap
盖在原来的canvas上面,合成过程如下:
分为3步:
- 在savelayer新建的画布上作画,并将图像做为目标图像(新的整块画布作为目标图像);
- 在新的透明图层(draw时会形成新的透明图层)做源画,并在其矩形所在的透明图层与目标图层相交,计算结果画在新建的透明画布上;
- 最终将计算结果直接盖在原始画布上,形成最终的显示效果;
没有saveLayer绘制流程
如果把saveLayer去掉了; 所有的绘图操作都放在了原始View的Canvas所对应的画布上了;
在应用xfermode来画源图像的时候,目标图像当前Bitmap上的所有图像了,也就是整个绿色的屏幕和一个圆形了。所以这时候源图像的相交区域是没有透明像素的,透明度全是100%;
我们可以把window的背景设置为透明。canvas去掉颜色,重试下绘制,看看效果;
saveLayer会创建一个全新透明的
bitmap
(当然也可以理解为图层),大小与指定保存的区域一致,其后的绘图操作都放在这个bitmap
上进行。在绘制结束后,会直接盖在上一层的Bitmap上显示。
画布与图层
图层(Layer)
每一次调用canvas.drawXXX系列函数时,都会生成一个透明图层来专门来画这个图形;-
画布(bitmap)
这里我理解不好,(我始终认为画布也是一个 layer;当然,只要知道这个含义就行,根据当前上下文而表现出不同的意思,这里以原博客的信息为准);
每一个画布都是一个bitmap,所有的图像都是画在bitmap上的!每一次调用canvas.drawxxx函数时,都会生成一个专用的透明图层来画这个图形,画完以后,就盖在了画布上。所以如果我们连续调用五个draw函数,那么就会生成五个透明图层,画完之后依次盖在画布上显示。
画布有两种:- view的原始画布,通过onDraw(Canvas canvas)函数传进来的,其中参数中的canvas就对应的是view的原始画布,控件的背景就是画在这个画布上的;
-
人造画布,通过saveLayer()、new Canvas(bitmap)这些方法来人为新建一个画布;
尤其是saveLayer(),一旦调用saveLayer()新建一个画布以后,以后的所有draw函数所画的图像都是画在这个画布上的,只有当调用restore()、resoreToCount()函数以后,才会返回到原始画布上绘制。 save是保存canvas状态,还原点的;
-
Canvas(原博客的这个解释相当到位,很棒)
可以把Canvas理解成画板,Bitmap理解成透明画纸,默认Canvas是仅有一层画纸的,而Layer则理解成图层;- 每一个draw函数都对应一个图层,在一个图形画完以后,就放在画纸上显示;
- 而一张张透明的画纸(通过saveLayer生成)则一层层地叠加在画板上显示出来;
- 我们知道画板和画纸都是用夹子夹在一起的,没有save方法调用的情况下, 当我们旋转画板时,所有画纸都会跟着旋转!当我们把整个画板裁小时,所以的画纸也都会变小了;
当我们利用saveLayer来生成多个画纸时,然后
最上层
的画纸调用canvas.rotate(30)是把画板给旋转了,后续的画纸也都被旋转30度!这一点非常注意 ;(也就是最上层的画纸跟Canvas对象是绑在一起的)
另外,如果最上层的画纸调用canvas.clipRect()将画板裁剪了,那么所有的画纸也都会被裁剪。唯一能够恢复的操作是调用canvas.revert()把上一次的动作给取消掉!
但是我们如果在多次调用saveLayer,并restoreToCount最顶层画布时,再去操作rotate、等
时没有效果的,因为图像已经画上去了;
但在利用canvas绘图与画板不一样的是,画布的影响只体现在以后的操作上,以前画上去的图像已经显示在屏幕上是不会受到影响的。
save()、saveLayer()、saveLayerAlpha()
saveLayer()
saveLayer后的所有动作都只对新建画布有效
saveLayer会新建一个画布(bitmap),后续的所有操作都是在这个画布上进行的;
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// 最顶层的画纸
canvas.drawBitmap(bitmap, 0f, 0f, paint)
// 创建一个新的透明画纸,后续的操作会在这个图纸上,不会对之前的造成影响
val layerID = canvas.saveLayer(0f, 0f, width * 1.0f, height * 1.0f, paint)
canvas.skew(1f, 0f)
canvas.drawRect(0f,0f,100f,100f,paint)
canvas.restoreToCount(layerID)
}
上图,左侧为效果,右侧为开启过度绘制时的提示,也就是说savelayer产生了多个图层造成了过度绘制;去掉savelayer可同时实现该效果,但少绘制了一个图层;
通过Rect指定新建的画布大小
可以通过指定Rect对象或者指定四个点来来指定一个矩形,这个矩形的大小就是新建画布的大小;之前都接触过了;
注意点:
屏幕大小的画布需要的空间,按一个像素需要8bit存储空间算,1024768的机器,所使用的bit数就是1024768*8=6.2M!所以我们在使用saveLayer新建画布时,一定要选择适当的大小;
saveLayerAlpha()
多一个alpha参数,用以指定新建画布透明度,取值范围为0-255,可以用16进制的oxAA表示;
其他与saveLayer特性一致;
比较简单;
saveLayer 区域间使用save、restore
做了一个尝试,发现在saveLayer区间块中,可操作save与restore,不过这里的save、restore操作的是 saveLayer返回的画布了;
init {
paint = Paint(Paint.ANTI_ALIAS_FLAG)
paint.color = Color.GREEN
paint.style = Paint.Style.FILL
paint.strokeWidth = 2f
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawRect(0f,0f,400f,400f, paint)
val layerId = canvas.saveLayer(300f,300f, 800f,800f, paint)
// 这里的save与restore操作的是layerId对应的画布
canvas.apply {
drawRect(300f,300f,800f,800f, paint)
drawColor(Color.RED)
save()
clipRect(400f,400f, 700f,700f)
drawColor(Color.GRAY)
save()
clipRect(450f,450f, 500f,500f)
drawColor(Color.YELLOW)
drawLine(450f,450f,600f,600f, paint) // canvas受限,不能超出
restore() // 回到400到700f
drawLine(420f,500f,600f,680f, paint)
}
canvas.restoreToCount(layerId)
}
save()、saveLayer()有啥区别呢?
之前我们提到过,这里在记录一下:
- 相同点
saveLayer可以实现save所能实现的功能 - 不同点
- saveLayer生成一个独立的图层而save只是保存了一下当时画布的状态类似于一个还原点。
- saveLayer因为多了一个图层的原因更加耗费内存慎用(过度绘制,前面提到了)。
- saveLayer可指定保存相应区域,尽量避免2中所指的情况。
- 在使用混合模式setXfermode时必须使用saveLayer,saveLayer会形成新图层;
示例代码(xfermode 使用save):
// 使用save看能否实现噢
val layerID = canvas.save()
canvas.drawBitmap(dstBmp, 0f, 0f, paint)
paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)
canvas.drawBitmap(srcBmp, wid / 2.toFloat(), hei / 2.toFloat(), paint)
canvas.restoreToCount(layerID)
restore()与restoreToCount()
restore()
restore()的作用就是把回退栈中的最上层画布状态出栈,恢复画布上一次状态。
之前我们用到过:
https://www.jianshu.com/p/95b3f3fdd0eb
restoreToCount(int i)
save相关函数(flag参数已经过时):
public int save()
public int save(int saveFlags)
public int saveLayer(RectF bounds, Paint paint, int saveFlags)
public int saveLayerAlpha(RectF bounds, int alpha, int saveFlags)
在save()系列方法保存画布后,都会返回一个ID值,这个ID值表示当前保存的画布信息的栈层索引
(从0开始),比如保存在第三层,则返回2;
restoreToCount方法如下:
public void restoreToCount(int saveCount);
表示一直退栈,一直退到指定count的层数为栈顶为止;注意这个saveCount起始值是从1开始的,也就是说它比对应栈的索引要多1(我的理解是:Canvas默认有一层画布嘛,她不能被出栈的);
比如下面的代码,saveLayer保存在第2层,所以返回1;
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val layerId = canvas.saveLayer(300f,300f, 800f,800f, paint) // 返回1,表示第2层
canvas.restoreToCount(layerId) // 退到第0层栈顶位置
}
canvas.restoreToCount(id);则表示一直退栈,把栈一直退到第0层在栈顶的位置,刚好把新建的第2层给出栈;
调用save函数的时候,把对应的id记录下来,然后canvas.restoreToCount(id)就可以把栈的状态回退到生成这个id前的状态;
如下代码(输出当前栈的层数和最高层的索引):
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val id1 = canvas.save()
canvas.clipRect(0, 0, 800, 800)
canvas.drawColor(resources.getColor(R.color.red_27))
e("count:" + canvas.saveCount + " id1:" + id1) // 2,1
// 注意:canvas.skew(1f,0f),会影响后续操作
val id2 = canvas.saveLayer(0f, 0f, width * 1.0f, height * 1.0f, paint)
canvas.clipRect(100, 100, 700, 700)
canvas.drawColor(resources.getColor(R.color.green_27))
e("count:" + canvas.saveCount + " id2:" + id2) // 3,2
val id3 = canvas.saveLayerAlpha(0f, 0f, width * 1.0f, height * 1.0f, 0xf0)
canvas.clipRect(200, 200, 600, 600)
canvas.drawColor(resources.getColor(R.color.yellow_27))
e("count:" + canvas.saveCount + " id3:" + id3) // 4,3
val id4 = canvas.save()
canvas.clipRect(300, 300, 500, 500)
canvas.drawColor(resources.getColor(R.color.blue_27))
e("count:" + canvas.saveCount + " id4:" + id4) // 5,4
}
添加restoreCount操作,如下:
canvas.restoreToCount(id3) // 回退到id3之前的状态
canvas.drawColor(Color.GRAY)
e("count:" + canvas.saveCount) // 输出 3
restore与restoreToCount的关系
它们两个针对的都是同一个栈,所以是完全可以通用的,不同的是restore()是默认将栈顶内容退出还原画布,而restoreToCount(int count)则是一直退栈,直到指定层count做为栈顶,将此之前的所有动作都恢复。
示例代码(在这些代码中,我们将 save与saveLayer交替使用):
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawRect(0f,0f,400f,400f, paint)
val layerId = canvas.saveLayer(300f,300f, 800f,800f, paint)
canvas.apply {
drawRect(100f,100f,800f,800f, paint)
drawColor(Color.RED)
val id = save()
e("better===>saveCount ${canvas.saveCount} , ${id}")
clipRect(400f,400f, 700f,700f)
drawColor(Color.GRAY)
val id2 = save()
e("better===>saveCount ${canvas.saveCount} , ${id2}")
clipRect(450f,450f, 500f,500f)
drawColor(Color.YELLOW)
e("better===>saveCount ${canvas.saveCount}")
drawLine(450f,450f,600f,600f, paint) // canvas受限,不能超出
restore() // 回到400到700f
e("better===>saveCount ${canvas.saveCount}")
drawLine(420f,500f,600f,680f, paint)
}
canvas.restoreToCount(layerId)
}
从输出看到:
- 每save一次,栈的层数都在加一,所以无论哪种save方法,保存画布时都使用的是同一个栈;
- restore()与restoreToCount(int count)针对的都是同一个栈,所以是完全可以通用和混用的。