仿淘宝,自定义ViewGroup实现自动换行布局

先来一张淘宝的效果图和自定义实现的效果图对比


淘宝图


自定义效果



效果实现的原理很简单,继承自ViewGroup来实现一个自定义的布局容器,主要逻辑在onMeasure和onLayout当中进行处理。


onMeasure: 对所有子控件的宽度和高度进行测量,并且根据子控件的个数,来设置布局本身的高度。

@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		// 测量出所有的childView的宽和高
		measureChildren(widthMeasureSpec, heightMeasureSpec);

		// 按全部控件的大小测量出实际的控件高度
		int height = getWrapHeight(widthMeasureSpec, heightMeasureSpec);

		setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), height);
	}

	/**
	 * 返回计算的包含内容的布局高度
	 * 
	 * @return
	 */
	private int getWrapHeight(int widthMeasureSpec, int heightMeasureSpec) {
		WrapLayoutParams parmas = null;
		int row = 1; // 总行数(默认是一行)
		int maxWidth = MeasureSpec.getSize(widthMeasureSpec); // 当前容器所拥有的最大宽度

		int hlength = 0;// 当前行的水平已占宽度
		int vlength = 0; // 当前控件的高度(返回结果)

		for (int i = 0; i < getChildCount(); i++) {
			View child = getChildAt(i);
			parmas = (WrapLayoutParams) child.getLayoutParams();
			int childWidth = child.getMeasuredWidth() + parmas.leftMargin
					+ parmas.rightMargin; // 子控件的左右边距加上宽度

			int childHeight = child.getMeasuredHeight() + parmas.topMargin
					+ parmas.bottomMargin;// 子控件的上下边距加上高度

			hlength += childWidth;// 水平轴上已经使用的宽度
			if (hlength > maxWidth) { // 换行了行数就自增加1
				row++;
				hlength = childWidth; // 当前控件被分配到下一行,从新计算
			}
			// ((上边距+下边距) + 子控件的高度 )* 总行数
			vlength = row * childHeight;
		}

		return vlength;
	}
主要逻辑已在代码中注释明白,布局容器本身的高度是由控件的个数来决定的,在onMeasure当中不但要对子控件的宽高进行测量,还需要根据子控件的数据来测量本身的宽和高,代码中并未对不同的布局方式( wrap_content, match_parent)做出处理,默认宽度使用父控件传递下来的宽度,高度则是根据所包含子控件的高度来自适应(wrap_content)大小。如果需要处理,只需根据widthMeasureSpec 和 heightMeasureSpec 的Mode来进行相应处理即可。

自定义ViewGroup的时候,想要子控件的Margin属性生效,需要重写下面这个方法:

@Override
	public LayoutParams generateLayoutParams(AttributeSet attrs) {
		// TODO Auto-generated method stub
		return new WrapLayoutParams(getContext(), attrs);
	}

	/**
	 * 
	 * @ClassName: WrapLayoutParams
	 * @Description: TODO 布局参数对象
	 * @date 2015-9-8 下午4:18:30
	 * 
	 */
	public class WrapLayoutParams extends ViewGroup.MarginLayoutParams {

		public WrapLayoutParams(Context ctxt, AttributeSet attrs) {
			super(ctxt, attrs);
		}

	}


ViewGroup和LayoutParams之间的关系

当在LinearLayout中写childView的时候,可以写layout_gravity,layout_weight属性;

在RelativeLayout中的childView有layout_centerInParent属性,却没有layout_gravity,layout_weight,这是为什么呢?

这是因为每个ViewGroup需要指定一个LayoutParams,用于确定支持childView支持哪些属性,比如LinearLayout指定LinearLayout.LayoutParams等。如果大家去看LinearLayout的源码,会发现其内部定义了LinearLayout.LayoutParams,在此类中,你可以发现weight和gravity的身影。

onLayout: 对所包含的所有子控件进行定位,指定子控件在哪个位置绘制自己。

	@Override
	protected void onLayout(boolean changed, int l, int t, int r, int b) {
		Log.d("---onLayout---", "l:" + l + "t:" + t + "r:" + r + "b:" + b);

		// TODO Auto-generated method stub
		WrapLayoutParams params = null;
		int row = 0; // 总行数
		int hlength = l; // 当前行的水平已占宽度
		int vlength = t; // 当前垂直布局已占用的高度

		for (int i = 0; i < getChildCount(); i++) {
			View child = getChildAt(i);
			params = (WrapLayoutParams) child.getLayoutParams();

			// 计算一个控件的有效宽度和高度
			int childWidth = child.getMeasuredWidth() + params.leftMargin
					+ params.rightMargin;
			int childHeight = child.getMeasuredHeight() + params.topMargin
					+ params.bottomMargin;

			// 定位水平光标位置
			if (hlength + childWidth > r) {// 换行了行数就自增加1
				hlength = l; // 当前控件被分配到下一行,从新计算
				row++;
			}

			// 定位垂直光标位置
			vlength = row * childHeight;

			// 先把控件布局到光标位置
			child.layout(hlength + params.leftMargin, vlength, hlength
					+ childWidth, vlength + childHeight);
			// 然后从新定位光标位置
			hlength += childWidth;
		}
	}

代码注释也已经写的很详细,主要就是检测当前所放置的行是否还可以放置当前子控件,超出宽度范围,则换行放置。

下面给出完整源码:

/**
 * 
 * @ClassName: WrapLayout
 * @Description: TODO 自动换行的Layout
 * @date 2015-9-6 上午11:23:47
 * 
 */
public class WrapLayout extends ViewGroup {

