@xujun94
2016-06-29T10:29:29.000000Z
字数 11013
阅读 1222
在Android开发中,我们经常会遇到流布式的布局,经常会用来一些标签的显示,比如qq中个人便签,搜索框下方提示的词语,这些是指都是流布式的布局,今天我就我们日常开放中遇到的流布式布局坐一些总结
转载请注明博客地址:http://blog.csdn.net/gdutxiaoxu/article/details/51765428
**源码下载地址:https://github.com/gdutxiaoxu/CustomViewDemo.git **


仔细观察,我们可以知道图二其实是图一效果的升级版,图一当我们控件的宽度超过这一行的时候,剩余的宽度它不会自动分布到每个控件中,而图二的效果当我们换行的时候,如控件还没有占满这一行的时候,它会自动把剩余的宽度分布到每个控件中
/*** 博客地址:http://blog.csdn.net/gdutxiaoxu* @author xujun* @time 2016/6/20 23:49.*/public class SimpleFlowLayout extends ViewGroup {private int verticalSpacing = 20;public SimpleFlowLayout(Context context ) {super(context);}/*** 重写onMeasure方法是为了确定最终的大小* @param widthMeasureSpec* @param heightMeasureSpec*/@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);int paddingLeft = getPaddingLeft();int paddingRight = getPaddingRight();int paddingTop = getPaddingTop();int paddingBottom = getPaddingBottom();//处理Padding属性,让当前的ViewGroup支持Paddingint widthUsed = paddingLeft + paddingRight;int heightUsed = paddingTop + paddingBottom;int childMaxHeightOfThisLine = 0;int childCount = getChildCount();for (int i = 0; i < childCount; i++) {View child = getChildAt(i);if (child.getVisibility() != GONE) {// 已用的宽度int childUsedWidth = 0;// 已用的高度int childUsedHeight = 0;// 调用ViewGroup自身的方法测量孩子的宽度和高度,我们也可以自己根据MeasureMode来测量measureChild(child,widthMeasureSpec,heightMeasureSpec);childUsedWidth += child.getMeasuredWidth();childUsedHeight += child.getMeasuredHeight();//处理Margin,支持孩子的Margin属性Rect marginRect = getMarginRect(child);int leftMargin=marginRect.left;int rightMargin=marginRect.right;int topMargin=marginRect.top;int bottomMargin=marginRect.bottom;childUsedWidth += leftMargin + rightMargin;childUsedHeight += topMargin + bottomMargin;//总宽度没有超过本行if (widthUsed + childUsedWidth < widthSpecSize) {widthUsed += childUsedWidth;if (childUsedHeight > childMaxHeightOfThisLine) {childMaxHeightOfThisLine = childUsedHeight;}} else {//总宽度已经超过本行heightUsed += childMaxHeightOfThisLine + verticalSpacing;widthUsed = paddingLeft + paddingRight + childUsedWidth;childMaxHeightOfThisLine = childUsedHeight;}}}//加上最后一行的最大高度heightUsed += childMaxHeightOfThisLine;setMeasuredDimension(widthSpecSize, heightUsed);}@Overrideprotected void onLayout(boolean changed, int l, int t, int r, int b) {int paddingLeft = getPaddingLeft();int paddingRight = getPaddingRight();int paddingTop = getPaddingTop();int paddingBottom = getPaddingBottom();/*** 为了 支持Padding属性*/int childStartLayoutX = paddingLeft;int childStartLayoutY = paddingTop;int widthUsed = paddingLeft + paddingRight;int childMaxHeight = 0;int childCount = getChildCount();//摆放每一个孩子的高度for (int i = 0; i < childCount; i++) {View child = getChildAt(i);if (child.getVisibility() != GONE) {int childNeededWidth, childNeedHeight;int left, top, right, bottom;int childMeasuredWidth = child.getMeasuredWidth();int childMeasuredHeight = child.getMeasuredHeight();Rect marginRect = getMarginRect(child);int leftMargin=marginRect.left;int rightMargin=marginRect.right;int topMargin=marginRect.top;int bottomMargin=marginRect.bottom;childNeededWidth = leftMargin + rightMargin + childMeasuredWidth;childNeedHeight = topMargin + topMargin + childMeasuredHeight;// 没有超过本行if (widthUsed + childNeededWidth <= r - l) {if (childNeedHeight > childMaxHeight) {childMaxHeight = childNeedHeight;}left = childStartLayoutX + leftMargin;top = childStartLayoutY + topMargin;right = left + childMeasuredWidth;bottom = top + childMeasuredHeight;widthUsed += childNeededWidth;childStartLayoutX += childNeededWidth;} else {childStartLayoutY += childMaxHeight + verticalSpacing;childStartLayoutX = paddingLeft;widthUsed = paddingLeft + paddingRight;left = childStartLayoutX + leftMargin;top = childStartLayoutY + topMargin;right = left + childMeasuredWidth;bottom = top + childMeasuredHeight;widthUsed += childNeededWidth;childStartLayoutX += childNeededWidth;childMaxHeight = childNeedHeight;}child.layout(left, top, right, bottom);}}}private Rect getMarginRect(View child) {LayoutParams layoutParams = child.getLayoutParams();int leftMargin = 0;int rightMargin = 0;int topMargin = 0;int bottomMargin = 0;if (layoutParams instanceof MarginLayoutParams) {MarginLayoutParams marginLayoutParams = (MarginLayoutParams) layoutParams;leftMargin = marginLayoutParams.leftMargin;rightMargin = marginLayoutParams.rightMargin;topMargin = marginLayoutParams.topMargin;bottomMargin = marginLayoutParams.bottomMargin;}return new Rect(leftMargin, topMargin, rightMargin, bottomMargin);}}
添加完一个孩子以后我们判断widthUsed是够超出控件本身的最大宽度widthSpecSize,
若没有超过执行
widthUsed += childUsedWidth;
if (childUsedHeight > childMaxHeightOfThisLine) {
childMaxHeightOfThisLine = childUsedHeight;
}
超过控件的宽度执行
heightUsed += childMaxHeightOfThisLine + verticalSpacing;
widthUsed = paddingLeft + paddingRight + childUsedWidth;
childMaxHeightOfThisLine = childUsedHeight;
最后调用 setMeasuredDimension(widthSpecSize, heightUsed);这个方法去设置它的大小
3.在OnLayout方法里面,所做的工作就是去摆放每一个孩子的位置 ,判断需不需要换行,不需要更改left值,需要换行,更改top值
讲解之前,我们先来了解一下一个基本知识

