androidandroid-jetpack-compose

Composable: don't clip views


I have a Row, which contains "Spacer - Box - Spacer - Box - etc" inside. All Boxes have same width and backgound with rounded corners. But when there isn't enough space left, the last Box is clipped and due to it's rounded background it looks like its width differs from other boxes.

I've tried to use requiredWidth instead of width, but in this case the box overlaps other boxes and spacers (requiredWidth centers the box). The only thing that helps, is adding horizontal scroll. But i don't need that view to be scrollable. Is there any way to achieve the desired result?

Some pictures for better explanation.

This is how width/requiredWidth work: enter image description here

This is how it maps to my real situation enter image description here


Solution

  • Default Composables or any Composable that is not assigned Modifier.clipToBounds(), Modifier.clip or Modifier.graphicsLayer{clip} do not clip content. You can draw anything outside a Composable using draw Modifiers for instance when no clip is set, it also applies changing position with Modifier.offset or Modifier.layout, etc.

    What happens is Composables are measured with Constraints and Row and Column uses

    var usedSpace = 0
    for(measurable in measurable list) {measurable->
      measurable.measure(maxWith=constraints.maxWidth/Height - usedSpace)
      usedSpace += placeable.width/height
    }
    

    Because of that when there is no avilable space last child Composable might shrink down to 0.dp or 0 px.

    When you assign Modifier.requiredWidth() you change maxWidth of Constraints to any value you assign but when layout width of parent Composable doesn't match its Constraints width it tries to center content. You can finde more detailed explanation about it in this answer.

    If you don't want to center you can use Modifier.wrapContentWidth(alignment=Start, unbounded=true)

    unBounded param increases Constraints maxWidth to Constraints.Infinity and content can be measured with their desired width and alignment sets its position instead of center.

    You can also use Modifier.layout same way wrapContentWidth does, under the hood every size Modifier calls measurement and layout().

    enter image description here

    I post 2 alternatives, you can comment out wrapContentWidth to see they both act same. And inside layout modifier you can inspect which Constraints.maxWith comes to next child and how and with what width they are set.

    @Preview
    @Composable
    private fun LayoutPlacingTest() {
    
        Column(
            modifier = Modifier.fillMaxSize().padding(20.dp)
        ) {
            Row(
                modifier = Modifier
                    .border(4.dp, Color.Gray)
                    .width(300.dp)
                    .background(Color.Yellow)
            ) {
                repeat(3) { index ->
                    Box(
                        modifier = Modifier
    //                        .wrapContentWidth(align = Alignment.Start, unbounded = true)
                            .layout { measurable, constraints ->
                                val placeable = measurable.measure(
                                    constraints.copy(maxWidth = Constraints.Infinity)
                                )
    
                                println("placeable: ${placeable.width}, Constraints: $constraints")
    
                                layout(placeable.width, placeable.height) {
                                    val x = ((placeable.width - constraints.maxWidth) / 2).coerceAtLeast(0)
                                    placeable.placeRelative(x, 0)
                                }
                            }
                            .size(100.dp)
                            .background(Color.Red, RoundedCornerShape(16.dp))
                    )
                    if (index != 2) {
                        Spacer(Modifier.width(20.dp))
                    }
                }
            }
        }
    }