raenardev

    raenardev

    1 year ago
    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? Feels like a bug to me. Screenshots and code in thread
    @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))
                }
            }
        }
    }
    with
    verticalScroll
    without scroll
    Adam Powell

    Adam Powell

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

    raenardev

    1 year ago
    Thanks! What is recommended way to ensure child has minimum height of its container? For scroll views we had
    android:fillViewport
    Here, i could use
    BoxWithConstraints
    , but maybe there is another way?
    @Composable
    fun TestComposable() {
        val scrollState = rememberScrollState()
        Column(
            Modifier
                .fillMaxSize()
                .background(Color(0xFFA05151))
        ) {
            BoxWithConstraints {
                val boxMaxHeight = maxHeight
                Box(
                    Modifier
                        .fillMaxSize()
                        .verticalScroll(scrollState)
                        .background(Color(0xFF1178AF))
                ) {
                    Column(
                        Modifier
                            .defaultMinSize(minHeight = boxMaxHeight)
                            .fillMaxWidth()
                            .background(Color(0xFF48AF11))
                    ) {
                        Text(text = "hello", color = Color(0xFFFFFFFF))
                    }
                }
            }
        }
    }
    Adam Powell

    Adam Powell

    1 year ago
    BoxWithConstraints
    is very expensive for this sort of use case, all you're looking to do is propagate the viewport constraints to a lower child during measurement, there's no need to return to composition or defer composition for that, which
    BoxWithConstraints
    will do.
    Something like this should do the trick:
    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)
        }
    }
    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
    raenardev

    raenardev

    1 year ago
    Thanks for such detailed answer! 🙂 Can you elaborate on last bit? Im not sure i understand it. 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 Powell

    Adam Powell

    1 year ago
    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
    raenardev

    raenardev

    1 year ago
    I see, will try to keep that in mind. Thanks for explanation 🙂