RecyclerView和ListView一样,存在因为ItemView被回收复用时导致的状态错乱问题。
先上一张页面
这个页面上放入了两个RecyclerView,各自放置了一个很大的RadioGroup,里面每一个RadioItem是一个复合控件,包含了一个可以显示被选中状态的CircleView(被选中时以一个蓝色圆环标识,通过Canvas直接绘制)。希望实现两个效果:
- RecyclerView滑动时,当前被选中的RadioItem圆环标识状态持续保存。
- 实现RadioItem选中状态切换,即同时显示上一个RadioItem选中状态消失和当前RadioItem选中状态出现。
中文网络里能看到的解决方案是这样的:
Android RecyclerView中ViewHolder的复用导致数据错乱解决办法 ,这是一个侵入式的方案,影响到了数据。我们希望选择操作只影响View本身。
Stackoverflow给出了一个方案,是在Adapter存放上一个被选中的RadioItemView,在切换的时候先取消上一个RadioItem的选中状态,再选中当前的RadioItem,代码:
public class RadioItemAdapter{
View lastSelectedView = null;
...
@Override
public void onBindViewHolder(final PortItemViewHolder holder, final int position) {
holder.getV().setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (lastSelectedView == null){
//new select 第一次选择
((CircleView) view.findViewById(R.id.circle)).select();
lastSelectedView = view;
}
else {
if (lastSelectedView.getTag() != position) {
//reselect 这里做切换选择
((CircleView) lastSelectedView.findViewById(R.id.port_circle)).unselect();
lastSelectedView = view;
((CircleCharView) view.findViewById(R.id.port_circle)).select();
}
else {
//unselect 取消选择
lastSelectedView = null;
if (itemClickListener != null) {
itemClickListener.onUnselect(view, (int) view.getTag());
}
}
}
}
});
//bind view here
if (datum != null){
Data data = datum.get(position);
....
holder.getConvertView().setTag(position); //item view position
}
}
}
问题出现在切换上面。前面说过,RecyclerView中为了提高性能,对ItemView采用回收操作,当一个Item被移出Window之后,它的ItemView会被直接拿来渲染Window中移入的View。所以lastSelectedView在被选中的Item移出Window之后也会被复用。所以一旦出现ItemView回收操作,执行选中状态切换时会出现显示的逻辑混乱。
为了避免出现逻辑混乱,同时又保留切换显示效果,我的解决方案是同时提供两个标识,一个是lastSelectedPosition,一个是lastSelectedView,并在每次bindView的时候重新指定一下lastSelectedView, 如下:
public class RadioItemAdapter{
int lastSelectedPos = -1;
View lastSelectedView = null;
...
@Override
public void onBindViewHolder(final PortItemViewHolder holder, final int position) {
holder.getV().setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (lastSelectedPos < 0){
//new select 新选择
lastSelectedPos = holder.pos;
((CircleCharView) view.findViewById(R.id.port_circle)).select();
lastSelectedView = view;
if (itemClickListener != null) {
itemClickListener.onSelect(view, (int) view.getTag());
}
}
else {
if (lastSelectedPos != holder.pos) {
//only unselect old selectedView if the view is still on the window 只有当lastSelectedView还在可视区域内时才实现unselect效果
if((int) lastSelectedView.getTag() == lastSelectedPos) {
((CircleCharView) lastSelectedView.findViewById(R.id.port_circle)).unselect();
}
lastSelectedPos = holder.pos;
lastSelectedView = view;
//reselect 切换选中状态
((CircleCharView) view.findViewById(R.id.port_circle)).select();
}
}
else {
//unselect 取消选中
lastSelectedPos = -1;
((CircleCharView) view.findViewById(R.id.port_circle)).unselect();
lastSelectedView = null;
}
}
}
});
//bind view here 每次新的Item进入到Window之后都会执行一次绑定
if (ports != null){
holder.pos = position;
//reset the view click state
if (lastSelectedPos == position){
holder.cView.select(0);
lastSelectedView = holder.getV(); //reset selectedView here (old one may be recycled) 这里是关键代码,为了确保lastSelectedView有效,需要在BindView的时候重新为它指定一下引用
}
else {
holder.cView.unselect(0); //这个操作不需要
}
holder.v.setTag(position); //view position
}
}
}
在使用RecyclerView的时候,需要时刻注意任何一个ItemView都有可能被回收,并且回收之后不会检查ItemView的状态。