preface

FlowLayout is a kind of layout commonly used in development, and how to customize FlowLayout is also a high-frequency problem in the interview. Recently, Compose has been released in official version. This article mainly takes FlowLayout as an example. Be familiar with the main process of Compose custom Layout. This article aims to achieve the following effects:

  1. The customLayout, from left to right, beyond a line will be shown on a newline
  2. Support for Settings childViewSpacing and line spacing
  3. whenViewIf the height of a row is inconsistent, the rows can be aligned in the top, middle, and bottom positions

The effect

First, let’s look at the end result

ComposeThe customLayoutprocess

In The Android View system, customizing the Layout generally has the following steps:

  1. Measuring the sonViewWide high
  2. Determine the parent based on the measurement resultsViewWide high
  3. Determine the sub as neededViewplaced

In Compose, we usually use Layout to measure and arrange the subitems, so as to implement a custom Layout. We first implement a custom Layout, as shown below:

@Composable
fun MyBasicColumn(
    modifier: Modifier = Modifier,
    content: @Composable() - >Unit
) {
    Layout(
        modifier = modifier,
        children = content
    ) { measurables, constraints ->
        // Measurement layout subitem}}Copy the code

The Layout has two parameters, measurables is a list of children to measure, and constraints is a constraint from the parent

@Composable
fun MyBasicColumn(
    modifier: Modifier = Modifier,
    content: @Composable() - >Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints ->
        // Subterms to be measured
        val placeables = measurables.map { measurable ->
            // 1. Measurement subitem
            measurable.measure(constraints)
        }

        // 2. Set the Layout width and height
        layout(constraints.maxWidth, constraints.maxHeight) {
            var yPosition = 0

            // Position the child in the parent Layout
            placeables.forEach { placeable ->
                // 3. Position the subitem on the screen
                placeable.placeRelative(x = 0, y = yPosition)

                // Record the y position of the subitem
                yPosition += placeable.height
            }
        }
    }
}
Copy the code

Three things have been done:

  1. Measurement items
  2. After measuring the child, set the parent based on the resultLayoutWide high
  3. Locate and set the position of the subitem on the screen

And then we have a simple custom Layout, and you can see that it’s not that different from the View system so let’s see how we can implement a FlowLayout, right

The customFlowLayout

Let’s first analyze what it takes to implement a FlowLayou.

  1. First we should identify the parentLayoutThe width of the
  2. Traversal measures the children if the width sum exceeds the parentLayoutThe newline
  3. The maximum height of each row is recorded at the same time, and the final height is the sum of the maximum heights of each row
  4. After the above steps, width and height are determined, you can set the parentLayoutThe width and height are up, and the measurement steps are complete
  5. The next step is positioning, traversing the measured subterms and determining their position based on the results of previous measurements

The process is about these, let’s take a look at the implementation

Traverse the measurement to determine the width and height

    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints ->
        val parentWidthSize = constraints.maxWidth
        var lineWidth = 0
        var totalHeight = 0
        var lineHeight = 0
        // Measure the subview to obtain the width and height of the FlowLayout
        measurables.mapIndexed { i, measurable ->
            // Measure the subview
            val placeable = measurable.measure(constraints)
            val childWidth = placeable.width
            val childHeight = placeable.height
            // Newline if the current line width exceeds the parent Layout
            if (lineWidth + childWidth > parentWidthSize) {
                // Record the total height
                totalHeight += lineHeight
                // Reset the line height and line width
                lineWidth = childWidth
                lineHeight = childHeight
                totalHeight += lineSpacing.toPx().toInt()
            } else {
            	// Record the width of each line
                lineWidth += childWidth + if (i == 0) 0 else itemSpacing.toPx().toInt()
                // Record the maximum height of each row
                lineHeight = maxOf(lineHeight, childHeight)
            }
            // The last line is special
            if (i == measurables.size - 1) {
                totalHeight += lineHeight
            }
        }

        / /... Set the width of high
        layout(parentWidthSize, totalHeight) {
            
        }
    }
Copy the code

