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>
效果图如下
结尾
在文章的结尾把代码送上,大家可以对照着看。有什么不对或者不懂的地方,请您及时留言。
在技术上我依旧是个小渣渣,加油!勉励自己!
如果觉的文章不错请点个赞吧,谢谢您!
参考文档
作者:mffandx