In the last article, we learned about the Column, Row, Box, and other common layouts in Compose. We also learned about the CompositionLocal class for composing. Scaffold Scaffold for App structure construction. We learned about Surface, Modifier and ConstraintLayout in Compose. While many Compose components are officially available, custom components are still necessary for actual requirements development.

In traditional View architecture, the system provides many component views that developers can use directly, such as TextView, ImageView, RelativeLayout, and so on. We can also customize views to create views with special functionality that the system does not provide us. Compose is not far behind. In Compose we can customize our Composable components using the Layout component. In fact, all components like Column and Row are implemented with Layout extensions at the bottom.

In View system, the two most common cases of custom View are: 1) Inherit existing View for function extension, such as inherit TextView or directly inherit View for rewriting; 2) Inherit the ViewGroup and rewrite the onMeasure and onLayout methods of the parent class. In Compose, you can simply customize it using the Layout component.

Before we start, we need to take a look at the basics of Layout Composable components.

1. Basic principles of Compose custom Layout

In Compose, when a Composable method is executed, it is added to the UI tree and rendered to the screen. The Composable method can be thought of as a Layout in the View system, which is called Layout in Compose. Each Layout has one parent Layout and zero or more children, much like a View. Of course, the Layout itself contains position information in its parent Layout, including position coordinates (x, y) and its dimensions width and height.

The Children Layout elements of a Layout are called to measure their own size and to satisfy the Constraints specified by the Constraints. These Constraints limit the maximum and minimum values of width and height. After the Layout completes its children Layout measurement, its own size will be determined, again recursive… Once a Layout element has finished measuring itself, it can place its children in its space according to the Constraints of Constraints. Is it the same as the View system? Measure before placing.

OK, here comes the most important! Compose UI does not allow multiple measurements. The Layout element cannot measure any of its children more than once in order to try different measurement Settings. Single-pass measurement certainly improves rendering efficiency, especially when Compose handles deep UI trees. If a Layout element needs to measure all of its children twice, the children within the children are measured four times, and so on, the number of measurements increases exponentially with Layout depth! In fact, View system is such, so in View system development must reduce the number of layers of the layout! Otherwise rendering efficiency will be extremely low when repeated measurements are required. For that reason, Compose does not allow multiple measurements. However, in some scenarios, multiple measurements of child elements are required to obtain information. For these cases, there is a way to do multiple measurements, limited to space reasons, after free again ~

Compose also has two cases for customizing a control (officially called Layout) :

  1. A custom Layout has no other child elements and is just itself, similar to a “custom View” in a View architecture.
  2. A custom Layout has child elements, and the placement of the child elements needs to be considered. This is similar to a custom ViewGroup in a View system.

So let’s look at the first case.

2. Compose custom “View”

The custom Layout in Compose is quite different from the View architecture. We need to customize the Layout is actually a custom Modifier property! Is to achieve their own Modifier in the Layout method, to achieve how to measure and place it itself. A common custom Layout Modifier structure code is as follows:

