图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;
}
}
}
}
}