Cover photo source: https://developer.android.google.cn/jetpack/compose/tutorialCopy the code

New feature – Column, this article is included in the personal column Jetpack Compose column

In the previous article, we introduced the basic layout for JetPack Compse. Compose provides a number of built-in layouts for you to use, but no amount is necessarily sufficient for your product’s needs. So we need to learn how to customize the layout.

To learn how to create a custom layout, check out JetPack Compose’s built-in layout source. This article implements a Row layout of its own, similar to JetPack Compose’s.

This function is also called for the Box, Row, and Column layouts. Let’s take a look at the Layout parameters.

/ * * *@paramChildren * for content layout@paramModifier for the layout@paramMeasurePolicy Layout policy */
@Composable inline fun Layout(
    content: @Composable() - >Unit,
    modifier: Modifier = Modifier,
    measurePolicy: MeasurePolicy
) 
Copy the code

The measurePolicy parameter is the most critical parameter in custom layout, which determines the measurement and placement of children in the layout. MeasurePolicy is a functional interface (a Kotlin 1.4 feature).

fun interface MeasurePolicy {
    // Measure the children and place them in the layout
	 fun MeasureScope.measure(
        measurables: List<Measurable>,// Children to be measured and placed
        constraints: Constraints // Layout constraints
    ): MeasureResult
    ……
}
Copy the code

The nice thing about writing a functional interface is that when we write code, we can abbreviate it

// If it is a normal interface we need to write this
val measurePolicy = object : MeasurePolicy {
        override fun MeasureScope.measure(
            measurables: List<Measurable>,
            constraints: Constraints
        ): MeasureResult { TODO() }
    }
// Functional interfaces support SAM conversions, so the above can be abbreviated to the following form
  val measurePolicy = MeasurePolicy { measurables, constraints -> TODO() }
Copy the code

In the example below, I’m using the short form, but remember that the lambda block is the measure method of the original interface.

How to write custom layout? To learn how to customize your layout, use the template to create your own layout. We’re going to write four versions of Row, and we’re going to iterate over the rows

Custom layout – Hand write a Row layout

The Row V1

This version simply implements the horizontal order of children.

/** * Customize Layout Row v1.0 */
@Composable
private fun MyRowV1(modifier: Modifier = Modifier, content: @Composable() - >Unit) {

    // The layout of the measurement strategy
    val measurePolicy = MeasurePolicy { measurables, constraints ->
        The children / / 0 ️ ⃣ measurements
        val placeables = measurables.map { child ->
            //1️ Measurement of each item, each child can only be measured once, and cannot be measured multiple times to control the width and height of the child within the constraint constraints
            child.measure(constraints)
        }
        // Record the x value of a child to be placed
        var xPosition = 0
        
        // 2️ Decides the size of the layout by referring to the measurement result, instead of directly using the maximum value of constraints
        layout(constraints.minWidth, constraints.minHeight) {/*this:Placeable.PlacementScope*/
            // 3️ placement children
            placeables.forEach { placeable ->
                // Place each child
                //4️ placeRelative This method must be called in the Placeable.PlacementScope scope
                placeable.placeRelative(xPosition, 0)
                // Arrange them horizontally
                xPosition += placeable.width
            }
        }
    }
    // Put the corresponding parameters in the Layout function
    Layout(content = content, modifier = modifier, measurePolicy = measurePolicy)
}
Copy the code

The code above summarizes the custom layout in about three steps

  1. Measurement of the children.
  2. Determine the size of the layout.
  3. Place children on the layout.

⚠️ Can only be measured once per child in Compose.

MyRowV1 is my low version of Row with a custom layout. Let’s see how it works.

MyRowV1(Modifier.width(200.dp).height(100.dp)) { RowChildren() }

@Composable
private fun RowChildren(a) {
    Box(
        modifier = Modifier
            .size(100.dp)
            .background(Blue)
    )
    Box(
        modifier = Modifier
            .size(100.dp)
            .background(Red)
    )
}
Copy the code

1-1 👆 code

That doesn’t seem to be a problem. The children do rank horizontally. But in fact the blue Box and the red Box are both 200.dp wide, and each child of MyRowV1 has the same width as MyRowV1 itself. Why is that? The child itself is set to a width of 100.dp. Why is it changed to 200.dp? Zha still not obedient 🙉?

