Canvas绘图PorterDuffXfermode使用

概述

在Android中Canvas进行绘图时,可以使用PorterDuffXfermode将所绘制的图像的像素与Canvas中对应位置的像素按照一定规则进行混合,形成新的像素值,从而更新Canvas中最终的像素颜色值,这样会创建很多有趣的效果。当使用PorterDuffXfermode时,需要将其作为参数传给Paint.setXfermode(Xfermode xfermode)方法,这样在用画笔进行绘图时,Android就会使用传入的PorterDuffXfermode。

示例1

我们在演示如何使用PorterDuffXfermode之前,先看以下下面这个例子,代码如下:

 override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)

        //设置背景色
        canvas?.drawARGB(255, 139, 197, 186)
        var canvasHeight = canvas.height
        var canvasWidth = canvas?.width?:0
        var r = canvasWidth / 3
        //绘制黄色的圆形
        drawPaint.color = -0x33bc
        canvas.drawCircle(r.toFloat(), r.toFloat(), r.toFloat(), drawPaint)
        //绘制蓝色的矩形
        drawPaint.color = -0x995501
        canvas.drawRect(r.toFloat(), r.toFloat(), r * 2.7f, r * 2.7f, drawPaint)
}

效果如下:

device-2020-09-15-160427.png

上面演示就是Canvas正常的绘图流程,没有使用PortDuffXfermode。我们简单分析下上面代码:
1.首先,我们调用了canvas.drawARGB方法将整个画布都绘制成一个颜色,在执行完这句代码后,canvas上所有像素的颜色值的ARGB颜色都是(255,139,197,186),由于像素的alpha分量时255而不是0,所以此时所有像素都不透明。
2.当我们执行了canvas.drawCircle(r,r,r,paint)之后,Android会在所画圆的位置用黄颜色的画笔绘制一个黄色的圆形,此时整个圆形内部所有的像素的颜色值的ARGB颜色都0xFFFFCC44,然后用这些黄色的像素替换掉Canvas中对应的同一位置中颜色值ARGB为(255,139,197,186)的像素,这样就将黄色圆形绘制到Canvas上了。
3,当我们执行了canvas.drawRect之后,Android会在所画矩形的位置用蓝色画笔绘制一个蓝色的矩形,此时矩形内部所有的像素颜色值的ARGB颜色都是0xFF66AAFF,然后用这些蓝色的像素替换Canvas中对应同一位置中的像素,这样黄色的圆中的右下角部分的像素与其他一些背景色像素就倍蓝色像素替换了,这样就将蓝色矩形绘制到Canvas上了。

示例2

我们使用PorterDuffXfermode对上面代码进行以下修改,修改之后的代码如下:

override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)

        //设置背景色

        //设置背景色
        canvas?.drawARGB(255, 139, 197, 186)

        var canvasHeight = canvas?.height


        var canvasWidth = canvas?.width?:0
        var r = canvasWidth / 3
        //绘制黄色的圆形
        drawPaint.color = -0x33bc
        canvas?.drawCircle(r.toFloat(), r.toFloat(), r.toFloat(), drawPaint)
        //绘制蓝色的矩形
        drawPaint.color = -0x995501
        drawPaint.xfermode=PorterDuffXfermode(PorterDuff.Mode.CLEAR)
        canvas?.drawRect(r.toFloat(), r.toFloat(), r * 2.7f, r * 2.7f, drawPaint)
}

效果如下:


device-2020-09-15-162039.png

下面我们对以上代码进行一下分析:

1.首先,我们调用了canvas.drawARGB(255, 139, 197, 186)方法将整个Canvas都绘制成一个颜色,此时所有像素都不透明。

2.然后我们通过调用canvas.drawCircle(r, r, r, paint)绘制了一个黄色的圆形到Canvas上面。

3.然后我们执行代码paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)),将画笔的PorterDuff模式设置为CLEAR。

4.然后调用canvas.drawRect(r, r, r * 2.7f, r * 2.7f, paint)方法绘制蓝色的矩形,但是最终界面上出现了一个白色的矩形。

