Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Modifier.wrapContentWidth() vs Modifier.width(IntrinsicSize.Max) in Android Jetpack Compose

Let's say I want to create a Column which is as wide as the widest child Text inside it. For this purpose the column can use .wrapContentWidth() modifier or .width(IntrinsicSize.Max), but the result looks the same. What is the difference between these two modifiers? For example:

Column(
    modifier = Modifier.wrapContentWidth()
//  modifier = Modifier.width(IntrinsicSize.Max)
) {
    Text("short text", Modifier.background(Color.LightGray))
    Text("some longer text", Modifier.background(Color.LightGray))
}

enter image description here enter image description here

like image 242
Valeriy Katkov Avatar asked Dec 31 '25 06:12

Valeriy Katkov


2 Answers

Let's add a Divider between the texts. We want the divider to be as wide as the widest text.

Column(
    modifier = Modifier.wrapContentWidth()
//  modifier = Modifier.width(IntrinsicSize.Max)
) {
    Text("short text", Modifier.background(Color.LightGray))
    Divider(color = Color.Red)
    Text("some longer text", Modifier.background(Color.LightGray))
}

enter image description here enter image description here

As you see, in case of Modifier.wrapContentWidth() the divider forces the column to be as wide as its parent. It happens because Divider uses .fillMaxWidth() under the hood and since the column's .wrapContentWidth() doesn't constraint its children width, the divider expands as much as it can.

In the same time Modifier.width(IntrinsicSize.Max) behaves as we'd expected. The divider doesn't occupy space if no constraints are given, so it's intrinsic width is 0 and it doesn't affect the column width.

like image 70
Valeriy Katkov Avatar answered Jan 02 '26 20:01

Valeriy Katkov


When we assign any size Modifier, we don't actually set size, we set Constraints that Composables are to be measured with.

Let's say we assigned Modifier.fillmaxSize(). Constraints is set to minWidth=MaxWidth and minHeight=MaxHeight and equal to width and height coming from parent. In that case Composable can only have width and height of parent.

If you assign Modifier.widthIn(min=100.dp, max=200.dp) Composable can be measured between 100.dp-200.dp.

It's done with any size, padding, aspectRatio, layout, wrapX modifiers basically as

 LayoutModifierNode, Modifier.Node() {
    override fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints
    ): MeasureResult {
        // Some logic here to set min/max of wrappedConstraints
        val placeable = measurable.measure(wrappedConstraints)
        return layout(placeable.width, placeable.height) {
            placeable.placeRelative(0, 0)
        }
    }
}

And the thing with default size Modifiers we can only shorten the interval Constraints min and max values are set to. You can check Constraints section of this answer to see what Constraints each size Modifier returns. Basically we can't widen range. Let's say we assigned Modifier.size(100.dp).size(200.dp) Composable gets measured with 100.dp and gets 100.dp size. Same goes for Modifier.size(200.dp).size(100.dp) as well. You limit range to min=200.dp, max=200.dp and because of that size can't be 100.dp

If we don't assign any size Modifier you can measure a Composable between 0 and parent size which means it gets is content dimension or wrap_content for xml is the default one. It means that assigning no size Modifiers corresponds to wrap_content in xml. This only works if parent doesn't have any limiting size Modifiers as well.

Modifier.wrapContent

However, as i mentioned above if parent Constraints or Constraints from previous Modifier is set a range bigger or smaller than your Composable you can use Modifier.wrapContentSize(unBounded) in 2 ways

@Preview
@Composable
fun WrapCOntentTest() {
    Text(
        modifier = Modifier
            .background(Color.Yellow)
            .fillMaxSize()
            .wrapContentSize(
                align = Alignment.Center,
                unbounded = false
            )
            .border(2.dp, Color.Blue),
        text = "Hello World",
        fontSize = 26.sp
    )
}

Results

enter image description here

Background modifier gets size of parent since we passed min and max dimensions which both are same so yellow background is measured and set on parent size. However, using wrapContentSize we let text to measured between 0 and max from fillMaxSize. Since range is bigger now it's measured with its own size. Check blue border.

If we were to do this with Modifier.layout which also Modifier.wrapContent does, it would be as

@Preview
@Composable
fun LayoutTest() {
    Text(
        modifier = Modifier
            .background(Color.Yellow)
            .fillMaxSize()
            .layout { measurable, constraints ->
                val wrappedConstraints = constraints.copy(
                    minWidth = 0,
                    minHeight = 0
                )

                val placeable = measurable.measure(wrappedConstraints)

                layout(placeable.width, placeable.height){
                    placeable.placeRelative(0,0)
                }
            }
            .border(2.dp, Color.Blue),
        text = "Hello World",
        fontSize = 26.sp
    )
}

We set minWidth and height to 0. We can also use alignment to change placement position in wrapContent modifier.

Also it's useful with Composables like Surface which forces its Constraints to first direct descendant.

