1

How can i set width of a group of composables, siblings layout from top to bottom, to width of longest one?

What i try to build is exactly same thing as in the images above. For simplicity let's say quote the component at the top and message box which contains message and another container that stores date and message status.

The longest one of quote and message box must be set as parent width and other one must be set to same width as longest one which requires a remeasuring for short one i assume.

Also if message box gets to resized there needs to be an internal parameter that passes this width to set position of container that stores date and status. As can be seen clearly with bounds message text is moved to start while status to end when quote is longer than message box. When message has more than one line message box width and height are set with a calculation as telegram or whatsapp does.

Built this initially with Layout as

@Composable
private fun DynamicLayout(
    modifier: Modifier = Modifier,
    quote: @Composable () -> Unit,
    message: @Composable () -> Unit
) {

    val content = @Composable {
        quote()
        message()
    }

    Layout(content = content, modifier = modifier) { measurables, constraints ->


        val placeableQuote = measurables.first().measure(constraints)
        val quoteWidth = placeableQuote.width

        val placeableMessage =
            measurables.last()
                .measure(Constraints(minWidth = quoteWidth, maxWidth = constraints.maxWidth))
        val messageWidth = placeableMessage.width

        val maxWidth = quoteWidth.coerceAtLeast(messageWidth)
        val totalHeight = placeableQuote.height + placeableMessage.height

        layout(maxWidth, totalHeight) {
            placeableQuote.placeRelative(x = 0, y = 0)
            placeableMessage.placeRelative(x = 0, y = placeableQuote.height)
        }
    }
}

Where i measure message box using width of quote constraint it works but only when quote is longer.

DynamicLayout(
    quote = {
        Text(
            "QUOTE with a very long text",
            modifier = Modifier
                .background(Color(0xffF44336))
                .height(60.dp),
            color = Color.White
        )
    },
    message = {
        Text(
            "MESSAGE Content",
            modifier = Modifier
                .background(Color(0xff9C27B0)),
            color = Color.White
        )
    }
)

DynamicLayout(
    quote = {
        Text(
            "QUOTE",
            modifier = Modifier
                .background(Color(0xffF44336))
                .height(60.dp),
            color = Color.White
        )
    },
    message = {
        Text(
            "MESSAGE with very long Content",
            modifier = Modifier
                .background(Color(0xff9C27B0)),
            color = Color.White
        )
    }
)

As it's must be remeasured i think solution for this question should be done with SubComposeLayout but couldn't figure out how to use it for this setup?

@Composable
private fun SubComponentLayout(
    modifier: Modifier = Modifier,
    mainContent: @Composable () -> Unit,
    dependentContent: @Composable (Int) -> Unit
) {

    SubcomposeLayout(modifier = modifier) { constraints ->

        val mainMeasurables: List<Measurable> = subcompose(SlotsEnum.Main, mainContent)

        val mainPlaceables: List<Placeable> = mainMeasurables.map {
            it.measure(constraints)
        }

        val maxSize =
            mainPlaceables.fold(IntSize.Zero) { currentMax: IntSize, placeable: Placeable ->
                IntSize(
                    width = maxOf(currentMax.width, placeable.width),
                    height = maxOf(currentMax.height, placeable.height)
                )
            }

        var maxWidth =
            mainPlaceables.maxOf { it.width }


        layout(maxSize.width, maxSize.height) {

            println("🔥 SubcomposeLayout-> layout() maxSize width: ${maxSize.width}, height: ${maxSize.height}")

            val dependentMeasurables: List<Measurable> = subcompose(
                slotId = SlotsEnum.Dependent,
                content = {
                    println("🍏 SubcomposeLayout-> layout()->subcompose() mainWidth ZERO")
                    dependentContent(0)
                }
            )

            val dependentPlaceables: List<Placeable> = dependentMeasurables.map {
                it.measure(constraints)
            }

            maxWidth = maxWidth.coerceAtLeast(
                dependentPlaceables.maxOf { it.width }
            )

            subcompose(SlotsEnum.NEW) {
                println("🍒 SubcomposeLayout-> layout()->subcompose() maxWidth: $maxWidth")
                dependentContent(maxWidth)
            }

            mainPlaceables.forEach { it.placeRelative(0, 0) }
            dependentPlaceables.forEach { it.placeRelative(0, 150) }
        }
    }
}

Why cannot remeasure same component second time with same id? When i try to call subCompose with SlotsEnum.Dependent it throws an exception

subcompose(SlotsEnum.NEW) {
    println("🍒 SubcomposeLayout-> layout()->subcompose() maxWidth: $maxWidth")
    dependentContent(maxWidth)
}

Still not remeasuring correctly after calling it? How can setting sibling can be solved with SubcomposeLayout?

2 Answers 2

4

I made a sample based on the sample provided by official documents and @chuckj's answer here.

enter image description here

Orange and pink containers are Columns, which direct children of DynamicWidthLayout, that uses SubcomposeLayout to remeasure.

