How to measure composables?

Viewed 1308

I have a composable button, which can display a text or a loader, depending on state

enum class State { NORMAL, LOADING }

@Composable
fun MyButton(onClick: () -> Unit, text: String, state: State) {
    Button(onClick, Modifier.height(60.dp)) {
        if (state == State.NORMAL) {
            Text(text, fontSize = 32.sp)
        } else {
            CircularProgressIndicator(color = Color.Yellow)
        }
    }
}

and I use it like this:

MyButton(
        onClick = {
            state = if (state == State.NORMAL) State.LOADING else State.NORMAL
        },
        "hello",
        state
)

But the button shrinks when going to loading state. How can I measure it in normal state, to assign this measured width in loading state later?

4 Answers

Assuming that the normal state comes before the loading state, you can use the onGloballyPositioned modifier to get the size of Text and apply it to the CircularProgressIndicator.

Something like:

@Composable
fun MyButton(onClick: () -> Unit, text: String, state: State) {
    var sizeText by remember { mutableStateOf(IntSize.Zero) }

    Button(onClick, Modifier.height(60.dp)) {
        if (state == State.NORMAL) {
            Text(text, fontSize = 32.sp,
                modifier = Modifier.onGloballyPositioned {
                    sizeText = it.size
                })
        } else {
            Box(Modifier.size(with(LocalDensity.current){ (sizeText.width).toDp()},with(LocalDensity.current){ (sizeText.height).toDp()}),
                contentAlignment = Alignment.Center
            ) {
                CircularProgressIndicator(color = Color.Yellow)
            }
        }
    }
}

enter image description here

You can use a custom layout - this works both when you start with Loading or Normal:

@Composable
fun CustomContainer(
    modifier: Modifier = Modifier,
    state: State,
    content: @Composable () -> Unit,
) {
    Layout(
        modifier = modifier,
        content = content,
    ) { measurables, constraints ->
        check(measurables.size == 2) { "This composable requires 2 children" }
        val first = measurables[0]
        val second = measurables[1]
        val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)
        val firstPlaceable = first.measure(looseConstraints)
        val secondPlaceable = second.measure(looseConstraints)
        val requiredWidth = max(firstPlaceable.width, secondPlaceable.width)
        val requiredHeight = max(firstPlaceable.height, secondPlaceable.height)
        layout(
            requiredWidth,
            requiredHeight
        ) {
            when (state) {
                State.LOADING -> {
                    firstPlaceable.place(
                        x = (requiredWidth - firstPlaceable.width) / 2,
                        y = (requiredHeight - firstPlaceable.height) / 2,
                    )
                }
                State.NORMAL -> {
                    secondPlaceable.place(
                        x = (requiredWidth - secondPlaceable.width) / 2,
                        y = (requiredHeight - secondPlaceable.height) / 2,
                    )
                }
            }
        }
    }
}

@Preview
@Composable
fun CustomButtonPreview() {
    SampleTheme {
        var state by remember {
            mutableStateOf(State.LOADING)
        }
        Button(
            onClick = {
                state = when (state) {
                    State.NORMAL -> State.LOADING
                    State.LOADING -> State.NORMAL
                }
            },
            Modifier.height(60.dp)
        ) {
            CustomContainer(state = state) {
                CircularProgressIndicator(color = Color.Yellow)
                Text("Text here", fontSize = 32.sp)
            }
        }
    }
}

enter image description here

You could create a variable (nullable) named size. Just wrap your Text in a BoxWithConstraints, then assign the size to the variable. Remeber this value through recompositions, and you should be good.

you could check the width of the button after it's been displayed and save it to use for later, here is a working solution using that way.

@Composable
fun MyButton(onClick: () -> Unit, text: String, state: State) {
    val currentContext= LocalContext.current
    var textWidth by remember{ mutableStateOf(0)}
    val textModifier= if(textWidth ==0)Modifier else Modifier.width(textWidth.dp)
    Button(onClick, textModifier.height(60.dp).onGloballyPositioned {
       textWidth= it.size.width.toDp(currentContext)
    }) {
        if (state == State.NORMAL) {
            Text(text, fontSize = 32.sp)
        } else {
            CircularProgressIndicator(color = Color.Yellow)
        }
    }
}

fun Int.toDp(context: Context): Int = (this / context.resources.displayMetrics.density).toInt()


Related