androidandroid-jetpack-composeandroid-jetpack-compose-layout

Why the constraint offsetting behave like sizing (width/height) while the layout sizing (width/height) behave like offset


I have the following top-level code where a Box is centered on the device

@Composable
fun Greeting() {
    Column(
        horizontalAlignment = CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        val size = 240.dp
        Box(
            modifier = Modifier
                .width(size)
                .height(size)
                .border(1.dp, Color.Red)

        ) {
            BoxLayout(size, Color.Green) {
                BoxLayout(size, Color.Blue)
            }
        }
    }
}

If my BoxLayout is coded as below, where when I compute the layout, I offset the constraint with 1/3 of the size.

@Composable
private fun BoxLayout(
    size: Dp,
    borderColor: Color,
    content: @Composable BoxScope.() -> Unit = {}) {
    Box(modifier = Modifier
        .width(size)
        .height(size)
        .layout { measurable, constraints ->
            val placeable = measurable.measure(
                constraints.offset(
                    -size.roundToPx() / 3,
                    -size.roundToPx() / 3
                )
            )
            layout(placeable.width, placeable.height) {
                placeable.place(0, 0)
            }
        }
        .border(1.dp, borderColor), content = content)
}

From the result, it looks like resizing the boxes (both width and height) smaller by 1/3.

enter image description here

However if my BoxLayout is coded as below, where when I compute the layout without offsetting the constraint, instead, I reduce the layout width and height by 1/3 of the size. as below

@Composable
private fun BoxLayout(
    size: Dp,
    borderColor: Color,
    content: @Composable BoxScope.() -> Unit = {}
) {
    Box(modifier = Modifier
        .width(size)
        .height(size)
        .layout { measurable, constraints ->
            val placeable = measurable.measure(constraints)
            layout(
                placeable.width - size.roundToPx() / 3,
                placeable.height - size.roundToPx() / 3
            ) {
                placeable.place(0, 0)
            }
        }
        .border(1.dp, borderColor), content = content)
}

The result became as below. It looks more like setting offset of my boxes by 1/3 of the sizes instead.

enter image description here

From the above, looks like the behavior of Offset the Constraint and Width/Height set on Layout, behave opposite of each other, whereby the

Can someone explain why the phenomena? i.e.


