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() ) }
問題点
スケールしている為、若干ずれます。背景色を重ねる場合は注意です。