自定义控件绘制(Canvas图层详细)篇九

参考与内容来自

  1. https://blog.csdn.net/harvic880925/article/details/51317746.
  2. https://blog.csdn.net/harvic880925/article/details/51332494

周末去天坛走了一圈,门票好像降了,如果没记错,2年前,好像是50的联票,现在是30多了;或许这是在京最后一次去天坛了,如果不是有亲友来北京玩,需要当导游的话;

关于图层这块,或是画布,一直很晕,前面我们也通过原博客,了解到一些,如save、restore,在xfermode的时候,用到saveLayer,那他们到底是什么,这是我们需要学习的;

关于saveLayer传递flag这块,因为标记为过时了,如想了解,可参加原博客;

好的,继续学习;

如何获得Canvas对象

  1. 自定义View时,重写onDraw()、dispathDraw()方法时,会有一个Canvas对象传过来;这个Canvas是View的Canvas对象,利用这个canvas对象绘图,效果会直接反应在View中;
  2. 使用Bitmap绘制,前面都有用到过
    val c = Canvas(srcBmp)
    

图层与画布

前面我们使用过 saverestore函数,简单回顾一下;
save用来保存当前状态,restore用来恢复之前的画布状态;除了save与restore还有其他函数也可以实现功能;

saveLayer()

相关方法
canvas获取layer相关函数

从上图,可以看到,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步:

  1. 在savelayer新建的画布上作画,并将图像做为目标图像(新的整块画布作为目标图像);
  2. 在新的透明图层(draw时会形成新的透明图层)做源画,并在其矩形所在的透明图层与目标图层相交,计算结果画在新建的透明画布上;
  3. 最终将计算结果直接盖在原始画布上,形成最终的显示效果;
没有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则理解成图层;

    1. 每一个draw函数都对应一个图层,在一个图形画完以后,就放在画纸上显示;
    2. 而一张张透明的画纸(通过saveLayer生成)则一层层地叠加在画板上显示出来;
    3. 我们知道画板和画纸都是用夹子夹在一起的,没有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可同时实现该效果,但少绘制了一个图层;


image.png
通过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所能实现的功能
  • 不同点
    1. saveLayer生成一个独立的图层而save只是保存了一下当时画布的状态类似于一个还原点。
    2. saveLayer因为多了一个图层的原因更加耗费内存慎用(过度绘制,前面提到了)。
    3. saveLayer可指定保存相应区域,尽量避免2中所指的情况。
    4. 在使用混合模式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)
}
输出结果

从输出看到:

  1. 每save一次,栈的层数都在加一,所以无论哪种save方法,保存画布时都使用的是同一个栈;
  2. restore()与restoreToCount(int count)针对的都是同一个栈,所以是完全可以通用和混用的。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容