	public WrapLayout(Context context) {
		super(context);
	}

	public WrapLayout(Context context, AttributeSet attrs) {
		super(context, attrs);
	}

	public WrapLayout(Context context, AttributeSet attrs, int defStyleAttr) {
		super(context, attrs, defStyleAttr);
	}

	@SuppressLint("NewApi")
	public WrapLayout(Context context, AttributeSet attrs, int defStyleAttr,
			int defStyleRes) {
		super(context, attrs, defStyleAttr, defStyleRes);
	}

	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		// 测量出所有的childView的宽和高
		measureChildren(widthMeasureSpec, heightMeasureSpec);

		// 按全部控件的大小测量出实际的控件高度
		int height = getWrapHeight(widthMeasureSpec, heightMeasureSpec);

		setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), height);
	}

	/**
	 * 返回计算的包含内容的布局高度
	 * 
	 * @return
	 */
	private int getWrapHeight(int widthMeasureSpec, int heightMeasureSpec) {
		WrapLayoutParams parmas = null;
		int row = 1; // 总行数(默认是一行)
		int maxWidth = MeasureSpec.getSize(widthMeasureSpec); // 当前控件所拥有的最大宽度

		int hlength = 0;// 当前行的水平已占宽度
		int vlength = 0; // 当前控件的高度(返回结果)

		for (int i = 0; i < getChildCount(); i++) {
			View child = getChildAt(i);
			parmas = (WrapLayoutParams) child.getLayoutParams();
			int childWidth = child.getMeasuredWidth() + parmas.leftMargin
					+ parmas.rightMargin; // 子控件的左右边距加上宽度

			int childHeight = child.getMeasuredHeight() + parmas.topMargin
					+ parmas.bottomMargin;// 子控件的上下边距加上高度

			hlength += childWidth;// 水平轴上已经使用的宽度
			if (hlength > maxWidth) { // 换行了行数就自增加1
				row++;
				hlength = childWidth; // 当前控件被分配到下一行,从新计算
			}
			// ((上边距+下边距) + 子控件的高度 )* 总行数
			vlength = row * childHeight;
		}

		return vlength;
	}

	@Override
	protected void onLayout(boolean changed, int l, int t, int r, int b) {
		Log.d("---onLayout---", "l:" + l + "t:" + t + "r:" + r + "b:" + b);

		// TODO Auto-generated method stub
		WrapLayoutParams params = null;
		int row = 0; // 总行数
		int hlength = l; // 当前行的水平已占宽度
		int vlength = t; // 当前垂直布局已占用的高度

		for (int i = 0; i < getChildCount(); i++) {
			View child = getChildAt(i);
			params = (WrapLayoutParams) child.getLayoutParams();

			// 计算一个控件的有效宽度和高度
			int childWidth = child.getMeasuredWidth() + params.leftMargin
					+ params.rightMargin;
			int childHeight = child.getMeasuredHeight() + params.topMargin
					+ params.bottomMargin;

			// 定位水平光标位置
			if (hlength + childWidth > r) {// 换行了行数就自增加1
				hlength = l; // 当前控件被分配到下一行,从新计算
				row++;
			}

			// 定位垂直光标位置
			vlength = row * childHeight;

			// 先把控件布局到光标位置
			child.layout(hlength + params.leftMargin, vlength, hlength
					+ childWidth, vlength + childHeight);
			// 然后从新定位光标位置
			hlength += childWidth;
		}
	}

	@Override
	public LayoutParams generateLayoutParams(AttributeSet attrs) {
		// TODO Auto-generated method stub
		return new WrapLayoutParams(getContext(), attrs);
	}

	/**
	 * 
	 * @ClassName: WrapLayoutParams
	 * @Description: TODO 布局参数对象
	 * @date 2015-9-8 下午4:18:30
	 * 
	 */
	public class WrapLayoutParams extends ViewGroup.MarginLayoutParams {

		public WrapLayoutParams(Context ctxt, AttributeSet attrs) {
			super(ctxt, attrs);
		}

	}
}

测试界面

public class MainActivity extends Activity {

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		String strs[] = { "世界基于MIUI", "阿里巴巴", "天猫", "亚马孙", "京东", "百度", "奇虎360",
				"微软", "Google", "雅虎", "华为", "小米", "中兴" };
		WrapLayout layout = (WrapLayout) findViewById(R.id.wlayout);

		LayoutInflater inflater = LayoutInflater.from(this);
		for (int i = 0; i < strs.length; i++) {
			LinearLayout item = (LinearLayout) inflater.inflate(
					R.layout.item_taobao, layout, false);
			TextView tValue = (TextView) item.findViewById(R.id.tv_value);
			tValue.setText(strs[i]);
			layout.addView(item);
		}
	}

}

测试主布局界面 activity_main:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <widgets.WrapLayout
        android:id="@+id/wlayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="20dp"
        android:background="@android:color/darker_gray" >
    </widgets.WrapLayout>

</LinearLayout>

子布局项 item_taobao:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:gravity="center_vertical"
    android:layout_margin="5dp"
    android:orientation="horizontal" >

    <ImageView
        android:id="@+id/iv_icon"
        android:layout_width="13dp"
        android:layout_height="13dp"
        android:scaleType="fitXY"
        android:src="@drawable/fav_item_icon_selected" />

    <TextView
        android:layout_marginLeft="2dp"
        android:id="@+id/tv_value"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="集分宝"
        android:textColor="@android:color/black"
        android:textSize="13sp" />

</LinearLayout>

OK,到此结束,有问题欢迎交流指正。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值