Jetpack Compose is a new toolkit for building native Android interfaces. It simplifies and speeds up interface development on Android, using less code, powerful tools, and an intuitive Kotlin API to make apps live and exciting quickly. Compose uses a new component, composables, to lay out the interface and modifiers to configure composables.

This article will explain for you by combination and modifier can support the combination of layout model, and further probe into its behind the working principle and their functions, allowing you to better understand the layout and the workings of a modifier, and should be how and when to build a custom layout, so as to realize the design of meet the application requirements exactly.

If you prefer to follow this article via video, watch it here.

Layout model

The goal of the Compose layout system is to provide layouts that are easy to create, especially custom layouts. This requires a layout system that is powerful enough to allow developers to create any layout they want for their applications, and to make the layout perform well. Next, take a look at how Compose’s layout model accomplishes these goals.

Jetpack Compose transforms state into an interface in three steps: composition, layout, and drawing. The composition phase performs composable functions that generate interfaces to create interface trees. For example, the SearchResult function in the figure below generates the corresponding interface tree:

△ Composable functions generate corresponding interface tree

Composable items can contain logic and control flow, so different interface trees can be generated based on different states. In the layout phase, Compose traverses the interface tree, measures various parts of the interface, and places each part in the 2D space of the screen. That is, each node determines its own width, height, and X and y coordinates. In the drawing phase, Compose will iterate through the interface tree again and render all elements.

This article delves into the layout phase. The layout phase is subdivided into two phases: measurement and placement. This is equivalent to onMeasure and onLayout in the View system. In Compose, however, the two phases intersect, so we treat it as a layout phase. The process of laying out each node in the interface tree is divided into three steps: each node must measure all of its children, determine its own size, and then place its children. In the following example, the entire interface tree can be laid out in a single pass.

△ Layout process

The process is summarized as follows:

  1. Measure the root layout Row;
  2. Row measures its first child, Image;
  3. Since the Image is a leaf node with no children, it measures and reports its own dimensions and also returns instructions on how to place its children. The leaf nodes of the Image are usually empty, but all layouts return these placement instructions as they set their dimensions;
  4. Row measures its second child, Column;
  5. Column measures the child node. First, the first child node Text is measured.
  6. Text measures and reports its dimensions and placement instructions;
  7. Column measures the second child node Text;
  8. Text measures and reports its dimensions and placement instructions;
  9. Column, after measuring its child node, can determine its own size and placement logic;
  10. Row determines its own dimensions and placement instructions based on measurements of all its children.

Once all the elements have been measured, the interface tree is traversed again and all the place instructions are executed during the place phase.

Layout Indicates the combinable items

Now that we know the steps involved in this process, let’s look at how it is implemented. Looking at the composition phase, we represent the interface tree with higher-level composables like Row, Column, and Text, each of which is actually built from lower-level composables. In the case of Text, you can see that it consists of several lower-level base building blocks, and each of these composables contains one or more Layout composables.

Each composable item contains one or more layouts

The Layout composable is the base building block of the Compose interface and generates a LayoutNode. In Compose, the interface tree, or composition, is a LayoutNode tree. Here is the function signature for the Layout composable:

@Composable
fun Layout(
    content: @Composable() - >Unit,
    modifier: Modifier = Modifier,
    measurePolicy: MeasurePolicy
){... }Copy the code

△ Layout function signature of combinable items

Content is a slot that can hold any subcomposable item, and for Layout purposes, a subLayout is included in content. The modifier specified by the Modifier parameter will be applied to the layout, as described in more detail below. The measurePolicy parameter is of type measurePolicy, which is a functional interface that specifies how the layout measures and places items. In general, to implement custom layout behavior, you implement this functional interface in your code:

