I’m in the following situation: My app has a kind...
# compose
c
I’m in the following situation: My app has a kind of “preview” widget that shows what some piece of UI is going to look like elsewhere (in another app altogether). I show this preview in a couple places, but not always at the same size, in most cases it gets scaled down. So I apply
Modifier.scale
to scale down the drawing, and I have a function which provides a “scaled layout”, which, given a fixed size, gives you a Box with scale applied to that size, in which you can draw your scaled content. More in 🧵
1
This is my scaled layout function
Copy code
@Composable
fun ScaledLayout(
  scale: Float,
  width: Dp,
  height: Dp,
  content: @Composable () -> Unit,
) {
  Box(
    modifier = Modifier
      .size(width = width * scale, height = height * scale)
      .wrapContentSize(unbounded = true),
    contentAlignment = Alignment.Center,
  ) {
    content()
  }
}
This works well when I know the size of the thing to scale in advance, but doesn’t work if the content to be scaled needs to be measured first
So my question is, how can I apply a scale factor to an arbitrary layout, without knowing the size of the children in advance?
Alternately, if there is a different approach I could consider, I’m open to suggestions. For example, if I could draw this to a canvas or something on the fly. It doesn’t need to be interactive. But the user will make changes (for example, there’s a photo in the background, and there’s a way for the user to change the photo. Also they can update some text). So I will update the drawing based on these changes.
I’m not really familiar with custom layouts, but currently I’m looking at
SubcomposeLayout
to see if there’s something I can do there. However this seems like something to use if you want to measure children based on their siblings. But what I want to do is measure a parent based on the (scaled) size of its children.
I figured it out with
SubcomposeLayout
, works well
Copy code
private enum class ScaledSlot { Original, Scaled }

@Composable
fun RowScope.ScaledLayout(
  modifier: Modifier = Modifier,
  content: @Composable () -> Unit,
) {
  SubcomposeLayout(modifier.weight(1f)) { constraints ->
    val placeables = subcompose(ScaledSlot.Original, content).map {
      it.measure(Constraints())
    }
    val maxWidthOfContent = placeables.maxByOrNull { it.measuredWidth }?.measuredWidth ?: 0
    val heightOfContent = placeables.sumOf { it.measuredHeight }
    val scale = constraints.maxWidth.toFloat() / maxWidthOfContent
    val scaledHeight = (heightOfContent * scale).roundToInt()

    val scaledBox = subcompose(ScaledSlot.Scaled) {
      Box(
        modifier = Modifier
          .graphicsLayer(
            scaleX = scale,
            scaleY = scale,
            transformOrigin = TransformOrigin(0.5f, 0f),
          )
          .wrapContentSize(unbounded = true),
      ) {
        content()
      }
    }.first().measure(constraints)

    layout(constraints.maxWidth, scaledHeight) {
      scaledBox.placeRelative(0, 0)
    }
  }
}
Couple notes if anyone is reading this • the method is on
RowScope
because I need to know the direction from which the constraints are coming. I could build a similar thing for
ColumnScope
but I don’t have need of it yet • The
transformOrigin
is important but I am not totally sure I understand how this interacts with
placeRelative
(if at all). If I use
TransformOrigin(0f, 0f)
, the content is drawn outside the bounds of the layout (to the left)
a
This is extremely inefficient as you are composing the content (also remembering all the states and running all the effects) twice. You can just use something like this:
Copy code
Modifier.layout { measurable, constraints ->
    val placeable = measurable.measure(Constraints())
    val scale = constraints.maxWidth.toFloat() / placeable.width
    val scaledHeight = (placeable.height * scale).roundToInt()
    layout(constraints.maxWidth, scaledHeight) {
        placeable.placeRelativeWithLayer(0, 0) {
            scaleX = scale
            scaleY = scale
            transformOrigin = TransformOrigin(0.5f, 0f)
        }
    }
},
c
I’ll try this in the morning. I didn’t know about
placeRelativeWithLayer