@OptIn(ExperimentalMaterialApi::class)
@Composable
private fun WrapWidthInsideSurfaceSample() {
    Row(
        modifier = Modifier.fillMaxWidth(),
        horizontalArrangement = Arrangement.SpaceEvenly
    ) {
        Surface(modifier = Modifier
            .size(100.dp)
            .border(2.dp, Color.Yellow), onClick = {}) {
            Column(
                modifier = Modifier
                    .size(50.dp)
                    .background(Color.Red, RoundedCornerShape(6.dp))
            ) {
                Box(
                    modifier = Modifier
                        .size(50.dp)
                        .background(Color.Green, RoundedCornerShape(6.dp))
                )

            }
        }
        Surface(modifier = Modifier
            .size(100.dp)
            .border(2.dp, Color.Yellow), onClick = {}) {
            Column(
                modifier = Modifier
                    .wrapContentWidth(Alignment.End)
                    .background(Color.Red, RoundedCornerShape(6.dp))
            ) {
                Box(
                    modifier = Modifier
                        .size(50.dp)
                        .background(Color.Green, RoundedCornerShape(6.dp))
                )

            }
        }


        Surface(modifier = Modifier
            .size(100.dp)
            .border(2.dp, Color.Yellow), onClick = {}) {
            Column(
                modifier = Modifier
                    .wrapContentHeight(Alignment.Top)
                    .background(Color.Red, RoundedCornerShape(6.dp))
            ) {
                Box(
                    modifier = Modifier
                        .size(50.dp)
                        .background(Color.Green, RoundedCornerShape(6.dp))
                )

            }
        }
    }
}

enter image description here

In examples above Surface with 100.dp size forces Column, direct descendant, to have 100.dp size as well. using wrapContentWidth or height it's possible to change width or heigh constraints to 0. Red sections mean that they are still measured with Surface's dimensions.

Second use is to unbound max dimensions instead of min ones like above.

Let's say you have 100.dp size from parent or Modifier but you want your Image Composable to be 150.dp then you set unBounded = true which sets maxWidth/Height to Cnstraints.Infinity and your Image can be measured between 0 and Constraints.Infinity, so it can be 150.dp in size.

First one is unbounded = false, second one unbounded = true

enter image description here

Column(
    modifier = Modifier.fillMaxSize(),
    verticalArrangement = Arrangement.Center,
    horizontalAlignment = Alignment.CenterHorizontally
) {
    Box(
        modifier = Modifier
            .size(100.dp)
            .border(2.dp, Color.Green)
    ) {
        Image(
            modifier = Modifier
                .wrapContentSize(unbounded = false)
                .size(150.dp),
            painter = painterResource(id = R.drawable.landscape6),
            contentScale = ContentScale.FillBounds,
            contentDescription = null
        )
    }

    Spacer(modifier = Modifier.height(100.dp))
    Box(
        modifier = Modifier
            .size(100.dp)
            .border(2.dp, Color.Green)
    ) {
        Image(
            modifier = Modifier
                .wrapContentSize(unbounded = true)
                .size(150.dp),
            painter = painterResource(id = R.drawable.landscape6),
            contentScale = ContentScale.FillBounds,
            contentDescription = null
        )
    }
}

Other modifiers that can widen Constraints range are requiredX modifiers. If you don't know you content dimensions you use wrapContent, if you know which size to assign you can use required ones.

Intrinsic Sizes

Intrinsic size Modifiers are defined like this

private class IntrinsicHeightNode(
    var height: IntrinsicSize,
    override var enforceIncoming: Boolean
) : IntrinsicSizeModifier() {
    override fun MeasureScope.calculateContentConstraints(
        measurable: Measurable,
        constraints: Constraints
    ): Constraints {
        var measuredHeight = if (height == IntrinsicSize.Min) {
            measurable.minIntrinsicHeight(constraints.maxWidth)
        } else {
            measurable.maxIntrinsicHeight(constraints.maxWidth)
        }
        if (measuredHeight < 0) { measuredHeight = 0 }
        return Constraints.fixedHeight(measuredHeight)
    }

    override fun IntrinsicMeasureScope.minIntrinsicHeight(
        measurable: IntrinsicMeasurable,
        width: Int
    ) = if (height == IntrinsicSize.Min) measurable.minIntrinsicHeight(width) else
        measurable.maxIntrinsicHeight(width)

    override fun IntrinsicMeasureScope.maxIntrinsicHeight(
        measurable: IntrinsicMeasurable,
        width: Int
    ) = if (height == IntrinsicSize.Min) measurable.minIntrinsicHeight(width) else
        measurable.maxIntrinsicHeight(width)
}