@Composable
fun MyCustomLayout(
    modifier: Modifier = Modifier,
    content: @Composable() - >Unit
) {
    Layout(
         modifier = modifier,
         content = content
    ) { measurables: List<Measurable>,
         constraints: Constraints ->
        // TODO measurement and placement items}}Copy the code

△ Implement MeasurePolicy functional interface

In the MyCustomLayout composable, we implement the desired measure function by calling the Layout function and providing MeasurePolicy as an argument in the form of Trailing Lambda. This function accepts a Constraints object to tell Layout its size limits. Constraints is a simple class that limits the maximum and minimum width and height of a Layout:

class Constraints {
    val minWidth: Int
    val maxWidth: Int
    val minHeight: Int
    val maxHeight: Int
}
Copy the code

Delta Constraints

The measure function also takes a List as an argument, which represents the child elements passed in. ____types expose functions used to measure items. As mentioned earlier, laying out each element requires three steps: each element must measure all of its children to determine its own size, and then place its children. Its code implementation is as follows:

@Composable
fun MyCustomLayout(
    content: @Composable() - >Unit,
    modifier: Modifier = Modifier
) {
    Layout(
         modifier = modifier,
         content = content
    ) { measurables: List<Measurable>,
         constraints: Constraints ->
        Placeables are measured child elements that have their own size values
        val placeables = measurables.map { measurable ->
            // Measure all child elements, no custom measurement logic is written here, just simple
            // Call Measurable measure functions and pass in constraints
            measurable.measure(constraints)
        }
        val width = // Calculated according to placeables
        val height = // Calculated according to placeables
        // Report the required size
        layout (width, height) {
            placeables.foreach { placeable ->
                // Place each item to the final desired location by traversingPlaceable. Place (x =... Y =...). }}}}Copy the code

△ Code examples for laying out each element

The place function used in the code above also has a placeRelative function for right-to-left language Settings, which automatically mirrors coordinates horizontally when used.

Note that the API is designed to prevent you from trying to place unmeasured elements, and the place function is only good for Placeable, which is the return value of the measure function. In the View system, the timing of onMeasure and onLayout calls is up to you, and the order of calls is not mandatory, but this can lead to subtle bugs and behavioral differences.

Custom layout examples

MyColumn sample

Delta Column

Compose provides a Column component for vertically arranging elements. To understand how this component works and how it uses Layout composables, let’s implement a Column of our own. Call it MyColumn for the moment, and the code is as follows:

@Composable fun MyColumn( modifier: Modifier = Modifier, content: @Composable () -> Unit ) { Layout( modifier = modifier, content = content ) { measurables, // Measure each item and convert it to measurables = measurables.map {measurable -> __measurable. Measure (constraints)} val height = placeables.sumOf {it. Height} // Val width = placeables.maxOf {it. Width} // Report the desired size layout (width, Var y = 0 placeables.forEach {placeable -> placeRelative(x = 0, Y = y) // Increment the y coordinate to the height of the item to be placed y += placeable. Height}}}}Copy the code

△ Customize Column

VerticalGrid sample

Delta VerticalGrid

Let’s look at another example: building a regular grid. Part of the code is as follows:

@Composable fun VerticalGrid( modifier: Modifier = Modifier, columns: Int = 2, content: @Composable () -> Unit ) { Layout( content = content, modifier = modifier ) { measurables, Constraints -> val itemWidth = constraints. MaxWidth/columns // use copy to keep the height constraints passed down. Val itemConstraints = constraints. Copy (minWidth = itemWidth, maxWidth = itemWidth, ) // Measure each item using these constraints and convert it to measurables = measurables.map {it. Measure (itemConstraints)}... }}Copy the code

Customize VerticalGrid

In this example, we create new constraints using the copy function. This concept of creating new constraints for child nodes is the way to implement custom measurement logic. The ability to create different Constraints to measure the child is the key to this model. There is no negotiation between the parent node and the child node. The parent node passes in the form of Constraints the range of dimensions it allows for the child node.

The advantage of this design is that we can measure the entire interface tree at once and prohibit multiple measurement cycles. This is a problem with View systems. Nested structures that perform multiple measurements can double the number of measurements on the leaf View, and Compose’s design prevents this from happening. In fact, if you measure an item twice, Compose will throw an exception:

Compose throws an exception when measuring an item repeatedly

Layout animation Example

With stronger performance guarantees, Compose offers new possibilities, such as adding animations to layouts. Layout Composable can be used to create not only general layouts but also specialized layouts that meet the requirements of the application design. Take the custom bottom navigation in the Jetsnack app. In this design, the label is displayed if an item is selected; If not selected, only the icon is displayed. Also, the design needs to animate the size and position of the item based on the current selected state.

△ Custom bottom navigation in the Jetsnack app

We can implement this design using a custom layout that gives us precise control over the animation of layout changes:

@Composable
fun BottomNavItem(
    icon: @Composable BoxScope. () - >Unit,
    text: @Composable BoxScope. () - >Unit.@floatrange (from = 0.0, to = 1.0) animationProgress: Float
) {
    Layout(
        content = {
            // Wrap icon and text in Box
            // This allows us to set layoutId for each projectBox(modifier = modifier. LayoutId (" icon ") content = icon) Box(modifier = modifier. LayoutId (" text ") content = text)} ) { measurables, constraints ->// Use layoutId to make sure __4__ is Measurable, rather than relying on the order of items
        valIconPlaceable = measurables.first {it. LayoutId == "icon"}. Measure (constraints)valTextPlaceable = measurables.first {it. LayoutId == "text"}.// Extract the placement logic into another function to improve code readability
        placeTextAndIcon(
            textPlaceable,
            iconPlaceable,
            constraints.maxWidth,
            constraints.maxHeight,
            animationProgress
        )
    }
}
 
fun MeasureScope.placeTextAndIcon(
    textPlaceable: Placeable,
    iconPlaceable: Placeable,
    width: Int,
    height: Int.@floatrange (from = 0.0, to = 1.0) animationProgress: Float
): MeasureResult {
 
    // Place text and ICONS according to the animation progress value
    val iconY = (height - iconPlaceable.height) / 2
    val textY = (height - textPlaceable.height) / 2
 
    val textWidth = textPlaceable.width * animationProgress
    val iconX = (width - textWidth - iconPlaceable.width) / 2
    val textX = iconX + iconPlaceable.width
 
    return layout(width, height) {
        iconPlaceable.placeRelative(iconX.toInt(), iconY)
        if(animationProgress ! =0f) {
            textPlaceable.placeRelative(textX.toInt(), textY)
        }
    }
}
Copy the code

△ Custom bottom navigation

Time to use custom layouts

Hopefully, the examples above have helped you understand how custom layouts work and how they are applied. Standard layouts are powerful and flexible, but they also need to accommodate many use cases. Sometimes, using a custom layout may be more appropriate if you know the specific implementation requirements.

We recommend using custom layouts when you encounter the following scenarios:

  • Designs that are difficult to achieve with standard layouts. While you can build most interfaces with enough rows and columns, this implementation is sometimes difficult to maintain and upgrade;
  • Very precise control of measurement and placement logic is required;
  • You need to animate the layout. We’re working on a new API that will animate placement, possibly without having to write your own layout;
  • You need complete control over performance. More on this later.

The modifier

So far, you’ve seen Layout composables and how to build a custom Layout. If you’ve ever built an interface with Compose, you know that modifiers play an important role in layout, configuration size, and location. As you can see from the previous example, a Layout composable takes a modifier chain as an argument. Modifiers decorate the elements to which they are attached and can participate in measurement and placement prior to the layout’s own measurement and placement operations. Let’s see how it works.

There are many different types of modifiers that affect different behavior, such as DrawModifier, PointerInputModifier, and FocusModifier. In this article we’ll focus on the Layout modifier, which provides a measure method that does much the same as Layout composables except that it works on individual Measurable terms rather than lists. This is because modifiers are applied to a single item. In the measure method, modifiers can modify constraints or implement custom placement logic, just like layout. This means that you don’t always need to write a custom layout, and you can use modifiers if you only want to operate on a single item.

In the case of the padding modifier, the factory function creates a PaddingModifier object that captures the desired padding value based on the modifier chain.

fun Modifier.padding(all: Dp) =
    this.then(PaddingModifier(
            start = all,
            top = all,
            end = all,
            bottom = all
        )
    )
 
private class PaddingModifier(
    val start: Dp = 0.dp,
    val top: Dp = 0.dp,
    val end: Dp = 0.dp,
    val bottom: Dp = 0.dp
) : LayoutModifier {
 
override fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints
    ): MeasureResult {
        val horizontal = start.roundToPx() + end.roundToPx()
        val vertical = top.roundToPx() + bottom.roundToPx()
 
        // Modify the measurement by shrinking the outer constraint according to the padding size
        val placeable = measurable.measure(constraints.offset(-horizontal, -vertical))
 
        val width = constraints.constrainWidth(placeable.width + horizontal)
        val height = constraints.constrainHeight(placeable.height + vertical)
        return layout(width, height) {
                // Perform the offset with the desired padding to place the content
                placeable.placeRelative(start.roundToPx(), top.roundToPx())
        }
    }
}
Copy the code

Implementation of the △ padding modifier

In addition to overwriting the measure method as in the above example, you can also use modifier.layout to add custom measurement and placement logic to any combinable item directly from the Modifier chain without creating a custom layout, as shown below:

Box(Modifier
            .background(Color.Gray)
            .layout { measurable, constraints ->
                // An example of adding 50 pixel padding in the vertical direction with the modifier
                val padding = 50
                val placeable = measurable.measure(constraints.offset(vertical = -padding))
                layout(placeable.width, placeable.height + padding) {
                    placeable.placeRelative(0, padding)
                }
            }
        ) {
            Box(Modifier.fillMaxSize().background(Color.DarkGray))
        }

Copy the code

Use Modifier. Layout to achieve layout

While Layout accepts a single Modifier parameter, that parameter creates a chain of modifiers that are applied sequentially. Let’s use an example to see how it interacts with the layout model. We’ll examine the effects of the following modifiers and how they work:

An example of the effect of a modifier chain

First, we size and draw the Box, but the Box is placed in the upper left corner of the parent layout. We can use the wrapContentSize modifier to center the Box. WrapContentSize allows content to measure its desired size and then place the content using the align parameter, which defaults to Center, so you can omit this parameter. But we see that Box is still in the upper left corner. This is because most layouts adapt their size to their content, and we need to make the measurement take up the entire space in order to center the Box within the space. Therefore, we add the fillMaxSize layout modifier in front of wrapContentSize to achieve this effect.

△ Application process of modifier chain

Let’s take a look at how these modifiers achieve this effect. You can use the following animation to help you understand this process:

How modifier chains work

Assuming that the Box is to be placed inside a container with a maximum size of 200 by 300 pixels, the container passes the corresponding constraint to the first modifier in the modifier chain. FillMaxSize actually creates a new set of constraints and sets the maximum and minimum widths and heights equal to those passed in to fill to the maximum, which in this case is 200 by 300 pixels. These constraints are passed along the modifier chain to measure the next element. The wrapContentSize modifier takes these parameters and creates new constraints to relax the incoming constraints so that the content measures its desired dimensions, that is, 0-200 wide and 0-300 high. This may seem like just the reverse of the fillMax step, but note that we are using this modifier to center the project, not to resize the project. These constraints are passed along the modifier chain to the size modifier, which creates a constraint of a specific size to measure the item, specifying that the size should be exactly 50 by 50. Finally, these constraints are passed to the layout of the Box, which performs the measurement and returns the parsed size (50*50) to the modifier chain. The size modifier therefore also parses its size to 50*50 and creates the placement instruction accordingly. WrapContent then parses its size and creates placement instructions to center the content. Because the wrapContent modifier knows that its dimensions are 200 by 300 and that the next element’s dimensions are 50 by 50, the placement instruction is created using center alignment to center the content. Finally, fillMaxSize resolves its size and performs the drop operation.

A modifier chain works much like a layout tree, except that each modifier has only one child node, which is the next element in the chain. The constraint is passed down so that subsequent elements can measure themselves with it, then return the parsed size and create the place instruction. The example also illustrates the importance of modifier order. By combining functions using modifiers, you can easily combine different measurement and layout strategies together.

Advanced features

Here are some of the more advanced features of the layout model that you may not always need, but can help you build more advanced features.

Measurement of Intrinsic characteristics

As mentioned earlier, Compose uses a single-spread office system. This is not entirely true, layout is not always done in a single pass, and sometimes we need information about child node sizes to finalize constraints.

Take the pop-up menu. Suppose you have a Column with five menu items, as shown in the figure below. It displays almost normally, but you can see that each menu item has a different size.

△ Different sizes of menu items

It is easy to imagine that each menu item should occupy the maximum allowed size:

△ Each menu item occupies the maximum allowable size

But that doesn’t completely solve the problem, because the menu window expands to its maximum size. An effective solution is to use the maximum natural width to determine the size:

Use the maximum proper width to determine the size

This confirms that Column will do its best to provide the space needed for each child node, and in the case of Text, the width is the width needed to render the entire Text in a single line. Once the intrinsic dimensions are determined, these values are used to set the dimensions of the Column, and the child nodes can then fill in the width of the Column.

What happens if you use a minimum instead of a maximum?

△ Use minimum proper width to determine dimensions

It determines that Column will use the minimum size of child nodes, and that the minimum proper width of Text is the width of one word per line. So we end up with a word-wrapped menu.

For more details on the inherent characteristics measurement, see the “inherent characteristics” section of the layout Codelab in Jetpack Compose.

ParentData

The modifiers we have seen so far are generic modifiers, that is, they can be applied to any composable item. Sometimes your layout provides behavior that requires information from child nodes, using the ParentDataModifier.

Let’s go back to the previous example of centering a blue Box in the parent node. This time, we’ll put this Box inside another Box. The contents of a Box are arranged within a receiver scope called BoxScope. BoxScope defines modifiers that are only available within boxes, and it provides a modifier called Align. This modifier provides the functionality we want to apply to the blue Box. Therefore, if we know that the blue Box is inside another Box, we can use the Align modifier to locate it.

In BoxScope, you can use the Align modifier to position the content

Align is a ParentDataModifier rather than the layout modifier we saw earlier, because it just passes information to its parent, so it’s not available if it’s not in the Box. It contains information that is provided to the parent Box to set up the child layout.

You can also write ParentDataModifier for your own custom layout to allow child nodes to tell their parents information that they can use during layout.

Alignment Lines

We can use alignment lines to set alignment based on criteria other than the top, bottom, or center of the layout. The most common alignment line is the text baseline. Suppose you need to implement a design like this:

△ Need to align ICONS and text in design drawings

It’s natural to think of something like this:

Row {
    Icon(modifier = Modifier
        .size(10. dp)
        .align(Alignment.CenterVertically)
    )
    Text(modifier = Modifier
        .padding(start = 8.dp)
        .align(Alignment.CenterVertically)
    )
}
Copy the code

△ Problematic alignment implementation

If you look closely, you’ll notice that the ICONS are not aligned to the baseline of the text as they are in the design.

△ The icon is centered with the text, and the bottom of the icon does not fall on the text baseline

We can fix this with the following code:

Row {
    Icon(modifier = Modifier
        .size(10. dp)
        .alignBy { it.measuredHeight }
    )
    Text(modifier = Modifier
        .padding(start = 8.dp)
        .alignByBaseline()
    )
}
Copy the code

△ Correct alignment implementation

First, use the alignByBaseline modifier for Text. An icon has neither a baseline nor other alignment lines. We can use the alignBy modifier to align the icon wherever we want. In this case, we know that the bottom of the icon is the target position for alignment, so we align the bottom of the icon. Finally, the desired effect is achieved:

△ The bottom of the icon is perfectly aligned with the text baseline

Because the alignment function goes through the parent node, when dealing with nested alignment, you simply set the alignment line for the parent node, which gets the corresponding value from the child node. As shown in the following example:

△ Nested layout without alignment

△ Set the alignment line through the parent node

You can even create your own custom alignment in a custom layout, allowing other composable items to be aligned to it.

BoxWithConstraints

BoxWithConstraints is a powerful and useful layout. In a composition, we can use logic and control flow to choose what to display based on conditions, but sometimes we may want to determine the layout content based on the amount of space available.

As we know from the previous article, dimensional information is not available until the layout phase, that is, it is generally not used in the composition phase to decide what to display. This is where BoxWithConstraints comes in. It is similar to Box, but it delays the composition of content until the layout phase, when the layout information is already available. The content in BoxWithConstraints is laid out in the sink scope, through which the constraints determined in the layout phase are exposed as pixel values or DP values.

@Composable
fun BoxWithConstraints(... content: @Composable BoxWithConstraintsScope. () - >Unit
)
 
// BoxWithConstraintsScope exposes the constraints determined in the layout phase
interface BoxWithConstraintsScope : BoxScope {
    val constraints: Constraints
    val minWidth: Dp
    val maxWidth: Dp
    val minHeight: Dp
    val maxHeight: Dp
}
Copy the code

Delta BoxWithConstraints and BoxWithConstraintsScope

The content inside it can use these constraints to choose what to combine. For example, select a different rendering depending on the maximum width:

@Composable
fun MyApp(...) {
    BoxWithConstraints() { // this: BoxWithConstraintsScope
        when {
            maxWidth < 400.dp -> CompactLayout()
            maxWidth < 800.dp -> MediumLayout()
            else -> LargeLayout()
        }
    }
}
Copy the code

Select different layouts based on maximum width in BoxWithConstraintsScope

performance

We showed how the single-spread site model prevents excessive time spent on measurement or placement, and also demonstrated two distinct sub-phases of the layout phase: measurement and placement. Now, we’ll cover performance.

Try to avoid restructuring

The single-spread office model is designed to the effect that any modifications that affect only the placement of items and do not affect measurements can be performed separately. Take Jetsnack:

▽ Coordinated scrolling of product details pages in Jetsnack app

The product detail page contains a coordinated scrolling effect where some elements on the page are moved or scaled according to the scrolling operation. Notice the title area, which scrolls along with the content of the page and is pinned to the top of the screen.

@Composable
fun SnackDetail(...). {
    Box {
        val scroll = rememberScrollState(0) Body(scroll) Title(scroll = scroll.value) ... }}@Composable
fun Body(scroll: ScrollState)Column(modifier = modifier. VerticalScroll (scroll)) {... }}Copy the code

△ Rough implementation of detail page

To achieve this effect, we stack the different elements in a Box as separate composable items, extract the scroll state and pass it into the Body component. The Body is set with the scroll state so that the content scrolls vertically. In other components, such as Title, you can observe the scrolling position, and the way we observe it can have an impact on performance. For example, using the most straightforward implementation, we simply offset the content with scroll values:

@Composable
fun Title(scroll: Int)Column(modifier = modifier. Offset (scroll)) {... Column(modifier = modifier. }}Copy the code

Simply use the scroll value to offset the content of the Title

The problem with this approach is that scrolling is an observable state value, and the scope in which that value is read dictates what Compose needs to redo when the state changes. In this example, we read the scroll offset value in the composition and then use it to create the offset modifier. Whenever the scroll offset value changes, the Title component needs to be reassembled, creating and executing a new offset modifier. Since the scroll state is read from the composition, any changes will result in reorganization, during which two subsequent phases of layout and drawing are also required.

However, instead of changing what is displayed, we are changing the location of the content. We can further improve efficiency by modifying the implementation to no longer accept the original scroll position, but to pass a function that provides the scroll position:

@Composable
fun Title(scrollProvider: () -> Int) {
    Column(
        modifier = Modifier.offset {
            val scroll = scrollProvider()
            val offset = (maxOffset - scroll).coerceAtLeast(minOffset)
            IntOffset(x = 0, y = offset)}) {... }}Copy the code

Replace the original scroll position with a function that provides the scroll position

At this point, we can just call the Lambda function and read the scroll state at different times. The offset modifier is used here, which takes as an argument a Lambda function that provides an offset value. This means that the modifier does not need to be recreated when the scroll changes, and the value of the scroll state is read only during the drop phase. As a result, we only need to perform place and draw operations when the scrolling state changes, without regrouping or measuring, thus improving performance.

Going back to the bottom navigation example, it has the same problem and can be fixed in the same way:

@Composable
fun BottomNavItem(
    icon: @Composable BoxScope. () - >Unit,
    text: @Composable BoxScope. () - >Unit,
    animationProgress: () -> Float
){...val progress = animationProgress()
 
    val textWidth = textPlaceable.width * progress
    val iconX = (width - textWidth - iconPlaceable.width) / 2
    val textX = iconX + iconPlaceable.width
 
    return layout(width, height) {
        iconPlaceable.placeRelative(iconX.toInt(), iconY)
        if(animationProgress ! =0f) {
            textPlaceable.placeRelative(textX.toInt(), textY)
        }
    }
}
Copy the code

△ Revised bottom navigation

We use a function that provides the current animation progress as an argument, so we don’t need to regroup, just do the layout.

You need to master the principle that caution should be exercised whenever the parameters of composable items or modifiers can change frequently, as this can lead to excessive composition. Regrouping is required only if you change what is displayed, not if you change where or how it is displayed.

BoxWithConstraints can perform composition based on layout because it starts subcomposition at layout stage. For performance reasons, we want to avoid performing composition during layout as much as possible. Therefore, we prefer to use a layout that changes according to size rather than BoxWithConstraints. BoxWithConstraints is only used when the information type changes with size.

Improve layout performance

Sometimes a layout does not need to measure all its children to know its size. For example, a card with the following composition:

△ Layout card example

The icon and title make up the title bar, and the rest is the text. Given that the icon size is fixed, the title height is the same as the icon height. When you measure the card, you only measure the body, and the constraint is the layout height minus 48 DP, and the card height is the body height plus 48 DP.

△ Only the body size is measured during the measurement process

The system recognizes that only the body is measured, so it is the only important child node that determines the size of the layout. ICONS and text still need to be measured, but can be performed during placement.

△ Place process measurement ICONS and text

Assuming the title is “Layout”, when the title changes, the system does not have to re-measure the Layout, so it does not re-measure the body and saves unnecessary work.

▷ There is no need to remeasure the title when it changes

conclusion

In this article, we’ve shown you how to implement a custom layout and how to use modifiers to build and merge layout behavior, making it even easier to meet exact functional requirements. In addition, some advanced features of the layout system were introduced, such as custom alignment across nested hierarchies, the creation of custom ParentDataModifier for your own layout, support for automatic right-to-left Settings, and the postponement of composition operations until the layout information is known. We also learned how to perform the single-location model and how to skip remeasurements so that they only perform relocation operations. With proficiency in these methods, you will be able to write high-performance layout logic that is animated by gestures.

An understanding of layout systems can help you build layouts that meet specific design requirements, creating great applications that users love. For more information, please refer to the resources listed below:

  • Jetpack Compose uses the getting started documentation
  • Jetpack Compose learning roadmap
  • Jetpack Compose example

Please click here to submit your feedback to us, or share your favorite content or questions. Your feedback is very important to us, thank you for your support!