我们具体分析一下白色矩形出现的原因。一般我们在调用canvas.drawXXX()方法时都会传入一个画笔Paint对象,Android在绘图时会先检查该画笔Paint对象有没有设置Xfermode,如果没有设置Xfermode,那么直接将绘制的图形覆盖Canvas对应位置原有的像素;如果设置了Xfermode,那么会按照Xfermode具体的规则来更新Canvas中对应位置的像素颜色。就本例来说,在执行canvas.drawCirlce()方法时,画笔Paint没有设置Xfermode对象,所以绘制的黄色圆形直接覆盖了Canvas上的像素。当我们调用canvas.drawRect()绘制矩形时,画笔Paint已经设置Xfermode的值为PorterDuff.Mode.CLEAR,此时Android首先是在内存中绘制了这么一个矩形,所绘制的图形中的像素称作源像素(source,简称src),所绘制的矩形在Canvas中对应位置的矩形内的像素称作目标像素(destination,简称dst)。源像素的ARGB四个分量会和Canvas上同一位置处的目标像素的ARGB四个分量按照Xfermode定义的规则进行计算,形成最终的ARGB值,然后用该最终的ARGB值更新目标像素的ARGB值。本例中的Xfermode是PorterDuff.Mode.CLEAR,该规则比较简单粗暴,直接要求目标像素的ARGB四个分量全置为0,即(0,0,0,0),即透明色,所以我们通过canvas.drawRect()在Canvas上绘制了一个透明的矩形,由于Activity本身屏幕的背景时白色的,所以此处就显示了一个白色的矩形。

示例3

下面我们对示例2中的代码进行一下修改,将绘制圆形和绘制矩形相关的代码放到canvas.saveLayer()和canvas.restore()之间,代码如下:

override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)

        //设置背景色

        //设置背景色
        canvas?.drawARGB(255, 139, 197, 186)

        var canvasHeight = canvas?.height?:0


        var canvasWidth = canvas?.width?:0

        val layerId = canvas?.saveLayer(
            0f,
            0f,
            canvasWidth.toFloat(),
            canvasHeight.toFloat(),
            null,
            Canvas.ALL_SAVE_FLAG
        )
        val r = canvasWidth / 3
        //正常绘制黄色的圆形
        drawPaint.color = -0x33bc
        canvas?.drawCircle(r.toFloat(), r.toFloat(), r.toFloat(), drawPaint)
        //使用CLEAR作为PorterDuffXfermode绘制蓝色的矩形
        drawPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR)
        drawPaint.color = -0x995501
        canvas?.drawRect(r.toFloat(), r.toFloat(), r * 2.7f, r * 2.7f, drawPaint)
        //最后将画笔去除Xfermode
        drawPaint.xfermode = null
        canvas?.restoreToCount(layerId)
}

效果如下:


device-2020-09-15-162642.png

下面对上述代码进行一下分析:

  1. 首先,我们调用了canvas.drawARGB(255, 139, 197, 186)方法将整个Canvas都绘制成一个颜色,此时所有像素都不透明。

  2. 然后我们将主要的代码都放到了canvas.saveLayer()以及canvas.restoreToCount()之间。

关于canvas绘图中的layer有以下几点需要说明:

  • canvas是支持图层layer渲染这种技术的,canvas默认就有一个layer,当我们平时调用canvas的各种drawXXX()方法时,其实是把所有的东西都绘制到canvas这个默认的layer上面。

  • 我们还可以通过canvas.saveLayer()新建一个layer,新建的layer放置在canvas默认layer的上部,当我们执行了canvas.saveLayer()之后,我们所有的绘制操作都绘制到了我们新建的layer上,而不是canvas默认的layer。

  • 用canvas.saveLayer()方法产生的layer所有像素的ARGB值都是(0,0,0,0),即canvas.saveLayer()方法产生的layer初始时是完全透明的。

  • canvas.saveLayer()方法会返回一个int值,用于表示layer的ID,在我们对这个新layer绘制完成后可以通过调用canvas.restoreToCount(layer)或者canvas.restore()把这个layer绘制到canvas默认的layer上去,这样就完成了一个layer的绘制工作。

那你可能感觉到很奇怪,我们只是将绘制圆形与矩形的代码放到了canvas.saveLayer()和canvas.restoreToCount()之间,为什么不再像示例二那样显示白色的矩形了?

