流式布局 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。