FlowLayout is a very common UI effect found in many apps. To implement FlowLayout, we need to customize the ViewGroup.

The complete code is as follows:

class FlowLayout
@JvmOverloads
constructor(context:Context, attrs: AttributeSet? = null, defStyleAttr:Int = 0)
    : ViewGroup(context, attrs, defStyleAttr)
{
    private val mHorizontalSpacing:Int = 8 // Horizontal spacing for each item

    private val mVerticalSpacing:Int = 8 // Vertical spacing for each item

    private val allLines:MutableList<MutableList<View>> = ArrayList() // Record each row of View, row by row, for onLayout

    private val lineHeights:MutableList<Int> = ArrayList() // Record the height of each row for onLayout

    private val lineViews:MutableList<View> = ArrayList() // Save all views in a row

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {

        /* Measure the children first, since Layout size is generally determined by the sum of all the child views in the Layout */

        val childCount = childCount // The total number of subviews

        /* The padding in px */
        val paddingLeft = paddingLeft
        val paddingRight = paddingRight
        val paddingTop = paddingTop
        val paddingBottom = paddingBottom

        val selfWidth = MeasureSpec.getSize(widthMeasureSpec) // The width estimate given by the parent Layout to the current Layout
        val selfHeight = MeasureSpec.getSize(heightMeasureSpec) // The height estimate given by the parent Layout to the current Layout

        var lineWidthUsed = 0 // Record how wide the line has been used
        var lineHeight = 0 // The height of a row

        /* Measure the width and height of the Layout calculated from all child views */
        var parentNeededWidth = 0
        var parentNeededHeight = 0

        for (index in 0 until childCount){

            val childView = getChildAt(index) // Get each child View

            val childLP = childView.layoutParams // Get the LayoutParams of each child View

            if(childView.visibility ! = View.GONE){val childWidthMeasureSpec = getChildMeasureSpec(
                        widthMeasureSpec, // The current Layout is a MeasureSpec at the level obtained by the parent Layout
                        paddingLeft + paddingRight, // Padding at the current Layout level
                        childLP.width) // The width of the child View's LayoutParams, which corresponds to the Android :layout_width in the XML layout file

                val childHeightMeasureSpec = getChildMeasureSpec(
                        heightMeasureSpec, // The current Layout is the vertical MeasureSpec derived from the parent Layout
                        paddingTop + paddingBottom, // The vertical padding of the current Layout
                        childLP.height) // The height of the child View's LayoutParams, which corresponds to android:layout_height in the XML layout file

                childView.measure(childWidthMeasureSpec, childHeightMeasureSpec) // The onMeasure method of the child View is recursive

                /* Get the width and height of the child view */
                val childMeasuredWidth = childView.measuredWidth
                val childMeasuredHeight = childView.measuredHeight

                /* If the width of a row exceeds the width given by the parent Layout, newline */
                if (childMeasuredWidth + lineWidthUsed + mHorizontalSpacing > selfWidth){

                    /* Once the line is wrapped, the View and height of the current line are determined and we can record it */
                    allLines.add(lineViews)
                    lineHeights.add(lineHeight)

                    /* The width of the layout is determined by the width of the row with the widest width; The height is the sum of the heights of all rows, recorded as */
                    parentNeededWidth = Math.max(parentNeededWidth, lineWidthUsed + mHorizontalSpacing)
                    parentNeededHeight = parentNeededHeight + lineHeight + mVerticalSpacing

                    /* Will record each row of related variables to zero */
                    lineViews.clear()
                    lineWidthUsed = 0
                    lineHeight = 0
                }

                lineViews.add(childView) // View is a branch layout, so keep track of the views in each row to facilitate onLayout
                lineWidthUsed = lineWidthUsed + childMeasuredWidth + mHorizontalSpacing // Add and record the size already used in each line
                lineHeight = Math.max(lineHeight, childMeasuredHeight) // The height of each row is determined by the highest item in each row

                /* Processes the last line of data. Since no newline condition is triggered, */ is processed separately
                if (index == childCount - 1) {

                    /* Record the View and height of the last line */
                    allLines.add(lineViews)
                    lineHeights.add(lineHeight)

                    /* Record the width and height of the final streaming layout */
                    parentNeededHeight = parentNeededHeight + lineHeight + mVerticalSpacing
                    parentNeededWidth = Math.max(parentNeededWidth, lineWidthUsed + mHorizontalSpacing)
                }
            }
        }

        /* Get mode */ from MeasureSpec
        val widthMode = MeasureSpec.getMode(widthMeasureSpec)
        val heightMode = MeasureSpec.getMode(heightMeasureSpec)

        /* If MeasureSpec.EXACTLY, the current Layout width is estimated; Otherwise, it is the width and height calculated based on all the child Views */
        val realWidth = if (widthMode == MeasureSpec.EXACTLY) selfWidth else parentNeededWidth
        val realHeight = if (heightMode == MeasureSpec.EXACTLY) selfHeight else parentNeededHeight

        /* Save the calculated width and height */
        setMeasuredDimension(realWidth, realHeight)
    }

    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {

        val lineCount = allLines.size / / the total number of rows

        /* The padding in px */
        var curL = paddingLeft
        var curT = paddingTop

        for (i in 0 until lineCount) {

            val lineViews: List<View> = allLines[i] // A row of recorded views

            val lineHeight = lineHeights[i] // The height of a row

            /* Iterate over a line of View */
            for (j in lineViews.indices) {

                val view = lineViews[j]

                /* Locate the View */
                val left = curL
                val top = curT
                val right = left + view.measuredWidth
                val bottom = top + view.measuredHeight

                view.layout(left, top, right, bottom) // Record the View position

                curL = right + mHorizontalSpacing // Set the left edge of the next item
            }

            /* After a line is identified, line break, reset the left margin and height */
            curT = curT + lineHeight + mVerticalSpacing
            curL = paddingLeft
        }
    }
}
Copy the code

The effect of FlowLayout can also be matched with RecyclerView to achieve. To match RecyclerView to achieve, you need to customize LayoutManager.