kotlincanvasandroid-jetpack-composeandroid-jetpack-compose-canvas

Applying a Blur Effect to the Entire Screen Except for a Specific Rectangle in Jetpack Compose


I’m working on a project using Jetpack Compose in Kotlin and I’m trying to apply a blur effect to the entire screen, excluding a specific rectangle. The goal is to blur the background, regardless of its content (it could be a photo or other UI elements), and have a clear, non-blurred rectangle within this blurred background.

Here’s an example of what I’m trying to achieve: {MY PHOTO}

I found a similar example where an overlay of another transparent color is used instead of blurring. Here’s the code:

@Preview(showBackground = true)
@Composable
fun BlurExample()
{
    Image(
        painter = painterResource(id = R.drawable.delete1),
        contentDescription = null,
        contentScale = ContentScale.FillBounds,
        modifier = Modifier.fillMaxSize().blur(20.dp)
    )
    TransparentClipLayout(
        modifier = Modifier.fillMaxSize(),
        width = 300.dp,
        height = 200.dp,
        offsetY = 150.dp
    )
}

@Composable
fun TransparentClipLayout(
    modifier: Modifier,
    width: Dp,
    height: Dp,
    offsetY: Dp
) {
    val offsetInPx: Float
    val widthInPx: Float
    val heightInPx: Float

    with(LocalDensity.current) {
        offsetInPx = offsetY.toPx()
        widthInPx = width.toPx()
        heightInPx = height.toPx()
    }

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

            // Destination
            drawRect(Color(0x77FF0000))

            // Source
            drawRoundRect(
                topLeft = Offset(
                    x = (size.width - widthInPx) / 2,
                    y = offsetInPx
                ),
                size = Size(widthInPx, heightInPx),
                cornerRadius = CornerRadius(30f,30f),
                color = Color.Transparent,
                blendMode = BlendMode.Clear
            )
            restoreToCount(checkPoint)
        }
    }
}

However, I haven’t been able to find a solution on how to apply a blur effect to the background. My question is: How can I achieve a clear, non-blurred rectangle within a blurred background, similar to the example above, using Jetpack Compose in Kotlin?

Note: I understand that Modifier.blur can be used to apply a blur effect, but it seems to blur the entire background, including the rectangle that I want to keep clear.


Solution

  • Somehow blur doesn't draw when there is no content. If it's ok to draw image twice you can do it like this

    @Preview(showBackground = true)
    @Composable
    fun BlurExample() {
    
        val offsetInPx: Float
        val widthInPx: Float
        val heightInPx: Float
    
        with(LocalDensity.current) {
            offsetInPx = 150.dp.toPx()
            widthInPx = 300.dp.toPx()
            heightInPx = 200.dp.toPx()
        }
    
    
        Box {
            Image(
                painter = painterResource(id = R.drawable.landscape11),
                contentDescription = null,
                contentScale = ContentScale.FillBounds,
                modifier = Modifier.fillMaxSize()
            )
    
            Box(modifier = Modifier
                .matchParentSize()
                .blur(20.dp, edgeTreatment = BlurredEdgeTreatment.Unbounded)
                .drawWithContent {
                    with(drawContext.canvas.nativeCanvas) {
                        val checkPoint = saveLayer(null, null)
    
                        // Destination
                        drawContent()
    
                        // Source
                        drawRoundRect(
                            topLeft = Offset(
                                x = (size.width - widthInPx) / 2,
                                y = offsetInPx
                            ),
                            size = Size(widthInPx, heightInPx),
                            cornerRadius = CornerRadius(30f, 30f),
                            color = Color.Transparent,
                            blendMode = BlendMode.Clear
                        )
                        restoreToCount(checkPoint)
                    }
                }
    
            ) {
                Image(
                    painter = painterResource(id = R.drawable.landscape11),
                    contentDescription = null,
                    contentScale = ContentScale.FillBounds,
                    modifier = Modifier.fillMaxSize()
                )
            }
        }
    }
    

    Or like this

    @Preview(showBackground = true)
    @Composable
    fun BlurExample2() {
    
        val offsetInPx: Float
        val widthInPx: Float
        val heightInPx: Float
    
        with(LocalDensity.current) {
            offsetInPx = 150.dp.toPx()
            widthInPx = 300.dp.toPx()
            heightInPx = 200.dp.toPx()
        }
    
        val painter = painterResource(R.drawable.landscape11)
    
        Box(modifier = Modifier.fillMaxSize()
            .drawBehind {
                with(painter){
                    draw(size)
                }
            }
            .drawWithContent {
                with(drawContext.canvas.nativeCanvas) {
                    val checkPoint = saveLayer(null, null)
    
                    // Destination
                    drawContent()
    
                    // Source
                    drawRoundRect(
                        topLeft = Offset(
                            x = (size.width - widthInPx) / 2,
                            y = offsetInPx
                        ),
                        size = Size(widthInPx, heightInPx),
                        cornerRadius = CornerRadius(30f, 30f),
                        color = Color.Transparent,
                        blendMode = BlendMode.Clear
                    )
                    restoreToCount(checkPoint)
                }
            }
            .blur(20.dp)
            .drawBehind {
                with(painter){
                    draw(size)
                }
            }
        )
    }