流式布局FlowLayout

     

                     图1                                                                       图2

流式布局的应用在很多的app上都可以看到,尤其是在一些购物类的app上,流式布局大致的布局原理就是先在一行上显示,一行显示不下了,就换行到下一行继续显示。它类似于LinearLayout的horizontal和vertical的结合体。

流式布局FlowLayout_第1张图片

        原理分析图

从上图我们可以看出流式布局在一行布局完成后换行的几种情况,就是在不断计算一行宽度的时候有没有超过父容器宽度,大致可以分为两种情况判断它有没有超过父容器,然后开始换行,第一种情况就是如图(原理分析图)第一行的情况:加完了view宽度和space之后再加view时判断时候超过父容器。第二种情况就是第二三行的情况:加完view宽度之后,在加space宽度就超出了父容器,在加space的时候判断时候超出父容器宽度。总结:就是在加view宽度和space宽度的时候都要判断时候超出父容器,超出就换行。核心代码如下

//获取子view的宽度
	int childWidth = childView.getMeasuredWidth();
	    //将子view的宽度加到一行的宽度中
            usedWidth += childWidth;
	    //加完子view的时候,就判断时候超出了父容器
            if (usedWidth <= widthSize) {
		//如果没有超出父容器,就将子view添加到一行的集合中
                mLine.addView(childView);
		//再加上space,
                usedWidth += mHorizontalSpacing;
                //判断时候超出
                if (usedWidth >= widthSize) {
		    //如果超出就换行
                    if (!newLine()) {
                        break;
                    }
                }
            } else {
                //如果添加子view宽度的时候超出父容器,就换行
                if (!newLine()) {
                    break;
                }
		//将子view添加到下一行的集合中
                mLine.addView(childView);
		//下一行的宽度重新计算,就算加mHorizontalSpacing超出父容器,再加下一个子view的宽度的时候,也还是换行
                usedWidth += childWidth + mHorizontalSpacing;
            }
在onMeasure的中获取每个子view,测量子view,约束它不能超过父容器的大小

//获取当前的子view
            View childView = getChildAt(i);
            if (childView.getVisibility() == GONE) {
                 //隐藏的就不予处理
                continue;
            }
            //测量子view 规范子view的大小,不让他超过父view的大小
            int childWidthSpec = MeasureSpec.makeMeasureSpec(widthSize, widthMode == MeasureSpec.EXACTLY ? MeasureSpec.AT_MOST : widthMode);
            int childHeightSpec = MeasureSpec.makeMeasureSpec(heightSize, heightMode == MeasureSpec.EXACTLY ? MeasureSpec.AT_MOST : heightMode);
            childView.measure(childWidthSpec, childHeightSpec);
在添加到最后一行的时候,循环就结束了,那么在循环结束的时候要将最后一行也要添加到集合中去

//将最后一行添加到行集合中
        if (mLine != null && mLine.getViewCount() > 0 && !lineList.contains(mLine)) {
            lineList.add(mLine);
        }
在onMeasure中就可以计算出每行的宽和高,这样就可以计算出父容器的宽和高

//flowLayout的宽
        int flowLayoutWidth = MeasureSpec.getSize(widthMeasureSpec);
        //当前控件行高的总和
        int totalLineHeight = 0;
        for (int i = 0; i < lineList.size(); i++) {
            //行高总和
            totalLineHeight += lineList.get(i).lineHeight;
        }
        //flowLayout的高
        int flowLayoutHeight = totalLineHeight + (lineList.size() - 1) * mVerticalSpacing + getPaddingTop() + getPaddingBottom();
        setMeasuredDimension(flowLayoutWidth, flowLayoutHeight);
如何来保存每一行的所有view和它的行高和行宽(当前所占的宽度),如果行宽没有占满父容器,可以将剩余的宽度平均分配给每个view,如果分配,效果图如图1;如果不分配,效果图如图2。在其内部定义一个内部类Line,用来保存行的一些信息,和将一行的布局layout交给它来负责。

内部属性定义如下

/**
         * 记录每一行view的集合
         */
        private List<View> viewList = new ArrayList<View>();
        /**
         * 行高
         */
        private int lineHeight;
        /**
         * 当前行控件宽度的和
         */
        private int totalLineWidth;
我们要将一行中的所有view都添加到Line对象中进行管理,view添加到Line集合中的方法如下

/**
         * 往当前行添加子view的方法
         *
         * @param view
         */
        private void addView(View view) {
	    //将view添加到集合中
            viewList.add(view);
            //获取当前行的行高
            int viewHeight = view.getMeasuredHeight();
             //保存一行中最大view的高度作为本行的高度
            lineHeight = Math.max(viewHeight, lineHeight);
            //获取当前行每一个控件的宽度
            int viewWidth = view.getMeasuredWidth();
	    //计算行宽,并保存
            totalLineWidth += viewWidth;
        }
