一步步实现自定义View之流式布局

首先看一下效果图吧:
这里写图片描述

1.实现原理

分为两个部分,容器:继承自ViewGroup的TagViewLayout,单个item:继承自VIew的TagView。
下面先看一下TagView
1.1 TagView 比较简单,主要就是绘制一个矩形(可以带有圆角),中间有文字。在onMeasure方法中做好测量即可。

1.2 TagViewLayout
需要重写generateLayoutParams方法,返回的LayoutParams需要带有Margins,所以返回一个MarginLayoutParams。

onMeasure方法中,如果TagViewLayout的模式是MeasureSpec.AT_MOST,我们需要做的是测量子View的尺寸信息,一个一个计算子View的高度和宽度。如果子View的宽度相加没有超过TagViewLayout的宽度,则将子View加在同一行,如果子View相加的宽度超过了TagViewLayout的宽度,则将子View加到另外一行。上一行的高度由上一行中的子View中的最大高度决定。
举个例子(当TagViewLayout的宽高不确定的时候 MeasureSpec.AT_MOST):
如上图所示,循环加入一个个子View,当加了四个子View之后,加第五个的时候,我们发现这五个子View加起来的宽度已经超过了TagViewLayout的宽度,我们需要将第五个子View加入到第二行,以此类推,测完所有的子View之后,就可以得到这些行中最大的行宽和所有行加起来的高度,从而确定了TagViewLayout的宽高

onLayout方法中,需要计算每一个子View的宽高
例如宽度

 childWidth = view.measuredWidth + lp.leftMargin + lp.rightMargin

高度

 childHeight = view.measuredHeight + lp.topMargin + lp.bottomMargin

原理同上面说的一样,用for循环一个一个将子View加进去,记录一行的最大高度,当n个子View相加的宽度大于测量的TagViewLayout的宽度就换行(left + childWidth > right),同时,将TagViewLayout的top属性加上当前行的高度。具体实现看下面的代码吧。

2具体实现
2.1重写generateLayoutParams方法

/*
* 让子控件能获取距离父控件的margin属性
* */
override fun generateLayoutParams(attrs: AttributeSet?): LayoutParams {
    return MarginLayoutParams(mContext,attrs)
}

2.2重写onMeasure方法

val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
val suggestWidth = MeasureSpec.getSize(widthMeasureSpec)
val suggestHeight = MeasureSpec.getSize(heightMeasureSpec)

//测量子view的尺寸信息
measureChildren(widthMeasureSpec,heightMeasureSpec)

var cWidth: Int//childView的宽度
var cHeight: Int
var lineWidth: Int = paddingLeft + paddingRight//当前行的宽度
var maxLineWidth: Int = lineWidth // 用于记录的最大行宽度,但是不能超过suggestWidth
var singleLineHeight: Int = 0
var maxLineHeight: Int = paddingTop + paddingBottom//用于记录控件的最大高度,同上
var childParams: MarginLayoutParams//childView的margin属性
var resultWidth: Int = suggestWidth//最终的宽度
var resultHeight: Int = suggestHeight//最终的高度

for (i in 0 until childCount){
    val view = getChildAt(i)

    if(view.layoutParams is MarginLayoutParams){
        childParams = view.layoutParams as MarginLayoutParams
    }else{
        childParams = ViewGroup.MarginLayoutParams(view.layoutParams)
    }

    cWidth = view.measuredWidth + childParams.leftMargin + childParams.rightMargin
    cHeight = view.measuredHeight + childParams.topMargin + childParams.bottomMargin

    /**如果后者不判断的话,当出现widthMode为MeasureSpec.EXACTLY,
     * 而heightMode == MeasureSpec.AT_MOST时
     * 会出现最后设置的高度为零的情况,导致界面不显示
     * */
    if(widthMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.AT_MOST){
        if(lineWidth + cWidth > suggestWidth){//需要换行
            lineWidth = cWidth + paddingLeft + paddingRight//新的一行的宽度
            maxLineHeight += singleLineHeight//将上一行的高度加上
            singleLineHeight = cHeight//新的一行的高度
        }else{
            lineWidth += cWidth
            if(lineWidth > maxLineWidth){
                maxLineWidth = lineWidth//用于记录这么多行中宽度最大的一行
            }
        }

        if(singleLineHeight < cHeight){//目的是设置单行的高度为这一行中最高的childView
            singleLineHeight = cHeight
        }

        if(i == childCount - 1){//当来到最后一个childview的时候,不管这行有没有满,都要加上这一行的高度
            maxLineHeight += singleLineHeight
        }
    }


}

if(widthMode == MeasureSpec.AT_MOST){
    resultWidth = maxLineWidth
}

if(heightMode == MeasureSpec.AT_MOST){
    resultHeight = maxLineHeight
    if(resultHeight > suggestHeight){
        resultHeight = suggestHeight
    }
}

setMeasuredDimension(resultWidth,resultHeight)

2.3重写onLayout方法

 var left: Int = paddingLeft
var right: Int = width - paddingRight
var top: Int = paddingTop
var bottom: Int = height - paddingBottom

var singleLineHeight: Int = 0
var lp: MarginLayoutParams
var childWidth: Int
var childHeight: Int

for (index in 0 until childCount){
    val view = getChildAt(index)

    if(view.layoutParams is MarginLayoutParams){
        lp = view.layoutParams as MarginLayoutParams
    }else{
        lp = ViewGroup.MarginLayoutParams(view.layoutParams)
    }
    childWidth = view.measuredWidth + lp.leftMargin + lp.rightMargin
    childHeight = view.measuredHeight + lp.topMargin + lp.bottomMargin

    if(left + childWidth > right){//该换行了
        left = paddingLeft//新起点都是这个
        top += singleLineHeight
        singleLineHeight = childHeight
    }else{
        if(singleLineHeight < childHeight){
            singleLineHeight = childHeight
        }
    }

    if(top >= bottom){
        break
    }

    //绘制子view的位置
    view.layout(left + lp.leftMargin,top + lp.topMargin,left + childWidth,top + childHeight)

    left += childWidth//绘制完一个view后,left的位置显然要增加上刚刚绘制的view的宽度
}

3.控件的使用
可以在xml中的TagViewLayout布局下直接加子View

 <com.ckw.customviewcollections.tag_view.TagViewLayout
        android:id="@+id/tag_view_layout"
        android:padding="4dp"
        android:background="@color/gray"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <com.ckw.customviewcollections.tag_view.TagView
            android:layout_margin="4dp"
            app:tagViewText="这是为你好"
            app:tagViewTextColor="@color/black"
            app:tagViewTextSize="10sp"
            app:tagViewCorner="16dp"
            app:tagViewBgColor="@color/colorYellow"
            android:layout_width="wrap_content"

 </com.ckw.customviewcollections.tag_view.TagViewLayout>

或者通过对外提供的方法

/*
* 添加childView
* */
fun addTagView(tagView: TagView) {
    addView(tagView)
}
val tagView = TagView(this)
tagView.setMargins(4,4,4,4)
tagView.setTagViewBackground(Color.RED)
tagView.setTagViewCorner(index)
tagView.setTagViewText("中国人")
tagView.setTagViewTextSize(10)
tagView.setTagViewTextColor(Color.BLACK)
tagViewLayout.addTagView(tagView)

如果你对这个自定义View有兴趣,或者对上面提到的某些点有疑问,可以去看这里猛戳这里
顺便求个star,么么哒

猜你喜欢

转载自blog.csdn.net/ckwccc/article/details/80782174