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