将所有的view都保存在相应的Line中,每个Line也都保存在集合中去管理,下面的工作就是如何来布局每行的view,和每个Line的布局。下面我们就把Line的布局和行中的所有view布局分开处理。我们将Line的布局放到onLayout()中去,而每行的布局放到Line中去,让Line去布局所在行的所有view,只需给它该行所在的left和top的位置即可。

Line的布局如下

/**
     * 布局每一行的位置
     *
     * @param changed
     * @param l
     * @param t
     * @param r
     * @param b
     */
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int left = getPaddingLeft();
        int top = getPaddingTop();
        for (int i = 0; i < lineList.size(); i++) {
            Line line = lineList.get(i);
            line.layout(left, top);
            top += line.lineHeight + mVerticalSpacing;
        }
    }
每行view的布局如下

/**
         * 确定当前行中所有子view的位置
         *
         * @param left
         * @param top
         */
        public void layout(int left, int top) {
            //1.处理水平留白区域
            int layoutWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
            //水平留白区域
            int surplusWidth = layoutWidth - totalLineWidth - (getViewCount() - 1) * mHorizontalSpacing;
            //将水平留白区域平均分配跟当前行的每一个控件
            int oneSurplusWidth =surplusWidth/getViewCount();
            if(oneSurplusWidth>=0){
                for (int i=0;i<viewList.size();i++){
                    View view = viewList.get(i);
                    int viewWidth = view.getMeasuredWidth() + oneSurplusWidth;
                    int viewheight = view.getMeasuredHeight();

                    int viewWidthSec =MeasureSpec.makeMeasureSpec(viewWidth, MeasureSpec.EXACTLY);
                    int viewHeightSec =MeasureSpec.makeMeasureSpec(viewheight,MeasureSpec.EXACTLY);
		    //重新测量子view的宽高
                    view.measure(viewWidthSec,viewHeightSec);
                    //解决细节2,获取让当前控件垂直居中的top
                    int childTop = (lineHeight -viewheight)/2;
                    //布局每一个子view的位置
                    view.layout(left,top+childTop,left+view.getMeasuredWidth(),top+childTop+viewheight);
		    //重新计算left
                    left+=view.getMeasuredWidth()+mHorizontalSpacing;

                }
            }
        }
流式布局的原理实现和代码分析道这里就分析完了,如发现问题欢迎留言


全部源码如下:

package com.cj.chenj.expandtextview;

import android.content.Context;
import android.view.View;
import android.view.ViewGroup;

import java.util.ArrayList;
import java.util.List;

public class FlowLayout extends ViewGroup {
	
	public static final int MAX_LINES_COUNT = 100; 
	
    /**
     * 行对象
     */
    private Line mLine;
    /**
     * 已使用的宽度
     */
    private int usedWidth;
    /**
     * 水平间距
     */
    private int mHorizontalSpacing = 6;
    /**
     * 垂直间隙
     */
    private int mVerticalSpacing = 6;
    /**
     * 保存行的集合
     */
    private List<Line> lineList = new ArrayList<Line>();

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

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        //获取当前控件的测量模式
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        //获取当前控件的测量尺寸
        int widthSize = MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() - getPaddingRight();
        int heightSize = MeasureSpec.getSize(heightMeasureSpec) - getPaddingTop() - getPaddingBottom();