// code 1
fun Modifier.customLayoutModifier(...). {    // You can customize some attributes
    Modifier.layout { measurable, constraints ->
        ...    // You need to implement your own measurement and placement methods here}}Copy the code

Can be seen, the key is the Modifier. Layout method, which has two lambda expressions:

  1. measurableUsed for the measurement and placement of child elements;
  2. constraints: constrains the maximum and minimum values of the width and height child elements.

Take a simple chestnut to illustrate. A normal Text component can only adjust the distance between the Text’s edge and the top, bottom, left, and right edges of the Text component, as shown in Figure 1. The Text can only set the padding around the Text, which I set to 15dp on the top and 30dp on the left and 30dp on the left.

What if I want to control the distance from the baseline to the top of the Text? What is baseline? This requires understanding of Android’s algorithm for copywriting.

As you can see from Figure 2, when Android draws the copy, the baseline determines the bottom position of the body of the copy. You can only use Modifier. Padding to set the distance between leading and the top of the Text component in Compose. The customized Layout should meet the Baseline distance from the top of Text. That’s the top of figure 3 below. How do I do that?

First, of course, is measurement. Remember that a Layout can only measure its child elements once. Call the measure method in code1 to measure:

// code 2
fun Modifier.firstBaselineToTop(  FirstBaselineToTop is the method name of your custom modifier
    firstBaselineToTop: Dp    // Customize the modifier method parameters, here is one
) = this.then(
    layout { measurable, constraints -> // Call the Layout method to measure and place child components
        val placeable = measurable.measure(constraints) // The first is measurement. })Copy the code

When the Measurable measure method is called, a Placeable object is returned. Here, we can pass the constraints from a layout to the measure method, or we can pass the lambdas of our custom constraints. Since we don’t need to impose any restrictions on measurement in this scenario, we can just pass in the constraints given in layout. The whole point of this step is to get the Placeable object, which you can then call the placeRelative method of the Placeable object to position the child elements!

OK, now that the Composable component has been measured, we can call layout(Width, height) to place the content according to the measured size. Width = 1; width = 1; width = 1;

// code 3
fun Modifier.firstBaselineToTop(
    firstBaselineToTop: Dp
) = this.then(
    layout { measurable, constraints ->
        val placeable = measurable.measure(constraints)
        // Check whether the Composable component has FirstBaselinecheck(placeable[FirstBaseline] ! = AlignmentLine.Unspecified)// If present, get FirstBaseline from the top of the Composable component
        val firstBaseline = placeable[FirstBaseline]
        // Calculate the position of the Composable components along the Y axis
        val placeableY = firstBaselineToTop.roundToPx() - firstBaseline
        // Calculate the true height of this Composable component
        valheight = placeable.height + placeableY layout(placeable.width, height) { ... }})Copy the code

To be honest, it took me a while to see this code at first… First, the check method acts like an assert assertion and throws an IllegalStateException if the result inside is false. Check whether the Composable component modified by our customized Modifier has the FirstBaseline attribute and the Text component has the baseline attribute. If it does not exist, of course, we cannot use the firstBaselineToTop Modifier.

If it does, get the distance from the top of the component, which is the length of C in Figure 4. The blue box represents the space occupied by the normal Text component; The black box represents the edge of the screen; The red dotted line represents Baseline in Text. A said is our custom Modifier. FirstBaselineToTop firstBaselintToTop parameter method. Our goal is to be able to calculate the position of the Text component on the Y-axis, as well as the true width and height values, based on the firstBaselintToTop argument passed in.

Before, the Measurable measure method was called in the layout method to measure the width and height of the common Text component, that is, the width and height of the blue box in Figure 4, while the width and height of our customized layout is the width and height marked in orange and green in figure 4. Width can be obtained directly from the Placeable object, and height can be calculated from the sketch: Height = height + d, the height of normal Text plus d, d = a-c, d = firstBaselinttotop-baseline. So, d is the placeableY parameter. The purpose of code 3 is to calculate the width and height of the custom Layout, and then set the width and height using the Layout method.

Next comes the placement of the position. Just call the placeRelative method of the Placeable object:

// code 4
fun Modifier.firstBaselineToTop(
    firstBaselineToTop: Dp
) = this.then(
    layout { measurable, constraints ->
        valplaceable = measurable.measure(constraints) check(placeable[FirstBaseline] ! = AlignmentLine.Unspecified)val firstBaseline = placeable[FirstBaseline]
        val placeableY = firstBaselineToTop.roundToPx() - firstBaseline
        val height = placeable.height + placeableY
        layout(placeable.width, height) {
            placeable.placeRelative(0, placeableY)
        }
    }
)
Copy the code

Note that a custom Layout must call the placeRelative method, otherwise the custom Layout will not be visible. The placeRelative method automatically positions a custom Layout based on the current layoutDirection Layout direction. Here, our customized Layout is relatively simple, that is, there is an offset on the Y axis and no offset on the X axis. It can be seen intuitively from Figure 2.

So how do you use it? As you might have guessed, just use the same Modifier you used to modify Text or other Composable components:

// code 5
@Composable
fun CustomLayoutDemo(a) {
    Row {
        Text(
            text = "I am chestnut 1.",
            modifier = Modifier.firstBaselineToTop(40.dp),
            fontSize = 20.sp
        )

        Spacer(modifier = Modifier.width(20.dp))

        Text(
            text = "I am Chestnut 2.",
            modifier = Modifier.firstBaselineToTop(40.dp),
            fontSize = 15.sp
        )

        Spacer(modifier = Modifier.width(20.dp))

        Text(
            text = "I am Chestnut 3.",
            modifier = Modifier.firstBaselineToTop(40.dp),
            fontSize = 30.sp
        )
    }
}
Copy the code

In Code 5, there are three texts respectively, all using our custom Modifier firstBaselineToTop, and the parameter is set to 40dp, the difference is the font size. According to the display effect in Figure 5, the customized Layout we want has been achieved. That is, although the size is different, the Baseline distance from the top of the customized Layout of each Text Chinese plan is the same.

3. Customize a ViewGroup

For Compose’s custom “View” method, there’s no need to customize the “ViewGroup”. Compose’s Row and Column components are composed using the Layout method, which is the core method for composing to customize a ViewGroup. The Layout component can be used to manually measure and place its children. A custom “ViewGroup” Layout code structure usually looks like this:

// code 6
@Composable
fun CustomLayout(
    modifier: Modifier = Modifier,
    // You can add user-defined parameters here
    content: @Composable() - >Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ){ measurable, constraints ->
        // Measure and place the children...}}Copy the code

For a custom Layout, you need at least three parameters:

  • Modifier: Modifier passed in from the outside to modify the properties or Constraints of our custom Layout component;
  • Content: The children element in our custom Layout component;
  • MeasurePolicy: If you are familiar with Kotlin’s syntax, you will know that the lambda expression following Layout in Code 6 is an argument to Layout. It is used to measure and place children. The default scenario is to implement only the measure method. When we want our custom Layout component to be compliant with Intrinsics (officially called intrinsic property measurement), The minIntrinsicWidth, minIntrinsicHeight, maxIntrinsicWidth, maxIntrinsicHeight methods need to be overwritten. The reason for the length will be discussed later

Here we use the Layout component to define a basic, simple Column component for placing child elements vertically, which we call MyOwnColumn. As mentioned earlier, the first thing we do is measure children, and only once. Unlike the previous custom “View”, we need to measure not the size of the View itself, but the size of all the children it contains:

// code 7
@Composable
fun MyOwnColumn(
    modifier: Modifier = Modifier,
    // You can add user-defined parameters here
    content: @Composable() - >Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ){ measurables, constraints ->
        // Measure and place the children
        val placeables = measurables.map { measurable ->
            // Measure the size of each childmeasurable.measure(constraints) } ... }}Copy the code

As you can see, each child in the map calls the measure method, and as before, we no longer need to restrict the measurement, so we just pass in constraints from the Layout. So far, we’ve measured all the children subelements.

Before setting the position of these children, we also need to calculate the width and height of our customized MyOwnColumn component according to the measured size of children. The following code sets the Layout size of our custom MyOwnColumn as much as possible:

// code 8
@Composable
fun MyOwnColumn(
    modifier: Modifier = Modifier,
    // You can add user-defined parameters here
    content: @Composable() - >Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ){ measurables, constraints ->
        // Measure and place the children
        val placeables = measurables.map { measurable ->
            // Measure the size of each child
            measurable.measure(constraints)
        }
        layout(constraints.maxWidth, constraints.maxHeight) {
            / / put the children. }}}Copy the code

Finally, you can place the children. As with the custom “View” above, we also call placeable. PlaceRelative (x,y) to place the location. Since it is a custom Column that needs to be placed vertically one by one, x in the horizontal direction of each child must start from the left and be set to 0. In the vertical direction, a variable is required to record the vertical position of a child. The detailed code is as follows:

// code 9
@Composable
fun MyOwnColumn(
    modifier: Modifier = Modifier,
    // You can add user-defined parameters here
    content: @Composable() - >Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ){ measurables, constraints ->
        // Measure and place the children
        val placeables = measurables.map { measurable ->
            // Measure the size of each child
            measurable.measure(constraints)
        }
        var yPosition = 0  // Record the vertical position of an element
        layout(constraints.maxWidth, constraints.maxHeight) {
            / / put the children
            placeables.forEach { placeable ->  
                placeable.placeRelative(x = 0, yPosition)
                yPosition += placeable.height
            }
        }
    }
}
Copy the code

Note that our custom Column is set to accommodate as many constraints as possible on the parent layout: Layout (constraints. MaxWidth, constraints. MaxHeight), so it is quite different from the official Column. This is just to illustrate the flow of methods for customizing a “ViewGroup” in Compose.

MyOwnColumn is used in the same way as Column, but with a different strategy for occupying space in the parent layout. The official Column layout defaults to the smallest possible width and height occupying the parent layout, similar to wrAP_content; MyOwnColumn is the largest possible parent layout, similar to match_parent. The effect can also be seen clearly in Figure 6 below.

// code 10
@Composable
fun MyOwnColumnDemo(a) {
    MyOwnColumn(Modifier.padding(20.dp)) {
        Text("I am chestnut 1.")
        Text("I am Chestnut 2.")
        Text("I am Chestnut 3.")}}Copy the code

Consider the two methods for customizing a Layout in Compose. One is a function extension for a component, similar to the method for customizing an existing View or directly inheriting a View in the View architecture. The other is for a container component customization, similar to the View system on an existing ViewGroup or directly inherited ViewGroup customization, it is actually a Layout component, is the main core component of the Layout. Let’s look at a more complex custom Layout.

4. Customize complex layouts

OK, now that you know the basic method steps for Compose’s custom Layout, let’s take a look at a slightly more complex version. Suppose you need to implement a horizontal sliding waterfall flow layout, as shown in the middle of the following image:

We can set how many lines to display, in this case, 3 lines, and we just pass in all the children. There is no component for this functionality in the existing official Compose component, which requires customization. Build the framework from the previous template code:

// code 11
@Composable
fun StaggeredGrid(
    modifier: Modifier = Modifier,
    rows: Int = 3.// A custom argument to control the number of lines displayed. Default is 3
    content: @Composable() - >Unit
){
    Layout(    // This is the Layout method
        modifier = modifier,
        content = content
    ) { measurables, constraints ->
        // Measure and position logic}}Copy the code

The process continues: 1) Measure all child elements; 2) Calculate the size of custom Layout; 3) Place child elements. Here is just the code from the Layout method:

// code 12
    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints ->
        // Record the width of each line
        val rowWidths = IntArray(rows){0}
        // Record the height of each row
        val rowHeights = IntArray(rows){0}
        val placeables = measurables.mapIndexed { index, measurable ->
            // Standard procedure: measure the size of each child to obtain
            val placeable = measurable.measure(constraints)
            // Group each child according to the serial number, record the width of each group information
            val row = index % rows
            rowWidths[row] += placeable.width
            rowHeights[row] = max(rowHeights[row], placeable.height)
            
            placeable // Remember to return the measurement object}... }Copy the code

Next, calculate the size of the custom Layout itself. Add the height of each children row to get the height of the custom Layout. The maximum width of all rows is the width of the custom Layout. And we also know where each row is on the Y-axis. The relevant code is as follows:

// code 13
    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints ->
        ...
        // The width of the custom Layout is the maximum width of all rows
        valwidth = rowWidths.maxOrNull() ? .coerceIn(constraints.minWidth.rangeTo(constraints.maxWidth)) ? : constraints.minWidth// The height of the custom Layout is, of course, the sum of all row heights
        val height = rowHeights.sumOf { it }
            .coerceIn(constraints.minHeight.rangeTo(constraints.maxHeight))
        // Calculate the position of each row on the Y axis
        val rowY = IntArray(rows) { 0 }
        for (i in 1 until rows) {
            rowY[i] = rowY[i - 1] + rowHeights[i - 1]}// Set the width and height of the custom Layout
        layout(width, height) {
            // Place each child. }}Copy the code

Yi? Is the code different from what you thought it would be? When finding the width, it also uses the coerceIn method to restrict the width between the minimum and maximum values of the CONSTRAINTS constraints, or if it is exceeded it will be set to either the minimum or maximum. Height is the same. Then we call the Layout method to set the width and height of our custom layout.

Finally, we just call the method placeable. PlaceRelative (x, y) to place our children on the screen. Of course, you still need variables to store your position on the X-axis. The specific code is as follows:

// code 14
    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints ->
        ...
        // Calculate the position of each row on the Y axis
        val rowY = IntArray(rows) { 0 }
        for (i in 1 until rows) {
            rowY[i] = rowY[i - 1] + rowHeights[i - 1]}// Set the width and height of the custom Layout
        layout(width, height) {
            // Place each child
            val rowX = IntArray(rows) { 0 }  // The position of child on the X-axis
            placeables.forEachIndexed { index, placeable ->
                val row = index % rows
                placeable.placeRelative(
                    rowX[row],
                    rowY[row]
                )
                rowX[row] += placeable.width
            }
        }
    }
Copy the code

The code logic is relatively simple, without much explanation. To sum up, the complete code of this custom Layout is as follows:

// code 15
// Custom layout for horizontal waterfall flow
@Composable
fun StaggeredGrid(
    modifier: Modifier = Modifier,
    rows: Int = 3.// A custom argument to control the number of lines displayed. Default is 3
    content: @Composable() - >Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints ->
        // Record the width of each row
        val rowWidths = IntArray(rows) { 0 }
        // Record the height of each row
        val rowHeights = IntArray(rows) { 0 }
        // Measure the size of each child to get the object for each child
        val placeables = measurables.mapIndexed { index, measurable ->
            // Standard procedure: measure the size of each child to obtain
            val placeable = measurable.measure(constraints)
            // Group each child according to the serial number, record the width of each row information
            val row = index % rows
            rowWidths[row] += placeable.width
            rowHeights[row] = max(rowHeights[row], placeable.height)
            placeable    // Return each child's number
        }

        // The width of the custom Layout is the maximum width of all rows
        valwidth = rowWidths.maxOrNull() ? .coerceIn(constraints.minWidth.rangeTo(constraints.maxWidth)) ? : constraints.minWidth// The height of the custom Layout is, of course, the sum of all row heights
        val height = rowHeights.sumOf { it }
            .coerceIn(constraints.minHeight.rangeTo(constraints.maxHeight))
        // Calculate the position of each row on the Y axis
        val rowY = IntArray(rows) { 0 }
        for (i in 1 until rows) {
            rowY[i] = rowY[i - 1] + rowHeights[i - 1]}// Set the width and height of the custom Layout
        layout(width, height) {
            // Place each child
            val rowX = IntArray(rows) { 0 }  // The position of child on the X-axis
            placeables.forEachIndexed { index, placeable ->
                val row = index % rows
                placeable.placeRelative(
                    rowX[row],
                    rowY[row]
                )
                rowX[row] += placeable.width
            }
        }
    }
}
Copy the code

OK, write a small component as the children element to display, as follows:

// code 16
@Composable
fun Chip(
    modifier: Modifier = Modifier, text: String
) {
    Card(
        modifier = modifier,
        border = BorderStroke(color = Color.Magenta, width = Dp.Hairline),
        shape = RoundedCornerShape(8.dp)
    ) {
        Row(
            modifier = Modifier.padding(start = 8.dp, top = 4.dp, end = 8.dp, bottom = 4.dp),
            verticalAlignment = Alignment.CenterVertically
        ) {
            Box(
                modifier = Modifier
                    .size(16.dp, 16.dp)
                    .background(color = MaterialTheme.colors.secondary)
            )
            Spacer(Modifier.width(4.dp))
            Text(text = text)
        }
    }
}
Copy the code

Also note that the width of our custom Layout StaggeredGrid will exceed the width of the screen, so in practice, we need to add a Modifier. HorizonalScroll for horizontal scrolling. This only uses the comfortable ~ actual use of the code sample such as the following:

// code 17
val topics = listOf(
    "Arts & Crafts"."Beauty"."Books"."Business"."Comics"."Culinary"."Design"."Fashion"."Film"."History"."Maths"."Music"."People"."Philosophy"."Religion"."Social sciences"."Technology"."TV"."Writing"
)
Row(modifier = Modifier.horizontalScroll(rememberScrollState())){
    StaggeredGrid() {
        for (topic in topics) {
            Chip(modifier = Modifier.padding(8.dp),text = topic)
        }
    }
}
Copy the code

Of course, you can also set your own styles that need to be displayed in several lines. The default is 3 lines.

To sum up, the basic process of customizing Layout in Compose is the same as that of customizing View in View system. The biggest difference lies in the measurement step. For the purpose of improving efficiency, Compose does not allow multiple measurements. In addition, the two cases of Compose’s custom Layout can also correspond to the two cases in the View system. However, it can be seen that Compose is adapted and programmed in Layout components, which enables developers to focus more on specific code logic. That’s where Compose’s custom Layout comes in handy. For Compose’s custom “View,” do you have a handle on that?

Ps. Give a rose, leave a lingering fragrance. Welcome to share and pay attention, your recognition is the spiritual source of my continued creation.

reference

  1. Developer. The android. Google. Cn/codelabs/je…
  2. A big conch is on a Utopia. The Android text Baseline (Baseline) algorithm. www.jianshu.com/u/79e66729b…
  3. Jetpack Compose museum – a custom Layout. Compose.net.cn/layout/cust…
  4. Developer. The android. Google. Cn/codelabs/je…