Above is the code to determine the width and height, mainly do the following things

  1. Cyclic measurement subterm
  2. If the current line width exceeds the parentLayoutThe newline
  3. The maximum height of each line is recorded for each line feed
  4. According to the measurement results, the final parent is determinedLayoutThe wide high

Record the subentries of each row and the maximum height of each row

We have already measured the width and height of the parent Layout, but in order to achieve the effect of center alignment when the height of the child items is inconsistent, we also need to record the maximum height of the child items in each row

    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints ->
        val mAllPlaceables = mutableListOf<MutableList<Placeable>>()  // All subterms
        val mLineHeight = mutableListOf<Int> ()// The maximum height of each row
        var lineViews = mutableListOf<Placeable>() // The content placed in each line
        // Measure the subview to obtain the width and height of the FlowLayout
        measurables.mapIndexed { i, measurable ->
            // Measure the subview
            val placeable = measurable.measure(constraints)
            val childWidth = placeable.width
            val childHeight = placeable.height
            // If the line width exceeds the Layout width, wrap the line
            if (lineWidth + childWidth > parentWidthSize) {
            	// The maximum height of each row is added to the list
                mLineHeight.add(lineHeight)
                // The secondary list holds all the subitems
                mAllPlaceables.add(lineViews)
                // Resets the sublist for each row
                lineViews = mutableListOf()
                lineViews.add(placeable)
            } else {
                // The maximum height of each row
                lineHeight = maxOf(lineHeight, childHeight)
                // The children of each line are added to the list
                lineViews.add(placeable)
            }
            // The last line is special
            if (i == measurables.size - 1) {
                mLineHeight.add(lineHeight)
                mAllPlaceables.add(lineViews)
            }
        }
    }
Copy the code

There are three main things that they do

  1. The maximum height of each row is added to the list
  2. The children of each line are added to the list
  3. willlineViewsList added tomAllPlaceablesTo store all subitems

Locate items

Now that we have completed the measurement above and obtained a list of all the subitems, we are ready to traverse the location

@Composable
fun ComposeFlowLayout(
    modifier: Modifier = Modifier,
    itemSpacing: Dp = 0.dp,
    lineSpacing: Dp = 0.dp,
    gravity: Int = Gravity.TOP,
    content: @Composable() - >Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints ->
        layout(parentWidthSize, totalHeight) {
            var topOffset = 0
            var leftOffset = 0
            // Loop location
            for (i in mAllPlaceables.indices) {
                lineViews = mAllPlaceables[i]
                lineHeight = mLineHeight[i]
                for (j in lineViews.indices) {
                    val child = lineViews[j]
                    val childWidth = child.width
                    val childHeight = child.height
                    // Obtain the y coordinate of the subitem based on Gravity
                    val childTop = getItemTop(gravity, lineHeight, topOffset, childHeight)
                    child.placeRelative(leftOffset, childTop)
                    // Update the x coordinate
                    leftOffset += childWidth + itemSpacing.toPx().toInt()
                }
                // Reset the x coordinate of the subitem
                leftOffset = 0
                // Update the y coordinate of the subterm
                topOffset += lineHeight + lineSpacing.toPx().toInt()
            }
        }
    }
}

private fun getItemTop(gravity: Int, lineHeight: Int, topOffset: Int, childHeight: Int): Int {
    return when (gravity) {
        Gravity.CENTER -> topOffset + (lineHeight - childHeight) / 2
        Gravity.BOTTOM -> topOffset + lineHeight - childHeight
        else -> topOffset
    }
}
Copy the code

To locate a subterm, in fact, is to determine its coordinates, the above mainly do the following things

  1. Iterate over all the subitems
  2. Subterms are determined by locationXwithYcoordinates
  3. According to theGravitySubitems can be aligned above, center, and below

So you have a simple ComposeFlowLayout

conclusion

This paper mainly implements a FlowLayout that supports setting sub-view spacing and line spacing, and supports the sub-view in the top, center and left alignment of Compose. It has learned the basic process of Compose custom Layout and more related knowledge in the future, please look forward to ~

All relevant code for this article

Compose version FlowLayout