前言
最近项目中有个需求,就是可以给用户动态添加标签。标签最大的特点就是横向排列,并且可以自动换行,而且标签的内容自定义,所以标签的长度是不固定的。
网上这种开源的一抓一大把,懒得找了,所以自己实现了一个。
先看一下效果
分析问题
首先先分析一下这个布局的特点:最大的特点就是自动换行。所以需要根据子view的宽度,计算换行的时机,并根据换行后的子View的高度计算布局的高度。
所以自定义ViewGroup,只需要重写onMeasure方法和onLayout方法即可。
注意我这里有个前提:每个子View(标签)的Margin属性分别一样
解决问题
1、onMeasure方法
在onMeasure方法中计算换行的时机,并计算总高度。设置layout的高度。
这种布局的宽度一般都是一定的大小,layout_width属性不是match_parent就是固定的值
所以宽度不用特殊处理。并且还有一个隐藏的点、一般这种需求中,子View(即标签)的高度是一致的。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (getChildCount() == 0){
super.onMeasure(widthMeasureSpec,heightMeasureSpec);
return;
}
//如果有子view,行数肯定至少1行
int lineCount = 1;
//此布局高度一般是wrap_content,所以需要对AT_MOST模式做处理
if (heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED){
//子View的宽度之和
int childrenTotalWidth = 0;
View childView = null;
LinearLayout.LayoutParams params = null;
//循环子View,分别测量子View的宽高
for (int i = 0 ; i < getChildCount() ; i ++){
childView = getChildAt(i);
if (childView.getVisibility() != GONE) {
params = ((LinearLayout.LayoutParams) childView.getLayoutParams());
//测量子View
measureChild(childView, widthMeasureSpec, heightMeasureSpec);
//把子View的宽度和margin属性做加和
childrenTotalWidth += childView.getMeasuredWidth() + params.leftMargin + params.rightMargin;
//比较此layout的width和子View的总宽度
if (childrenTotalWidth > width) {//条件成立,即折行
//行数加1
lineCount++;
//把子view的总宽度置为当前子view的宽度,以便后续的子view宽度的继续加和操作
childrenTotalWidth = childView.getMeasuredWidth() + params.leftMargin + params.rightMargin;
}
}
}
//循环结束,即可得到lineCount的值
LayoutParams layoutParams = (LayoutParams) getChildAt(0).getLayoutParams();
//注意这里设置的所有的子View的topMargin和bottomMargin分别一样。
//由于子View的高度一致,所以取第一个子View的高度和其上下margin属性,乘以行数,即可得到layout在AT_MOST模式下总高度
height = (getChildAt(0).getMeasuredHeight() + layoutParams.topMargin + layoutParams.bottomMargin) * lineCount;
}
setMeasuredDimension(width,height);
}
1、onLayout方法
在onLayout方法中,需要计算每一个子View位于第几行、第几列,计算在第几行,就知道当前子View的top属性,计算在第几列就知道当前子View的left属性,这样就确定了子View的位置了。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (getChildCount() == 0){
super.onLayout(changed,l,t,r,b);
return;
}
//子view的总宽度,用来做折行判断,并计算出所有位于每一行行首的子View的索引
int currentLineTotalWidth = 0;
//存储每一行行首子View的索引
List<Integer> list = new ArrayList<>();
//第一个子View肯定位于行首
list.add(0);
View child = null;
LinearLayout.LayoutParams p = null;
//当前行
int currentLine = 0;
for (int i = 0 ; i < getChildCount() ; i ++){
child = getChildAt(i);
p = ((LayoutParams) child.getLayoutParams());
currentLineTotalWidth += child.getMeasuredWidth() + p.leftMargin + p.rightMargin;
//同onMeasure方法中的判断,判断折行的位置
if (currentLineTotalWidth > getMeasuredWidth()){//条件满足则折行
//并把当前View的索引存储list中
list.add(i);
//重置currentLineTotalWidth
currentLineTotalWidth = child.getMeasuredWidth() + p.leftMargin + p.rightMargin;
}
int left = 0;
int top = 0;
//设置当前行
currentLine = list.size() - 1;
//循环每一行的textView计算当前view的left
for (int m = list.get(currentLine); m < i; m++) {
left += getChildAt(m).getMeasuredWidth() + ((LayoutParams) getChildAt(m).getLayoutParams()).leftMargin + ((LayoutParams) getChildAt(m).getLayoutParams()).rightMargin;
}
//计算出的left需要加上当前子View的leftMargin属性
left += p.leftMargin;
//注意这里设置的所有的子View的topMargin和bottomMargin分别一样。
//top属性是由行数乘以行高,并加上当前View的top属性
top = (getChildAt(0).getMeasuredHeight() + p.topMargin + p.bottomMargin) * currentLine + p.topMargin;
//调用子view的layout方法去完成布局
child.layout(left, top, left + child.getMeasuredWidth(), top + child.getMeasuredHeight());
}
}
以上就是全部内容了,比较简单。