Android开发自定义ViewGroup流式布局

流式布局

又到了一年一度的高考,回想起来自己参加高考已是八年前的事情了。有时候还是在梦中梦到高中生活,真的是,流光容易把人抛,红了樱桃,绿了芭蕉。
项目中有需求要用到流式布局,自己就自定义了一个FlowLayout,感觉还是有必要记录一下的。网上也有人写这种布局,不过连最基本的子控件的margin,viewgroup的wrap_content都没有处理,所以还是自己重新写了一遍。
以下的代码是从项目里抽出来的,已经去掉了不相关的代码,如果有需要的话可以自己扩展。先看下最终效果吧。

在这里插入图片描述
图中背景黄色部分就是我们的自定义ViewGroup,所谓流式布局最重要的一点就是可以换行。下面一起学习下怎么去实现这种布局吧。

实现过程

首先先思考一下如果要实现这种布局需要考虑什么,需要考虑viewgroup的宽高,如果是wrap_content的话就需要我们自己去测量出它的宽高。还要考虑子控件的margin值和viewgroup的padding值。这两个值在测量和摆放时候都需要加上。
整体思路就是在onMeasure时候去计算每行的子布局,并且记录下来,用一个List记录。然后根据这个List去摆放每一个子控件。这个List是二维的,它的每个item也是一个list,item的list是记录的每一行的view。下面看代码吧:

import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import java.util.ArrayList;
import java.util.List;

public class FlowLayout extends ViewGroup {
    //子布局的数量
    private int childCount = 0;
    //记录所有的子view 每一项就是每一行view的集合
    private List<List<View>> childList = new ArrayList<>();
    //每一行的view
    private List<View> lineList = new ArrayList<>();
    //记录每一行的高度 只记录这一行最高的控件
    private List<Integer> heightList = new ArrayList<>();
    //FlowLayout的padding值
    private int paddingLeft = 0;
    private int paddingRight = 0;
    private int paddingTop = 0;
    private int paddingBottom = 0;
    //标志位 不要重复测量
    private boolean isMeasure = false;

    public FlowLayout(Context context) {
        super(context);
    }

    public FlowLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    /***
     * xml布局被解析成View对象的过程中,viewGroup.addView(view, params)传入的
     * params就是通过viewGroup.generateLayoutParams(attrs)获得的,参数attrs
     * 里包装的就是这个view在xml中的属性,所以如果我们不重写generateLayoutParams()
     * 方法,那这个viewGroup里的子view就不支持margin设置了。
     * @param attrs
     * @return
     */
    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MarginLayoutParams(getContext(), attrs);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //获取自身宽度的测量规则和父容器允许的宽度大小
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        //获取自身高度的测量规则和父容器允许的高度大小
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        //得到子布局个数
        childCount = getChildCount();
        //获取自身设置的Padding值
        paddingLeft = getPaddingLeft();
        paddingRight = getPaddingRight();
        paddingTop = getPaddingTop();
        paddingBottom = getPaddingBottom();

        //测量的宽度 记录实际的宽高 wrap_content才用得到
        int measureWidth = paddingLeft + paddingRight;
        int measureHeight = paddingTop + paddingBottom;
        //记录一行的宽度
        int lineWidth = paddingRight + paddingLeft;
        //开始测量子控件
        measureChildren(widthMeasureSpec, heightMeasureSpec);

        //子view
        View view;
        //MarginLayoutParams 用来获取每个子控件的margin值
        MarginLayoutParams params;
        //记录每行最大的高度值
        int lineHeight = 0;

        if (!isMeasure) {
            isMeasure = true;
        } else {
            //遍历每个子view
            for (int i = 0; i < childCount; i++) {
                view = getChildAt(i);
                params = (MarginLayoutParams) view.getLayoutParams();
                //lineWidth代表当前已经添加view的总宽度  如果当前已添加的宽度加上这个子view的宽度加margin超过的最大宽度,就换行
                if ((lineWidth + params.leftMargin + params.rightMargin + view.getMeasuredWidth()) > widthSize) {
                    //换行 将上一行的list记录加到总的list中
                    childList.add(lineList);
                    //重置行宽 初始值为当前行第一个view的宽度加上margin再加上viewgroup的padding值
                    lineWidth = view.getMeasuredWidth() + params.leftMargin + params.rightMargin+paddingRight + paddingLeft;
                    //新建一个list来记录当前行的view
                    lineList = new ArrayList<>();
                    //将当前view加到lineList中 当前view是每行的第一个view
                    lineList.add(view);
                    //记录当前行的高度
                    heightList.add(lineHeight);
                    //记录总的高度
                    measureHeight += lineHeight;
                    //记录当前子view的高度
                    lineHeight = view.getMeasuredHeight() + params.topMargin + params.bottomMargin;
                } else {
                    //如果当前行能放下一个子view 则将该view加到lineList中 并记录行宽行高
                    lineWidth += params.leftMargin + params.rightMargin + view.getMeasuredWidth();
                    lineList.add(view);
                    //高度取遍历本行的过程中子view最大的高
                    lineHeight = Math.max(lineHeight, view.getMeasuredHeight() + params.topMargin + params.bottomMargin);
                }
                //宽度取最宽的那一行的值
                measureWidth = Math.max(measureWidth, lineWidth);
                //因为最后一个子view不会触发换行,而换行才会将上一行的list添加到总的list中
                //所以当遍历到最后一个子view后需要手动再添加一次,并且计算总高度
                if (i == childCount - 1) {
                    childList.add(lineList);
                    heightList.add(lineHeight);
                    measureHeight += lineHeight;
                }
            }
        }
        //如果宽是wrap_content 宽就是测量出来的宽
        if (widthMode == MeasureSpec.AT_MOST) {
            widthSize = measureWidth;
        }
        //如果高是wrap_content 高是测量出来的高
        if (heightMode == MeasureSpec.AT_MOST) {
            heightSize = measureHeight;
        }

