流式布局
流式布局是一种特殊的布局模式,生活中有很多地方会使用到流式布局,比如网站的标签云就是使用的流式布局,流式布局的特点是,按方向一个一个的排列元素,当一行无法容纳新元素的时候,会自动换行,将新元素放置到下一行,如下图局势一个典型的流式布局。
自定义ViewGroup打造流式布局
还是老规矩,介绍之前贴出Demo效果图,看看是否为你想要的样子。
- 自定义View的步骤
自定义一个View往往有三个方法需要注意。
onMeasure(); 用于测量View的大小
onLayout(); 用于分配View的位置,定义布局
onDraw(); 绘制View
由于网上关于这三个函数的讲解已经很多了,而且都是大神级别的人物写的,所以我就不献丑了,只贴出一些我觉得好的博客。
- 实战编码
如果你详细阅读了上面3篇博客,那么你一定对View的绘制流程有了基本了解,现在我们开始实际编码。首先是自定义一个类FlowLayout继承自ViewGroup。我先贴出全部源码,然后讲解。
/** * 流式布局 * * @author Jay */ public class FlowLayout extends ViewGroup { public FlowLayout(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } public FlowLayout(Context context, AttributeSet attrs) { super(context, attrs, 0); } public FlowLayout(Context context) { super(context, null); } /** * 测量视图 */ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int sizeWidth = MeasureSpec.getSize(widthMeasureSpec); int modeWidth = MeasureSpec.getMode(widthMeasureSpec); int sizeHeight = MeasureSpec.getSize(heightMeasureSpec); int modeHeight = MeasureSpec.getMode(heightMeasureSpec); // 最终测量的大小 int width = 0; int height = 0; // 每一行的宽高 int lineWidth = 0; // 第N-1行的高度 int tempHeight = 0; // 获取有多少个子布局 int childCount = getChildCount(); for (int index = 0; index < childCount; index++) { // 获取子View View child = getChildAt(index); // 如果子View不可见 if (child.getVisibility() == View.GONE) { continue; } // 测量子View measureChild(child, widthMeasureSpec, heightMeasureSpec); // 获取子View的布局参数 MarginLayoutParams lp = (MarginLayoutParams) child .getLayoutParams(); // 获取子布局的宽高 int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin; int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; // 如果一行放不下了,那么这个子View应该放到下一行 if (lineWidth + childWidth + getPaddingLeft() + getPaddingRight() > sizeWidth) { tempHeight = height;// 记录N-1行的高度 width = Math.max(width, lineWidth); lineWidth = childWidth; height = height + childHeight; } else { lineWidth = lineWidth + childWidth; height = Math.max(height, tempHeight + childHeight); } } // 加上边距 height = height + getPaddingTop() + getPaddingBottom(); width = width + getPaddingLeft() + getPaddingRight(); Log.v("x", height + "/" + width); setMeasuredDimension( // modeWidth == MeasureSpec.EXACTLY ? sizeWidth : width, modeHeight == MeasureSpec.EXACTLY ? sizeHeight : height); } /** * 视图布局 */ @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { // 获取视图的宽度 int width = getWidth(); // 布局坐标,考虑到布局有可能设置了内边距 int left = getPaddingLeft(); int top = getPaddingTop(); // 一行中最高的一个view的高度 int maxHeight = 0; int viewCount = getChildCount(); for (int index = 0; index < viewCount; index++) { View child = getChildAt(index); MarginLayoutParams lp = (MarginLayoutParams) child .getLayoutParams(); int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin; // 换行 if (left + childWidth + getPaddingRight() > width) { left = getPaddingLeft(); top = top + maxHeight; maxHeight = 0; } else { // 得到最高高度 maxHeight = Math.max(childHeight, maxHeight); } int lc = left + lp.leftMargin; int tc = top + lp.topMargin; int rc = lc + child.getMeasuredWidth(); int bc = tc + child.getMeasuredHeight(); child.layout(lc, tc, rc, bc); left = left + childWidth; } } /** * 返回默认的LayoutParams,如果一个视图使用addView(View)添加而没有指定LayoutParams的时候 * 使用MarginLayoutParams当做LayoutParams * 这个函数返回的类型就是View.getLayoutParams的返回值类型 */ @Override public LayoutParams generateLayoutParams(AttributeSet attrs) { return new MarginLayoutParams(getContext(), attrs); } }
首先自定义一个类FlowLayout去继承ViewGroup然后覆写其三个构造函数,还有覆写onMeasure()和onLayout()。下面还覆写了一个generateLayoutParams(),这个函数的作用是给子View也就是FlowLayout里面包含的View去设置默认的LayoutParams,这里使用MarginLayoutParams的原因是我们的子View有可能会设置外边距。
先来看看onMeasure()方法。
总体思路是如果测量模式是MeasureSpec.EXACTLY(match_parent),那么直接使用传递进来的Size,如果为自适应大小,那么我们就需要测量了,测量的思路是,遍历所有的子View,如果当前一行已经使用了的宽度+将放入的View的宽度大于总宽度,说明要换行了,将这个View放入下一行,具体请看代码,这样遍历完所有的View以后,则获取了FlowLayout的宽高,然后根据布局文件中宽高的配置设置大小。
onLayout()方法
onLayout()是用来分配子布局的位置,基本思路和上面onMeasure()的一样,遍历所有子View,然后根据每个子View的位置获取具体坐标然后设置,具体请看代码实现。
注意:计算需要考虑到内外边距,所以计算得细心。当然如果你不想自己实现,也可以直接下载源码中的FlowLayout.java文件,然后在布局文件中使用。
在布局文件中使用FlowLayout。使用方法和普通控件是一样的,只是需要带上包名。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > <com.jay.flowlayout.FlowLayout android:padding="5dp" android:id="@+id/flowlayout" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="#E5E5F5"> </com.jay.flowlayout.FlowLayout> </LinearLayout>
在Acitivity中进行动态添加控件就不再贴出了,可自行查看源码。
源码下载:360云盘 访问密码 8337
另外在慕课网有视频讲解,不过具体实现麻烦了一点,可以对照本博客共同学习