图1 图2
流式布局的应用在很多的app上都可以看到,尤其是在一些购物类的app上,流式布局大致的布局原理就是先在一行上显示,一行显示不下了,就换行到下一行继续显示。它类似于LinearLayout的horizontal和vertical的结合体。
原理分析图
从上图我们可以看出流式布局在一行布局完成后换行的几种情况,就是在不断计算一行宽度的时候有没有超过父容器宽度,大致可以分为两种情况判断它有没有超过父容器,然后开始换行,第一种情况就是如图(原理分析图)第一行的情况:加完了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; } } } } }