Paint PorterDuffXfermode详解

多年以来自己都有一个毛病,知识或者说技术储备看到过或者知道在哪里就觉得自己掌握了,但实际上并没有,当自己开始做,两眼抓瞎。最好的例子在于ImageView scaleType 相关的。以前一直记不住,直到我写demo 去对比区别才很清楚明白,所以对于PorterDuffXfermode 也一样。

以下理论解释来自于Xfermode in android

什么是Xfermode

有三个实现类: AvoidXfermode,PixelXorXfermode,PorterDuffXfermode

AvoidXfermode

AvoidXfermode xfermode will draw the src everywhere except on top of the
opColor or, depending on the Mode, draw only on top of the opColor.

官方解释,按照我的理解是如果想把原来图像进行处理,比如绿色换成红色,可以使用。这里有个容差值的概念,比如红色是0xff0000,但在一定范围内都是红色,如果设置一个容差,在范围内的 各种符合要求的红色 都会被处理。

PixelXofXermode

没设么用,不支持硬件加速

接下来说说重点,也就是最常用的

PorterDuffXfermode

Porter-Duff 来由

Porter-Duff 操作是 1 组 12 项用于描述数字图像合成的基本手法,包括
Clear、Source Only、Destination Only、Source Over、Source In、Source
Out、Source Atop、Destination Over、Destination In、Destination
Out、Destination Atop、XOR。通过组合使用 Porter-Duff 操作,可完成任意 2D
图像的合成。
Thomas Porter 和 Tom Duff 发表于 1984年原始论文的扫描版本

可以支持任何2D图像的合成。理论支撑

PorterDuffXfermode 各种模式之间的区别

有哪些种类?

android 中共有18种不同模式,分别是:

  • CLEAR
  • SRC
  • DST
  • SRC_OVER
  • DST_OVER
  • SRC_IN
  • DST_IN
  • SRC_OUT
  • DST_OUT
  • SRC_ATOP
  • DST_ATOP
  • XOR
  • DARKEN
  • LIGHTEN
  • MULTIPLY
  • SCREEN
  • ADD
  • OVERLAY

文档解释

public enum Mode {
    /** [0, 0] */
    CLEAR       (0),
    /** [Sa, Sc] */
    SRC         (1),
    /** [Da, Dc] */
    DST         (2),
    /** [Sa + (1 - Sa)*Da, Rc = Sc + (1 - Sa)*Dc] */
    SRC_OVER    (3),
    /** [Sa + (1 - Sa)*Da, Rc = Dc + (1 - Da)*Sc] */
    DST_OVER    (4),
    /** [Sa * Da, Sc * Da] */
    SRC_IN      (5),
    /** [Sa * Da, Sa * Dc] */
    DST_IN      (6),
    // ...以下省略

结合Paint 如何使用

1.声明Paint

    private val paint by lazy {
        Paint().apply {
            isAntiAlias = true
            color = ContextCompat.getColor(ctx, R.color.app_color_blue_2_pressed)
            style = Paint.Style.FILL
        }
    }

2.对Paint 设置

            paint.xfermode = PorterDuffXfermode(modes[i])

3.canvas 绘制

            // draw dst
            canvas.drawBitmap(makeDst()
            ,0f,0f,paint)

            paint.xfermode = PorterDuffXfermode(modes[i])
            // draw src

            canvas.drawBitmap(makeSrc(),0f,0f,paint)
            paint.xfermode = null

XfermodeSampleView 分析

先上图

为了仔细对比以及理解各种模式之间的区别,以及使用中遇到的问题,还有疑惑,接下来仔细的分析各种出现的情况。

  1. 首先xfermode 绘图需要两部分,DST,SRC 两种。可以理解为DST 在下边,SEC在上面。也就是说DST先绘制,SRC 后绘制。看一下代码生成
    fun makeSrc() : Bitmap{
        val radius = rectSize.div(3f)
        val bitmap = Bitmap.createBitmap(rectSize
                ,rectSize,Bitmap.Config.ARGB_8888)
        val c = Canvas(bitmap)
        val p = Paint().apply {
            style = Paint.Style.FILL
            color = ContextCompat.getColor(context, R.color.app_color_theme_3)
        }
        c.drawRect(radius,radius,rectSize.times(0.75f),rectSize.times(0.75f),p)
        return bitmap
    }

    fun makeDst() : Bitmap{
        val radius = rectSize.div(3f)
        val bitmap = Bitmap.createBitmap(radius.times(2).toInt()
                ,radius.times(2).toInt(),Bitmap.Config.ARGB_8888)
        val c = Canvas(bitmap)
        val p = Paint().apply {
            style = Paint.Style.FILL
            color = ContextCompat.getColor(context, R.color.app_color_blue_2_pressed)
        }
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            c.drawOval(0f,0f,radius.times(2),radius.times(2),p)
        }
        return bitmap
    }

