流式布局 FlowLayout

566 阅读3分钟

流式布局 FlowLayout 在很多的 APP 中都有,是一种非常常见的 UI 效果。要实现 FlowLayout,我们需要通过自定义 ViewGroup。

完整代码如下:

class FlowLayout
@JvmOverloads
constructor(context:Context, attrs: AttributeSet? = null, defStyleAttr:Int = 0)
    : ViewGroup(context, attrs, defStyleAttr)
{
    private val mHorizontalSpacing:Int = 8 //每个item横向间距

    private val mVerticalSpacing:Int = 8 // 每个item纵向间距

    private val allLines:MutableList<MutableList<View>> = ArrayList() // 记录每一行的View,一行一行的存储,用于onLayout

    private val lineHeights:MutableList<Int> = ArrayList() // 记录每一行的行高,用于onLayout

    private val lineViews:MutableList<View> = ArrayList() // 保存一行中的所有的View

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

        /* 先度量孩子,因为Layout的大小一般由布局中所有子View大小之和决定 */

        val childCount = childCount // 子View的总数

        /* 以px为单位的padding大小 */
        val paddingLeft = paddingLeft
        val paddingRight = paddingRight
        val paddingTop = paddingTop
        val paddingBottom = paddingBottom

        val selfWidth = MeasureSpec.getSize(widthMeasureSpec) // 父Layout给当前Layout的宽度估计值
        val selfHeight = MeasureSpec.getSize(heightMeasureSpec) // 父Layout给当前Layout的高度估计值

        var lineWidthUsed = 0 // 记录这行已经使用了多宽的size
        var lineHeight = 0 // 一行的行高

        /* measure过程中,根据所有子View计算得到的Layout应该具有的宽高大小 */
        var parentNeededWidth = 0
        var parentNeededHeight = 0

        for (index in 0 until childCount){

            val childView = getChildAt(index) // 获取每一个子View

            val childLP = childView.layoutParams // 获取每一个子View的LayoutParams

            if (childView.visibility != View.GONE){

                val childWidthMeasureSpec = getChildMeasureSpec(
                        widthMeasureSpec, // 当前Layout从父Layout得到的水平上的MeasureSpec
                        paddingLeft + paddingRight, // 当前Layout水平上的padding
                        childLP.width) // 子View的LayoutParams的宽度,也就是xml布局文件里的android:layout_width所对应的值

                val childHeightMeasureSpec = getChildMeasureSpec(
                        heightMeasureSpec, // 当前Layout从父布局得到的竖直上的MeasureSpec
                        paddingTop + paddingBottom, // 当前Layout竖直上的padding
                        childLP.height) // 子View的LayoutParams的高度,也就是xml布局文件里的android:layout_height所对应的值

                childView.measure(childWidthMeasureSpec, childHeightMeasureSpec) // 子View调用measure进行测量,开始递归子View的onMeasure方法

                /* 获取子view的度量宽高 */
                val childMeasuredWidth = childView.measuredWidth
                val childMeasuredHeight = childView.measuredHeight

                /* 如果一行的宽度超过父Layout所给的宽度,换行 */
                if (childMeasuredWidth + lineWidthUsed + mHorizontalSpacing > selfWidth){

                    /* 一旦换行,当前行的View和高度就确定了,此时我们就可以记录下来了 */
                    allLines.add(lineViews)
                    lineHeights.add(lineHeight)

                    /* 流式布局,宽度由布局中宽度最大的那一行的宽度决定;高度是所有行高之和,记录 */
                    parentNeededWidth = Math.max(parentNeededWidth, lineWidthUsed + mHorizontalSpacing)
                    parentNeededHeight = parentNeededHeight + lineHeight + mVerticalSpacing

                    /* 将记录每一行的相关变量清零 */
                    lineViews.clear()
                    lineWidthUsed = 0
                    lineHeight = 0
                }

                lineViews.add(childView) // view是分行布局的,所以要记录每一行有哪些view,这样可以方便onLayout布局
                lineWidthUsed = lineWidthUsed + childMeasuredWidth + mHorizontalSpacing // 对每一行已经使用的size进行累加、记录
                lineHeight = Math.max(lineHeight, childMeasuredHeight) // 每一行的高度是由每一行中高度最大的item决定

                /* 处理最后一行数据,因为不会触发换行条件,所以单独处理 */
                if (index == childCount - 1) {

                    /* 记录最后一行的View和高度 */
                    allLines.add(lineViews)
                    lineHeights.add(lineHeight)

                    /* 记录最终流式布局的宽高 */
                    parentNeededHeight = parentNeededHeight + lineHeight + mVerticalSpacing
                    parentNeededWidth = Math.max(parentNeededWidth, lineWidthUsed + mHorizontalSpacing)
                }
            }
        }

        /* 获取MeasureSpec中的mode */
        val widthMode = MeasureSpec.getMode(widthMeasureSpec)
        val heightMode = MeasureSpec.getMode(heightMeasureSpec)

        /* 根据mode判断是否是MeasureSpec.EXACTLY,如果是MeasureSpec.EXACTLY,
           则当前Layout的实际宽高就是估计值;否则就是根据所有子View进行计算之后的宽高 */
        val realWidth = if (widthMode == MeasureSpec.EXACTLY) selfWidth else parentNeededWidth
        val realHeight = if (heightMode == MeasureSpec.EXACTLY) selfHeight else parentNeededHeight

        /* 保存计算后得到的真实宽高 */
        setMeasuredDimension(realWidth, realHeight)
    }

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

        val lineCount = allLines.size // 总行数

        /* 以px为单位的padding大小 */
        var curL = paddingLeft
        var curT = paddingTop

        for (i in 0 until lineCount) {

            val lineViews: List<View> = allLines[i] // 一行的所记录下来的View

            val lineHeight = lineHeights[i] // 一行的高度

            /* 遍历一行的View */
            for (j in lineViews.indices) {

                val view = lineViews[j]

                /* 确定View的所在位置 */
                val left = curL
                val top = curT
                val right = left + view.measuredWidth
                val bottom = top + view.measuredHeight

                view.layout(left, top, right, bottom) // 将View的位置记录下来

                curL = right + mHorizontalSpacing // 将下个item的左边界进行设置
            }

            /* 一行确定之后,换行,重新设置左边界和高度 */
            curT = curT + lineHeight + mVerticalSpacing
            curL = paddingLeft
        }
    }
}

FlowLayout 的效果还可以搭配 RecyclerView 来实现。要搭配 RecyclerView 来实现,就需要自定义 LayoutManager。