最近,公司的项目中需要展示商品的规格和属性,但是不同的商品属性个数也是不一样的,
怎么能够让超过一行的属性自动换行呢?这就需要用到我们的流式布局,下面先看看效果图
这里先声明一下:这个自定义控件是基于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(); } }); } 效果图在最上边,大家已经看过了
点击这里下载源码