自定义自动换行功能的LinearLayout

时间:2024-06-03 07:40:16

前言

最近项目中有个需求,就是可以给用户动态添加标签。标签最大的特点就是横向排列,并且可以自动换行,而且标签的内容自定义,所以标签的长度是不固定的
网上这种开源的一抓一大把,懒得找了,所以自己实现了一个。
先看一下效果自定义自动换行功能的LinearLayout

分析问题

首先先分析一下这个布局的特点:最大的特点就是自动换行。所以需要根据子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());

    }
}

以上就是全部内容了,比较简单。