这一段代码需要理解的地方在于,canvas.drawRect,canvas.drawOval,为什么是传递这些参数。比如说 c.drawOval(0f,0f,radius.times(2),radius.times(2),p),为什么传的left = 0f,top = 0f,right = 2 * radius,bottom = 2 * radius。首先我们创建宽高为 2 * radius。 所以画布总的大小为固定2 * radius 大小。canvas 坐标还是以左上有起点。

  1. 在对DST,SRC进行绘制的时候,为什么是需要传入bitmap 呢?
    例如 canvas.drawBitmap(makeSrc(),0f,0f,paint)
    在之前的测试中也写过不是bitmap的情况。直接drawOval ,drawRect。但是情况跟现在的情况完全不一样。各种模式之间的混合不如预期,也只有现在通过bitmap 之间的混合才会生效。
    一下错误的用法(具体原因不是很明确)
            canvas.drawCircle(cx, cy, radius, paint)

            paint.xfermode = PorterDuffXfermode(modes[i])
            paint.color = ContextCompat.getColor(context, R.color.app_color_theme_3)
            // draw src
            canvas.drawRect(cx, cy
                    , cx + radius.times(2), cy + radius.times(2), paint)

            paint.xfermode = null

可以看到这两种绘制的方式,一种通过生成biamp,为它创建canvas 并且绘图。另一种直接使用canvas 去绘制。本来我认为这两种没有区别,也不会有问题。可实际上出现了问题。没有达到预期的混合。具体的原因目前还没有明确,我猜测可能是因为通过生成bitmap,这是本来已经在新的画布上绘制的。而且xfermode 本来就是图像的绘图混合。drawRect,drawCircle本身不是图像。所以会有根本的差异。也就是说使用xfermode 要在于场景,在drawBitmap()中去使用。
采用drawXXX 的方式,混合错误,如下


错误的使用
  1. 还有一个问题也是混合中很常见的。混合之后会出现混合处会有黑色占位的情况。对于这样的情况,很多次没有搞明白的时候我都是拒绝的。这到底是什么情况? 现在我可以理解为是因为dst,src混合的窗口是透明的,其实对于这种解释,也很疑惑,因为调用过canvas.drawColor(Color.WHITE)使canvas 背景为白色,可惜这样也会有问题。在混合之后,进行裁剪了。就黑色了。这样子我还没有找到根本确切的解释。但是解决方法是有的。在进行混合之前需要保存画布
val sc = canvas.saveLayer(posX.toFloat() * rectSize
                    , posY.toFloat() * rectSize
                    , (posX.toFloat() + 1) * rectSize, (posY.toFloat() + 1) * rectSize, null,
                    Canvas.ALL_SAVE_FLAG )
                    
                    ...
                    canvas.restoreToCount(sc)
                    

可以看到调用结束之后,恢复了画布。关于canvas 的saveLayer,canvas.restoreToCount的分析理解,请看另一篇文章。还有为什么是saveLayouer 里面是一部分保存。而不是canvas.save() 它们有什么区别?

可以看到通过restoreToCount 的处理,并没有黑色部分了。结果也很符合预期

  1. 对于背景的添加,这里用到了bitmapShader。也是Paint 另外一个很值得好好理解的方法paint.setShader(),shader 也就是着色器
  • 创建bitmapShadow
        // make a ckeckerboard pattern
        val bm = Bitmap.createBitmap(intArrayOf(-0x1, -0x333334, -0x333334, -0x1), 2, 2,
                Bitmap.Config.RGB_565)
        mBG = BitmapShader(bm,
                Shader.TileMode.REPEAT,
                Shader.TileMode.REPEAT)

        val m = Matrix()
        m.setScale(6f, 6f)
        mBG.setLocalMatrix(m)`

具体的参数意义,以及mBg.setLocalMatrix 以后在好好写一下。

  • 使用
            paint.setShader(mBG)
            // draw bg
            canvas.drawRect(x,y, x + rectSize.toFloat() - 25,y + rectSize.toFloat(),paint)
            paint.setShader(null)

在这里,paint.setShadow() 设置好shadow,在drawRect中会把创建bitmapShader 传入的bitmap 绘制上去。至于绘制的顺序,比如是绘制的shader 的bitmap 和 canvas.drawCircle 的图形,谁在上谁在下。可以认为shader 是在最下的。

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

推荐阅读更多精彩内容