自定义ViewGroup打造流式布局

/ 0评 / 0

流式布局

流式布局是一种特殊的布局模式,生活中有很多地方会使用到流式布局,比如网站的标签云就是使用的流式布局,流式布局的特点是,按方向一个一个的排列元素,当一行无法容纳新元素的时候,会自动换行,将新元素放置到下一行,如下图局势一个典型的流式布局。

QQ截图20160220181343

自定义ViewGroup打造流式布局

还是老规矩,介绍之前贴出Demo效果图,看看是否为你想要的样子。

QQ截图20160220181824

自定义一个View往往有三个方法需要注意。

onMeasure();    用于测量View的大小

onLayout();    用于分配View的位置,定义布局

onDraw();    绘制View

由于网上关于这三个函数的讲解已经很多了,而且都是大神级别的人物写的,所以我就不献丑了,只贴出一些我觉得好的博客。

onMeasure源码详解

一步步深入了解View

自定义ViewGroup

如果你详细阅读了上面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

另外在慕课网有视频讲解,不过具体实现麻烦了一点,可以对照本博客共同学习

http://www.imooc.com/learn/237

发表评论

您的电子邮箱地址不会被公开。