The size of the child is measured by child.measure(constraints). The child gets its size from the measure method, which has only one parameter, Constraints. The source code for Constraints related to view size is roughly as follows, with four variables: maximum/small width and maximum/small height.

// Simplified version of the non-constraints source code
class Constraints{
    val minWidth: Int ,
    val maxWidth: Int ,
    val minHeight: Int.val maxHeight: Int
 }
Copy the code

We will not do source code analysis here. If we have time to write a separate one in the future, we will first use a pseudo code to simulate the rough process of measure source code for analysis

fun main(a) {
    MeasurePolicy {measurables, constraints -> measure policy {measurables, constraints ->
    val constraints = Constraints(minWidth = 200, maxWidth = 200, minHeight = 100, maxHeight = 100)
    / / child. The measure (constraints) to measure the child
    measure(constraints)
}

// Simulate the child measurement method
fun measure(measurementConstraints: Constraints) {
    // The child itself is set by Modifier to the size corresponding to the Constraints
    val childConstraints = Constraints(minWidth = 100, maxWidth = 100, minHeight = 100, maxHeight = 100)
    //1. Merge the parent constraint with the constrain function to obtain the new constraints
    val constraints = measurementConstraints.constrain(childConstraints)

    //-- {if the child has no children, otherwise it will continue to measure the children}----

    //2. Child reports a size value to the parent based on the new constraints.
    val intSize = IntSize(constraints.minWidth, constraints.minHeight)
    //3. Verify that the child does not conform to its Constraints, so that the child does not report problems
    // coerceIn is a method under the kotlin.Ranges package
    val width = intSize.width.coerceIn(measurementConstraints.minWidth, measurementConstraints.maxWidth)
    val height = intSize.height.coerceIn(measurementConstraints.minHeight, measurementConstraints.maxHeight)
    println("width=$width,height=$height")}/ * * * kotlin. Ranges under the package of a method, ensure that the caller to receive return values within the scope of the [minimumValue maximumValue] * /
public fun Int.coerceIn(minimumValue: Int, maximumValue: Int): Int {
    if (minimumValue > maximumValue) throw IllegalArgumentException("Cannot coerce value to an empty range: maximum $maximumValue is less than minimum $minimumValue.")
    if (this < minimumValue) return minimumValue
    if (this > maximumValue) return maximumValue
    return this
}
Int. CoerceIn is similar to Int. CoerceIn in that it ensures that the return value is in the caller's return
fun Constraints.constrain(otherConstraints: Constraints)= Constraints( minWidth = otherConstraints.minWidth.coerceIn(minWidth, maxWidth), maxWidth = otherConstraints.maxWidth.coerceIn(minWidth, maxWidth), minHeight = otherConstraints.minHeight.coerceIn(minHeight, maxHeight), MaxHeight = otherConstraints. MaxHeight. CoerceIn (minHeight, maxHeight)) output -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - width =200,height=100
Copy the code

👆 Constraining the child’s constraints and the child’s constraints will be evaluated as constrain. The child reports its size to the parent container based on the new Constraints 3. This, combined with the constraints of the measurement, determines the final size of the child.

The code above roughly says: Whatever size you want the child to be, you must be within the measurement constraints I gave you.

In general, the parameter Constraints of the Measure policy’s measure is corresponding to the size set through the Modifier (other conditions are not considered for now). For example, if MyRowV1 modifie.width (200.dp).height(100.dp) is given, the parameter of measure constraints should be constraints (minWidth =) 200.dp, maxWidth = ‘200.dp’, minHeight = ‘100.dp’, maxHeight = ‘100.dp’) The measured size of the child is width=200,height=100, so the width of the blue Box and the red Box are 200.dp.

The Row V2 version

From the analysis at the end of version V1, we know that the Constraints that affect the size of the child are the Constraints of the measurement. I want to solve the V1 problem by changing the Constraints of the measurement of the child. So the Row V2 version of the code looks like this

/** * Customize Layout Row v2.0 * Expand the scope of measurement constraints, so that children's measured width and height as much as possible consistent with the size set by themselves */
@Composable
private fun MyRowV2(modifier: Modifier = Modifier, content: @Composable() - >Unit) {
    val measurePolicy = MeasurePolicy { measurables, constraints ->
        val placeables = measurables.map { child ->
            // 0️ 8% code differs from V1 version only
            child.measure(Constraints(0, constraints.maxWidth, 0, constraints.maxHeight))
        }
        // The logic of the placement is unchanged
        var xPosition = 0
        layout(constraints.minWidth, constraints.minHeight) {
            placeables.forEach { placeable ->
          		placeable.placeRelative(xPosition, 0)
                xPosition += placeable.width
            }
        }
    }
    Layout(content = content, modifier = modifier, measurePolicy = measurePolicy)
}

Copy the code

We get a conclusion in code 1-2: Whatever size you want the child to be, you have to be within the measurement constraints I gave you. In the example above, the child itself sets the size to [width=100,height=100]. Constraints(minWidth = 200, maxWidth = 200, minHeight = 100, maxHeight = 100) So the child wants to make its width 100 which is impossible, so the child’s width becomes 200. However, in version V2, we changed the minimum value of Constraints to 0, and the maximum value remained unchanged. < span style = “box-sizing: border-box; color: RGB (255, 255, 255); line-height: 200; font-size: 10px! Important; word-break: break-word! Important;” Of course it can, because 100 is between [0,200]. For the V2 version, children and can be set in their own size display. (if the child size is equal to the size of the parent container)

The other problem with V2 is that the order of the children in this order may be beyond the parent layout

// The total width of the children is 200.dp. We specify that the width of MyRowV2 is 160.dp
MyRowV2(Modifier.width(160.dp).height(100.dp)) { RowChildren() /*RowChildren() This function code is the same as in 1-1 */}
Copy the code

The Row V3 version

Fixed an issue where children exceed the parent container

/** * Custom Layout Row v3.0 * solve the problem of child size exceeding the size of the parent container */
@Composable
private fun MyRowV3(modifier: Modifier = Modifier, content: @Composable() - >Unit) {
    val measurePolicy = MeasurePolicy { measurables, constraints ->
        // Record the space occupied by children
        var childrenSpace = 0
        val placeables = measurables.mapIndexed { index, child ->
            val placeable = child.measure(
                Constraints(
                    0,
                    constraints.maxWidth - childrenSpace, // Use the remaining space to measure
                    0,
                    constraints.maxHeight
                )
            )
            // Update the space occupied by children after each measurement
            childrenSpace += placeable.width
            placeable
        }
        
        // The logic of the placement is unchanged
        var xPosition = 0
        layout(constraints.minWidth, constraints.minHeight) {
            placeables.forEach { placeable ->
                // Place each child
                placeable.placeRelative(xPosition, 0)
                xPosition += placeable.width
            }
        }
    }
    Layout(content = content, modifier = modifier, measurePolicy = measurePolicy)
}
Copy the code

In versions V1 and V2, the measurement constraints are the same for each child, but in fact for a Row layout, when we measure a child, the next child should be allocated from the remaining space. So for version V3, we define the childrenSpace variable to record the space that children have occupied (for Row, we only need to consider the x-direction, that is, the width). The maximum width of each measurement should be the remaining width of the parent container (maximum width of the parent container -childrenSpace) so that the measured child does not exceed the total width of the parent container. Update the childrenSpace value after measuring each child.

The Row V4 version

This version builds on the previous version by adding weight. The main purpose of this feature is to learn how to have a child tell the parent something extra when measuring.

In the previous version, we traversed measurables: He has made it So that he can measure the size of the child as he has. He has made it so that he can put extra information into it as he has.

  val placeables = measurables.map { child:Measurable ->
          	// Fetch additional information
            child.parentData
            child.measure(constraints)
        }
Copy the code

We have no way to directly modify the parentData attribute of Measurable Values. We need to modify it by setting the Modifier. ParentDataModifier is responsible for modifying parentData. We write a class to implement the modifyParentData method in the ParentDataModifier interface. Write the modify data logic in the modifyParentData method.

data class MyRowWeightModifier(val weight: Float/* The weight to be set */) : ParentDataModifier {
    // Change the value of parentData
    override fun Density.modifyParentData(parentData: Any?).: Any {
        // If the parameter type is correct and not empty, change it and return directly, otherwise create a new object, change it and return again
        var data = parentData as? MyRowParentData?
        if (data= =null) {
            data = MyRowParentData()
        }
        data.weight = weight
        return data}}// Used to save the ParentData data model
data class MyRowParentData(var weight: Float = 0f)
Copy the code

Next we define a scope in which we write a Modifier extension function to create the MyRowWeightModifier

interface MyRowScope {
    // Then is a method of Modifier, used to form the Modifier chain
    fun Modifier.weight(weight: Float) = this.then(MyRowWeightModifier(weight))

    // Create a singleton
    companion object : MyRowScope
}
Copy the code

We write the weight method in an interface because we want to limit the scope of the call. We just want the weight method to be called from our custom MyRow layout. This is the same thing as we said before with the Box child setting align. According to the matting above, the dot below is the integrity of MyRowV4

/** * Custom Layout Row v4.0 ** Row with weight */
@Composable
private fun MyRowV4(modifier: Modifier = Modifier, content: @Composable MyRowScope. () - >Unit /* The scope of the content function is MyRowScope) {
    val measurePolicy = MeasurePolicy { measurables, constraints ->
        // Store the measured child information
        val placeables = arrayOfNulls<Placeable>(measurables.size)
        MyRowParentData = MyRowParentData = MyRowParentData
        val parentDatas =
            Array(measurables.size) { measurables[it].parentData as? MyRowParentData }
        // Record the space occupied by children
        var childrenSpace = 0
        // Sum of ownership weights
        var totalWeight = 0f
        // Set the number of children for weight
        var weightChildrenCount = 0
        measurables.fastForEachIndexed { index, child ->
            valweight = parentDatas[index]? .weightif(weight ! =null) { // If there is a child with weight, record it
                totalWeight += weight
                weightChildrenCount++
            } else { // Direct measurement of child without weight
                val placeable = child.measure(
                    Constraints(
                        0,
                        constraints.maxWidth - childrenSpace, // Restrict the child to the remaining space
                        0,
                        constraints.maxHeight
                    )
                )
                // Update the space occupied by children after each measurement
                childrenSpace += placeable.width
                placeables[index] = placeable
            }
        }
        // Distribute the rest of the space equally
        val weightUnitSpace =
            if (totalWeight > 0) (constraints.maxWidth - childrenSpace) / totalWeight else 0f
        measurables.fastForEachIndexed { index, child ->
            valweight = parentDatas[index]? .weightif(weight ! =null) {
                // Allocate space based on child weight
                val distributionSpace = (weightUnitSpace * weight).roundToInt()
                val placeable = child.measure(
                    Constraints(
                        distributionSpace,
                        distributionSpace,
                        0,
                        constraints.maxHeight
                    )
                )
                placeables[index] = placeable
            }
        }

        var xPosition = 0
        layout(constraints.minWidth, constraints.minHeight) {
            placeables.forEach { placeable ->
                if (placeable == null) {
                    return@layout
                }
                // Place each child
                placeable.placeRelative(xPosition, 0)
                xPosition += placeable.width
            }
        }
    }

    Layout(content = { MyRowScope.content() }, modifier = modifier, measurePolicy = measurePolicy)
}

Copy the code

use

MyRowV4(Modifier.size(300.dp, 100.dp)) {
                Box(
                    modifier = Modifier
                        .size(100.dp)
                        .background(Blue)
                )
                Box(
                    modifier = Modifier
                        .height(100.dp)
                        .weight(4f)
                        .background(Red)
                )
                Box(
                    modifier = Modifier
                        .height(100.dp)
                        .weight(1f)
                        .background(Green)
                )

            }
Copy the code

The effect

conclusion

Learn how JetPack Compose can customize layout 1 by mimicking the Row layout. In custom Row V1 we learned the simple step 2 of custom layout. In the custom layouts Row V2 and V3, we know the factors that affect the size of children and how to correct the size of the child measurement. 3. In custom layout Row V4, we learned that the child tells the parent container some extra data if the parentData is changed.