Contenuti

Custom Layout with Compose

Contenuti

Have you ever needed to layout components in a way other than rows or columns?

Composable doc cover this use case but perhaps a little more information could be helpful. The example provided by the documentation creates a custom column layout:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Composable
fun MyBasicColumn(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints ->
        // Don't constrain child views further, measure them with given constraints
        // List of measured children
        val placeables = measurables.map { measurable ->
            // Measure each children
            measurable.measure(constraints)
        }

        // Set the size of the layout as big as it can
        layout(constraints.maxWidth, constraints.maxHeight) {
            // Track the y co-ord we have placed children up to
            var yPosition = 0

            // Place children in the parent layout
            placeables.forEach { placeable ->
                // Position item on the screen
                placeable.placeRelative(x = 0, y = yPosition)

                // Record the y co-ord placed up to
                yPosition += placeable.height
            }
        }
    }
}

let’s try the above layout with the following preview. What do you expect to see? Three lines of text with different background colors?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Preview
@Composable
fun MyBasicColumnPreview() {
    Column {
        MyBasicColumn(modifier = Modifier.fillMaxSize()) {
            Text(
                modifier = Modifier
                    .fillMaxWidth()
                    .background(color = Color.Magenta),
                text = "First"
            )
            Text(
                modifier = Modifier
                    .fillMaxWidth()
                    .background(color = Color.Cyan),
                text = "Second"
            )
            Text(
                modifier = Modifier
                    .fillMaxWidth()
                    .background(color = Color.Yellow),
                text = "Third"
            )
        }
    }
}

Nope, you get a different result. The first line of text expand to fill the full screen height.

/images/custom-layout/first-preview.png

Is the documentation example wrong? No, it’s not. If we look at the constraints used inside the custom column layout we can see how everything is correct

1
2
3
4
5
6
7
...
	// Don't constrain child views further, measure them with given constraints
    // List of measured children
    val placeables = measurables.map { measurable ->
        // Measure each children
        measurable.measure(constraints)
    }

The given constraints are those coming from the parent Layout composable and we used Modifier.fillMaxSize() so the custom layout will occupy the full screen size. The first child (the magenta text) inherit those constraints and expand itself as indicated.

How can we achieve the desired result? We can add another modifier into the custom layout: Modifier.wrapContentSize()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
...
@Composable
fun MyBasicColumn(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Layout(
        modifier = modifier.wrapContentSize(),
        content = content
    ) { measurables, constraints ->
...    

Here we are still taking into consideration the wishes dictated by the incoming modifier argument and narrow down further with wrapContentSize.

/images/custom-layout/second-preview.png

Let’s see what the doc says about the added modifier:

1
2
3
4
5
6
7
/**
 * Allow the content to measure at its desired size without regard for the incoming measurement minimum width or minimum 
 * height constraints, and, if unbounded is true, also without regard for the incoming maximum constraints. If the content's 
 * measured size is smaller than the minimum size constraint, align it within that minimum sized space. If the content's 
 * measured size is larger than the maximum size constraint (only possible when unbounded is true), align within the maximum 
 * space. 
 */

With this added modifier we can arrange different compositions, like the following one where the custom column and a box share the vertical space equally.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Preview
@Composable
fun MyWeightedBasicColumnPreview() {
    Column {
        MyBasicColumn(modifier = Modifier
            .fillMaxSize()
            .weight(1f)) {
            Text(
                modifier = Modifier
                    .fillMaxWidth()
                    .background(color = Color.Magenta),
                text = "First"
            )
            Text(
                modifier = Modifier
                    .fillMaxWidth()
                    .background(color = Color.Cyan),
                text = "Second"
            )
            Text(
                modifier = Modifier
                    .fillMaxWidth()
                    .background(color = Color.Yellow),
                text = "Third"
            )
        }
        Box(modifier = Modifier
            .fillMaxSize()
            .weight(1f)
            .background(Color.Blue))
    }
}

/images/custom-layout/third-preview.png

That’s all. Happy composing.