Android
下自定义FlowLayout
(流式布局), 类似于微信的标签,1、实现效果
效果类似于微信的标签功能,依次显示标签名,当标签的总宽度(标签宽度 + 边距)超过总的屏幕宽度时,进行换行显示。本篇文章的实现前提是字体大小一致,标签高度一致。
2、实现步骤
上述效果实现主要以下几步:
1.重写ViewGroup
的onMeasure
方法
2.测量单个标签的宽度,包含标签的边距即leftMargin
、rightMargin
3. 测量单个标签的高度,包含标签的边距即topMarin
、bottomMargin
4. 测量父控件的宽度和高度
5. 重写ViewGroup
的onLayout
方法
6. 对标签进行布局、根据规则摆放在父控件中
根据上图可以分析实现功能需要的参数:
lineTotalW:
一行的子标签的总宽度,用于和屏幕宽度比较大小,决定是标签是否换行显示
totalHeight:
所有行子标签的总高度,用于测量父控件的高度
lineTotalW = paddingLeft + (child.measuredWidth + 左边距 + 右边距)+ ......... + paddingRight
totalHeight = paddingTop + (child.measuredHeight + 上边距 + 下边距)+ ........+ paddingBottom
通过上面的分析,对FlowLayout
的实现有了个基本的了解,下面来通过代码实现。
1、重写onMeasure
方法,对标签进行测量、对父控件进行测量
private var totalWSize = 0
/**
* [MeasureSpec] 封装父对象传递给子view的布局要求
* [MeasureSpec] 有尺寸和模式组成
*/
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
//父布局的测量模式
val wMode = MeasureSpec.getMode(widthMeasureSpec)
val hMode = MeasureSpec.getMode(heightMeasureSpec)
//屏幕宽度去除内边距剩余的宽度
totalWSize = MeasureSpec.getSize(widthMeasureSpec) - paddingRight - paddingLeft
var totalHeight = paddingTop
var lineTotalW = paddingLeft
//记录有几行
var lineCount = 1
//去除内边距父容器的最大宽高
val sizeW = MeasureSpec.getSize(widthMeasureSpec) - paddingRight - paddingLeft
val sizeH = MeasureSpec.getSize(heightMeasureSpec) - paddingTop - paddingBottom
val count = childCount
for (i in 0 until count) {
val child = getChildAt(i)
val lp = child.layoutParams as MarginLayoutParams
if (child.visibility == View.GONE) {
break
}
//【1】、创建子view宽的MeasureSpec
val childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(sizeW, if (wMode == MeasureSpec.EXACTLY) MeasureSpec.AT_MOST else wMode)
//【2】、创建子view高的MeasureSpec
val childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(sizeH, if (hMode == MeasureSpec.EXACTLY) MeasureSpec.AT_MOST else hMode)
//【3】、讲测量说明书传递给子view,让子view测量自己
child.measure(childWidthMeasureSpec, childHeightMeasureSpec)
//【4】、计算出一行宽度
lineTotalW += child.measuredWidth + lp.leftMargin + lp.rightMargin
//【5】、第一行的高度处理
if(lineCount == 1 && lineTotalW < totalWSize){
totalHeight = child.measuredHeight + lp.topMargin + lp.bottomMargin
}
//【6】、子view的总宽度和大于屏幕宽度
if(lineTotalW > totalWSize){
//【7】、将一行长度初始化
lineTotalW = paddingLeft + lp.leftMargin + lp.rightMargin + child.measuredWidth
lineCount += 1
//【8】、计算几行子view高度
totalHeight = lineCount * (child.measuredHeight + lp.topMargin + lp.bottomMargin)
}
}
//【9】、计算父控件总高度
totalHeight += paddingTop + paddingBottom
//【10】、测量父控件
setMeasuredDimension(sizeW, measureParentHeight(totalHeight, heightMeasureSpec))
}
/**
* 计算父控件的总高
*/
private fun measureParentHeight(calculateSize:Int,heightMeasureSpec: Int):Int{
val hSize = MeasureSpec.getSize(heightMeasureSpec)
val hMode = MeasureSpec.getMode(heightMeasureSpec)
var result = 0
//测量模式为精确,match_parent或者定值
if(hMode == MeasureSpec.EXACTLY){
result = hSize
}else if(hMode == MeasureSpec.AT_MOST){
//测量模式至多,这里是计算n行子view的行高
result = calculateSize
}
return result
}
2、重写onLayout
方法,对标签进行布局
/**
* 布局子view
*/
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
var lineTotalW = paddingLeft
var l: Int
var r: Int
var t: Int
var b: Int
//记录有几行
var lineCount = 1
val childCount = childCount
for (i in 0 until childCount) {
val child = getChildAt(i)
val lp = child.layoutParams as MarginLayoutParams
//【1】、计算行子view总的宽度
lineTotalW += child.measuredWidth + lp.leftMargin + lp.rightMargin
if(lineTotalW > totalWSize){
//将一行长度初始化
lineTotalW = child.measuredWidth + lp.leftMargin + lp.rightMargin + paddingLeft
lineCount += 1
}
l = if(i == 0){
paddingLeft + lp.leftMargin
}else{
lineTotalW - child.measuredWidth - lp.rightMargin
}
r = l + child.measuredWidth + lp.rightMargin
t = (lineCount - 1) * (lp.topMargin + lp.bottomMargin + child.measuredHeight) + lp.topMargin + paddingTop
b = t + child.measuredHeight + lp.bottomMargin
child.layout(l, t, r, b)
}
}
在开发过程中,为了能够获取标签的左右边距值,即leftMargin
、rightMargin
,需要去获取layoutParams对象,这里用到的是MarginLayoutParams
。直接将child
的layoutParams
转换为MarginLayoutParams
会报ClassCastException
。
val lp = child.layoutParams as MarginLayoutParams
public static class MarginLayoutParams extends ViewGroup.LayoutParams {
}
/**
* Returns a new set of layout parameters based on the supplied attributes set.
*
* @param attrs the attributes to build the layout parameters from
*
* @return an instance of {@link android.view.ViewGroup.LayoutParams} or one
* of its descendants
*/
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
MarginLayoutParams
继承LayoutParams
将ViewGroup
LayoutParams转化为MarginLayoutParams
。
3、重写generateLayoutParams
方法,将ViewGroup
的LayoutParams
转换为MarginLayoutParams
override fun generateLayoutParams(attrs: AttributeSet): LayoutParams {
return MarginLayoutParams(context, attrs)
}
4、XML布局文件中使用
<com.xn.customview.widget.LabelView
android:id="@+id/labelView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/px_15"
android:background="@color/white"
android:paddingBottom="@dimen/px_45"
android:paddingStart="@dimen/px_15"
android:paddingEnd="@dimen/px_15">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_flow_item"
android:paddingStart="@dimen/px_30"
android:paddingEnd="@dimen/px_30"
android:layout_marginTop="@dimen/px_15"
android:layout_marginStart="@dimen/px_15"
android:text="iPhone 13"
android:gravity="center"
android:textColor="@color/white" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_flow_item"
android:paddingStart="@dimen/px_30"
android:paddingEnd="@dimen/px_30"
android:layout_marginTop="@dimen/px_15"
android:text="小米"
android:layout_marginStart="@dimen/px_15"
android:gravity="center"
android:textColor="@color/white" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_flow_item"
android:paddingStart="@dimen/px_30"
android:paddingEnd="@dimen/px_30"
android:layout_marginTop="@dimen/px_15"
android:layout_marginStart="@dimen/px_15"
android:text="HUAWEI Mate X2"
android:gravity="center"
android:textColor="@color/white" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_flow_item"
android:paddingStart="@dimen/px_30"
android:paddingEnd="@dimen/px_30"
android:layout_marginTop="@dimen/px_15"
android:layout_marginStart="@dimen/px_15"
android:text="OPPO Reno8"
android:gravity="center"
android:textColor="@color/white" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_flow_item"
android:paddingStart="@dimen/px_30"
android:paddingEnd="@dimen/px_30"
android:layout_marginTop="@dimen/px_15"
android:layout_marginStart="@dimen/px_15"
android:text="金立S10金钻版手机"
android:gravity="center"
android:textColor="@color/white" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_flow_item"
android:paddingStart="@dimen/px_30"
android:paddingEnd="@dimen/px_30"
android:layout_marginTop="@dimen/px_15"
android:layout_marginStart="@dimen/px_15"
android:text="Meizu"
android:gravity="center"
android:textColor="@color/white" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_flow_item"
android:paddingStart="@dimen/px_30"
android:paddingEnd="@dimen/px_30"
android:layout_marginStart="@dimen/px_15"
android:layout_marginTop="@dimen/px_15"
android:layout_marginEnd="@dimen/px_15"
android:text="三星手机"
android:gravity="center"
android:textColor="@color/white" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_flow_item"
android:paddingStart="@dimen/px_30"
android:paddingEnd="@dimen/px_30"
android:layout_marginTop="@dimen/px_15"
android:layout_marginStart="@dimen/px_15"
android:layout_marginEnd="@dimen/px_15"
android:text="格力手机"
android:gravity="center"
android:textColor="@color/white" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_flow_item"
android:paddingStart="@dimen/px_30"
android:paddingEnd="@dimen/px_30"
android:layout_marginTop="@dimen/px_15"
android:layout_marginStart="@dimen/px_15"
android:layout_marginEnd="@dimen/px_15"
android:text="诺基亚手机"
android:gravity="center"
android:textColor="@color/white" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_flow_item"
android:paddingStart="@dimen/px_30"
android:paddingEnd="@dimen/px_30"
android:layout_marginTop="@dimen/px_15"
android:layout_marginStart="@dimen/px_15"
android:layout_marginEnd="@dimen/px_15"
android:text="Google手机"
android:gravity="center"
android:textColor="@color/white" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_flow_item"
android:paddingStart="@dimen/px_30"
android:paddingEnd="@dimen/px_30"
android:layout_marginTop="@dimen/px_15"
android:layout_marginStart="@dimen/px_15"
android:layout_marginEnd="@dimen/px_15"
android:text="乐视手机"
android:gravity="center"
android:textColor="@color/white" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_flow_item"
android:paddingStart="@dimen/px_30"
android:paddingEnd="@dimen/px_30"
android:layout_marginTop="@dimen/px_15"
android:layout_marginStart="@dimen/px_15"
android:layout_marginEnd="@dimen/px_15"
android:text="Nubia努比亚"
android:gravity="center"
android:textColor="@color/white" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_flow_item"
android:paddingStart="@dimen/px_30"
android:paddingEnd="@dimen/px_30"
android:layout_marginTop="@dimen/px_15"
android:layout_marginStart="@dimen/px_15"
android:layout_marginEnd="@dimen/px_15"
android:text="MOTO 摩托罗拉"
android:gravity="center"
android:textColor="@color/white" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_flow_item"
android:paddingStart="@dimen/px_30"
android:paddingEnd="@dimen/px_30"
android:layout_marginTop="@dimen/px_15"
android:layout_marginStart="@dimen/px_15"
android:layout_marginEnd="@dimen/px_15"
android:text="BlackBerry 黑莓手机"
android:gravity="center"
android:textColor="@color/white" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_flow_item"
android:paddingStart="@dimen/px_30"
android:paddingEnd="@dimen/px_30"
android:layout_marginTop="@dimen/px_15"
android:layout_marginStart="@dimen/px_15"
android:layout_marginEnd="@dimen/px_15"
android:text="Smartisan 锤子科技"
android:gravity="center"
android:textColor="@color/white" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_flow_item"
android:paddingStart="@dimen/px_30"
android:paddingEnd="@dimen/px_30"
android:layout_marginTop="@dimen/px_15"
android:layout_marginStart="@dimen/px_15"
android:layout_marginEnd="@dimen/px_15"
android:text="OnePlus"
android:gravity="center"
android:textColor="@color/white" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_flow_item"
android:paddingStart="@dimen/px_30"
android:paddingEnd="@dimen/px_30"
android:layout_marginTop="@dimen/px_15"
android:layout_marginStart="@dimen/px_15"
android:layout_marginEnd="@dimen/px_15"
android:text="Philips 飞利浦手机"
android:gravity="center"
android:textColor="@color/white" />
com.xn.customview.widget.LabelView>
本篇文章记录了自定义一个简易的FlowLayout
过程,自定义ViewGroup
重写onMeasure
和onLayout
方法。根据规则进行测量和对子View
布局。