注:本文章在编写的过程中参考鸿洋_大神的博客,效果图基本都借用它的,但我觉的他实现有点繁琐,所以我又按照自己的想法写了一遍。
1.前言
在文章的开头,把代码送上,以方便您对照着学习。
本章教您如何实现自动换行的布局,其实这种控件在很早以前github就有大神实现了,但是不妨碍我们研究它是如何实现的,这对我们的进步有莫大的好处。先看效果图吧
相信大家能看出它们的共同特点:控件根据ViewGroup的宽,自动的往右添加,如果当前行剩余空间不足,则自动添加到下一行。
2.实现的总体逻辑
1.新建一个类FlowLayout,继承viewGroup。这个不做解释。
2.重写generateLayoutParams。在代码中有详细的介绍。
3.重写onmeasure方法。重写这个方法的目标是获取Flowout的总体宽度,并用setMeasuredDimension设置。
4.重写onLayout方法。重写这个方法的目标是设置每个控件的具体位置。
好了总体的思路我们就按照这个来,下面我们详细的介绍。
3.具体实现
3.1重写generateLayoutParams方法
/**
* 这个方法必须重新,否则报错!
* public void setLayoutParams(ViewGroup.LayoutParams params){
* if (params == null) {
* throw new NullPointerException("Layout parameters cannot be null");
* }
* mLayoutParams = params; requestLayout();
* }
* 我搜遍整个view源码,请看view源码中的setLayoutParams方法,给mLayoutParams赋值的只有这个方法,
* 大家仔细看:其实是把viewGroup中的params赋值给mLayoutParams。
* 所以childView.getLayoutParams()
* ,其实是获取viewGroup中的LayoutParams,也就是我们在generateLayoutParams方法中返回的值。
*/
@Override
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
3.2重写onMeasure方法
/**
* 请牢记本方法的目标: 1.计算高度,并通过setMeasuredDimension设置(本方法的目标)
* 注意:宽度不需要计算,用MeasureSpec.getSize(widthMeasureSpec)就行。
* 我们的逻辑如下:
* 1.遍历viewgroup中的所有子view
* 2.遍历期间,我们会不断累加lineWidth
* if(lineWidth > MeasureSpec.getSize(widthMeasureSpec)){ 需要换行;
* endHeigth += 每一行中view最大高度。
* lineWidth = 0; 重置为0,
* }else{
* lineWidth += childWidth; 不断累加
* }
* 3.setMeasuredDimension设置最终高度。
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int endHeigth = 0;// 这个是最后setMeasuredDimension要设置的高度,每次换行都要 endHeigth +=
int lineWidth = 0;// 临时记录当前宽度,如果(lineWidth > measureWidth)就要换行。
int lineHeigth = 0;// 我们用这个变量记录每一行的最大高度。
// 以下拆分widthMeasureSpec 和 heightMeasureSpec;具体的原理这里不做详细介绍
final int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
final int measureHeigth = MeasureSpec.getSize(heightMeasureSpec);
final int measureHeigthModel = MeasureSpec.getMode(heightMeasureSpec);
// 获取viewGroup中子控件的数量。
int childNum = getChildCount();
// 循环所有的控件是为了计算出总的高度,这也是我们这个方法的目标
for (int i = 0; i < childNum; i++) {
View childView = getChildAt(i);
// 调用子类的onmeasure方法,这个不需要我们干涉,它具体多大的值,其实你在xml中都已经确定了。
measureChild(childView, widthMeasureSpec, widthMeasureSpec);
// 得到child的lp,是为了获取子控件的margin值。注意:一定要重写generateLayoutParams方法,不然这里会报错
MarginLayoutParams lp = (MarginLayoutParams) childView
.getLayoutParams();
// 当前子空间实际占据的宽度
int childWidth = childView.getMeasuredWidth() + lp.leftMargin
+ lp.rightMargin;
// 当前子空间实际占据的高度
int childHeight = childView.getMeasuredHeight() + lp.topMargin
+ lp.bottomMargin;
/**
* 核心代码区。这是为了判断是否要换行并且做一些操作。
* 如果(lineWidth + childWidth) > measureWidth
* 我们应该换行了,并做如下处理 :
1.endHeigth += lineHeigth;
2.lineHeigth = 0;将最大高度重新设置为0;
3.lineWidth = 0;将最大宽度重新设置为0; 否则: lineHeigth 比较获取最大值 lineWidth递增
*
* 核心代码就这么一点,是不是很简单啊。
*/
if (lineWidth + childWidth > measureWidth) {
endHeigth += lineHeigth;
lineHeigth = 0;
lineWidth = lp.leftMargin;
} else {
lineHeigth = Math.max(lineHeigth, childHeight);
lineWidth += childWidth;
}
}
/**
* 宽度不变,按照测量的来。我们要要设置高度就行,确保每一个控件都能显示出来。
* 如果measureHeigthModel == MeasureSpec.EXACTLY ,我们应该以系统给我们的值来确定最终的值 否则就用我的计算的值。
*/
setMeasuredDimension(measureWidth, (measureHeigthModel == MeasureSpec.EXACTLY) ? measureHeigth : endHeigth);
}
这里,我们并没有设置宽度,是因为我觉的没有必要。我们只设置了高度。
重写onLayout方法
/**
* 本方法的目标:摆放每个子控件的位置
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
final int width = r - l;// 好像有多种方法,都尝试一下
/**
* 我们下面for循环的目的就是给每个view确定坐标(即以下的4个值),
* 然后通过childView.layout(preLeft, preTop, endBottom, endRight)设置。
*/
int preLeft = 0; // 这个是记录每个控件最终l 坐标
int preTop = 0; // 这个是记录每个控件最终的 t 坐标
int endBottom = 0;// 这个是最终记录每个控件的b坐标
int endRight = 0;// 这个是最终记录每个控件的r坐标
// 获取viewGroup中子控件的数量。
int childNum = getChildCount();
// 循环所有的控件是为了计算出总的高度,这也是我们这个方法的目标
for (int i = 0; i < childNum; i++) {
View childView = getChildAt(i);
if (childView.getVisibility() == View.GONE) {
continue;
}
// 得到child的lp,是为了获取子控件的margin值。注意:一定要重写generateLayoutParams方法,不然这里会报错
MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();
// 当前子空间实际占据的宽度
int childWidth = childView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
/**
* 以下是逻辑处理核心代码,我这个逻辑调了半天才调出来,人个感觉不太满意(肯定有更简单的算法!)大家可以重新写一个。
* 逻辑如下:
* 1.我的逻辑始终是给下面这4个变量赋值,这4个变量标识着当前控件的4个点的坐标
* preLeft = 0; // 这个是记录每个控件最终left 坐标
preTop = 0; // 这个是记录每个控件最终的 top 坐标
endBottom = 0;// 这个是最终记录每个控件的bottom坐标
endRight = 0;// 这个是最终记录每个控件的right坐标
而
2.if(endRight + childWidth) > width){//如果横向再加一个控件超出了layout总宽度就换行。
处理逻辑步骤如下 :
1.preLeft = lp.leftMargin;//首先要初始化preLeft,别忘了还有marginLeft;
2.preTop = endBottom + lp.topMargin;//起始坐标应该是 endBottom,别忘了还有下一个控件的MarginTop
3.有了left坐标和top坐标就好办多了,我们只需要在它们的基础上分别加上控件的宽度和高度。
4.坐标都有了,调用 childView.layout确定控件的位置
5.最后别忘了endBottom 和 endRight 加上相应的margin值
}
*/
if((endRight + childWidth) > width){ //如果横向再加一个控件超出了layout总宽度就换行。
preLeft = lp.leftMargin;//首先要初始化preLeft,别忘了还有marginLeft;
preTop = endBottom + lp.topMargin;//起始坐标应该是 endBottom,别忘了还有下一个控件的MarginTop
//有了left坐标和top坐标就好办多了,我们只需要在它们的基础上分别加上控件的宽度和高度,下面2句话就做这个事。
endBottom = preTop + childView.getMeasuredHeight();
endRight = preLeft + childView.getMeasuredWidth();
childView.layout(preLeft, preTop,endRight , endBottom);//画控件的位置
//最后别忘了endBottom 和 endRight 加上相应的margin值 ,为了保持下一行坐标的正确。
endBottom += lp.bottomMargin;
endRight += lp.rightMargin;
}else{//else中的代码很明显的特点是所有的控件在一行上,所以preLeft和preTop的值是不变的。
if(i==0){//之所以要判断0,是因为一开始控件的4个坐标值是0,我们要初始化一下,后面的在else中自动赋值了。
//为4个坐标初始化
preLeft = lp.leftMargin;
preTop = lp.topMargin;
endBottom = preTop + childView.getMeasuredHeight();
endRight = preLeft + childView.getMeasuredWidth();
//设置控件的位置
childView.layout(preLeft, preTop,endRight , endBottom);
//最后别忘了endBottom 和 endRight 加上相应的margin值 ,为了保持下一行坐标的正确。
endBottom += lp.bottomMargin;
endRight += lp.rightMargin;
}else{
preLeft = endRight + lp.leftMargin;
endRight = preLeft + childView.getMeasuredWidth();
childView.layout(preLeft, preTop,endRight , endBottom);
//最后别忘了endBottom 和 endRight 加上相应的margin值 ,为了保持下一个控件坐标的正确。
endRight += lp.rightMargin;
}
}
}
}
其他代码
res/values/styles.xml中:
<style name="text_flag_01">
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_margin">4dp</item>
<item name="android:background">@drawable/flag_01</item>
<item name="android:textColor">#ffffff</item>
res/drawable/flag_01.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" >
<solid android:color="#7690A5" >
</solid>
<corners android:radius="5dp"/>
<padding
android:bottom="2dp"
android:left="10dp"
android:right="10dp"
android:top="2dp" />
</shape>
布局文件:
<com.czh.weigetstudy.FlowLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/flowLayout"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<TextView
style="@style/text_flag_01"
android:text="Welcome" />
<TextView
style="@style/text_flag_01"
android:text="IT工程师" />
<TextView
style="@style/text_flag_01"
android:text="学习ing" />
<TextView
style="@style/text_flag_01"
android:text="恋爱ing" />
<TextView
style="@style/text_flag_01"
android:text="挣钱ing" />
<TextView
style="@style/text_flag_01"
android:text="努力ing" />
<TextView
style="@style/text_flag_01"
android:text="I thick i can" />
</com.czh.weigetstudy.FlowLayout>
效果图如下
结尾
在文章的结尾把代码送上,大家可以对照着看。有什么不对或者不懂的地方,请您及时留言。
在技术上我依旧是个小渣渣,加油!勉励自己!
如果觉的文章不错请点个赞吧,谢谢您!