DEV Community

Cover image for Exploring the Impact of verticalScroll() on fillMaxHeight() Modifier in Android Compose
Atsuko Fukui
Atsuko Fukui

Posted on

Exploring the Impact of verticalScroll() on fillMaxHeight() Modifier in Android Compose

Encountering a peculiar situation in Android Compose where the Modifier.verticalScroll() seems to ignore Modifier.fillMaxHeight() when applied to the parent composable sparked my curiosity. Let me share my notes on what I discovered while digging into the internal workings.

What Caught My Eye

Consider this scenario: without verticalScroll() on the parent, the child's fillMaxHeight() behaves as expected.

@Composable
fun Sample(modifier: Modifier = Modifier) {
    Column(
        modifier = modifier
    ) {
        Text(
            modifier = Modifier.fillMaxHeight(),
            text = "Without verticalScroll",
        )
    }
}
Enter fullscreen mode Exit fullscreen mode

screen shout without verticalScroll()

However, once you add verticalScroll() to the parent, suddenly fillMaxHeight() is overlooked.

@Composable
fun Sample(modifier: Modifier = Modifier) {
    Column(
        modifier = modifier.verticalScroll(rememberScrollState())
    ) {
        Text(
            modifier = Modifier.fillMaxHeight(),
            text = "With verticalScroll",
        )
    }
}
Enter fullscreen mode Exit fullscreen mode

screen shout with verticalScroll()

I'm eager to understand the specifics of what verticalScroll() is doing to cause this shift in behavior.

Insight from the Official Documentation

The documentation sheds some light: "Modify element to allow to scroll vertically when height of the content is bigger than max constraints allow."
This suggests that if the content's size relies on the parent's size, determining whether to scroll or not may become challenging, leading to the observed behavior of being ignored.

Peeking into the Internal Mechanism

Let's take a closer look at what's happening inside Modifier.verticalScroll(). Skipping over some details, the crucial point is the utilization of ScrollingLayoutModifier.

fun Modifier.verticalScroll(
    state: ScrollState,
    enabled: Boolean = true,
    flingBehavior: FlingBehavior? = null,
    reverseScrolling: Boolean = false
) = scroll(
    state = state,
    isScrollable = enabled,
    reverseScrolling = reverseScrolling,
    flingBehavior = flingBehavior,
    isVertical = true
)
Enter fullscreen mode Exit fullscreen mode
@OptIn(ExperimentalFoundationApi::class)
private fun Modifier.scroll(
    state: ScrollState,
    reverseScrolling: Boolean,
    flingBehavior: FlingBehavior?,
    isScrollable: Boolean,
    isVertical: Boolean
) = composed(
    factory = {


        val layout =
            ScrollingLayoutModifier(state, reverseScrolling, isVertical)
        semantics
            .clipScrollableContainer(orientation)
            .overscroll(overscrollEffect)
            .then(scrolling)
            .then(layout)
    },
Enter fullscreen mode Exit fullscreen mode

The ScrollingLayoutModifier() seems to be the culprit as it's overwriting the child's constraints, setting maxHeight to Infinity, regardless of the original setting.

private data class ScrollingLayoutModifier(
    val scrollerState: ScrollState,
    val isReversed: Boolean,
    val isVertical: Boolean
) : LayoutModifier {
    override fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints
    ): MeasureResult {
        checkScrollableContainerConstraints(
            constraints,
            if (isVertical) Orientation.Vertical else Orientation.Horizontal
        )

        val childConstraints = constraints.copy(
            maxHeight = if (isVertical) Constraints.Infinity else constraints.maxHeight,
            maxWidth = if (isVertical) constraints.maxWidth else Constraints.Infinity
        )
        val placeable = measurable.measure(childConstraints)
        val width = placeable.width.coerceAtMost(constraints.maxWidth)
        val height = placeable.height.coerceAtMost(constraints.maxHeight)



        return layout(width, height) {
            val scroll = scrollerState.value.coerceIn(0, side)
            val absScroll = if (isReversed) scroll - side else -scroll
            val xOffset = if (isVertical) 0 else absScroll
            val yOffset = if (isVertical) absScroll else 0
            placeable.placeRelativeWithLayer(xOffset, yOffset)
        }
    }
Enter fullscreen mode Exit fullscreen mode

This means that, with constraints' maxHeight set to Constraints.Infinity, the child's height becomes unbounded, effectively immune to the influence of the parent. Consequently, it adopts the height of the Text portion within the child.

For more insights into Compose's Layout and Constraints, you might find this reference useful.

Top comments (0)