androidcanvasandroid-jetpack-composeporter-duff

Jetpack Compose Applying PorterDuffMode to Image


Based on the images and PorterDuffModes in this page

I downloaded images, initially even though they are png they had light and dark gray rectangles which were not transparent and removed them.

Destination Source

And checked out using this sample code, replacing drawables with the ones in original code with the ones below and i get result

enter image description here

As it seem it works as it should with Android View, but when i use Jetpack Canvas as

androidx.compose.foundation.Canvas(modifier = Modifier.size(500.dp),
    onDraw = {

        drawImage(imageBitmapDst)
        drawImage(imageBitmapSrc, blendMode = BlendMode.SrcIn)

    })

BlendMode.SrcIn draws blue rectangle over black rectangle, other modes do not return correct results either. BlendMode.SrcOut returns black screen.

And using 2 Images stacked on top of each other with Box

val imageBitmapSrc: ImageBitmap = imageResource(id = R.drawable.c_src)
val imageBitmapDst: ImageBitmap = imageResource(id = R.drawable.c_dst)

Box {
    Image(bitmap = imageBitmapSrc)
    Image(
        bitmap = imageBitmapDst,
        colorFilter = ColorFilter(color = Color.Unspecified, blendMode = BlendMode.SrcOut)
    )
}

Only blue src rectangle is visible.

Also tried with Painter, and couldn't able to make it work either

val imageBitmapSrc: ImageBitmap = imageResource(id = R.drawable.c_src)
val imageBitmapDst: ImageBitmap = imageResource(id = R.drawable.c_dst)

val blendPainter = remember {
    object : Painter() {

        override val intrinsicSize: Size
            get() = Size(imageBitmapSrc.width.toFloat(), imageBitmapSrc.height.toFloat())

        override fun DrawScope.onDraw() {
            drawImage(imageBitmapDst, blendMode = BlendMode.SrcOut)
            drawImage(imageBitmapSrc)
        }
    }
}

Image(blendPainter)

How should Blend or PorterDuff mode be used with Jetpack Compose?


Solution

  • I was really frustrated for a whole week with similar problem, however your question helped me find the solution how to make it work.

    EDIT1

    I'm using compose 1.0.0

    In my case I'm using something like double buffering instead of drawing directly on canva - just as a workaround.

    Canvas(modifier = Modifier.fillMaxWidth().fillMaxHeight()) {
    
        // First I create bitmap with real canva size
        val bitmap = ImageBitmap(size.width.toInt(), size.height.toInt())
    
        // here I'm creating canvas of my bitmap
        Canvas(bitmap).apply {
           // here I'm driving on canvas
        }
       
        // here I'm drawing my buffered image
        drawImage(bitmap)
    }
    

    Inside Canvas(bitmap) I'm using drawPath, drawText, etc with paint:

    val colorPaint = Paint().apply {
        color = Color.Red
        blendMode = BlendMode.SrcAtop
    }
    

    And in this way BlendMode works correctly - I've tried many of modes and everything worked as expected.

    I don't know why this isn't working directly on canvas of Composable, but my workaround works fine for me.

    EDIT2

    After investigating Image's Painter's source code i saw that Android team also use alpha trick either to decide to create a layer or not

    In Painter

    private fun configureAlpha(alpha: Float) {
        if (this.alpha != alpha) {
            val consumed = applyAlpha(alpha)
            if (!consumed) {
                if (alpha == DefaultAlpha) {
                    // Only update the paint parameter if we had it allocated before
                    layerPaint?.alpha = alpha
                    useLayer = false
                } else {
                    obtainPaint().alpha = alpha
                    useLayer = true
                }
            }
            this.alpha = alpha
        }
    }
    

    And applies here

        fun DrawScope.draw(
            size: Size,
            alpha: Float = DefaultAlpha,
            colorFilter: ColorFilter? = null
        ) {
            configureAlpha(alpha)
            configureColorFilter(colorFilter)
            configureLayoutDirection(layoutDirection)
    
            // b/156512437 to expose saveLayer on DrawScope
            inset(
                left = 0.0f,
                top = 0.0f,
                right = this.size.width - size.width,
                bottom = this.size.height - size.height
            ) {
    
                if (alpha > 0.0f && size.width > 0 && size.height > 0) {
                    if (useLayer) {
                        val layerRect = Rect(Offset.Zero, Size(size.width, size.height))
                        // TODO (b/154550724) njawad replace with RenderNode/Layer API usage
                        drawIntoCanvas { canvas ->
                            canvas.withSaveLayer(layerRect, obtainPaint()) {
                                onDraw()
                            }
                        }
                    } else {
                        onDraw()
                    }
                }
            }
        }
    }