Suraj Kumar Sau
Suraj Kumar Sau

Reputation: 458

Jetpack Compose Canvas BlendMode.SRC_IN makes even background transparent

I'm trying to add an overlay color on top of a PNG image (with a transparent background) using BlendMode.SRC_IN but the background becomes black instead of the set background color as if masking out the background pixels as well.

@Composable
fun Icon(
        fraction: Float,
        image: ImageAsset,
        defaultColor: Color = Color(0xFFEEEEEE),
        progressColor: Color = Color(0xFF888888),
        size: Dp = image.width.dp
) {
    Box(modifier = Modifier.size(size = size)) {
        Canvas(modifier = Modifier.fillMaxSize()) {
            drawImage(
                    image = image,
                    dstSize = IntSize(
                            width = size.toIntPx(),
                            height = size.toIntPx()
                    ),
                    colorFilter = ColorFilter.tint(
                            color = defaultColor
                    ),
                    blendMode = BlendMode.Src
            )
            drawIntoCanvas {
                val paint = Paint().apply {
                    color = progressColor
                    blendMode = BlendMode.SrcIn
                }

                it.restore()
                it.drawRect(
                        rect = Rect(
                                offset = Offset.Zero,
                                size = Size(
                                        width = size.toPx() * fraction,
                                        height = size.toPx()
                                )
                        ),
                        paint = paint
                )
                it.save()
            }
        }
    }
}

Here is how it looks when I line up multiple Icon() on top of a Screen(color = Color.WHITE) like,

Surface(color = Color.White) {
        Row {
            listOf(
                    R.drawable.anemo,
                    R.drawable.cryo,
                    R.drawable.dendro,
                    R.drawable.electro,
                    R.drawable.geo,
                    R.drawable.hydro,
                    R.drawable.pyro
            ).forEachIndexed { index, imageRes ->
                val from = 100f/7 * index
                val to = 100f/7 * (index + 1)
                val fraction = when {
                    progress > to -> 1f
                    progress > from -> (progress - from)/(to - from)
                    else -> 0f
                }
                Icon(
                        fraction = fraction,
                        image = imageResource(id = imageRes),
                        size = 50.dp
                )
            }
        }
    }

icons

The desired result I want is, result

Am I doing something wrong here?

Here is the Github repository where I'm trying this out.

Upvotes: 13

Views: 7374

Answers (4)

alex9xu
alex9xu

Reputation: 1

To display the effect picture of "The desired result I want is", you should put image in ShaderBrush, then paint it, and draw a rectangle with this ShaderBrush and BlendMode, such as:

val imageBrush = ShaderBrush(ImageShader(ImageBitmap.imageResource(id = R.drawable.xxx)))

modifier = modifier.paint(painter = painterResource(id = R.drawable.xxx))
            .drawWithCache {
                onDrawWithContent {
                    drawRect(
                        brush = imageBrush,
                        colorFilter = ColorFilter.tint(Color.Gray),
                        blendMode = BlendMode.Difference
                    )
                }
            }

Upvotes: 0

V Mircan
V Mircan

Reputation: 588

The original answer given by @LN-12 is valid and relies on the internal logic of the framework. By applying the .graphicsLayer(alpha = 0.99f) modifier, Compose changes the CompositingStrategy used when rendering.

However, changing the alpha value can be an undesirable effect and requires a comment to explain the purpose of the modifier. Fortunately, starting with compose 1.4.0, the same outcome can be achieved in a much more elegant way using: .graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen)

Upvotes: 6

C.Allan
C.Allan

Reputation: 151

.graphicsLayer(alpha = 0.99f) is a valid workaround, however, it changes the alpha of the content which is not necessary.

To redirect drawing to an offscreen rendering target, android.graphics.Canvas#saveLayer(android.graphics.RectF, android.graphics.Paint) can be utilised, a simple modifier can be like:

fun Modifier.drawOffscreen(): Modifier = this.drawWithContent {
    with(drawContext.canvas.nativeCanvas) {
        val checkPoint = saveLayer(null, null)
        drawContent()
        restoreToCount(checkPoint)
    }
}

And the content will be drawn offscreen before restoreToCount is called.

Upvotes: 6

LN-12
LN-12

Reputation: 1049

In my case, I could solve the issue of having black pixels where they should be transparent pixels by adding the graphicsLayer modifier like this:

Box(
    modifier = Modifier.fillMaxSize()
) {
    Canvas(modifier = Modifier
        .fillMaxSize()
        .graphicsLayer(alpha = 0.99f)
    ) {
        drawRect(
            color = Color.Black,
            size = size,
            blendMode = BlendMode.Xor
        )

        drawCircle(
            color = Color.Black,
            radius = 300f,
            blendMode = BlendMode.Xor
        )
    }
}

Without the modifier:

enter image description here

With modifier:

enter image description here

I took that idea from here: https://gist.github.com/nadewad/14f04c788aea43bd6d31d94cd8100ab5 (note that drawLayer was renamed to graphicsLayer.

Upvotes: 8

Related Questions