How to get exact size without recomposition?

If you are gonna use size of your Composable for drawing lines you can change your function to extension of DrawScope which returns size of your Composable. If that’s not the case check answer below.

fun DrawScope.drawLine() {
    this.size
}

And call this function inside either of

Modifier.drawBehind{}, Modifier.drawWithContent{} or Modifier.drawWithCache{}. Also you can pass size inside these Modifiers if you don’t want to change your function.

BoxWithConstraints and SubcomposeLayout

BoxWithConstraints is not always reliable to get exact size of your content, it, as the name suggests, is good for getting Constraints. I have a detailed answer about Constraints and what BoxConstraints return with which size modifier here. You can check out Constraints section of answer to examine outcomes with each Modifier.

For instance

BoxWithConstraints() {
    Text(
        modifier = Modifier
            .size(200.dp)
            .border(2.dp, Color.Red),
        text = "Constraints: ${constraints.minWidth}, max: ${constraints.maxWidth}"
    )
}

Will return minWidth = 0px, and maxWidth 1080px(width of my device in px) instead of 525px which is 200.dp in my device.

And you can’t get dimensions from Layout alone without recomposing either that’s why BoxWithConstraints uses SubcomposeLayout to pass Constraints to content. You can check out this question to learn about SubcomposeLayout.

BoxWithConstraints source code

@Composable
@UiComposable
fun BoxWithConstraints(
    modifier: Modifier = Modifier,
    contentAlignment: Alignment = Alignment.TopStart,
    propagateMinConstraints: Boolean = false,
    content:
        @Composable @UiComposable BoxWithConstraintsScope.() -> Unit
) {
    val measurePolicy = rememberBoxMeasurePolicy(contentAlignment, propagateMinConstraints)
    SubcomposeLayout(modifier) { constraints ->
        val scope = BoxWithConstraintsScopeImpl(this, constraints)
        val measurables = subcompose(Unit) { scope.content() }
        with(measurePolicy) { measure(measurables, constraints) }
    }
}

SubcomposeLayout allows deferring the composition and measure of
content until its constraints from its parent are known and some its
content can be measured, the results from which and can be passed as a
parameter to the deferred content.

In the implementation below, it can be customized as required, i use several versions of it based on why it’s needed.

You can customize how you sum or max width or height, what will be
layout width or height, how you place your items to have a behavior like Row, Column or Box depends on your needs. You can limit to one Composable or multiple ones is up to you. Only thing that is required is passing Size/IntSize/DpSize from one Composable to another.

/**
 * SubcomposeLayout that [SubcomposeMeasureScope.subcompose]s [mainContent]
 * and gets total size of [mainContent] and passes this size to [dependentContent].
 * This layout passes exact size of content unlike
 * BoxWithConstraints which returns [Constraints] that doesn't match Composable dimensions under
 * some circumstances
 *
 * @param placeMainContent when set to true places main content. Set this flag to false
 * when dimensions of content is required for inside [mainContent]. Just measure it then pass
 * its dimensions to any child composable
 *
 * @param mainContent Composable is used for calculating size and pass it
 * to Composables that depend on it
 *
 * @param dependentContent Composable requires dimensions of [mainContent] to set its size.
 * One example for this is overlay over Composable that should match [mainContent] size.
 *
 */
@Composable
fun DimensionSubcomposeLayout(
    modifier: Modifier = Modifier,
    placeMainContent: Boolean = true,
    mainContent: @Composable () -> Unit,
    dependentContent: @Composable (Size) -> Unit
) {
    SubcomposeLayout(
        modifier = modifier
    ) { constraints: Constraints ->

        // Subcompose(compose only a section) main content and get Placeable
        val mainPlaceables: List<Placeable> = subcompose(SlotsEnum.Main, mainContent)
            .map {
                it.measure(constraints.copy(minWidth = 0, minHeight = 0))
            }

        // Get max width and height of main component
        var maxWidth = 0
        var maxHeight = 0

        mainPlaceables.forEach { placeable: Placeable ->
            maxWidth += placeable.width
            maxHeight = placeable.height
        }

        val dependentPlaceables: List<Placeable> = subcompose(SlotsEnum.Dependent) {
            dependentContent(Size(maxWidth.toFloat(), maxHeight.toFloat()))
        }
            .map { measurable: Measurable ->
                measurable.measure(constraints)
            }


        layout(maxWidth, maxHeight) {

            if (placeMainContent) {
                mainPlaceables.forEach { placeable: Placeable ->
                    placeable.placeRelative(0, 0)
                }
            }

            dependentPlaceables.forEach { placeable: Placeable ->
                placeable.placeRelative(0, 0)
            }
        }
    }
}

enum class SlotsEnum { Main, Dependent }

Usage

val content = @Composable {
    Box(
        modifier = Modifier
            .size(200.dp)
            .background(Color.Red)
    )
}

val density = LocalDensity.current

