2

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

  • Column
    • Row
      • Box 1f weight
      • Box 1f weight
    • Rest of the UI

Now I have to move the content of the row outside

  • Box
    • Column
      • Empty Box 1f weight
    • Rest of the UI
    • Box left preview
    • Box right preview

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.

3
  • What happens when you draw the preview in its full size taking up the entire screen and then scale it down?
    – Leviathan
    Commented Jul 8 at 7:30
  • take a look at BoxWithConstraints layout same as box with the ability to set min and max witdth and height: foso.github.io/Jetpack-Compose-Playground/foundation/layout/…
    – Hezy Ziv
    Commented Jul 8 at 8:53
  • @Leviathan Just tried this solution. It works but it makes it hard to make the UI responsible. My current UI looks something like this - Column - Row - Box 1f weight - Box 1f weight - Rest of the UI Now I have to move the content of the row outside - Box - Column - Empty Box 1f weight - Rest of the UI - Box left preview - Box right preview 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. When I test this on multiple screen sizes, alignments are a bit off based on the device. Commented Jul 8 at 16:35

1 Answer 1

2
+50

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

1
  • This is exactly what I was looking for. Thanks 😊 Commented Jul 13 at 17:57

Not the answer you're looking for? Browse other questions tagged or ask your own question.