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,么么哒