Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Jetpack Compose: Fixed width and height based on the largest view in a Box

I would like to have a simple composable component that could have 2 states

  1. If State 1 -> Shows a button
  2. If State 2 -> Shows a text

The problem I'm facing is that the button is much larger than the text, so when state changes, this whole composable size changes in size. What I'd like to have is a fixed width and height (based on the button which is larger), so when the text is shown it does not mess up with the width and height. How can I achieve this?

This is the closest I could get but still it's not working perfectly

@Composable
fun MyComposable(
    modifier: Modifier = Modifier,
    state: State,
    button: @Composable () -> Unit,
) {
    BoxWithConstraints(modifier = modifier) {
        val maxWidth = this.maxWidth.value.toInt()
        val maxHeight = this.maxHeight.value.toInt()
        // Invisibly layout button and measure its size
        var contentSize by remember { mutableStateOf(Pair(0,0)) }
        Layout(content = button, modifier = Modifier.alpha(0f)) { measurables, constraints ->
            val placeable = measurables.first().measure(constraints)
            contentSize = Pair(placeable.width, placeable.height)
            layout(0, 0) {}  // No visible layout
        }

        when (state) {
            State1 -> {
                button()
            }

            State2 -> {
                // enforce text view to have maxWidth and maxHeight
                Text(
                    modifier = modifier.requiredSize(
                        width = contentSize.first.coerceIn(0, maxWidth).dp,
                        height = contentSize.second.coerceIn(0, maxHeight).dp
                    ),
                )
            }
        }
    }
}
like image 262
Mehdi Satei Avatar asked Oct 30 '25 01:10

Mehdi Satei


1 Answers

Layout in this case is not required if Button is always bigger than Text and it appears first to get contentSize and. You can get size of Button from Modifier.onSizeChanged/onGloballyPositioned/onPlaced.

You can do it like this

@Composable
fun MyComposable(
    modifier: Modifier = Modifier,
    state: LayoutState,
    button: @Composable () -> Unit,
) {

    val density = LocalDensity.current
    Box(modifier = modifier) {
        var contentSize by remember {
            mutableStateOf(DpSize.Zero)
        }

        when (state) {
            LayoutState.Button -> {
                Box(
                    modifier = Modifier.onSizeChanged { size: IntSize ->
                        contentSize = with(density) {
                            size.toSize().toDpSize()
                        }
                    }
                ){
                    button()
                }
            }

            LayoutState.Text -> {
                // enforce text view to have maxWidth and maxHeight
                Text(
                    modifier = modifier.size(contentSize),
                    text = "Some Text"
                )
            }
        }
    }
}

Extra

When you don't know which Composable can be bigger

If you wish to use a Layout you can have both child composables instead of empty one, also using this method you won't have an extra recomposition to get set size of container to match biggest size for it not to change size when state changes.

https://stackoverflow.com/a/73357119/5457853

With Layout you can get max dimensions and place only one of them depending on state. when you don't know which Composable can be bigger you can use this Layout, also this applicable to this question as well as it is with any Composable.

enum class LayoutState {
    Button, Text
}

And implementation of Layout that measures max dimensions. In this sample i measured Text with its own dimensions but you can measure it with a Constraints.Fixed() Button dimensions if available instead of looseConstraints.

@Composable
private fun MyLayout(
    modifier: Modifier = Modifier,
    layoutState: LayoutState,
    button: @Composable (() -> Unit)? = null,
    text: @Composable (() -> Unit)? = null
) {

    val measurePolicy = remember(layoutState) {

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

                val looseConstraints = constraints.copy(
                    minWidth = 0,
                    minHeight = 0
                )

                val buttonPlaceable = measurables.firstOrNull { it.layoutId == "button" }
                    ?.measure(looseConstraints)

                val buttonWidth = buttonPlaceable?.width ?: 0
                val buttonHeight = buttonPlaceable?.height ?: 0

                val textPlaceable = measurables.firstOrNull { it.layoutId == "text" }
                    ?.measure(looseConstraints)

                val textWidth = textPlaceable?.width ?: 0
                val textHeight = textPlaceable?.height ?: 0


                val layoutWidth = buttonWidth.coerceAtLeast(textWidth)
                val layoutHeight = buttonHeight.coerceAtLeast(textHeight)

                return layout(layoutWidth, layoutHeight) {

                    val placeable = if (layoutState == LayoutState.Button) {
                        buttonPlaceable
                    } else {
                        textPlaceable
                    }

                    placeable?.let {
                        // This is for centering Placeable inside Container
                        val xPos = (layoutWidth - placeable.width) / 2
                        val yPos = (layoutHeight - placeable.height) / 2
                        placeable.placeRelative(xPos, yPos)
                    }
                }
            }
        }
    }
    Layout(
        modifier = modifier,
        content = {
            if (button != null) {
                Box(modifier = Modifier.layoutId("button")) {
                    button()
                }
            }

            if (text != null) {
                Box(modifier = Modifier.layoutId("text")) {
                    text()
                }
            }
        },
        measurePolicy = measurePolicy
    )
}

Usage

@Preview
@Composable
private fun LayoutTest() {

    var layoutState by remember {
        mutableStateOf(LayoutState.Button)
    }

    Column {

        Button(
            onClick = {
                if (layoutState == LayoutState.Button) {
                    layoutState = LayoutState.Text
                } else {
                    layoutState = LayoutState.Button
                }
            }
        ) {
            Text("State: $layoutState")

        }
        MyLayout(
            modifier = Modifier.border(2.dp, Color.Red),
            layoutState = layoutState,
            button = {
                Button(
                    onClick = {

                    }
                ) {
                    Text("Some Button")
                }
            },
            text = {
                Text("Some Text")
            }
        )
        Spacer(modifier = Modifier.height(20.dp))
        MyLayout(
            modifier = Modifier.border(2.dp, Color.Red),
            layoutState = layoutState,
            button = {
                Button(
                    modifier = Modifier.size(200.dp),
                    onClick = {

                    }
                ) {
                    Text("Some Button")
                }
            },
            text = {
                Text("Some Text")
            }
        )

        Spacer(modifier = Modifier.height(20.dp))

        MyLayout(
            modifier = Modifier.border(2.dp, Color.Red),
            layoutState = layoutState,
            button = {
                Button(
                    onClick = {

                    }
                ) {
                    Text("Some Button")
                }
            },
            text = {
                Text(
                    "Some Text",
                    modifier = Modifier.width(160.dp).height(60.dp).background(Color.Red),
                    color = Color.White,
                    textAlign = TextAlign.Center
                )
            }
        )
    }
}

In both cases we measure each Composable with their own size but container is sized as the biggest one. If you wish to remeasure smaller one with contain all of the space you can use SubcomposeLayout

How to adjust size of component to it's child and remain unchanged when it's child size will change? (Jetpack Compose)

like image 176
Thracian Avatar answered Oct 31 '25 17:10

Thracian