DimensionSubcomposeLayout(
    mainContent = { content() },
    dependentContent = { size: Size ->
        content()
        val dpSize = density.run {size.toDpSize() }
        Box(Modifier.size(dpSize).border(3.dp, Color.Green))
    },
    placeMainContent = false
)

or

DimensionSubcomposeLayout(
    mainContent = { content() },
    dependentContent = { size: Size ->
        val dpSize = density.run {size.toDpSize() }
        Box(Modifier.size(dpSize).border(3.dp, Color.Green))
    }
)

Result

In example below we set size of Box with green border based on Box with red background. This can be complicated for a beginner but that’s how you get dimensions without recomposing a Composable. SubcomposeLayout question and answers in the link provided above might help. I posted several answers and linked other answers show how to use it.

enter image description here

Extra Section

Layouts, Scopes, and Constraining Siblings

You can use layout in similar way Box, Row, Column does with a scope to pass information from inside to content using an interface, implementation and changing properties of this implementation

interface DimensionScope {
    var size: Size
}


class DimensionScopeImpl(override var size: Size = Size.Zero) : DimensionScope

And implementing DimensionScope and Layout.

@Composable
private fun DimensionLayout(
    modifier: Modifier = Modifier,
    content: @Composable DimensionScope.() -> Unit
) {
    val dimensionScope = remember{DimensionScopeImpl()}

    Layout(
        modifier = modifier,
     // 🔥 since we invoke it here it will have Size.Zero
     // on Composition then will have size value below
        content = { dimensionScope.content() }
    ) { measurables: List<Measurable>, constraints: Constraints ->

        val placeables = measurables.map { measurable: Measurable ->
            measurable.measure(constraints)
        }

        val maxWidth = placeables.maxOf { it.width }
        val maxHeight = placeables.maxOf { it.height }

        dimensionScope.size = Size(maxWidth.toFloat(), maxHeight.toFloat())

        layout(maxWidth, maxHeight) {
            placeables.forEach { placeable: Placeable ->
                placeable.placeRelative(0, 0)
            }
        }
    }
}

Since we invoke before being able to measure, and with Layout we can only measure once, we won’t be able to pass correct Size to DimensionScopeImpl on first composition as i mentioned above. On next recompositions since we remember DimensionScopeImpl we get the correct size and Text size is correctly set and we see Text with border.

Column(modifier = Modifier.fillMaxSize().padding(20.dp)) {
    val density = LocalDensity.current

    var counter by remember { mutableStateOf(0) }

    DimensionLayout {
        Box(
            modifier = Modifier
                .size(200.dp)
                .background(Color.Red)
        )
        val dpSize = density.run { size.toDpSize() }

        Text(
            text = "counter: $counter", modifier = Modifier
                .size(dpSize)
                .border(3.dp, Color.Green)
        )
    }

    Button(onClick = { counter++ }) {
        Text("Counter")
    }

}

enter image description here

We are not able to get correct size because we needed to invoke dimensionScope.content() before measuring but in some cases you might be able to get Constraints, size or parameters from parent or your calculation. When that’s the case you can pass you Size. I made an image that passes drawing area based on ContentScale as you can see here using scope.

Selectively measuring to match one Sibling to Another

Not being able to pass using Layout doesn’t mean we can’t set other sibling to same size and use its dimensions if needed.

For demonstration we will change dimensions of second Composable to firs one’s

@Composable
private fun MatchDimensionsLayout(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ) { measurables: List<Measurable>, constraints: Constraints ->
        // For demonstration we will change dimensions of second Composable to firs ones
        require(measurables.size == 2)
        val firstMeasurable = measurables.first()
        val secondMeasurable = measurables.last()

        val firsPlaceable = firstMeasurable.measure(constraints)
        // Measure with first one's width and height
        val secondPlaceable =
            secondMeasurable.measure(Constraints.fixed(firsPlaceable.width, firsPlaceable.height))

        // Set width and height of this Composable width of first one, height total of first
        // and second

        val containerWidth = firsPlaceable.width
        val containerHeight = firsPlaceable.height + secondPlaceable.height

        layout(containerWidth, containerHeight) {
            firsPlaceable.placeRelative(0,0)
            val y = firsPlaceable.height
            secondPlaceable.placeRelative(0,y)
        }
    }
}

Demonstration

MatchDimensionsLayout {
    BoxWithConstraints {
        Text(
            modifier = Modifier
                .size(200.dp)
                .border(2.dp, Color.Red),
            text = "Constraints: ${constraints.minWidth}\n" +
                    "max: ${constraints.maxWidth}"
        )
    }
    BoxWithConstraints {
        Text(
            modifier = Modifier
                .size(400.dp)
                .border(2.dp, Color.Red),
            text = "Constraints: ${constraints.minWidth}\n" +
                    "max: ${constraints.maxWidth}"
        )
    }
}

Since we matched size of second one to first one using Constraints.fixed for measuring BoxWithConstraints now returns dimensions of first or main Composable even if we are not able to pass dimensions from Layout as parameters.

You can also use Modifier.layoutId() instead of first or second to select Composable that you need to use as reference for measuring others

enter image description here

Leave a Comment