Usecase
デザイン上、1行で表示させたいが、デバイスによっては横幅に収まらない。しかし改行をさせたくないものがあったので作りました。
上の様な感じではなく、下のように表示したい場合に使用します。
両方とも文字サイズ30spを使用し、表示できるデバイスでは30sp、横幅的に表示できないデバイスは表示できるまで縮小します。

Code
@Composable public fun AutoScaling( modifier: Modifier, onlyScaleDown: Boolean = true, content: @Composable () -> Unit ) { val scaleState = remember { mutableStateOf(1.0f) } Box( modifier = modifier .clipToBounds() ) { Layout( modifier = Modifier .graphicsLayer { scaleX = scaleState.value scaleY = scaleState.value }, content = { Box { content() } } ) { measurables, constraints -> val containerWidth = constraints.maxWidth val measureResults = measurables.map { it.measure(constraints.copy(maxWidth = Int.MAX_VALUE)) } val contentWidth = measureResults.sumOf { it.width } val contentHeight = measureResults.sumOf { it.height } val scale = (containerWidth.toFloat() / contentWidth).let { scale -> if (onlyScaleDown) { scale.coerceAtMost(1f) } else { scale } } scaleState.value = scale layout(floor(contentWidth * scale).toInt(), floor(contentHeight * scale).toInt()) { measureResults.onEach { it.place( -ceil((contentWidth - (contentWidth * scale)) / 2).toInt(), -ceil((contentHeight - (contentHeight * scale)) / 2).toInt() ) } } } } }
Usage
Preview Code
@Composable public fun CustomAutoScalingView( textSize: TextUnit, onlyScaleDown: Boolean, ) { Text("onlyScaleDown=$onlyScaleDown, $textSize") AutoScaling( modifier = Modifier .background(Color.Green) .fillMaxWidth(), onlyScaleDown = onlyScaleDown, ) { CustomView(textSize) } } @Composable public fun CustomView( textSize: TextUnit, ) { Row(modifier = Modifier.height(IntrinsicSize.Min)) { Text( modifier = Modifier.background(Color.Yellow), text = "1 2 3 4 5 6 7 8 9", fontSize = textSize ) Icon( modifier = Modifier.fillMaxHeight().aspectRatio(1f), imageVector = Icons.Filled.Menu, contentDescription = null, ) Text( modifier = Modifier.background(Color.Yellow), text = "1 2 3 4 5 6 7 8 9", fontSize = textSize ) } }
Preview

Column(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
) {
CustomView(14.sp)
CustomAutoScalingView(
textSize = 14.sp,
onlyScaleDown = true,
)
CustomAutoScalingView(
textSize = 14.sp,
onlyScaleDown = false,
)
Spacer(modifier = Modifier.fillMaxWidth().height(16.dp).background(Color.Magenta))
CustomView(30.sp)
CustomAutoScalingView(
textSize = 30.sp,
onlyScaleDown = true,
)
CustomAutoScalingView(
textSize = 30.sp,
onlyScaleDown = false,
)
}
Description
clipToBounds はPreviewがうまく行かないので、他のPreviewに影響が出ないように追加しました。実機では無くても問題ないです。
Box(
modifier = modifier
.clipToBounds()
) { /* Layout */ }

Layout
measurePolicyを実行した後にサイズが確定するので、それを元にしてscaleします。
Layout(
modifier = Modifier
.graphicsLayer {
scaleX = scaleState.value
scaleY = scaleState.value
},
content = {
Box {
content()
}
}
) { measurables, constraints -> /* measurePolicy */ }
measurePolicy
maxWidth = Int.MAX_VALUE で横幅の制限を無くした場合のサイズを計算します。
val containerWidth = constraints.maxWidth val measureResults = measurables.map { it.measure(constraints.copy(maxWidth = Int.MAX_VALUE)) }
計算結果を取得します。 content はBoxしか無いので、sumOfじゃなくてfirstでもいいのですが、動作に違いは無いです。
val contentWidth = measureResults.sumOf { it.width } val contentHeight = measureResults.sumOf { it.height }
Scaleを計算します。縮小にしか使用しないつもりですが、拡大も対応するために onlyScaleDown を追加しました。
そしてMutableStateにScaleを入れます。
val scale = (containerWidth.toFloat() / contentWidth).let { scale -> if (onlyScaleDown) { scale.coerceAtMost(1f) } else { scale } } scaleState.value = scale
スケール後のサイズを通知します
layout(floor(contentWidth * scale).toInt(), floor(contentHeight * scale).toInt()) { /* place */ }
place
以下のコードで place(0, 0) だと以下のようにレイアウトされます。
元のサイズの時のスケールされたサイズで中央に設置されます。

なので拡大縮小した差分の半分ずらします。
measureResults.onEach {
it.place(
-ceil((contentWidth - (contentWidth * scale)) / 2).toInt(),
-ceil((contentHeight - (contentHeight * scale)) / 2).toInt()
)
}
問題点
スケールしている為、若干ずれます。背景色を重ねる場合は注意です。