        //清空数据
        restore();
        //获取当前控件所有字view的个数
        int childCount = getChildCount();
        //遍历获取所有子view
        for (int i = 0; i < childCount; i++) {
            //获取当前的子view
            View childView = getChildAt(i);
            if (childView.getVisibility() == GONE) {
                continue;
            }
            //测量子view 规范子view的大小,不让他超过父view的大小
            int childWidthSpec = MeasureSpec.makeMeasureSpec(widthSize, widthMode == MeasureSpec.EXACTLY ? MeasureSpec.AT_MOST : widthMode);
            int childHeightSpec = MeasureSpec.makeMeasureSpec(heightSize, heightMode == MeasureSpec.EXACTLY ? MeasureSpec.AT_MOST : heightMode);
            childView.measure(childWidthSpec, childHeightSpec);
            //创建行对象
            if (mLine == null) {
                mLine = new Line();
            }

            //获取子view的宽度
            int childWidth = childView.getMeasuredWidth();
            //将子view的宽度加到一行的宽度中
            usedWidth += childWidth;
            //加完子view的时候,就判断时候超出了父容器
            if (usedWidth <= widthSize) {
                //如果没有超出父容器,就将子view添加到一行的集合中
                mLine.addView(childView);
                //再加上space,
                usedWidth += mHorizontalSpacing;
                //判断时候超出
                if (usedWidth >= widthSize) {
                    //如果超出就换行
                    if (!newLine()) {
                        break;
                    }
                }
            } else {
                //如果添加子view宽度的时候超出父容器,就换行
                if (!newLine()) {
                    break;
                }
                //将子view添加到下一行的集合中
                mLine.addView(childView);
                //下一行的宽度重新计算,就算加mHorizontalSpacing超出父容器,再加下一个子view的宽度的时候,也还是换行
                usedWidth += childWidth + mHorizontalSpacing;
            }
        }
        //将最后一行添加到行集合中
        if (mLine != null && mLine.getViewCount() > 0 && !lineList.contains(mLine)) {
            lineList.add(mLine);
        }
        //flowLayout的宽
        int flowLayoutWidth = MeasureSpec.getSize(widthMeasureSpec);
        //当前控件行高的总和
        int totalLineHeight = 0;
        for (int i = 0; i < lineList.size(); i++) {
            //行高总和
            totalLineHeight += lineList.get(i).lineHeight;
        }
        //flowLayout的高
        int flowLayoutHeight = totalLineHeight + (lineList.size() - 1) * mVerticalSpacing + getPaddingTop() + getPaddingBottom();
        setMeasuredDimension(flowLayoutWidth, flowLayoutHeight);
//        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    /**
     * 清空数据的方法
     */
    private void restore() {
        lineList.clear();
        mLine = new Line();
        usedWidth=0;
    }

    /**
     * 创建一个新行
     *
     * @return
     */
    private boolean newLine() {
        lineList.add(mLine);
        if (lineList.size() < MAX_LINES_COUNT) {
            mLine = new Line();
            usedWidth = 0;
            return true;
        }

        return false;
    }

    /**
     * 布局每一行的位置
     *
     * @param changed
     * @param l
     * @param t
     * @param r
     * @param b
     */
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int left = getPaddingLeft();
        int top = getPaddingTop();
        for (int i = 0; i < lineList.size(); i++) {
            Line line = lineList.get(i);
            line.layout(left, top);
            top += line.lineHeight + mVerticalSpacing;
        }
    }

    /**
     * 行对象
     */
    class Line {
        /**
         * 记录每一行view的集合
         */
        private List<View> viewList = new ArrayList<View>();
        /**
         * 行高
         */
        private int lineHeight;
        /**
         * 当前行控件宽度的和
         */
        private int totalLineWidth;

        /**
         * 往当前行添加子view的方法
         *
         * @param view
         */
        private void addView(View view) {
            viewList.add(view);
            //获取当前行的行高
            int viewHeight = view.getMeasuredHeight();
            lineHeight = Math.max(viewHeight, lineHeight);
            //获取当前行每一个控件的宽度
            int viewWidth = view.getMeasuredWidth();
            totalLineWidth += viewWidth;
        }

        /**
         * 获取当前行中有多少个子view
         *
         * @return
         */
        private int getViewCount() {
            return viewList.size();
        }

        /**
         * 确定当前行中所有子view的位置
         *
         * @param left
         * @param top
         */
        public void layout(int left, int top) {
            //1.处理水平留白区域
            int layoutWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
            //水平留白区域
            int surplusWidth = layoutWidth - totalLineWidth - (getViewCount() - 1) * mHorizontalSpacing;
            //将水平留白区域平均分配跟当前行的每一个控件
            int oneSurplusWidth =surplusWidth/getViewCount();
            if(oneSurplusWidth>=0){
                for (int i=0;i<viewList.size();i++){
                    View view = viewList.get(i);
                    int viewWidth = view.getMeasuredWidth() + oneSurplusWidth;
                    int viewheight = view.getMeasuredHeight();

                    int viewWidthSec =MeasureSpec.makeMeasureSpec(viewWidth, MeasureSpec.EXACTLY);
                    int viewHeightSec =MeasureSpec.makeMeasureSpec(viewheight,MeasureSpec.EXACTLY);
                    view.measure(viewWidthSec,viewHeightSec);
                    //解决细节2,获取让当前控件垂直居中的top
                    int childTop = (lineHeight -viewheight)/2;
                    //布局每一个子view的位置
                    view.layout(left,top+childTop,left+view.getMeasuredWidth(),top+childTop+viewheight);
                    left+=view.getMeasuredWidth()+mHorizontalSpacing;

                }
            }
        }
    }
}



你可能感兴趣的:(FlowLayout,流式布局)