androidkotlinuser-interfaceandroid-jetpack-compose

How to scale down the entire UI content to fit in a preview area in Jetpack Compose?


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:

Home screen preview

This is how my theme chooser screen looks:

Theme chooser preview with scale 1f

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.

Theme chooser with scale 0.2f

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?

Edit 1:

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.


Solution

  • 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:

    Screen recording