我们在分析示例二代码时知道了最终矩形区域的目标颜色都被重置为透明色(0,0,0,0)了,最后只是由于Activity背景色为白色,所以才最终显示成白色矩形。在本例中,我们在新建的layer上面绘制完成后,其实矩形区域的目标颜色也还是被重置为透明色(0,0,0,0)了,这样整个新建layer只有圆的3/4不是透明的,其余像素全是透明的,然后我们调用canvas.restoreToCount()将该layer又绘制到了Canvas上面去了。在将一个新建的layer绘制到Canvas上去时,Android会用整个layer上面的像素颜色去更新Canvas对应位置上像素的颜色,并不是简单的替换,而是Canvas和新layer进行Alpha混合。由于我们的layer中只有两种像素:完全透明的和完全不透明的,不存在部分透明的像素,并且完全透明的像素的颜色值的四个分量都为0,所以本例就将Canvas和新layer进行Alpha混合的规则简化了,具体来说:

  • 如果新建layer上面某个像素的Alpha分量为255,即该像素完全不透明,那么Android会直接用该像素的ARGB值作为Canvas对应位置上像素的颜色值。
  • 如果新建layer上面某个像素的Alpha分量为0,即该像素完全透明,在本例中Alpha分量为0的像素,其RGB分量也都为0,那么Android会保留Canvas对应位置上像素的颜色值。

这样当将新layer绘制到Canvas上时,完全不透明的3/4黄色圆中的像素会完全覆盖Canvas对应位置的像素,而由于在新layer上面绘制的矩形区域的像素ARGB都为(0,0,0,0),所以最终Canvas上对应矩形区域还是保持之前的背景色,这样就不会出现白色的矩形了。

大部分情况下,我们想要本例中实现的效果,而不是想要示例二中形成的白色矩形,所以大部分情况下在使用PorterDuffXfermode时都是结合canvas.saveLayer()、canvas.restoreToCount()的,将关键代码写在这两个方法之间。

不同混合模式计算规则

device-2020-09-15-163218.png

上面的例子演示了了16种混合模式的效果,并且关键代码都放在了canvas.saveLayer()与canvas.restoreToCount()之间.代码如下:


/**
 * Created by maozonghong
 * on 2020/9/15
 */
class PortDufferView  @JvmOverloads constructor(context: Context, attr:AttributeSet?) : View(context,attr) {

    var textPaint=Paint(Paint.ANTI_ALIAS_FLAG)

    var drawPaint=Paint(Paint.ANTI_ALIAS_FLAG)

    val textArray = arrayOf(
        arrayOf("Clear", "Src", "Dst", "SrcOver"),
        arrayOf("DstOver", "SrcIn", "DstIn", "SrcOut"),
        arrayOf("DstOut", "SrcATop", "DstATop", "Xor"),
        arrayOf("Darken", "Lighten", "Multiply", "Screen")
    )

    var duffMode= arrayOf(
        arrayOf(PorterDuff.Mode.CLEAR,PorterDuff.Mode.SRC,PorterDuff.Mode.DST,PorterDuff.Mode.SRC_OVER),
        arrayOf(PorterDuff.Mode.DST_OVER,PorterDuff.Mode.SRC_IN,PorterDuff.Mode.DST_IN,PorterDuff.Mode.SRC_OUT),
        arrayOf(PorterDuff.Mode.DST_OUT,PorterDuff.Mode.SRC_ATOP,PorterDuff.Mode.DST_ATOP,PorterDuff.Mode.XOR),
        arrayOf(PorterDuff.Mode.DARKEN,PorterDuff.Mode.LIGHTEN,PorterDuff.Mode.MULTIPLY,PorterDuff.Mode.SCREEN)
    )