@Composable
private fun DynamicWidthLayout(
    modifier: Modifier = Modifier,
    mainContent: @Composable () -> Unit,
    dependentContent: @Composable (IntSize) -> Unit
) {

    SubcomposeLayout(modifier = modifier) { constraints ->


        var mainPlaceables: List<Placeable> = subcompose(SlotsEnum.Main, mainContent).map {
            it.measure(constraints)
        }

        var maxSize =
            mainPlaceables.fold(IntSize.Zero) { currentMax: IntSize, placeable: Placeable ->
                IntSize(
                    width = maxOf(currentMax.width, placeable.width),
                    height = maxOf(currentMax.height, placeable.height)
                )
            }

        val dependentMeasurables: List<Measurable> = subcompose(SlotsEnum.Dependent) {
            // 🔥🔥 Send maxSize of mainComponent to
            // dependent composable in case it might be used
            dependentContent(maxSize)
        }

        val dependentPlaceables: List<Placeable> = dependentMeasurables
            .map { measurable: Measurable ->
                measurable.measure(Constraints(maxSize.width, constraints.maxWidth))
            }

        // Get maximum width of dependent composable
        val maxWidth = dependentPlaceables.maxOf { it.width }


        println("🔥 DynamicWidthLayout-> maxSize width: ${maxSize.width}, height: ${maxSize.height}")

        // If width of dependent composable is longer than main one, remeasure main one
        // with dependent composable's width using it as minimumWidthConstraint
        if (maxWidth > maxSize.width) {

            println("🚀 DynamicWidthLayout REMEASURE MAIN COMPONENT")

            // !!! 🔥🤔 CANNOT use SlotsEnum.Main here why?
            mainPlaceables = subcompose(2, mainContent).map {
                it.measure(Constraints(maxWidth, constraints.maxWidth))
            }
        }

        // Our final maxSize is longest width and total height of main and dependent composables
        maxSize = IntSize(
            maxSize.width.coerceAtLeast(maxWidth),
            maxSize.height + dependentPlaceables.maxOf { it.height }
        )


        layout(maxSize.width, maxSize.height) {

            // Place layouts
            mainPlaceables.forEach { it.placeRelative(0, 0) }
            dependentPlaceables.forEach {
                it.placeRelative(0, mainPlaceables.maxOf { it.height })
            }
        }
    }
}


enum class SlotsEnum { Main, Dependent }

Usage

@Composable
private fun TutorialContent() {

    val density = LocalDensity.current.density

    Column(
        modifier = Modifier
            .fillMaxSize()
            .verticalScroll(rememberScrollState())
    ) {


        var mainText by remember { mutableStateOf(TextFieldValue("Main Component")) }
        var dependentText by remember { mutableStateOf(TextFieldValue("Dependent Component")) }


        OutlinedTextField(
            modifier = Modifier
                .padding(horizontal = 8.dp)
                .fillMaxWidth(),
            value = mainText,
            label = { Text("Main") },
            placeholder = { Text("Set text to change main width") },
            onValueChange = { newValue: TextFieldValue ->
                mainText = newValue
            }
        )

        OutlinedTextField(
            modifier = Modifier
                .padding(horizontal = 8.dp)
                .fillMaxWidth(),
            value = dependentText,
            label = { Text("Dependent") },
            placeholder = { Text("Set text to change dependent width") },
            onValueChange = { newValue ->
                dependentText = newValue
            }
        )

        DynamicWidthLayout(
            modifier = Modifier
                .padding(8.dp)
                .background(Color.LightGray)
                .padding(8.dp),
            mainContent = {

                println("🍏 DynamicWidthLayout-> MainContent {} composed")

                Column(
                    modifier = Modifier
                        .background(orange400)
                        .padding(4.dp)
                ) {
                    Text(
                        text = mainText.text,
                        modifier = Modifier
                            .background(blue400)
                            .height(40.dp),
                        color = Color.White
                    )
                }
            },
            dependentContent = { size: IntSize ->


                // 🔥 Measure max width of main component in dp  retrieved
                // by subCompose of dependent component from IntSize
                val maxWidth = with(density) {
                    size.width / this
                }.dp

                println(
                    "🍎 DynamicWidthLayout-> DependentContent composed " +
                            "Dependent size: $size, "
                            + "maxWidth: $maxWidth"
                )

                Column(
                    modifier = Modifier
                        .background(pink400)
                        .padding(4.dp)
                ) {

                    Text(
                        text = dependentText.text,
                        modifier = Modifier
                            .background(green400),
                        color = Color.White
                    )
                }
            }
        )
    }
}

And full source code is here.

1
1

Jetpack Compose has built-in support for this.

In the parent, use Modifier.width(IntrinsicSize.Max)

In the children, use Modifier.fillMaxWidth()

5
  • Not all the time. If Composable you use like image or a Custom Composable don't return an Intrinsic size you need to use SubcomposeLayout.
    – Thracian
    Commented Aug 18, 2023 at 15:13
  • Intrinsic sizes are pre-measurement that return values from IntrinsicMeasureScope.min/maxIntrinsicWidth in some cases you need measurement of every child to decide what actual width should be.
    – Thracian
    Commented Aug 18, 2023 at 15:18
  • Also it doesn't work when you have a LazyColumn/Row inside the composable assigned with Intrinsic sizes. It crashes with Asking for intrinsic measurements of SubcomposeLayout layouts is not supported. This includes components that are built on top of SubcomposeLayout, such as lazy lists, BoxWithConstraints, TabRow, etc. To mitigate this: so both have uses in different scenarios. Because of this you can't use none of these inside a Row with width(IntrinsicSize.Max)
    – Thracian
    Commented Aug 18, 2023 at 15:23
  • I also asked how it can be done using SubcomposeLayout as in title.
    – Thracian
    Commented Aug 18, 2023 at 15:47

Not the answer you're looking for? Browse other questions tagged or ask your own question.