        setMeasuredDimension(widthSize, heightSize);
    }

    //摆放子view
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int left, right, top, bottom;
        MarginLayoutParams marginLayoutParams;
        top = paddingTop;
        //记录总的行高
        int allHeight = paddingTop;
        //指针 从heightList中取出每行的高度
        int i = 0;
        for (List<View> list : childList) {
            //每行都从viewgroup的paddingleft处摆放
            left = paddingLeft;
            for (View view : list) {
                marginLayoutParams = (MarginLayoutParams) view.getLayoutParams();
                //子view的左边需要加上自身的leftMargin值
                left += marginLayoutParams.leftMargin;
                //子view的上边需要加上自身的topmargin
                top += marginLayoutParams.topMargin;
                //右边就是左边加上自身宽度
                right = left + view.getMeasuredWidth();
                //底部就是顶部加上自身高度
                bottom = top + view.getMeasuredHeight();
                //开始摆放
                view.layout(left, top, right, bottom);
                //一行一行的摆放 所以left需要累加 以供它右边的子view使用
                left += view.getMeasuredWidth() + marginLayoutParams.rightMargin;
                //重置top值 因为每个子view的topmargin可能不一样 每次都有重新计算top 注意top不需要累加
                top = allHeight;
            }
            //记录当前行的高度
            allHeight += heightList.get(i++);
            //初始化行高
            top = allHeight;
        }
    }

    //这里不需要重写onDraw 子控件具体的绘制由每个子控件完成
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
    }

    //动态addView(View)需要重写该方法
    @Override
    protected LayoutParams generateDefaultLayoutParams() {
        return new MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
    }

}

代码已经加了详细注释,最好静下心来看,或者自己撸一遍印象才更加深刻。下面是在xml中的使用:

<com.honeywell.flowlayout.FlowLayout
        android:id="@+id/flowlayout"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#FFF000"
        android:paddingLeft="20dp"
        android:paddingTop="15dp"
        android:paddingBottom="20dp"
        android:paddingRight="10dp"
        >

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="#61DF33"
            android:layout_marginLeft="5dp"
            android:text="陆军三十八集团军"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="#61DF33"
            android:layout_marginLeft="5dp"
            android:layout_marginBottom="10dp"
            android:textSize="14sp"
            android:text="空军航空兵第二师"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="#61DF33"
            android:layout_marginLeft="5dp"
            android:text="海军南海舰队"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="#61DF33"
            android:layout_marginLeft="5dp"
            android:text="第二炮兵"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="#61DF33"
            android:layout_marginLeft="5dp"
            android:textSize="20sp"
            android:layout_marginBottom="10dp"
            android:text="武警机动师187师"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="#61DF33"
            android:layout_marginLeft="5dp"
            android:layout_marginBottom="9dp"
            android:text="解放军信息工程大学"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="#61DF33"
            android:layout_marginLeft="5dp"
            android:layout_marginBottom="5dp"
            android:text="大连舰艇学院"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="#61DF33"
            android:layout_marginLeft="5dp"
            android:text="武警海警总队"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="#61DF33"
            android:layout_marginLeft="5dp"
            android:text="陆军第20集团军"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="#61DF33"
            android:layout_marginLeft="5dp"
            android:text="海军陆战队164旅"/>

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="#61DF33"
            android:layout_marginLeft="5dp"
            android:text="济南军区"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="#61DF33"
            android:layout_marginLeft="5dp"
            android:layout_marginTop="5dp"
            android:textSize="20sp"
            android:text="空军95357部队"/>

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="#61DF33"
            android:layout_marginLeft="20dp"
            android:layout_marginTop="10dp"
            android:padding="5dp"
            android:text="空军空降兵15军"/>

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="陆军航空兵"/>

    </com.honeywell.flowlayout.FlowLayout>

下面代码是在java中动态给FlowLayout添加子view:

import androidx.appcompat.app.AppCompatActivity;
import android.graphics.Color;
import android.os.Bundle;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

    FlowLayout flowLayout;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        flowLayout=findViewById(R.id.flowlayout);
        TextView textView = new TextView(this);
        textView.setText("武警黄金部队");
        textView.setBackgroundColor(Color.GREEN);
        LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
        layoutParams.setMargins(20,30,0,0);
        textView.setLayoutParams(layoutParams);
        flowLayout.addView(textView,layoutParams);
    }
}

总结

其实总体来说不是很复杂,最多就是每行宽高的计算,摆放时候的左上右下的处理。源码下载:源码,不过还是希望大家可以自己去实现一遍。

猜你喜欢

转载自blog.csdn.net/u012894808/article/details/107164169