Jetpack Compose Applying PorterDuffMode to Image
Asked Answered
C

2

8

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?

Cutlor answered 10/1, 2021 at 13:1 Comment(0)
C
2

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()
                }
            }
        }
    }
}
Canterbury answered 26/3, 2021 at 22:3 Comment(5)
I wasn't be able to make it work. Can you provide a full sample or gist/github example to try out.Cutlor
I'll try do this in this week Thracian, Using compose 1.0.0 stable will be ok for you?Canterbury
It's more than okay. Thanks again. I checked it out with 1.0.0 and not able to work it out as in the image.Cutlor
@Cutlor I've just updated my answer and I hope it will be more helpful. If it still not solving your issue I will make one more try to help you :)Canterbury
checking out Image's Painter source code i saw that Android team also use this hack whether to decide to use a layer or not.Cutlor
C
9

Edit

As of 1.4.1 or later versions you can assign

Modifier.graphicsLayer { 
    compositingStrategy = CompositingStrategy.Offscreen
}

to Canvas or any draw Modifier after graphicsLayer to be able to use BlendModes correctly,

Another way to solve issue is to add .graphicsLayer(alpha = 0.99f) to Modifier to make sure an offscreen buffer

@Composable
fun DrawWithBlendMode() {


    val imageBitmapSrc = ImageBitmap.imageResource(
        LocalContext.current.resources,
        R.drawable.composite_src
    )
    val imageBitmapDst = ImageBitmap.imageResource(
        LocalContext.current.resources,
        R.drawable.composite_dst
    )


    Canvas(
        modifier = Modifier
            .fillMaxSize()
            // Provide a slight opacity to for compositing into an
            // offscreen buffer to ensure blend modes are applied to empty pixel information
            // By default any alpha != 1.0f will use a compositing layer by default
            .graphicsLayer(alpha = 0.99f)
    ) {


        val dimension = (size.height.coerceAtMost(size.width) / 2f).toInt()

        drawImage(
            image = imageBitmapDst,
            dstSize = IntSize(dimension, dimension)
        )
        drawImage(
            image = imageBitmapSrc,
            dstSize = IntSize(dimension, dimension),
            blendMode = BlendMode.SrcOut
        )
    }
}

Result

enter image description here

Or adding a layer in Canvas does the trick

    with(drawContext.canvas.nativeCanvas) {
        val checkPoint = saveLayer(null, null)

    // Destination
    drawImage(
        image = dstImage,
        srcSize = IntSize(canvasWidth / 2, canvasHeight / 2),
        dstSize = IntSize(canvasWidth, canvasHeight),
    )

    // Source
    drawImage(
        image = srcImage,
        srcSize = IntSize(canvasWidth / 2, canvasHeight / 2),
        dstSize = IntSize(canvasWidth, canvasHeight),
        blendMode = blendMode
    )
    restoreToCount(checkPoint)
}

I created some tutorials for applying blend modes here

Cutlor answered 31/10, 2021 at 21:13 Comment(5)
This is a simple solution that works, however it feels like an evil hack that could break in the future. Is there any other stable solution with the current version 1.1.1. of Jetpack Compose?Charlottcharlotta
@SvenJacobs second one, saving to layer is not a hack, it's how it was used inside Default Composables with extension function drawWithLayer. And you can see samples of saving to layer and restoring it, but creating a layer is an expensive operation. Basically what Blendmodes to work is a non-opaque bitmap so you do it by either setting alpha less than 1f or using another bitmap as layer and saving it back to original one.developer.android.com/reference/android/graphics/…Cutlor
Note: this method is very expensive, incurring more than double rendering cost for contained content. Avoid using this method, especially if the bounds provided are large. It is recommended to use a hardware layer on a View to apply an xfermode, color filter, or alpha, as it will perform much better than this method. All drawing calls are directed to a newly allocated offscreen bitmap. Only when the balancing call to restore() is made, is that offscreen buffer drawn back to the current target of the Canvas (either the screen, it's target Bitmap, or the previous layer).Cutlor
Thanks, I was referring to the first solution graphicsLayer(alpha = 0.99f). It works but feels like a hack, because it changes the behaviour of the Canvas composable in a way that is unexpected :)Charlottcharlotta
Nice Answer.it was usefulPia
C
2

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()
                }
            }
        }
    }
}
Canterbury answered 26/3, 2021 at 22:3 Comment(5)
I wasn't be able to make it work. Can you provide a full sample or gist/github example to try out.Cutlor
I'll try do this in this week Thracian, Using compose 1.0.0 stable will be ok for you?Canterbury
It's more than okay. Thanks again. I checked it out with 1.0.0 and not able to work it out as in the image.Cutlor
@Cutlor I've just updated my answer and I hope it will be more helpful. If it still not solving your issue I will make one more try to help you :)Canterbury
checking out Image's Painter source code i saw that Android team also use this hack whether to decide to use a layer or not.Cutlor

© 2022 - 2024 — McMap. All rights reserved.