Solution

  • First, of all constraints.offset has nothing to do with the example above.

    @Stable
    fun Constraints.offset(horizontal: Int = 0, vertical: Int = 0) = Constraints(
        (minWidth + horizontal).coerceAtLeast(0),
        addMaxWithMinimum(maxWidth, horizontal),
        (minHeight + vertical).coerceAtLeast(0),
        addMaxWithMinimum(maxHeight, vertical)
    )
    
    private fun addMaxWithMinimum(max: Int, value: Int): Int {
        return if (max == Constraints.Infinity) {
            max
        } else {
            (max + value).coerceAtLeast(0)
        }
    }
    

    In both examples content or child Composables are not placed at top left position even though Placable.place(0,0) is used.

    But in second example child chooses 240.dp as content dimension so it's jumped to right side as this reference.

    I changed this example as below.

    @Preview
    @Composable
    fun Greeting() {
        Column(
            modifier = Modifier.fillMaxSize(),
            horizontalAlignment = CenterHorizontally,
            verticalArrangement = Arrangement.Center
        ) {
            val size = 240.dp
            Column {
                Box(
                    modifier = Modifier
                        .width(size)
                        .height(size)
                        .border(1.dp, Color.Red)
    
                ) {
                    BoxLayout(0, size, Color.Green) {
                        BoxLayout(1, size, Color.Blue)
                    }
                }
    
                Box(
                    modifier = Modifier
                        .width(size)
                        .height(size)
                        .border(1.dp, Color.Red)
    
                ) {
                    BoxLayout2(0, size, Color.Green) {
                        BoxLayout2(1, size, Color.Blue)
                    }
                }
    
                Box(
                    modifier = Modifier
                        .width(size)
                        .height(size)
                        .border(1.dp, Color.Red)
    
                ) {
                    BoxLayout3(
                        index = 0, size = size, borderColor = Color.Green
                    ) {
                        BoxLayout3(
                            index = 1,
                            size = size,
                            borderColor = Color.Blue
                        ) {}
                    }
                }
            }
        }
    }
    
    @Composable
    private fun BoxLayout(
        index: Int,
        size: Dp,
        borderColor: Color,
        content: @Composable BoxScope.() -> Unit = {}
    ) {
        Box(modifier = Modifier
            .width(size)
            .height(size)
            .layout { measurable, constraints ->
                val placeable = measurable.measure(
                    constraints.offset(
                        -size.roundToPx() / 3,
                        -size.roundToPx() / 3
                    )
                )
    
                val sizePX = size.roundToPx()
    
                println(
                    "🔥 index: $index, SizePx: $sizePX, " +
                            "minWidth: ${constraints.minWidth}, " +
                            "maxWidth: ${constraints.maxWidth}, " +
                            "width: ${placeable.width}"
                )
    
                layout(placeable.width, placeable.height) {
                    placeable.place(0, 0)
                }
            }
            .border(1.dp, borderColor), content = content)
    }
    
    @Composable
    private fun BoxLayout2(
        index: Int,
        size: Dp,
        borderColor: Color,
        content: @Composable BoxScope.() -> Unit = {}
    ) {
        Box(modifier = Modifier
            .width(size)
            .height(size)
            .layout { measurable, constraints ->
                val placeable = measurable.measure(constraints)
    
                val sizePX = size.roundToPx()
    
                println(
                    "🍏 index: $index, SizePx: $sizePX, " +
                            "minWidth: ${constraints.minWidth}, " +
                            "maxWidth: ${constraints.maxWidth}, " +
                            "width: ${placeable.width}"
                )
    
                layout(
                    placeable.width - size.roundToPx() / 3,
                    placeable.height - size.roundToPx() / 3
                ) {
                    placeable.place(0, 0)
                }
            }
            .border(1.dp, borderColor), content = content)
    }
    
    @Composable
    private fun BoxLayout3(
        index: Int,
        size: Dp,
        borderColor: Color,
        content: @Composable BoxScope.() -> Unit = {}
    ) {
        Box(modifier = Modifier
            .width(size)
            .height(size)
            .layout { measurable, constraints ->
                val placeable = measurable.measure(
                    if (index == 0) {
                        constraints.copy(
                            minWidth = constraints.maxWidth - 60,
                            maxWidth = constraints.maxWidth - 60,
                            minHeight = constraints.maxHeight - 60,
                            maxHeight = constraints.maxHeight - 60
                        )
                    } else {
                        constraints.copy(
                            minWidth = constraints.maxWidth - 110,
                            maxWidth = constraints.maxWidth - 110,
                            minHeight = constraints.maxHeight - 110,
                            maxHeight = constraints.maxHeight - 110
                        )
                    }
                )
    
                val sizePX = size.roundToPx()
    
                println(
                    "🍎 index: $index, SizePx: $sizePX, " +
                            "minWidth: ${constraints.minWidth}, " +
                            "maxWidth: ${constraints.maxWidth}, " +
                            "width: ${placeable.width}"
                )
    
                layout(
                    placeable.width,
                    placeable.height
                ) {
                    placeable.place(0, 0)
                }
            }
            .border(1.dp, borderColor), content = content)
    }
    

    And constraints.offset is actually this in your question, depending on which values you change offset it can be different as can be seen in source code.

    constraints.copy(
        minWidth = constraints.maxWidth - size.roundToPx() / 3,
        maxWidth = constraints.maxWidth - size.roundToPx() / 3,
        minHeight = constraints.maxHeight - size.roundToPx() / 3,
        maxHeight = constraints.maxHeight - size.roundToPx() / 3
    )
    

    In third example i used a different values this is exactly same as in first one as constraints.offset(-60) or constraints.offset(-110), but to clearly show how child Composable has different Constraints reported to it by parent based on its measurement via width/height we choose with first measurement or at index 1.

    It prints

     I  🔥 index: 1, SizePx: 660, minWidth: 440, maxWidth: 440, width: 220
     I  🔥 index: 0, SizePx: 660, minWidth: 660, maxWidth: 660, width: 440
     I  🍏 index: 1, SizePx: 660, minWidth: 660, maxWidth: 660, width: 660
     I  🍏 index: 0, SizePx: 660, minWidth: 660, maxWidth: 660, width: 660
     I  🍎 index: 1, SizePx: 660, minWidth: 600, maxWidth: 600, width: 490
     I  🍎 index: 0, SizePx: 660, minWidth: 660, maxWidth: 660, width: 600
    

    And to elaborate these results,

    in both examples child chooses the data in last section width while as you can see both minWidth and maxWidth of Constraints are not met and they both jump.

    In second example as you can see you always choose content size as big as parent but set layout width smaller so it jumps as while child size being as big as parent. That's the difference between first and second example.

    It's also mystery how for instance parent knows to pass 600px Constraints to child even though neither it nor child has been measured yet. This is how child Composables are measured in first and third exampe.

    I  🍎 index: 1, SizePx: 660, minWidth: 600, maxWidth: 600, width: 490
    I  🍎 index: 0, SizePx: 660, minWidth: 660, maxWidth: 660, width: 600
    

    to pass 600px to child when it's measured with constraints.maxWidth - 60

    Also i add an example to show measurable dimensions and layout dimensions are not the same thing. Measurable can be measured with dimensions bigger than parent. And in this example as you can see it doesn't jump from (0,0) because parent layout() width abides fixed size Modifier that returns same min-max width for Constraints

    enter image description here

    @Composable
    private fun SomeLayout(
        modifier: Modifier,
        content: @Composable () -> Unit
    ) {
        Layout(
            modifier = modifier,
            content = content
        ) { measurables, constraints ->
    
            val placeable = measurables.first().measure(
                constraints.copy(
                    maxWidth = constraints.maxWidth + 100,
                    maxHeight = constraints.maxHeight + 100
                )
            )
            layout(constraints.maxWidth, constraints.maxHeight) {
                placeable.placeRelative(0, 0)
            }
        }
    }
    
    @Preview
    @Composable
    private fun Test() {
        Row(
            modifier = Modifier.padding(100.dp)
        ) {
            SomeLayout(
                modifier = Modifier
                    .size(100.dp)
                    .border(2.dp, Color.Green)
            ) {
                Box(
                    modifier = Modifier
                        .fillMaxSize()
                        .background(Color.Magenta)
                        .clickable {
    
                        }
                )
            }
            Box(
                modifier = Modifier
                    .size(100.dp)
                    .border(2.dp, Color.Red)
            )
        }
    }