在上一篇文章中,我们学习了商城购物车加减控件的简单封装,知道了封装的思路过程和使用方法。还没有看过上一篇文章的朋友,建议先去阅读 商城购物车加减控件的简单封装 。这段时间收到一些小伙伴的反馈,在ListView或者是RecyclerView中存在item复用导致数据错乱的问题,这篇文章就重点解决item复用导致数据错乱的问题和在ListView或者RecyclerView中的用法。下面为了方便我们以ListView为例(RecyclerView是一样的)。
1. 先看下效果图
效果图:
Github地址:https://github.com/Jmengfei/AddSubUtils
2. 为什么复用会导致数据错乱?
有过开发经验的人都知道ListView的缓存复用机制虽然提升了它的性能,但是同样也带来了其他问题,复用item导致数据错乱就是其中一个。在这里我们不讨论为什么会这样。因为如果展开说这个问题的话就失去了本文章的重点。这里主要说下解决方法的思路。
3. 解决思路
解决这个问题的思路很多,网上一般给出这两种办法
上来就说是因为convertview对象共用的原因,不能用convetView,而是每次getView()的时候都new一个对象的view出来.这种办法大概是用屁股想出来的.
即然错乱,那我就自己再弄一个集合保存checkBox的状态,再错乱,弄死你.即然adapter里有一个list集合里保存checkBox的状态了,为什么还要自己再保存一次checkBox的状态呢,不是多此一举吗?
这里我们先看一个例子
这是再正常不过的例子了,item布局非常简单。点击item条目改变CheckBox的选中状态。看下adapter中getView()的代码
@Override
public View getView(int position, View convertView, ViewGroup parent) {
final ViewHolder holder;
if(convertView == null) {
convertView = View.inflate(mContext, R.layout.lv_item,null);
holder = new ViewHolder(convertView);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
holder.tvItem.setText(datas.get(position));
// 点击事件
holder.rlItem.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
holder.cbItem.setChecked(!holder.cbItem.isChecked());
}
});
return convertView;
}
现在咱们修改datas数据bean的数据,增加一个控制checkbox的字段
public class ListBean {
// 为了便于组装数据
public ListBean(String title, boolean itemChecked) {
this.title = title;
this.itemChecked = itemChecked;
}
private String title;
private boolean itemChecked; // 新增字段控制是否被点击,并设置get,set方法
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public boolean isItemChecked() {
return itemChecked;
}
public void setItemChecked(boolean itemChecked) {
this.itemChecked = itemChecked;
}
}
现在咱们利用bean中的属性进行控制checkbox的选中状态,现在再来看getView()中代码
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
final ViewHolder holder;
if(convertView == null) {
convertView = View.inflate(mContext, R.layout.lv_item,null);
holder = new ViewHolder(convertView);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
ListBean listBean = datas.get(position);
holder.tvItem.setText(listBean.getTitle());
holder.cbItem.setChecked(listBean.isItemChecked());
// 点击事件
holder.rlItem.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
datas.get(position).setItemChecked(!(listBean.isItemChecked()));
notifyDataSetChanged();
}
});
holder.cbItem.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
boolean ischecked = holder.cbItem.isChecked();
datas.get(position).setItemChecked(ischecked);
}
});
return convertView;
}
现在我们再次运行起来,发现已经解决了那个问题。
思路很简单,就是利用集合数据中新增字段itemChecked来控制,当我们点击的时候要把checkbox的属性也同步到数据集合中。然后利用数据集合中的itemChecked来设置checkbox的状态。记住一点,同步集合中的状态是解决的关键点。
4. 项目改版
对于项目中,复用的是item中的当前数量,咱们正好是要通过数量来控制数据,所以不用更改集合样式,这简直太棒了有没有?对于外层,咱们需要知道当前输入框的值,怎么时时获取呢,这里我通过接口的方式来传递数据,而且为了适应不在ListView中的情况,我这里重新声明了一个接口,代码如下:
// 当数量改变的接口
public interface OnChangeValueListener {
void onChangeValue(int value,int position);
}
咱们在输入框数量改变的监听函数里面来调用:
/**
* 监听输入的数据变化
*/
private void onNumberInput() {
//当前数量
int count = getNumber();
if (count < mBuyMin) {
//手动输入
inputValue = mBuyMin;
etInput.setText(inputValue + "");
if(mOnChangeValueListener != null) {
mOnChangeValueListener.onChangeValue(inputValue,mPosition);
}
return;
}
int limit = Math.min(mBuyMax, inventory);
if (count > limit) {
if (inventory < mBuyMax) {
//库存不足
warningForInventory();
} else {
//超过最大购买数
warningForBuyMax();
}
}else{
inputValue = count;
if(mOnChangeValueListener != null) {
mOnChangeValueListener.onChangeValue(inputValue,mPosition);
}
}
}
这里有人会问?为什么要传一个position过来,这个position又是什么呢?
大家都知道数据的复用是因为convertView的复用导致了position位置的数据也跟着乱了,假如我知道这个位置,然后用这个位置的数据来赋值给该位置的数据的话,不就解决了,这里就是利用这一点。我们新加入一个字段,并且实现get、set方法,注意set方法返回该类的实例(这样做是为了链式调用),代码如下:
private int mPosition = 0; // 设置改变的位置,默认是0; //集合数据中会用到
public AddSubUtils setPosition(int position) {
mPosition = position;
return this;
}
public int getPosition() {
return mPosition;
}
到这里库中的代码已经修改完毕了,现在咱们看看怎么使用
- 首先你需要在build.gradle中添加引用:
dependencies {
compile 'com.mengfei:AddSubUtils:1.5.0'
}
- 在ListView中Adapter中的getView()方法中:
@Override
public View getView( int position, View convertView, ViewGroup parent) {
ViewHolder holder;
if (convertView == null) {
convertView = View.inflate(ListViewActivity.this, R.layout.listview_item, null);
holder = new ViewHolder(convertView);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
AddSubBean dataBean = mList.get(position);
holder.tv_item.setText(dataBean.getName());
Glide.with(ListViewActivity.this).load(dataBean.getImageId()).into(holder.iv_item);
holder.list_item_utils
.setStep(1)
.setBuyMax(30)
.setPosition(position) // 传入当前位置,一定要传,不然数据会错乱
.setCurrentNumber(dataBean.getValue())
.setInventory(50)
.setOnWarnListener(new AddSubUtils.OnWarnListener() {
@Override
public void onWarningForInventory(int inventory) {
Toast.makeText(ListViewActivity.this, "不能超过当前库存:" + inventory, Toast.LENGTH_SHORT).show();
}
@Override
public void onWarningForBuyMax(int max) {
Toast.makeText(ListViewActivity.this, "超过最大购买数:" + max, Toast.LENGTH_SHORT).show();
}
@Override
public void onWarningForBuyMin(int min) {
Toast.makeText(ListViewActivity.this, "低于最小购买数:" + min, Toast.LENGTH_SHORT).show();
}
})
.setOnChangeValueListener(new AddSubUtils.OnChangeValueListener() {
@Override
public void onChangeValue(int value,int position) {
setValue(position,value); // 使用传回来的position设置数据
}
});
return convertView;
}
}
代码里的两处标注是重点。当然,如果你不在ListView中使用,那就还和之前一样,是不用传入position的,也不用实现AddSubUtils.OnChangeValueListener接口。setValue()方法就简单了:
private void setValue(final int position, int inputValue) {
mList.get(position).setValue(inputValue);
}
到这里你已经解决了在ListView中item复用导致数据错乱的问题,当然,RecyclerView同样适用,这里我就不再给出。是不是用起来很简单?如果你还在为商城购物车加减控件的各种问题而烦恼,那么你不妨试一下这个库,可以帮助你快速的完成这块的工作。需要源码的请到Github上下载,AddSubUtils ,你如果还有其他的需求,欢迎留言,我们共同改进。如果觉得对你有帮助,欢迎star。