I'm implementing a theme chooser screen for my app. The idea is to provide various themes that the user can choose from and show previews of two screens so that the user can see how the theme will look after selecting it.
I display this home screen as a preview:
This is how my theme chooser screen looks:
I want to scale down the content of the home screen to display the entire home screen content within the available area of the theme chooser preview.
I've tried using Modifier.scale(0.4f)
and Modifier.graphicsLayer(scaleX = 0.4f, scaleY = 0.4f)
, but this only scales down what's already visible.
One idea that seems to work is using a local provider to provide a scale factor to the Compose UI like this:
val LocalScaleFactor = compositionLocalOf { 1f }
@Composable
fun ScaleFactorProvider(
scaleFactor: Float = 1f,
content: @Composable () -> Unit,
) {
CompositionLocalProvider(
LocalScaleFactor provides scaleFactor,
content = content
)
}
val Int.scaleDp: Dp
@Composable
get() = (this * localScaleFactor).dp
val Int.scaleSp: TextUnit
@Composable
get() = (this * localScaleFactor).sp
val localScaleFactor: Float
@Composable get() = LocalScaleFactor.current
ScaleFactorProvider(0.4f) {
HomeScreenPreview()
}
While this method works, it requires extensive code changes. I have to use scaleDp
and scaleSP
everywhere, and for some views like Icon
, which I don't usually provide any size and just use the default size, I now have to define the size. This is quite inconvenient.
Is there an alternative way to scale down the dimensions of the entire UI tree to fit the available parent size in Jetpack Compose?
I've tried what @Leviathan suggested in the comment. It works but it makes it hard to make the UI responsible. My current UI looks something like this
Now I have to move the content of the row outside
After scaling down, both previews will be at the centre and I have to provide x and y offsets to place where I want them to be.
The issue with this method is that since the preview box is bound to the parent, the alignments could be off based on the user device dimension.
Try providing a custom Density
to your composable.
Density
implementation:
private data class MyDensity(override val density: Float, override val fontScale: Float) : Density
Composable:
val scale = 0.4f
val curDensity = LocalDensity.current
val myDensity = MyDensity(curDensity.density * scale, curDensity.fontScale)
CompositionLocalProvider(LocalDensity provides myDensity) {
//Content
}
Edit:
An example:
private data class MyDensity(override val density: Float, override val fontScale: Float) : Density
@Composable
fun ScalableScreen() {
var scale by remember { mutableFloatStateOf(1f) }
fun doScale(value: Float) {
scale *= value
}
Column(
horizontalAlignment = Alignment.CenterHorizontally,
) {
Row {
Button(onClick = { doScale(0.9f) }) { Text("-") }
Button(onClick = { doScale(1.1f) }) { Text("+") }
}
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize(scale),
) {
val curDensity = LocalDensity.current
val myDensity = MyDensity(curDensity.density * scale, curDensity.fontScale)
CompositionLocalProvider(LocalDensity provides myDensity) {
Content(Color.DarkGray, Color.LightGray)
}
}
}
}
@Composable
private fun Content(color1: Color, color2: Color, ) {
Column(
modifier = Modifier.background(color1).padding(8.dp)
) {
Row {
repeat(2) {
Box(
modifier = Modifier.height(100.dp).weight(1f).padding(8.dp).background(color2)
) {
Button(onClick = {}) { Text(text = "Button $it") }
}
}
}
Text(
text = "Lorem ipsum dolor sit amet. ".repeat(20),
modifier = Modifier.background(color2)
)
}
}
Screen recording: