安卓流式布局(可换行的标签)

最近,公司的项目中需要展示商品的规格和属性,但是不同的商品属性个数也是不一样的,

怎么能够让超过一行的属性自动换行呢?这就需要用到我们的流式布局,下面先看看效果图


        这里先声明一下:这个自定义控件是基于http://blog.csdn.net/jdsjlzx/article/details/45042081?ref=myread

这篇文章更改的,流式是怎么实现的还是请先看完上边这篇文章.


在将楼主的源码下载下来使用的时候遇到以下几个问题,本文将围绕这几个小问题进行讲解

  首先楼主的这个自定义控件始终默认铺满屏幕的,但是感觉很奇怪,因为在onMeasure这个方法中

          已经根据控件设置的模式测量模式进行过计算了,按道理说不应该是铺满屏幕的!

         而且我设置的高度是wrap_content(自适应)   打了断点试的时候  发现测量出来的距离并不是铺满屏幕,

         而是真是高度  又认认真真的看了一下测量发方法  发现楼主将super.onMeasure(widthMeasureSpec, heightMeasureSpec);放在了

         方法结束的位置大哭  是不是很扯   这句话放在最后的话,就相当于我测量了半天,测量到最后不使用这个

        测量值,而使用其父类(ViewGroup)的测量结果,也就是默认结果 

    解决方案:把这句话移到方法首句或者直接删除这句话

接着  确实是自适应了  但是只是单纯的换行不行   我们需要点击之后知道我们选中了那个   并且选中的这个背景颜色需要变

    简单地分析下,我们需要做以下几件事:

        1.两个自定义属性  分别是选中和未选中的背景颜色

         2.获取所有控件的的位置

         3.判断点击的点是不是包含在某个子控件中

         4,如果是包含在某个子控件中,设置回调

下面我们具体去完成我们这几个步骤:

1.两个自定义属性

     先在values下创建一个attrs的xml文件   分别代表选中和未选中的两个状态

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="flowlayout">
        <attr name="back_selected" format="reference" />
        <attr name="back_un_selected" format="reference" />
    </declare-styleable>
</resources>
      接着  创建两个shape   分别代表选中和未选中时的背景颜色状态

       选中状态

<?xml version="1.0" encoding="utf-8"?>    
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle" >
    <corners android:radius="1dp" />
    <stroke
        android:width="2dp"
        android:color="#ff6600" />
    <solid android:color="#ffffff" />
</shape>
      未选中状态
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle" >
    <corners android:radius="1dp" />
    <stroke
        android:width="2dp"
        android:color="#000000" />
    <solid android:color="#ffffff" />
</shape>
      接着在自定义控件的构造方法中获取这两个自定义属性:

     public FlowLayout(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
		childLocationList = new HashMap<Integer, Rect>();
		// 获取自定义属性的值
		TypedArray typedArray = context.getTheme().obtainStyledAttributes(
				attrs, R.styleable.flowlayout, defStyle, 0);
		int  back_selected = typedArray.getResourceId(
				R.styleable.flowlayout_back_selected, 0);// 选中时的背景资源id
		int back_unselected = typedArray.getResourceId(
				R.styleable.flowlayout_back_un_selected, 0);// 未选中时的背景资源id
		typedArray.recycle();
	}
     最后在  布局文件中为这两个属性赋值  注意命名空间

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:flayout="http://schemas.android.com/apk/res/com.czm.flowlayout"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" >
    <com.czm.flowlayout.FlowLayout
        android:id="@+id/flowlayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        flayout:back_selected="@drawable/shape_label_selected"
        flayout:back_un_selected="@drawable/shape_label_unselected" >
    </com.czm.flowlayout.FlowLayout>
</RelativeLayout>
2.获得所有控件的位置

    获得子控件的位置    应该在我们指定了子控件的位置之后再去获取  也就是说在onLayout方法的最后

      我们添加一个方法  getAllChildViewLocation

     我们先分析一下思路 :楼主当时是这样存储所有的子控件的

  <span style="font-size:14px;"> // 存储所有子View
	private List<List<View>> mAllChildViews = new ArrayList<List<View>>();</span>
     先将每一行的控件存在一个List集合中  再将所有的行List再存到List集合中

       这样  我们就可以根据行数获取到指定行中所有的控件  再获取指定行指定第几个的控件

         我们能拿到这个具体的控件  就能拿到控件所在的位置

        将拿到的位置存到一个键值对类型的集合中      键表示在这个流式布局中第几个   值是对应控件的位置  

        至于为什么要存在一个键值对类型的集合中,等会再说

         接着怎么去存  


     看了这个图,应该知道键怎么存了

      值是获取了子控件所在的矩形,因为矩形有个方法contains(int x, int y)   可以判断一个点是否包含在这个矩形中

       创建这个集合最好是在构造方法中创建

    private HashMap<Integer, Rect> childLocationList = new HashMap<Integer, Rect>();
     获取并记录子控件的位置记录

        /**
	 * 获取所有的子控件的位置并记录
	 */
	private void getAllChildViewLocation() {
		int countBefore = 0;
		for (int i = 0; i < mAllChildViews.size(); i++) {
			if (i > 0) {
				countBefore += mAllChildViews.get(i - 1).size();
			}
			for (int j = 0; j < mAllChildViews.get(i).size(); j++) {
				View view = mAllChildViews.get(i).get(j);
				Rect rect = new Rect(view.getLeft(), view.getTop(),
						view.getRight(), view.getBottom());
				childLocationList.put(countBefore + j, rect);
			}
		}

	}
