One Off the Slack: Filling the Viewport

Ilia Voitcekhovskii asked:

I have Column > Box > Column hierarchy. All have fillMaxSize modifier, while box also has verticalScroll. As soon as box has scroll, its child column can no longer be at max size. Is this expected?

@Composable
fun TestComposable() {
    val scrollState = rememberScrollState()
    Column(
        Modifier
            .fillMaxSize()
            .background(Color(0xFFA05151))
    ) {
        Box(
            Modifier
                .fillMaxSize()
                .verticalScroll(scrollState)
                .background(Color(0xFF1178AF))
        ) {
            Column(
                Modifier
                    .fillMaxSize()
                    .background(Color(0xFF48AF11))
            ) {
                Text(text = "hello", color = Color(0xFFFFFFFF))
            }
        }
    }
}

Google’s Adam Powell said, in short, “yes”:

This is expected. The content of a scrolling region has infinite max size, and it is not possible to fill an infinite max size.

Illa was wondering how to achieve the same basic logic as android:fillViewport in the classic View system.

Adam offered:

var constraints by remember { mutableStateOf(Constraints()) }
    Column(
        Modifier
            .fillMaxSize()
            .onMeasureConstraints { constraints = it }
            .verticalScroll(rememberScrollState())
    ) {
        Box(
            Modifier
                .fillMaxWidth()
                .constrainSize { constraints }
                .background(Color.Blue)
        )
        Box(Modifier.fillMaxWidth().height(100.dp).background(Color.Green))
    }
// elsewhere at the top level...
fun Modifier.onMeasureConstraints(
    block: (Constraints) -> Unit
) = layout { measurable, constraints ->
    // record the constraints *before* measuring so that they're available during recursive measurement
    block(constraints)
    val placeable = measurable.measure(constraints)
    layout(placeable.width, placeable.height) {
        placeable.place(0, 0)
    }
}
fun Modifier.constrainSize(
    getConstraints: () -> Constraints
) = layout { measurable, constraints ->
    val placeable = measurable.measure(constraints.constrain(getConstraints()))
    layout(placeable.width, placeable.height) {
        placeable.place(0, 0)
    }
}

Adam elaborated:

then the two modifiers there and the general technique are reusable wherever else you might want to apply it

you could do things like fillMaxHeight(0.9) to make sure that there’s always a bit of scrollable content still visible as opposed to filling the entire viewport, etc.

it’s specifically important that constrainSize defines getConstraints as a function and not a raw value, since invoking that function performs a state read of the previously written constraints, invalidating the measurement of the constrainSize‘d element when the input constraints change, but without invalidating the composition that created it

Illa had questions about that last bit:

I assume that everything that reads from state container will be invalidated when value inside it changes, right? So the difference here is that if i read raw value - then whole scope that uses constraints state needs to be invalidated (which in this example starts with Column) since there no other way to trigger change, but if there is a function inside layout that reads it, then that invalidation scope is just that layout block - so only Box and its children will get invalidated?

Adam responded:

Yes. By using a lambda given to the layout modifier to read the state, you invalidate the layout that calls that lambda function rather than the composition


Read the original thread in the kotlinlang Slack workspace. Not a member? Join that Slack workspace here!