Search code examples
androidandroid-jetpack-composeandroid-jetpackaspect-ratio

Why Does Modifier Order Affect the Positioning and Sizing of aspectRatio in Jetpack Compose?


Actually i am reading this answer , but there is not enough information about aspect ratio .

RectangleScreen :

@Composable
fun RectangleScreen(modifier: Modifier) {
    Canvas(modifier.border(7.dp , color = Color.Red)) {
        drawRect(
            Color.Black
        )
    }
}

Code 1 :

setContent {
    RectangleScreen(modifier = Modifier
        .aspectRatio(1.0f)
        .border(7.dp , Color.Magenta)
        .fillMaxSize()
        .border(7.dp , Color.Yellow )
        .padding(64.dp)
        .border(7.dp , Color.Blue)
    )
}

In this case, the RectangleScreen appears at the top of the screen with a square shape.

Code 2 :

setContent {
    RectangleScreen(modifier = Modifier
        .fillMaxSize()
        .border(7.dp , Color.Magenta)
        .aspectRatio(1.0f)
        .border(7.dp , Color.Yellow )
        .padding(64.dp)
        .border(7.dp , Color.Blue)
    )
}

In this case, the RectangleScreen also has a square shape, but it is positioned in the center of the screen.

My Understanding

code 1 :

as height and width are not specify before aspectRatio(1f) and by default matchHeightConstraintsFirst: Boolean = false , so it again attempt to size content by incoming constraint by first check Constraint.MaxWidth then Constraint.MaxHeight and then for minWidth then minHeight so none of constraints are valid so at ends so the constraints will not be respected and the content will be sized such that the Constraints. maxWidth or Constraints. maxHeight is matched (depending on matchHeightConstraintsFirst). Why i get square rectangle on top ?

code 2 :

as height and width are specify before aspectRatio(1f) and by default matchHeightConstraintsFirst: Boolean = false , so it attempt to size content to match specified aspect ratio as 1f , by trying to match one of the incoming constraints so the first constraint is Constraint.MaxWidth which let say is 1080.dp so to maintain aspect ratio like height : width equal to 1:1 so our height will be 1080.dp so we get square shape . why position of square is in middle of screen

My Questions: Why does the square shape appear at the top of the screen in Code 1? Why does the square appear in the center of the screen in Code 2? How are the size and position calculated in both cases?


Solution

  • To understand why it happens you need to understand how LayoutModifiers and Constraints work. You can check this answer for more details.

    Size, aspectRatio, padding modifiers, LayoutModifier, get suitable Constraints and measure with Measurable, get a Placeable and place it accordingly. Basically all of them work the same way. Snippet below belongs to aspectRatio modifier.

    override fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints
    ): MeasureResult {
        val size = constraints.findSize()
        val wrappedConstraints = if (size != IntSize.Zero) {
            Constraints.fixed(size.width, size.height)
        } else {
            constraints
        }
        val placeable = measurable.measure(wrappedConstraints)
        return layout(placeable.width, placeable.height) {
            placeable.placeRelative(0, 0)
        }
    }
    

    After aspectRatio gets correct width and height it sets layout(placeable.width, placeable.height) which is width and height of Composable. If this width or height doesn't abide parent Constraints next Modifier is centered in previous or parent size Modifier Constraints.

    Modifier.fillMaxSize() returns minWidth=maxWidth and minHeight=maxHeight which are from parent or previous Constraints depending on your Layout or applied Modifiers before that.

    if you set width and height to another value that is not in range of min..max next Modifier is centered. I mean any by next Modifier, border, background, gesture, or size Modifiers.

    If you check the link i explain it for requiredSize but under the hood, requiredSize, layout or aspectRatio can do same thing layout sizes do not match constraints from parent.

    @Preview
    @Composable
    fun RequiredSizeTest() {
        RectangleScreen(
            modifier = Modifier
                .border(7.dp, Color.Red)
                .fillMaxSize()
                .border(7.dp, Color.Magenta)
                .requiredHeight(400.dp)
                .border(7.dp, Color.Yellow)
                .padding(64.dp)
                .border(7.dp, Color.Blue)
        )
    }
    

    Results

    enter image description here

    You can do the same thing with Modifier.layout as well, and example below is how aspectRatio does after getting with and height

    @Preview
    @Composable
    fun LayoutTest() {
        RectangleScreen(modifier = Modifier
            .border(7.dp, Color.Red)
            .fillMaxSize()
            .border(7.dp, Color.Magenta)
            .layout { measurable, constraints ->
    
                val width = constraints.maxWidth
                val height = width
                val wrappedConstraints = Constraints.fixed(width, height)
                val placeable = measurable.measure(wrappedConstraints)
    
                layout(placeable.width, placeable.height) {
                    placeable.placeRelative(0, 0)
                }
    
            }
            .border(7.dp, Color.Yellow)
            .padding(64.dp)
            .border(7.dp, Color.Blue)
        )
    }
    

    enter image description here

    You can ask why it happens with Modifier.fillmaxSize/Width/Height and the answer is, it limits min and max value to same value, let's say minHeight=maxHeight is 2000px while width is 1000px and aspect ratio with 1f height becomes 1000px as well, because of that next Modifier or Composable is centered since 2000px is not abided.

    If you don't set a size Modifier, generally Constraints coming parent, unless you change it is, minHeight/minWidht =0 , screen width and screen height. Since it's an interval parents constraints get abided with 1000px from aspectRatio modifier.

    And if you change height from placasble.height to constraints.maxHeight which is parent height in this case

    @Preview
    @Composable
    fun LayoutTest() {
        RectangleScreen(modifier = Modifier
            .border(7.dp, Color.Red)
            .fillMaxSize()
            .border(7.dp, Color.Magenta)
            .layout { measurable, constraints ->
    
                val width = constraints.maxWidth
                val height = width
                val wrappedConstraints = Constraints.fixed(width, height)
                val placeable = measurable.measure(wrappedConstraints)
    
                layout(placeable.width, constraints.maxHeight) {
                    placeable.placeRelative(0, 0)
                }
    
            }
            .border(7.dp, Color.Yellow)
            .padding(64.dp)
            .border(7.dp, Color.Blue)
        )
    }
    

    you can see that it's placed on (0,0) as it's supposed to be.

    enter image description here