FlowLayout
FlowLayout就是流式布局,为什么要起这么一个名字?因为大家都这么叫它,不管名字起得多么花哨,功能都是大同小异的。如图:
我为什么要实现流式布局? 源于工作的需要,最近要做这么一个需求,有这么一些标签(数组,数量不确定,字数不确定),他们需要一个一个的往一个容器里边装,每个标签宽度不确定,如果标签在这一排放不下(剩余宽度不够),那么就要放到下一排。而且因为是数组,所以首先联想到的就是Adapter。
我也试着找寻官方提供的控件和第三方的开源框架,发现都没有完全满足我的需求的:
我需要的是一个容器控件,能够支持适配器,负责它所有的ChildView的布局(不需要ChildView的缓存),ChildView的宽高不定,不能限定每一行放多少个ChildView,不需要任何方向的滚动。所以,综上,我还是自己定义吧,与此同时,也能在自己实现的过程中提高自己的开发能力。
那么开始吧 自定义ViewGroup是老生常谈的话题,一个自定义ViewGroup的基本流程就是获取布局参数->测量->布局
框架搭建 FlowLayout我使用Kotlin来实现,基础框架搭建如下:
1 2 3 4 5 6 7 8 9 class FlowLayout : ViewGroup { constructor (context: Context, attrs: AttributeSet?, defStyleAttr: Int ) : super (context, attrs, defStyleAttr) { getParams(context, attrs, defStyleAttr) } constructor (context: Context, attrs: AttributeSet?) : this (context, attrs, 0 ) constructor (context: Context) : this (context, null ) }
获取布局参数 这里不需要过多的参数,只要两个,水平和竖直两个方向的边距:
1 2 3 4 5 6 7 private fun getParams (context: Context , attrs: AttributeSet ?, defStyleAttr: Int ) { context.obtainStyledAttributes(attrs, R.styleable.FlowLayout, defStyleAttr, 0 ).apply { columnSpace = getDimensionPixelSize(R.styleable.FlowLayout_fl_columnSpace, 0 ) rowSpace = getDimensionPixelSize(R.styleable.FlowLayout_fl_rowSpace, 0 ) recycle() } }
测量 测量的主要目的是告诉FlowLayout的父级容器,我FlowLayout要占多大的空间。首先父级会告诉我们,我的测量模式和预设宽高:
1 2 3 4 val widthMode = View.MeasureSpec.getMode(widthMeasureSpec)val heightMode = View.MeasureSpec.getMode(heightMeasureSpec)val widthSize = View.MeasureSpec.getSize(widthMeasureSpec)val heightSize = View.MeasureSpec.getSize(heightMeasureSpec)
测量模式有且仅有三种:
EXACTLY,“定了就这么多”,父级告诉自己一个确定的值
AT_MOST,“最多就这么多了”,父级给定一个最大值
UNSPECIFIED,“要多少有多少”,这种情况很少见
那么是不是在EXACTLY模式下,就不用测量了呢?当然不是,这里测量之后,还要给每个子View设置参数,方便在布局的时候计算每个子View放置的位置。
在测量时,需要考虑左右的padding:
1 val availableWidth = widthSize - paddingStart - paddingEnd
测量子View时的基本方法是循环,在每次循环时判断是否需要换行,参考的依据就是,当前行已经占有的宽度加上当前ChildView要占据的宽度:
1 2 3 4 5 6 7 val childWidth = childView.measuredWidth + layoutParams.marginStart + layoutParams.marginEndval predictLineWidth = lineWidth + (if (lineWidth == 0 ) childWidth else (columnSpace + childWidth))if (predictLineWidth <= availableWidth) { } else { }
为了便于布局,要在测量时,计算出每一个ChildView的行列号并赋值给相应的LayoutParams。因此我们需要自定义一个LayoutParams类:
1 2 3 4 5 6 7 8 9 10 open class LayoutParams : MarginLayoutParams { constructor (context: Context, attrs: AttributeSet) : super (context, attrs) @Suppress("unused" ) constructor (width: Int , height: Int ) : super (width, height) constructor (source: ViewGroup.LayoutParams) : super (source) var layoutColumn = 0 var layoutRow = 0 }
循环计算时,需要几个变量:
1 2 3 4 5 6 var maxLineWidth = 0 var maxLineHeight = 0 var lineWidth = 0 var totalHeight = 0 var columnIndex = 0 var rowIndex = 0
循环计算的完整代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 (0 until childCount).forEach { val childView = getChildAt(it) measureChildWithMargins(childView, widthMeasureSpec, 0 , heightMeasureSpec, 0 ) val layoutParams = childView.layoutParams as FlowLayout.LayoutParams val childWidth = childView.measuredWidth + layoutParams.marginStart + layoutParams.marginEnd val childHeight = childView.measuredHeight + layoutParams.topMargin + layoutParams.bottomMargin val predictLineWidth = lineWidth + (if (lineWidth == 0 ) childWidth else (columnSpace + childWidth)) if (predictLineWidth <= availableWidth) { lineWidth = predictLineWidth maxLineHeight = Math.max(maxLineHeight, childHeight) layoutParams.layoutColumn = columnIndex layoutParams.layoutRow = rowIndex columnIndex++ } else { columnIndex = 0 rowIndex++ maxLineWidth = Math.max(lineWidth, maxLineWidth) totalHeight += (maxLineHeight + rowSpace) lineHeightList.add(maxLineHeight) layoutParams.layoutColumn = columnIndex layoutParams.layoutRow = rowIndex maxLineHeight = if (it == childCount - 1 ) maxLineHeight else 0 lineWidth = childWidth columnIndex++ } } totalHeight += maxLineHeight lineHeightList.add(maxLineHeight)
其中有几个要点如下:
布局 布局相对简单,因为大多数的工作前边测量时已经完成了:
1 2 3 4 5 6 7 8 9 10 11 (0 until childCount).forEach { val childView = getChildAt(it) val layoutParams = childView.layoutParams as FlowLayout.LayoutParams val cLeft = paddingStart + layoutParams.marginStart + calculateLeftPosition(it, layoutParams.layoutColumn) val cTop = paddingTop + calculateTopPosition(it, layoutParams.layoutRow) val cRight = cLeft + childView.measuredWidth val cBottom = cTop + childView.measuredHeight childView.layout(cLeft, cTop, cRight, cBottom) }
在计算childView的左边的距离时,使用递归来计算,不要算上本身的左边距:
1 2 3 4 5 6 7 8 9 10 private fun calculateLeftPosition (layoutIndex: Int , colIndex: Int ) : Int { return if (colIndex > 0 ) { val previousChild = getChildAt(layoutIndex - 1 ) val previousLayoutParams = previousChild.layoutParams as FlowLayout.LayoutParams val previousWidth = previousChild.measuredWidth + previousLayoutParams.marginStart + previousLayoutParams.marginEnd columnSpace + previousWidth + calculateLeftPosition(layoutIndex - 1 , colIndex - 1 ) } else { 0 } }
上边的距离则相对简单,原理同计算左边距:
1 2 3 4 5 6 7 private fun calculateTopPosition (layoutIndex: Int , rowIndex: Int ) : Int { return if (rowIndex > 0 ) { rowSpace + lineHeightList[rowIndex] + calculateTopPosition(layoutIndex - 1 , rowIndex - 1 ) } else { 0 } }
监听适配器刷新 当我们在调用适配器的notifyDataSetChanged方法的时候,FlowLayout需要作出响应,刷新布局,因此,需要一个DataSetObserver:
1 2 3 4 5 6 7 8 9 10 11 inner class AdapterDataSetObserver : DataSetObserver () { override fun onChanged () { resetData() requestLayout() } override fun onInvalidated () { resetData() requestLayout() } }
调用notifyDataSetChanged()
会触发这里的onChanged()
方法,调用notifyDataSetInvalidated()
会触发这里的onInvalidated()
方法,但是还没完,写好了之后还要“注册”:
1 2 3 4 5 6 7 override fun onAttachedToWindow () { super .onAttachedToWindow() if ((adapter != null ) and (adapterDataSetObserver == null )) { adapterDataSetObserver = AdapterDataSetObserver() adapter?.registerDataSetObserver(adapterDataSetObserver) } }
在重设适配器时也需要重新“绑定”:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 fun setAdapter (adapter: BaseAdapter ?) { if (this .adapter !== adapter) { this .adapter?.let { a -> adapterDataSetObserver?.let { o -> a.unregisterDataSetObserver(o) } } adapter?.let { adapterDataSetObserver = AdapterDataSetObserver() it.registerDataSetObserver(adapterDataSetObserver) this .adapter = it.apply { (0 until this @apply .count).forEach { val childView = this @apply .getView(it, null , this @FlowLayout ) addView(childView,generateLayoutParams(childView.layoutParams)) } } requestLayout() } ?: let { this .adapter = null removeAllViews() } } }
剩下的就没什么可说的了,当然这个FlowLayout设计肯定还是有缺陷的,以后会逐渐的改进
Github地址:https://github.com/peceoqicka/FlowLayoutDemo