3. 判断点击的是哪个

      现在有控件的位置了  我们只要能知道点的位置就够了  下面重写onTouchEvent方法

       /**
	 * 这里我们需要判断子控件是否被点击 点击的是哪个子控件
	 */
	@Override
	public boolean onTouchEvent(MotionEvent event) {
		switch (event.getAction()) {
		case MotionEvent.ACTION_DOWN:
			downX = (int) event.getX();
			downY = (int) event.getY();
			downTime = SystemClock.currentThreadTimeMillis();
			break;
		case MotionEvent.ACTION_UP:
			long upTime = SystemClock.currentThreadTimeMillis();
			if (upTime - downTime <= 50) {// 如果手指按下和手指离开键盘的时间小于50毫秒有效
				for (int i = 0; i < childLocationList.size(); i++) {
					if (childLocationList.get(i).contains(downX, downY)) {// 如果子控件所在位置(矩形)包括这个点
						if (onLabelSelectedListener != null) {
							onLabelSelectedListener.onSelected(i);//这个i是什么   就是子控件是第几个 直接把这个传过去   现在知<span style="white-space:pre">												</span>//道这个集合的键做什么用了吧
						}
						// 将记录的上一个控件颜色改成未选中状态
						if (lastSelectedPosition != -1) {
							TextView lastSelectedView = (TextView) getChildAt(lastSelectedPosition);
							lastSelectedView
									.setBackgroundResource(back_unselected);
						}
						// 将当前选中的控件背景改成选中状态 并记录
						TextView childAt = (TextView) getChildAt(i);
						childAt.setBackgroundResource(back_selected);
						lastSelectedPosition = i;
						break;
					}
				}
			}
			break;

		default:
			break;
		}
		return true;
	}
   4.设置回调和改变状态
<span style="white-space:pre">	</span>/**
	 * 子控件(标签)选中监听
	 * 
	 * @author HaiPeng
	 * 
	 */
	public interface OnLabelSelectedListener {
		void onSelected(int position);
	}

	/**
	 * 设置子控件选中时的监听
	 * 
	 * @param onLabelSelectedListener
	 */
	public void setOnLabelSelectedListener(
			OnLabelSelectedListener onLabelSelectedListener) {
		this.onLabelSelectedListener = onLabelSelectedListener;
	}

	/**
	 * 记录上一次点击的是哪个子控件
	 */
	private int lastSelectedPosition = -1;
        private OnLabelSelectedListener onLabelSelectedListener;
对了在测量之前给子控件设置上没有选中的背景
	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		// TODO Auto-generated method stub
		super.onMeasure(widthMeasureSpec, heightMeasureSpec);

		// 父控件传进来的宽度和高度以及对应的测量模式
		int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
		int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
		int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
		int modeHeight = MeasureSpec.getMode(heightMeasureSpec);

		// 如果当前ViewGroup的宽高为wrap_content的情况
		int width = 0;// 自己测量的 宽度
		int height = 0;// 自己测量的高度
		// 记录每一行的宽度和高度
		int lineWidth = 0;
		int lineHeight = 0;

		// 获取子view的个数
		int childCount = getChildCount();
		for (int i = 0; i < childCount; i++) {
			View child = getChildAt(i);
			<span style="color:#FF6666;">child.setBackgroundResource(back_unselected);</span>
			// 测量子View的宽和高
			measureChild(child, widthMeasureSpec, heightMeasureSpec);
			// 得到LayoutParams
			MarginLayoutParams lp = (MarginLayoutParams) child
					.getLayoutParams();
			// 子View占据的宽度
			int childWidth = child.getMeasuredWidth() + lp.leftMargin
					+ lp.rightMargin;
			// 子View占据的高度
			int childHeight = child.getMeasuredHeight() + lp.topMargin
					+ lp.bottomMargin;
			// 换行时候
			if (lineWidth + childWidth > sizeWidth) {
				// 对比得到最大的宽度
				width = Math.max(width, lineWidth);
				// 重置lineWidth
				lineWidth = childWidth;
				// 记录行高
				height += lineHeight;
				lineHeight = childHeight;
			} else {// 不换行情况
					// 叠加行宽
				lineWidth += childWidth;
				// 得到最大行高
				lineHeight = Math.max(lineHeight, childHeight);
			}
			// 处理最后一个子View的情况
			if (i == childCount - 1) {
				width = Math.max(width, lineWidth);
				height += lineHeight;
			}
		}
		// wrap_content
		setMeasuredDimension(modeWidth == MeasureSpec.EXACTLY ? sizeWidth
				: width, modeHeight == MeasureSpec.EXACTLY ? sizeHeight
				: height);
	}
最后我们在MainActivity中调用一下
<span style="white-space:pre">	</span>private void initChildViews() {
		// TODO Auto-generated method stub
		mFlowLayout = (FlowLayout) findViewById(R.id.flowlayout);
		MarginLayoutParams lp = new MarginLayoutParams(
				LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
		lp.leftMargin = 5;
		lp.rightMargin = 5;
		lp.topMargin = 5;
		lp.bottomMargin = 5;
		for (int i = 0; i < mNames.length; i++) {
			TextView view = new TextView(this);
			view.setText(mNames[i]);
			view.setTextColor(Color.BLACK);
			view.setPadding(5, 5, 5, 5);
			// view.setBackgroundDrawable(getResources().getDrawable(R.drawable.textview_bg));
			mFlowLayout.addView(view, lp);
		}
		mFlowLayout.setOnLabelSelectedListener(new OnLabelSelectedListener() {

			@Override
			public void onSelected(int position) {
				Toast.makeText(MainActivity.this, "第" + position + "个被点击了",
						Toast.LENGTH_SHORT).show();
			}
		});
	}
效果图在最上边,大家已经看过了

点击这里下载源码









评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值