Search code examples
androidkotlinandroid-layoutandroid-jetpack-composeandroid-jetpack

Android Jetpack compose partial border in Box


I am trying to prepare a design in jetpack compose with a partial border on one side of the box. Here is the UI I have right now,

The background is a solid color as of now but will be replaced with a image. I want to break border on bottom left of and add some text similar to the screenshot below while keeping the background as it is.

Here is my code:

Box(modifier = Modifier
    .fillMaxWidth()
    .fillMaxHeight()) {
    Box(modifier = Modifier.fillMaxHeight().fillMaxWidth().background(Color(0xFF37C7D7).copy(alpha = 0.6f)))
    Box(modifier = Modifier
        .fillMaxHeight()
        .fillMaxWidth()
        .background(Color.Transparent)
        .padding(20.dp,30.dp)
        .border(width = 0.8.dp, color = Color.White.copy(alpha = 0.5f), shape=RoundedCornerShape(32.dp))
    )
    Box(modifier = Modifier
        .fillMaxHeight()
        .fillMaxWidth()
        .background(Color.Transparent)
        .padding(28.dp,38.dp)
        .border(width = 0.8.dp, color = Color.White.copy(alpha = 0.5f), shape=RoundedCornerShape(28.dp))
    )
    Column(modifier = Modifier.statusBarsPadding()
        .fillMaxWidth()
        .fillMaxHeight().padding(20.dp,40.dp),
        verticalArrangement = Arrangement.Bottom) {
        Text(text = "this is Test",modifier = Modifier.padding(0.dp,30.dp))
    }
}

Solution

  • You can prevent part of the view from being drawn with Modifier.drawWithContent and DrawScope.clipRect. Using this method, you can create the following modifier:

    fun Modifier.drawWithoutRect(rect: Rect?) =
        drawWithContent {
            if (rect != null) {
                clipRect(
                    left = rect.left,
                    top = rect.top,
                    right = rect.right,
                    bottom = rect.bottom,
                    clipOp = ClipOp.Difference,
                ) {
                    [email protected]()
                }
            } else {
                drawContent()
            }
        }
    

    Use it like this:

    Box(
        modifier = Modifier
            .fillMaxWidth()
            .fillMaxHeight()
    ) {
        Image(
            painterResource(id = R.drawable.my_image),
            contentDescription = null,
            contentScale = ContentScale.FillBounds,
            modifier = Modifier.fillMaxSize()
        )
        var textCoordinates by remember { mutableStateOf<Rect?>(null) }
        Box(
            modifier = Modifier
                .fillMaxHeight()
                .fillMaxWidth()
                .background(Color(0xFF37C7D7).copy(alpha = 0.6f))
        )
        Box(
            modifier = Modifier
                .fillMaxHeight()
                .fillMaxWidth()
                .drawWithoutRect(textCoordinates)
                .padding(20.dp, 30.dp)
                .border(
                    width = 0.8.dp,
                    color = Color.White.copy(alpha = 0.5f),
                    shape = RoundedCornerShape(32.dp)
                )
        )
        Box(
            modifier = Modifier
                .fillMaxHeight()
                .fillMaxWidth()
                .drawWithoutRect(textCoordinates)
                .padding(28.dp, 38.dp)
                .border(
                    width = 0.8.dp,
                    color = Color.White.copy(alpha = 0.5f),
                    shape = RoundedCornerShape(28.dp)
                )
        )
        Column(
            verticalArrangement = Arrangement.Bottom,
            modifier = Modifier
                .statusBarsPadding()
                .padding(20.dp, 40.dp)
                .onGloballyPositioned {
                    textCoordinates = it.boundsInParent()
                }
                .align(Alignment.BottomStart)
        ) {
            Text(text = "this is Test", modifier = Modifier.padding(0.dp, 30.dp))
        }
    }
    

    Result: