Modifier.verticalScroll() を親に設定すると子のfillMaxHeight() が無視される仕組みを調べた
Android ComposeのModifier.verticalScroll()
が親のcomposableに設定されていた場合に、子に Modifier.fillMaxHeight()
を設定してもこれが無視される現象に遭遇した。どういった仕組みでそうなっているのか内部実装を読んだ時のメモを残す。
知りたいこと
例えば、以下のように親に verticalScroll()
がついていないと子のfillMaxHeight()
は期待通りに動作する。
@Composable fun Sample(modifier: Modifier = Modifier) { Column( modifier = modifier ) { Text( modifier = Modifier.fillMaxHeight(), text = "Without verticalScroll", ) } }
しかし親に verticalScroll()
をつけると挙動が変わりfillMaxHeight()
が無視される。
@Composable fun Sample(modifier: Modifier = Modifier) { Column( modifier = modifier.verticalScroll(rememberScrollState()) ) { Text( modifier = Modifier.fillMaxHeight(), text = "With verticalScroll", ) } }
具体的にverticalScroll()
が何をしてこの差が生まれるのか知りたい。
公式ドキュメント
Modify element to allow to scroll vertically when height of the content is bigger than max constraints allow.
とあるので、コンテンツのサイズが親のサイズに依存しているとscrollするかどうかの計算ができないだろうから無視されるんだろうなーという空気を感じる。
内部実装
Modifier.verticalScroll()
が中で何をやっているかを読んでいく。
色々省略するが、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 )
@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) },
で、このScrollingLayoutModifier()
が子のconstraints
を上書いてしまっていて、maxHeight
を設定していたとしてもInfinity
にしてしまっている。
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) } }
constraintsのmaxHeightがConstraints.Infinity
に設定されてしまったせいで子の高さはUnboundedになるので親の影響を受けない。よって子であるText分の高さになる。
ComposeのLayoutやらConstraintについてはこちらが参考になる。