How to clip or cut a Composable?

One of the ways for achieving cutting or clipping a Composable without the need of creating a custom Composable is using

Modifier.drawWithContent{} with a layer and a BlendMode or PorterDuff modes.

With Jetpack Compose for these modes to work you either need to set alpha less than 1f or use a Layer as in answer here.

I go with layer solution because i don’t want to change content alpha

fun ContentDrawScope.drawWithLayer(block: ContentDrawScope.() -> Unit) {
    with(drawContext.canvas.nativeCanvas) {
        val checkPoint = saveLayer(null, null)
        block()
        restoreToCount(checkPoint)
    }
}

block lambda is the draw scope for Modifier.drawWithContent{} to do clipping

and another extension for simplifying further

fun Modifier.drawWithLayer(block: ContentDrawScope.() -> Unit) = this.then(
    Modifier.drawWithContent {
        drawWithLayer {
            block()
        }
    }
)

Clip button at the left side

First let’s draw the button that is cleared a circle at left side

@Composable
private fun WhoAteMyButton() {
    val circleSize = LocalDensity.current.run { 100.dp.toPx() }
    Box(
        modifier = Modifier
            .fillMaxWidth()
            .drawWithLayer {
                // Destination
                drawContent()
               
                // Source
                drawCircle(
                    center = Offset(0f, 10f),
                    radius = circleSize,
                    blendMode = BlendMode.SrcOut,
                    color = Color.Transparent
                )
            }
    ) {
        Button(
            modifier = Modifier
                .padding(horizontal = 10.dp)
                .fillMaxWidth(),
            onClick = { /*TODO*/ }) {
            Text("Hello World")
        }
    }
}

We simply draw a circle but because of BlendMode.SrcOut intersection of destination is removed.

Clip button and Image with custom image

For squircle button i found an image from web

And clipped button and image using this image with

@Composable
private fun ClipComposables() {
    Row(
        modifier = Modifier.fillMaxWidth(),
        horizontalArrangement = Arrangement.SpaceEvenly
    ) {
        val imageBitmap = ImageBitmap.imageResource(id = R.drawable.squircle)

        Box(modifier = Modifier
            .size(150.dp)
            .drawWithLayer {

                // Destination
                drawContent()

                // Source
                drawImage(
                    image = imageBitmap,
                    dstSize = IntSize(width = size.width.toInt(), height = size.height.toInt()),
                    blendMode = BlendMode.DstIn
                )

            }
        ) {

            Box(
                modifier = Modifier
                    .size(150.dp)
                    .clickable { }
                    .background(MaterialTheme.colorScheme.inversePrimary),
                contentAlignment = Alignment.Center
            ) {
                Text(text = "Squircle", fontSize = 20.sp)
            }
        }

        Box(modifier = Modifier
            .size(150.dp)
            .drawWithLayer {
                // Destination
                drawContent()

                // Source
                drawImage(
                    image = imageBitmap,
                    dstSize = IntSize(width = size.width.toInt(), height = size.height.toInt()),
                    blendMode = BlendMode.DstIn
                )

            }
        ) {

            Image(
                painterResource(id = R.drawable.squirtle),
                modifier = Modifier
                    .size(150.dp),
                contentScale = ContentScale.Crop,
                contentDescription = ""
            )
        }

    }
}

There are 2 things to note here

1- Blend mode is BlendMode.DstIn because we want texture of Destination with shape of Source
2- Drawing image inside ContentDrawScope with dstSize to match Composable size. By default it’s drawn with png size posted above.

Creating a BottomNavigation with cutout shape

@Composable
private fun BottomBarWithCutOutShape() {
    val density = LocalDensity.current
    val shapeSize = density.run { 70.dp.toPx() }

    val cutCornerShape = CutCornerShape(50)
    val outline = cutCornerShape.createOutline(
        Size(shapeSize, shapeSize),
        LocalLayoutDirection.current,
        density
    )

    val icons =
        listOf(Icons.Filled.Home, Icons.Filled.Map, Icons.Filled.Settings, Icons.Filled.LocationOn)

    Box(
        modifier = Modifier.fillMaxWidth()
    ) {
        BottomNavigation(
            modifier = Modifier
                .drawWithLayer {
                    with(drawContext.canvas.nativeCanvas) {

                        val checkPoint = saveLayer(null, null)
                        val width = size.width

                        val outlineWidth = outline.bounds.width
                        val outlineHeight = outline.bounds.height

                        // Destination
                        drawContent()

                        // Source
                        withTransform(
                            {
                                translate(
                                    left = (width - outlineWidth) / 2,
                                    top = -outlineHeight / 2
                                )
                            }
                        ) {
                            drawOutline(
                                outline = outline,
                                color = Color.Transparent,
                                blendMode = BlendMode.Clear
                            )
                        }

                        restoreToCount(checkPoint)
                    }
                },
            backgroundColor = Color.White
        ) {

            var selectedIndex by remember { mutableStateOf(0) }

            icons.forEachIndexed { index, imageVector: ImageVector ->
                if (index == 2) {
                    Spacer(modifier = Modifier.weight(1f))
                    BottomNavigationItem(
                        icon = { Icon(imageVector, contentDescription = null) },
                        label = null,
                        selected = selectedIndex == index,
                        onClick = {
                            selectedIndex = index
                        }
                    )
                } else {
                    BottomNavigationItem(
                        icon = { Icon(imageVector, contentDescription = null) },
                        label = null,
                        selected = selectedIndex == index,
                        onClick = {
                            selectedIndex = index
                        }
                    )
                }
            }
        }

        // This is size fo BottomNavigationItem
        val bottomNavigationHeight = LocalDensity.current.run { 56.dp.roundToPx() }

        FloatingActionButton(
            modifier = Modifier
                .align(Alignment.TopCenter)
                .offset {
                    IntOffset(0, -bottomNavigationHeight / 2)
                },
            shape = cutCornerShape,
            onClick = {}
        ) {
            Icon(imageVector = Icons.Default.Add, contentDescription = null)
        }
    }
}

This code is a bit long but we basically create a shape like we always and create an outline to clip

    val cutCornerShape = CutCornerShape(50)
    val outline = cutCornerShape.createOutline(
        Size(shapeSize, shapeSize),
        LocalLayoutDirection.current,
        density
    )

And before clipping we move this shape section up as half of the height to cut only with half of the outline

withTransform(
{
    translate(
        left = (width - outlineWidth) / 2,
        top = -outlineHeight / 2
    )
}
) {
    drawOutline(
        outline = outline,
        color = Color.Transparent,
        blendMode = BlendMode.Clear
    )
}

Also to have a BottomNavigation such as BottomAppBar that places children on both side
i used a Spacer

icons.forEachIndexed { index, imageVector: ImageVector ->
    if (index == 2) {
        Spacer(modifier = Modifier.weight(1f))
        BottomNavigationItem(
            icon = { Icon(imageVector, contentDescription = null) },
            label = null,
            selected = selectedIndex == index,
            onClick = {
                selectedIndex = index
            }
        )
    } else {
        BottomNavigationItem(
            icon = { Icon(imageVector, contentDescription = null) },
            label = null,
            selected = selectedIndex == index,
            onClick = {
                selectedIndex = index
            }
        )
    }
}

Then we simply add a FloatingActionButton, i used offset but you can create a bigger parent and put our custom BottomNavigation and button inside it.

Leave a Comment