When you assign them to a Column or Row, the calculations are called, they are actual calculation functions of Row and Column.

   override fun IntrinsicMeasureScope.minIntrinsicWidth(
        measurables: List<IntrinsicMeasurable>,
        height: Int
    ) = IntrinsicMeasureBlocks.VerticalMinWidth(
        measurables,
        height,
        verticalArrangement.spacing.roundToPx(),
    )

    override fun IntrinsicMeasureScope.minIntrinsicHeight(
        measurables: List<IntrinsicMeasurable>,
        width: Int
    ) = IntrinsicMeasureBlocks.VerticalMinHeight(
        measurables,
        width,
        verticalArrangement.spacing.roundToPx(),
    )

    override fun IntrinsicMeasureScope.maxIntrinsicWidth(
        measurables: List<IntrinsicMeasurable>,
        height: Int
    ) = IntrinsicMeasureBlocks.VerticalMaxWidth(
        measurables,
        height,
        verticalArrangement.spacing.roundToPx(),
    )

    override fun IntrinsicMeasureScope.maxIntrinsicHeight(
        measurables: List<IntrinsicMeasurable>,
        width: Int
    ) = IntrinsicMeasureBlocks.VerticalMaxHeight(
        measurables,
        width,
        verticalArrangement.spacing.roundToPx(),
    )

Intsinsic sizes do 2 passes of measure and layout traversing every children and get correct size based on their intrinsic size calculations in descendants.

enter image description here

@Preview
@Composable
fun IntrinsicSizeTest() {
    Row(
        modifier = Modifier
            .height(IntrinsicSize.Max)
            .fillMaxWidth().border(2.dp, Color.Black),
        horizontalArrangement = Arrangement.SpaceEvenly
    ) {

        Box(
            modifier = Modifier.width(100.dp).height(200.dp).background(Color.Red)
        )

        Box(
            modifier = Modifier.width(100.dp).fillMaxHeight().background(Color.Green)
        )

        Box(
            modifier = Modifier.width(100.dp).height(150.dp).background(Color.Yellow)
        )
    }

In this example Green gets height from intrinsic calculation of Boxes because parent is set with Intrinsic height. If you remove height(IntrinsicSize.Max) green box can be as big as screen since it calls fillMaxHeight but by limiting parent height to children the biggest it can get is parent with intrinsic height.

Using intrinsic size functions you can define how Modifier.Intrinsic sizes will react in your Layout. While size Modifiers act same for whether its a default or custom Layout but you can write a logic for intrinsic sizes if you wish to write a custom Layout.

When you assign measurement and placement twice, and it's based on how size Modifiers or layouts like Column, Row set intrinsic size logic as above. But by default it works like this, first time they call it with 0-Constraints.Infinity, then after getting correct dimension, they set their own size with this so it can be min or max of children.

When you write you custom Layouts you can also set this instead of default logic.

For instance, in this custom Layout i set them fixed for demonstration, by default there is a calculation done in Row and Column which are the ones above.

@Composable
fun CustomColumnWithIntrinsicDimensions2(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    val measurePolicy = object : MeasurePolicy {

        override fun MeasureScope.measure(
            measurables: List<Measurable>,
            constraints: Constraints
        ): MeasureResult {

            val looseConstraints = constraints.copy(minHeight = 0)
            val placeables = measurables.map { measurable ->
                measurable.measure(looseConstraints)
            }

            var yPosition = 0

            val totalHeight: Int = placeables.sumOf {
                it.height
            }

            // 🔥 This can be sum or longest of Composable widths, or maxWidth of Constraints
            val maxWidth: Int = placeables.maxOf {
                it.width
            }

            return layout(maxWidth, totalHeight) {
                placeables.forEach { placeable ->
                    placeable.placeRelative(x = 0, y = yPosition)
                    yPosition += placeable.height
                }
            }
        }

        override fun IntrinsicMeasureScope.minIntrinsicHeight(
            measurables: List<IntrinsicMeasurable>,
            width: Int
        ): Int {

            println("🚙 minIntrinsicHeight() width: $width, measurables: ${measurables.size}")
            // 🔥 This is just sample to show usage of minIntrinsicHeight, don't set
            // static values
            return 80
        }

        override fun IntrinsicMeasureScope.maxIntrinsicHeight(
            measurables: List<IntrinsicMeasurable>,
            width: Int
        ): Int {

            println("🚗 maxIntrinsicHeight() width: $width, measurables: ${measurables.size}")

            // 🔥 This is just sample to show usage of maxIntrinsicHeight, don't set
            // static values
            return 500
        }
    }

    Layout(modifier = modifier, content = content, measurePolicy = measurePolicy)
}

And you can see that where intrinsic size comes for this layout with

   CustomColumnWithIntrinsicDimensions2(
                modifier = Modifier
                    // This effects height of this composable
                    // Parent height comes from the one in Layout by comparing
                    // it with other Composable's intrinsic height
                    // Even without this parent will have same height
                    .height(IntrinsicSize.Min)
                    .width(100.dp)
                    .background(Yellow400)
                    .padding(4.dp)
            ) {
                Text(
                    "First Text",
                    modifier = Modifier
                        .background(Color(0xffF44336)),
                    color = Color.White
                )
            }
        }

You can check more samples and examples in this tutorial about Layouts, Constraints, and Modifiers as well.

https://github.com/SmartToolFactory/Jetpack-Compose-Tutorials/tree/master/Tutorial1-1Basics/src/main/java/com/smarttoolfactory/tutorial1_1basics/chapter3_layout

like image 32
Thracian Avatar answered Jan 02 '26 19:01

Thracian