0

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

  • Constraint Offset - is resizing the box (instead of offsetting the location)
  • Width/Height Layout - is like offsetting the location of the layout (and not resizing the box)

Can someone explain why the phenomena? i.e.

  • Why changing the Layout width and height, isn't changing it's width and height?
  • Changing the constraint offset, is changing it's width and height instead?
Elye
  • 53,639
  • 54
  • 212
  • 474
  • This question is also closely related with this one. https://stackoverflow.com/questions/76491064/why-adding-and-subtracting-compose-placeable-constraint-behave-differently/76491298#76491298 – Thracian Jun 17 '23 at 15:33
  • The previous answer tried to explain just the constraint side, where the constraint through used by offset, is affecting the side. It is not clear how this will prevent it from being sized through the layout. Can you elaborate clearer on how the constraint measurement and layout relate to each other? Thanks – Elye Jun 18 '23 at 00:00
  • Can the width and height of the layout redefine the size of the layout (instead of just moving the position)? From the example above, looks like that is not possible. If it is not possible, why is it even called the width and height parameter to set? – Elye Jun 18 '23 at 04:20
  • What do you mean by "tried to explain"? I clearly with examples and with a tutorial showing how layout width changes where content is positioned when layout width not inside `Constraints`bounds explained how it works, it has nothing to do with `constraints.offset`. Offset is a simple function that limits Constraints not being less than zero. Why didn't you post a reply if you didn't understand that answer?`where the constraint through used by offset, is affecting the side`. This is not exactly true. – Thracian Jun 18 '23 at 04:41
  • Is it true that the layout width can never change the "box"'s width? I don't get that point. I thought the layout width can be smaller than the box's width? – Elye Jun 18 '23 at 04:55
  • It can but if it's in bounds of Constraints of parent or size Modifier it's assigned with. If it has 0-1000 range for instance, let's say they are in a Row parent can expect it to be in this range and position other sibling as the size changes in this range as you can see in my tutorial sibling example – Thracian Jun 18 '23 at 04:56
  • The issue you have is you use Modifier.size which returns same value for constraints.min and constraints.max as 660px for my device. The moment you set layout smaller than this it jumps to right and bottom since being smaller than Constraints. If the content chooses a size that does not satisfy the incoming Constraints, the parent layout will be reported a size coerced in the Constraints, and the position of the content will be automatically offset to be centered on the space assigned to the child by the parent layout under the assumption that Constraints were respected. – Thracian Jun 18 '23 at 05:10

1 Answers1

1

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)
        )
    }
}
Thracian
  • 43,021
  • 16
  • 133
  • 222
  • Thanks @Thracian for expanding the example. This println shows clearly that the final "width" is change when the "offset" is changed or when we reset the "constraint". The layout's "width" parameter is not used to fix the layout's "width", but just it's starting point. Instead it's the placeable's width that determined the layout's "width, right? – Elye Jun 18 '23 at 05:25
  • I guess, my main confusion comes is, why when I set may layout's width and height, it is not setting it, but just moving the layout position (feel like same behavior as placeable.place(...)) – Elye Jun 18 '23 at 05:26
  • 1
    Let's go step by step. First, offset acts as the same i posted above. It only changes the placable width in first example which is 440px then 200px. In second example you always choose 660px – Thracian Jun 18 '23 at 05:28
  • 1
    That's why the sizes are different but parent knows these sizes even before its measured. And last point since you choose a fixed size modifier and set `layout` width different than the `Constraints` range they jump in both examples. Except in second examples you always choose placeable size as 660px so why both rectangles have same dimensions – Thracian Jun 18 '23 at 05:29
  • Basically when you get a placable via measurement its Content width. But layout width is how parent will be measured and it can be in range of constraints from its parent or Modifier you assigned to it. If layout width you choose do not meet these requirements it jumps to center itself as it has valid Constraints – Thracian Jun 18 '23 at 05:40
  • @Elye added a placeable sample to make the distinction more clear. – Thracian Jun 18 '23 at 05:48
  • In you examples you are referencing these dimensions as layout dimensions that's why the jumps occur in the first place and since you choose different placeable sizes in both samples they jump in different amounts – Thracian Jun 18 '23 at 05:49
  • And as you can see Row abides custom Layouts layout width since it's in the bounds of Constraints come from Modifier.size(100.dp) – Thracian Jun 18 '23 at 05:50
  • And the last thing you can set layout width, it still acts as parent dimensions but it has to be in Constraints bounds. When you don't assign a modifier contraints are set min=0, max=1080 for instance. You can choose a layout width in this range and parent when positioning with other siblings abide what has been assigned as layout width. I suggest you to read Constraints section of this answer to be familiar with which Modifier returns which Constraints. https://stackoverflow.com/questions/65779226/android-jetpack-compose-width-height-size-modifier-vs-requiredwidth-requir/73316247#73316247 – Thracian Jun 18 '23 at 05:55
  • Thanks much. I started to grasp some ideas now. Is it true it's best to start with is to put aside the layout size by setting it to "layout(constraints.maxWidth, constraints.maxHeight)" (assuming the parent has proper constraint), so we can just focus on the peaceable constraint and placement first? – Elye Jun 18 '23 at 06:05
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/254123/discussion-between-thracian-and-elye). – Thracian Jun 18 '23 at 06:05