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.