    init{

        textPaint.color=Color.BLACK
        textPaint.textSize=25f
        setLayerType(LAYER_TYPE_SOFTWARE,null)
    }

    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)

        //设置背景色

        canvas?.drawARGB(255, 139, 197, 186)

        var canvasHeight = canvas?.height?:0

        var canvasWidth = canvas?.width?:0

        var itemWidth=(canvasWidth-30)/4

        var translateY=0f

            for(i in 0..3) {

                translateY = (itemWidth + 40f) * i

                for (j in 0..3) {
                    var paint = Paint(Paint.ANTI_ALIAS_FLAG)

                    var translateX = (itemWidth.toFloat() + 10) * j

                    canvas?.translate(translateX, translateY)

                    canvas?.saveLayer(
                        0f,
                        0f,
                        itemWidth.toFloat(),
                        itemWidth.toFloat(),
                        null,
                        Canvas.ALL_SAVE_FLAG
                    )

                    paint.style = Paint.Style.STROKE
                    paint.color = Color.BLACK
                    canvas?.drawRect(0f, 0f, itemWidth.toFloat(), itemWidth.toFloat(), paint)

                    paint.style = Paint.Style.FILL
                    canvas?.drawBitmap(makeDest(itemWidth / 2, itemWidth / 2), 0f, 0f, paint)

                    var textStartX = itemWidth - textPaint.measureText(textArray[i][j]) - 20
                    var textStartY = 40f
                    Log.e("mode", "i=${i},j=${j} " + textArray[i][j] + "," + duffMode[i][j].name)

                    canvas?.drawText(textArray[i][j], textStartX, textStartY, textPaint)

                    paint.xfermode = PorterDuffXfermode(duffMode[i][j])

                    canvas?.drawBitmap(
                        makeSrc(itemWidth / 2 + 30, itemWidth / 2 + 30),
                        itemWidth / 4f,
                        itemWidth / 4f,
                        paint
                    )

                    canvas?.restore()

                    canvas?.translate(-translateX, -translateY)
                }
            }
    }

    fun makeSrc(width:Int,height:Int):Bitmap{

        var bmp=Bitmap.createBitmap(width,height,Bitmap.Config.ARGB_8888)

        var canvas=Canvas(bmp)
        var paint=Paint(Paint.ANTI_ALIAS_FLAG)
        paint.color=-0x995501
        canvas.drawRect(0f,0f,width.toFloat(),height.toFloat(),paint)
        return bmp
    }


    fun makeDest(width:Int,height: Int):Bitmap{
        var bmp=Bitmap.createBitmap(width,height,Bitmap.Config.ARGB_8888)

        var canvas=Canvas(bmp)
        var paint=Paint(Paint.ANTI_ALIAS_FLAG)

        paint.color=-0x33bc
        canvas.drawCircle(width/2f,height/2f,width/2f,paint)
        return bmp
    }
}
1)PorterDuff.Mode.CLEAR:

所绘制不会提交到画布上,正常是没东西

2)PorterDuff.Mode.DARKEN:

取两图层全部区域,交集部分 ,颜色加深

3)PorterDuff.Mode.DST:

只保留目标图。

4)PorterDuff.Mode.DST_ATOP:

源图和目标图相交处绘制目标图,不相交的地方绘制源图。

5)PorterDuff.Mode.DST_IN:

相交的地方绘制目标图,绘制效果会受到原图处的透明度影响

6)PorterDuff.Mode.DST_OUT:

不相交的地方绘制目标图

7)PorterDuff.Mode.DST_OVER:

目标图绘制在上方

8)PorterDuff.Mode.LIGHTEN:

取两图层全部区域,点亮交集部分颜色

9)PorterDuff.Mode.MULTIPLY:

取两图层交集部分叠加后颜色

10)PorterDuff.Mode.SRC:

只绘制原图

11)PorterDuff.Mode.SRC_ATOP:

原图和目标图相交处绘制原图,不相交绘制目标图

12)PorterDuff.Mode.SRC_IN:

两者相交的地方绘制原图。

13)PorterDuff.Mode.SRC_OUT:

不相交的地方绘制原图

14)PorterDuff.Mode.SRC_OVER:

把原图绘制在上方

15)PorterDuff.Mode.XOR:

不相交的地方按原样绘制原图和目标图

16)PorterDuff.Mode.SCREEN:

取两图层全部区域,交集部分透明.

最后总结一下,PorterDuffXfermode用于实现新绘制的像素与Canvas上对应位置已有的像素按照混合规则进行颜色混合。

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