从这张图片里面我们可以得出这样结论
注意事项
int widthUsed = paddingLeft + paddingRight;int heightUsed = paddingTop + paddingBottom;----------if (widthUsed + childUsedWidth < widthSpecSize) {widthUsed += childUsedWidth;if (childUsedHeight > childMaxHeightOfThisLine) {childMaxHeightOfThisLine = childUsedHeight;}}
Rect marginRect = getMarginRect(child);int leftMargin=marginRect.left;int rightMargin=marginRect.right;int topMargin=marginRect.top;int bottomMargin=marginRect.bottom;childUsedWidth += leftMargin + rightMargin;childUsedHeight += topMargin + bottomMargin;
即我们在计算孩子所占用的宽度和高度的时候加上margin属性的高度,接着在计算需要孩子总共用的宽高度的时候加上每个孩子的margin属性的宽高度,这样自然就支持了孩子的margin属性了
如下图所见,在控件宽度参差不齐的情况下,控件换行会留下一些剩余的宽度,作为想写出鲁棒性的代码的我们会觉得别扭,于是我们相处了解决办法。

解决方法见下面

/*** 博客地址:http://blog.csdn.net/gdutxiaoxu* @author xujun* @time 2016/6/26 22:54.*/public class PrefectFlowLayout extends ViewGroup {public PrefectFlowLayout(Context context) {super(context);}public PrefectFlowLayout(Context context, AttributeSet attrs) {super(context, attrs);}public PrefectFlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);}//父容器宽度private int parentWidthSize;//水平间距private int horizontalSpacing = 16;//垂直间距private int verticalSpacing = 16;//当前行private Line currentLine;//所有行的集合private List<Line> mLines = new ArrayList<>();//当前行已使用宽度private int userWidth = 0;/*** 行对象*/private class Line {//一行里面所添加的子View集合private List<View> children;//当前行高度private int height;//当前行已使用宽度private int lineWidth = 0;public Line() {children = new ArrayList<>();}/*** 添加一个子控件** @param child*/private void addChild(View child) {children.add(child);if (child.getMeasuredHeight() > height) {//当前行高度以子控件最大高度为准height = child.getMeasuredHeight();}//将每个子控件宽度进行累加,记录使用的宽度lineWidth += child.getMeasuredWidth();}/*** 获取行的高度** @return*/public int getHeight() {return height;}/*** 获取子控件的数量** @return*/public int getChildCount() {return children.size();}/*** 放置每一行里面的子控件的位置** @param l 距离最左边的距离* @param t 距离最顶端的距离*/public void onLayout(int l, int t) {//当前行使用的宽度,等于每个子控件宽度之和+子控件之间的水平距离lineWidth += horizontalSpacing * (children.size() - 1);int surplusChild = 0;int surplus = parentWidthSize - lineWidth;//剩余宽度if (surplus > 0) {//如果有剩余宽度,则将剩余宽度平分给每一个子控件surplusChild = (int) (surplus / children.size()+0.5);}for (int i = 0; i < children.size(); i++) {View child = children.get(i);child.getLayoutParams().width=child.getMeasuredWidth()+surplusChild;if (surplusChild>0){//如果长度改变了后,需要重新测量,否则布局中的属性大小还会是原来的大小child.measure(MeasureSpec.makeMeasureSpec(child.getMeasuredWidth()+surplusChild,MeasureSpec.EXACTLY),MeasureSpec.makeMeasureSpec(height,MeasureSpec.EXACTLY));}child.layout(l, t, l + child.getMeasuredWidth(), t + child.getMeasuredHeight());l += child.getMeasuredWidth() + horizontalSpacing;}}}// getMeasuredWidth() 控件实际的大小// getWidth() 控件显示的大小@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {//将之前测量的数据进行清空,以防复用时影响下次测量mLines.clear();currentLine = null;userWidth = 0;//获取父容器的宽度和模式int widthMode = MeasureSpec.getMode(widthMeasureSpec);parentWidthSize = MeasureSpec.getSize(widthMeasureSpec)- getPaddingLeft() - getPaddingRight();//获取父容器的高度和模式int heigthMode = MeasureSpec.getMode(heightMeasureSpec);int heightSize = MeasureSpec.getSize(heightMeasureSpec)- getPaddingTop() - getPaddingBottom();int childWidthMode, childHeightMode;//为了测量每个子控件,需要指定每个子控件的测量规则//子控件设置为WRAP_CONTENT,具体测量规则详见,ViewGroup的getChildMeasureSpec()方法if (widthMode == MeasureSpec.EXACTLY) {childWidthMode = MeasureSpec.AT_MOST;} else {childWidthMode = widthMode;}if (heigthMode == MeasureSpec.EXACTLY) {childHeightMode = MeasureSpec.AT_MOST;} else {childHeightMode = heigthMode;}//获取到子控件高和宽的测量规则int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(parentWidthSize, childWidthMode);int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(heightSize, childHeightMode);currentLine = new Line();//创建第一行for (int i = 0; i < getChildCount(); i++) {View child = getChildAt(i);//测量每一个孩子child.measure(childWidthMeasureSpec, childHeightMeasureSpec);//获取当前子控件的实际宽度int childMeasuredWidth = child.getMeasuredWidth();//让当前行使用宽度加上当前子控件宽度userWidth += childMeasuredWidth;if (userWidth <= parentWidthSize) {//如果当前行使用宽度小于父控件的宽度,则加入该行currentLine.addChild(child);//当前行使用宽度加上子控件之间的水平距离userWidth += horizontalSpacing;//如果当前行加上水平距离后超出父控件宽度,则换行if (userWidth > parentWidthSize) {newLine();}} else {//以防出现一个子控件宽度超过父控件的情况出现if (currentLine.getChildCount() == 0) {currentLine.addChild(child);}newLine();//并将超出范围的当前的子控件加入新的行中currentLine.addChild(child);//并将使用宽度加上子控件的宽度;userWidth = child.getMeasuredWidth()+horizontalSpacing;}}//加入最后一行,因为如果最后一行宽度不足父控件宽度时,就未换行if (!mLines.contains(currentLine)) {mLines.add(currentLine);}int totalHeight = 0;//总高度for (Line line : mLines) {//总高度等于每一行的高度+垂直间距totalHeight += line.getHeight() + verticalSpacing;}//resolveSize(),将实际高度与父控件高度进行比较,选取最合适的setMeasuredDimension(parentWidthSize + getPaddingLeft() + getPaddingRight(),resolveSize(totalHeight + getPaddingTop() + getPaddingBottom(), heightMeasureSpec));}/*** 换行*/private void newLine() {mLines.add(currentLine);//记录之前行currentLine = new Line();//重新创建新的行userWidth = 0;//将使用宽度初始化}/*** 放置每个子控件的位置** @param changed* @param l* @param t* @param r* @param b*/@Overrideprotected void onLayout(boolean changed, int l, int t, int r, int b) {l += getPaddingLeft();t += getPaddingTop();for (int i = 0; i < mLines.size(); i++) {Line line = mLines.get(i);//设置每一行的位置,每一行的子控件由其自己去分配line.onLayout(l, t);//距离最顶端的距离,即每一行高度和垂直间距的累加t += line.getHeight() + verticalSpacing;}}/*** 获取子控件的测量规则** @param mode 父控件的测量规则* @return 子控件设置为WRAP_CONTENT,具体测量规则详见,ViewGroup的getChildMeasureSpec()方法*/private int getMode(int mode) {int childMode = 0;if (mode == MeasureSpec.EXACTLY) {childMode = MeasureSpec.AT_MOST;} else {childMode = mode;}return childMode;}}
对比图一的实现思路,我们封装了Line这个内部类,看到这个名字,相信大家都猜到是什么意思了,其实就是一个Line实例对象代表一行,Line里面的List children用来存放孩子
private List<View> children;//一行里面所添加的子View集合
if (surplus > 0) {//如果有剩余宽度,则将剩余宽度平分给每一个子控件surplusChild = (int) (surplus / children.size()+0.5);}-------//重新分配每个孩子的大小child.measure(MeasureSpec.makeMeasureSpec(child.getMeasuredWidth()+surplusChild,MeasureSpec.EXACTLY),MeasureSpec.makeMeasureSpec(height,MeasureSpec.EXACTLY));
**源码下载地址:https://github.com/gdutxiaoxu/CustomViewDemo.git **