前面讲过 Canvas 的 save() 和 restore() 函数,除这两个函数以外,还有其他一些函数来保存和恢复画布状态。
9.2.1 saveLayer() 函数
saveLayer 有两个构造函数,如下:
参数:
- RectF bounds:要保存的区域所对应的矩形对象。
- int saveFlags:取值有 ALL_SAVE_FLAG、MATRIX_SAVE_FLAG、CLIP_SAVE_FLAG、HAS_ALPHA_LAYER_SAVE_FLAG、FULL_COLOR_LAYER_SAVE_FLAG、 和
CLIP_TO_LAYER_SAVE_FLAG,其中 ALL_SAVE_FLAG 表示保存全部内容,这些标识的具体意义我们后面会具体讲。
第二个构造函数实际与第一个是一样的,只不过它是根据 4 个点来构造一个矩形。下面以 Xfermode 为例,来看看 saveLayer() 函数都做了什么。
这段代码我们应该很熟悉,这是在讲解 setXfermode() 函数时的示例代码,但在调用 saveLayer() 函数前把整个屏幕画成了绿色,效果图如下。
那么问题来了,如果我们把 saveLayer() 函数去掉,则会怎样?代码如下:
效果如下图所示。
可以看到,效果居然不一样。先来回顾下 Mode.SRC_IN 模式的效果:在处理源图像时,以显示源图像为主,在相交时利用目标图像的透明度来改变源图像的透明度和饱和度;当目标图像透明度为 0 时,源图像就完全不显示。
再回过来看结果,第一个结果是对的,因为除与圆相交以外的区域透明度都是 0,而第二个结果怎么变成了这样,源图像为什么全部都显示出来了?
1. 调用 saveLayer() 函数时的绘图流程
在调用 saveLayer() 函数时,会生成一个全新的画布(Bitmap),这块画布的大小就是我们指定的所要保存区域的大小。新生成的画布是全透明的,在调用 saveLayer() 函数后所有的绘图操作都是在这个画布上进行的。
我们讲过,在利用 Xfermode 画源图像时,会把之前画布上所有的内容都做为目标图像,而在调用 saveLayer() 函数新生成的画布上,只有 dstBmp 对应的圆形。所以,除与圆形相交之外的位置都是空白像素。
对于 Xfermode 而言,在绘图完成之后,会把调用 saveLayer() 函数所生成的透明画布覆盖在原来的画布上面,以形成最终的显示结果。
此时的 Xfermode 的合成过程如下图所示。
中间的透明画布就是调用 saveLayer() 函数自动生成的,最上方的透明图层是调用 drawBitmap() 函数生成的。我们知道,每次调用 canvas.drawXXX 系列函数,都会生成一个透明层来专门绘制这个图形,而每次生成的图层都会叠加到最近的画布上。因为我们在这里对源图像应用了 Xfermode 算法,所以在叠加到就近的调用 saveLayer() 函数生成的画布上时,会进行计算。在新建的画布上绘制完成以后,整体覆盖在原始画布上显示出来。
正是因为在使用 Xfermode 计算时,目标图像是绘制在新建的透明画布上的,所以除圆形以外的区域全部是透明像素,最终的显示结果是正确的。
2. 没有 saveLayer() 函数时的绘图流程
在第二个示例中,唯一不同的就是把 saveLayer() 函数去掉了。
在去掉 saveLayer() 函数后,就不会新建画布了。当然,所有的绘图操作都会在原始画布上进行。
由于先把整块画布染成了绿色,再画上一个圆形,所有在应用 Xfermode 来画源图像的时候,在目标画布上是没有透明像素的。这也就不难解释结果为什么是这样的。
此时的 Xfermode 合成过程如下图所示。
由于没有调用 saveLayer() 函数,所以圆形是直接画在原始画布上的,而当矩形与其相交时,就是直接与原始画布上的所有图像做计算的。
结论:调用 saveLayer() 函数会创建一个全新的透明画布,大小与指定保存的区域大小一致,其后的绘图操作都放在这块画布上进行。在绘制结束后,会直接盖在原始画布上显示。
9.2.2 画布与图层
上面讲到了画布(Bitmap)、图层(Layer)和 Canvas 的概念,下面具体讲解下它们之间的关系。
- 图层(Layer):每一次调用 canvas.drawXXX 系列函数时,都会生成一个透明图层来专门来绘制这个图形,比如前面在绘制矩形时的透明图层就是这个概念。
- 画布(Bitmap):每块画布都是一个 Bitmap,所有的图像都是画在 Bitmap上的。我们知道,每次调用 canvas.drawXXX 函数时,都会生成一个专用的透明图层来绘制这个图形,绘制完成以后,就覆盖在画布上。所以,如果我们连续调用 5 个 draw 函数,就会生成 5 个透明图层,画完之后依次覆盖在画布上显示。画布有两种:第一种是 View 的原始画布,是通过 onDraw(Canvas canvas) 函数传入的,参数中的 canvas 就对应的是 View 的原始画布,控件的背景就是画在这块画布上的;另一种是人造画布,通过 saveLayer()、new Canvas(bitmap) 等函数来人为地新建一块画布。尤其是 saveLayer() 函数,一旦调用 saveLayer() 函数新建一块画布,以后的所有 draw 函数所画的图像都是画在这块画布上的,只有在调用 restore()、resoreToCount() 函数以后,才会返回到原始画布上进行绘制。
- Canvas:Canvas 是画布的表现形式,我们所要绘制的任何东西都是利用 Canvas 来实现的。在代码中,Canvas 的生成方式只有一种——new Canvas(bitmap),即只能通过 Bitmap 生成,无论是原始画布还是人造画布,所有的画布最后都是通过 Canvas 画到 Bitmap 上的。可以把 Canvas 理解成绘图工具,利用它所封装的绘图函数来绘图,而所要绘制的内容最后是画在 Bitmap 上的。所以,如果我们利用 Canvas.clipXXX 系列函数将画布进行裁剪,其实就是把它对应的 Bitmap 进行裁剪,与之对应的结果就是再利用 Canvas 绘图的区域会减小。
9.2.3 saveLayer() 和 saveLayerAlpha() 函数的用法
1. saveLayer() 函数的用法
|
|
参数:
- RectF bounds:新建画布的尺寸。
- Paint paint:画笔实例。
- int saveFlags:新建画布的标识(详见 9.3 节)。
saveLayer() 函数会新建一块画布(Bitmap),后续的所有操作都是在这块画布上进行的。下面我们来看一下 saveLayer() 函数使用中的注意事项。
1)saveLayer() 函数后的所有动作都只对新建画布有小。
|
|
在 onDraw() 函数中,我们先在 View 的原始画布上画上了小狗的图像,然后利用 saveLayer() 函数新建了一个图层,然后利用 canvas.skew() 函数将新建的图层水平斜切 45°,所以之后画的矩形 (0,0,100,100) 就是斜切的。
而正是由于在新建画布后的各种操作都是针对新建画布进行的,所以不会对以前的画布产生影响。从效果图中也可以明显看出,将画布水平斜切 45° 也只影响了 saveLayer() 函数的新建画布,并没有对之前的原始画布产生影响。
2)通过 Rect 指定的矩形大小就是新建的画布大小。
在 saveLayer() 函数的参数中,可以通过指定 Rect 对象或者指定 4 个点来指定一个矩形,这个矩形的大小就是新建画布的大小。我们举例来看一下:
在绘图时,我们先把小狗图片绘制在原始画布上的,然后新建一个大小为 (0,0,200,200) 的透明画布,并将画布填充为灰色。由于画布大小只有 (0,0,200,200),所以从效果图中可以看出,也只有这一小部分区域被填充为灰色。
有些读者可能会想,为了避免画布太小而出现问题,每次都新建一块屏幕大小的画布多好。这样虽然是不会出现问题,但屏幕大小的画布需要多少存储空间呢?按一个像素需要 8bit 存储空间算,分辨率为 1024 像素 x 768 像素的机器,所占用的存储空间就是 1024 * 768 * 8 = 6.2MB。所以我们在使用 saveLayer() 函数新建画布时,一定要选择适当的大小,否则你的 APP 很可能 OOM。
注意:在前面示例中都是直接新建全屏画布的,这只是为了方便展示,在现实使用中一定要创建适当的画布大小。
2. saveLayerAlpha() 函数的用法
|
|
相比 saveLayer() 函数,多了一个 alpha 参数,用于指定新建画布的透明度,取值范围为 0~255,可以用十六进制的 0xAA 表示,取 0 时表示全透明。
这个函数的意义也是在调用的时候会新建一块画布,以后的各种绘图操作都作用在这个画布上,但这个画布是有透明度的,透明度就是通过 alpha 参数指定的。
将上述示例中的 saveLayer() 函数改为 saveLayerAlpha() 函数来重新作图。
在调用 saveLayerAlpha() 函数时,将新建画布的透明度设置为 100%,然后将画布同样填充为白色。从效果图中可以看出,在新建图像与上层画布合成以后,是具有透明度的。