多年以来自己都有一个毛病,知识或者说技术储备看到过或者知道在哪里就觉得自己掌握了,但实际上并没有,当自己开始做,两眼抓瞎。最好的例子在于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 即可去除