2

How can I rotate composables and still make them fill their parent?

For example, I have a Column filling the Screen with two Boxes taking up half the size. The size of the boxes seems to be calculated before the rotation and not after.

Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(8.dp),
            verticalArrangement = Arrangement.spacedBy(8.dp)
        ) {
            Box(
                modifier = Modifier
                    .rotate(90f)
                    .fillMaxSize()
                    .weight(1f)
                    .background(Color.Red),
                contentAlignment = Alignment.Center
            ) {
                Text("1", fontSize = 100.sp)
            }
            Box(
                modifier = Modifier
                    .rotate(90f)
                    .fillMaxSize()
                    .weight(1f)
                    .background(Color.Blue),
                contentAlignment = Alignment.Center
            ) {
                Text("2", fontSize = 100.sp)
            }
}

enter image description here

Edit after Thracians comment:

This looks better but the maxHeight I get seems wrong, I can see in the layout inspector that the size of the BoxWithConstraints is right

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(8.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        BoxWithConstraints(modifier = Modifier.fillMaxSize().weight(1f)) {
            Box(
                modifier = Modifier
                    .rotate(90f)
                    .width(maxHeight)
                    .height(maxWidth)
                    .background(Color.Red),
                contentAlignment = Alignment.Center
            ) {
                Text("1", fontSize = 100.sp)
            }
        }
        BoxWithConstraints(modifier = Modifier.fillMaxSize().weight(1f)) {
            Box(
                modifier = Modifier
                    .rotate(90f)
                    .width(maxHeight)
                    .height(maxWidth)
                    .background(Color.Blue),
                contentAlignment = Alignment.Center
            ) {
                Text("2", fontSize = 100.sp)
            }
        }

enter image description here

TeKo
  • 465
  • 1
  • 5
  • 17

1 Answers1

2

Modifier.rotate is

@Stable
fun Modifier.rotate(degrees: Float) =
    if (degrees != 0f) graphicsLayer(rotationZ = degrees) else this

Modifier.graphicsLayer{} does not change dimensions and physical position of a Composable, and doesn't trigger recomposition which is very good for animating or changing visual presentation.

You can also see in my question here even i change scale and position green rectangle, position in parent is not changing.

  • A [Modifier.Element] that makes content draw into a draw layer. The draw layer can be * invalidated separately from parents. A [graphicsLayer] should be used when the content * updates independently from anything above it to minimize the invalidated content. * * [graphicsLayer] can also be used to apply effects to content, such as scaling ([scaleX], [scaleY]), * rotation ([rotationX], [rotationY], [rotationZ]), opacity ([alpha]), shadow

However any Modifier after Modifier.graphicsLayer is applied based on new scale, translation or rotation. Easiest to see is drawing border before and after graphicsLayer.

Column(modifier = Modifier.fillMaxSize()) {
    val context = LocalContext.current
    Row(modifier = Modifier.fillMaxSize()) {
        Box(
            modifier = Modifier
                .size(100.dp, 200.dp)
                .border(2.dp, Color.Red)
                .zIndex(1f)
                .clickable {
                    Toast
                        .makeText(context, "Before Layer", Toast.LENGTH_SHORT)
                        .show()
                }
                .graphicsLayer {
                    translationX = 300f
                    rotationZ = 90f
                }
                .border(2.dp, Color.Green)
                .clickable {
                    Toast
                        .makeText(context, "After Layer", Toast.LENGTH_SHORT)
                        .show()
                }
        )
        Box(
            modifier = Modifier
                .fillMaxWidth()
                .height(200.dp)
                .background(Color.Yellow)
        )
    }
}

If you check this example you will see that even after we rotate Composable on left position of Box with yellow background doesn't change because we don't change actual form of Composable on left side.

enter image description here

@Composable
private fun MyComposable() {
    Column(modifier = Modifier.fillMaxSize()) {

        Spacer(modifier = Modifier.height(300.dp))


            Column(modifier = Modifier.fillMaxSize()) {

                var angle by remember { mutableStateOf(0f) }

                LaunchedEffect(key1 = Unit) {
                    delay(2000)
                    angle = 90f
                }


                Box(
                    modifier = Modifier
                        .fillMaxSize()
                        .weight(1f)
                        .background(Color.Red)
                        .rotate(angle),
                    contentAlignment = Alignment.Center
                ) {
                    Text("1", fontSize = 100.sp)
                }

                Spacer(modifier =Modifier.height(8.dp))

                Box(
                    modifier = Modifier
                        .fillMaxSize()
                        .weight(1f)
                        .background(Color.Blue)
                        .rotate(angle),
                    contentAlignment = Alignment.Center
                ) {
                    Text("2", fontSize = 100.sp)
                }
            }
        }
}

enter image description here

Added Spacer to have uneven width and height for Boxes for demonstration.

As i posted in example gif order of rotate determines what we rotate.

modifier = Modifier .fillMaxSize() .weight(1f) .background(Color.Blue) .rotate(angle)

This sets the size, it never rotates parent but the content or child because we set size and background before rotation. This answer works if width of the child is not greater than height of the parent.

If child has Modifier.fillSize() or child's width is bigger than parent's height when we rotate as in the image left below. So we need to scale it back to parent after rotation since we didn't change parents dimensions.

@Composable
private fun MyComposable2() {
    var angle by remember { mutableStateOf(0f) }

    LaunchedEffect(key1 = Unit) {
        delay(2000)
        angle = 90f
    }

    

    Column(modifier = Modifier.fillMaxSize()) {

        Spacer(modifier = Modifier.height(100.dp))

        BoxWithConstraints(
            modifier = Modifier
                .fillMaxSize()
                .padding(8.dp)
        ) {

            val width = maxWidth
            val height = maxHeight / 2

            val newWidth = if (angle == 0f) width else height
            val newHeight = if (angle == 0f) height else width


            Column(modifier = Modifier.fillMaxSize()) {

                Box(
                    modifier = Modifier
                        .border(3.dp, Color.Red)
                        .size(width, height)
                        .graphicsLayer {
                            rotationZ = angle
                            scaleX = newWidth/width
                            scaleY = newHeight/height
                        }
                        .border(5.dp, Color.Yellow),
                    contentAlignment = Alignment.Center
                ) {

                    Image(
                        modifier = Modifier
                            .border(4.dp, getRandomColor())
                            .fillMaxSize(),
                        painter = painterResource(id = R.drawable.landscape1),
                        contentDescription = "",
                        contentScale = ContentScale.FillBounds
                    )
                }

                Box(
                    modifier = Modifier
                        .border(3.dp, Color.Red)
                        .graphicsLayer {
                            rotationZ = angle
                            scaleX = newWidth/width
                            scaleY = newHeight/height
                        }
                        .size(width, height)
                        .border(5.dp, Color.Yellow),
                    contentAlignment = Alignment.Center
                ) {

                    Image(
                        modifier = Modifier
                            .border(4.dp, getRandomColor())
                            .fillMaxSize(),
                        painter = painterResource(id = R.drawable.landscape1),
                        contentDescription = "",
                        contentScale = ContentScale.FillBounds
                    )
                }

            }
        }
    }
}

on left we don't scale only rotate as in question on right we scale child into parent based on height/width ratio

enter image description here enter image description here

Thracian
  • 43,021
  • 16
  • 133
  • 222
  • The Box is just an example and is going to be a more complex composable later. What I tried is wrapping the Box in a BoxWithConstraints then if it's not rotated I set Modifier.width(maxWidth).height(maxHeight) and when its rotated by 90 I set Modifier.width(maxHeight).height(maxWidth). For some reason, this is not working. It gives me the correct maxWidth (height) but the maxHeight is the same as maxWidth so the inner Box is not filling the BoxWithConstraints. – TeKo Jul 03 '22 at 13:54
  • I tried you code. It works fine, i put Spacer above Column to check with different heights, rotation(90f) initially and rotating with a delay Boxes are placed correctly under each condition. What i suggested in my answer is not required. I meant wrapping Column with BoxWithConstraints not changing `Box` to `BoxWithConstraints`. That way you would be able get max and min dimensions of `Column`. – Thracian Jul 03 '22 at 14:40
  • @TeKo Updated answer, is this correct behavior? – Thracian Jul 03 '22 at 14:50
  • Thanks for the help, moving .rotate to the end helped. But when I then nest a few more Composables I get the same problem. Guess I have to test some more or use something else instead of rotate. – TeKo Jul 03 '22 at 17:54
  • Red border is the actual parent Composable. Yellow borders are after rotating parent. If size doesn't match after rotation even if it's in same bounds, like in first gif with its layer is not same as physical bounds. Because of that you need to scale it back to Composable bounds. – Thracian Jul 03 '22 at 18:08
  • Thanks, that's working. I can probably write a custom layout to be able to use this with different amounts of children and rotations. – TeKo Jul 03 '22 at 20:25
  • After testing some more this is not really working. In your example with borders and the image it looks fine but when using multiple nested Composables the outer size and rotation work fine but everything inside gets scaled with the new dimensions and looks distorted. – TeKo Jul 09 '22 at 18:21
  • Hey. It's not unexpected to scale when nested Composables are available. When we scale we don't keep aspect ratio of content to match it's rotated dimension exactly be same as parents. First method, when your Composable's height is smaller than its width rotation wouldn't need to scale content which more preferable if your requirements match. Another method, i'm unfortunately busy to implement it myself at the moment, is to scale down your content with same aspect ratio then rotate with first method without scaling content to match parents dimensions – Thracian Jul 10 '22 at 12:29
  • What i'm suggesting is more like camera preview selection, if you have any experience with camera, or if you check some preview scaling code to match available content coordinate you will see that camera previews put black rectangles sides or above to keep aspect ratio while matching screen. If this is possible for you get child's aspect ratio, then scale it with parent's small dimension then rotate it. – Thracian Jul 10 '22 at 12:31
  • Thanks for your help but at this point, it's probably easier to just use XML Layouts. I'm not super deep into Compose but I thought rotating the outer layout would happen before the inner layouts, so they should just fill the space and scale properly. I liked compose because it's easy to reuse Composables but for this, I would have to create different versions for the rotation (I guess only two because rotating by 180° works). – TeKo Jul 16 '22 at 18:29