RecyclerView 在回收时导致问题
RecyclerView causes issue when recycling
我有一个使用 RecyclerView
创建的项目列表。当用户单击其中一个时,我会更改该选定项目的背景颜色。
问题是,当我滚动浏览我的项目并且它们被回收时,一些项目获得了所选项目的背景颜色(这是错误的)。
在这里你可以看到我的 Adapter
的代码:
public class OrderAdapter extends RecyclerView.Adapter<OrderAdapter.ViewHolder> {
private static final String SELECTED_COLOR = "#ffedcc";
private List<OrderModel> mOrders;
public OrderAdapter() {
this.mOrders = new ArrayList<>();
}
public void setOrders(List<OrderModel> orders) {
mOrders = orders;
}
public void addOrders(List<OrderModel> orders) {
mOrders.addAll(0, orders);
}
public void addOrder(OrderModel order) {
mOrders.add(0, order);
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
Context context = parent.getContext();
LayoutInflater inflater = LayoutInflater.from(context);
// Inflate the custom layout
View contactView = inflater.inflate(R.layout.order_main_item, parent, false);
// Return a new holder instance
ViewHolder viewHolder = new ViewHolder(contactView);
return viewHolder;
}
@Override
public void onBindViewHolder(final ViewHolder viewHolder, final int position) {
final OrderModel orderModel = mOrders.get(position);
// Set item views based on the data model
TextView customerName = viewHolder.customerNameText;
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("MM/dd/yyyy' 'HH:mm:ss:S");
String time = simpleDateFormat.format(orderModel.getOrderTime());
customerName.setText(time);
TextView orderNumber = viewHolder.orderNumberText;
orderNumber.setText("Order No: " + orderModel.getOrderNumber());
Button button = viewHolder.acceptButton;
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
viewHolder.userActions.acceptButtonClicked(position);
}
});
final LinearLayout orderItem = viewHolder.orderItem;
orderItem.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
viewHolder.userActions.itemClicked(orderModel);
viewHolder.orderItem.setBackgroundColor(Color.parseColor(SELECTED_COLOR));
}
});
}
@Override
public int getItemCount() {
return mOrders.size();
}
public static class ViewHolder extends RecyclerView.ViewHolder implements OrderContract.View {
public TextView customerNameText;
public Button acceptButton;
public TextView orderNumberText;
public OrderContract.UserActions userActions;
public LinearLayout orderItem;
public ViewHolder(View itemView) {
super(itemView);
userActions = new OrderPresenter(this);
customerNameText = (TextView) itemView.findViewById(R.id.customer_name);
acceptButton = (Button) itemView.findViewById(R.id.accept_button);
orderNumberText = (TextView) itemView.findViewById(R.id.order_number);
orderItem = (LinearLayout) itemView.findViewById(R.id.order_item_selection);
}
@Override
public void removeItem() {
}
}
您应该修改您的逻辑,在项目(对象)而不是视图中分配值:
orderItem.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
orderItem.setSelected(xxxx);
}
});
然后在您的 onBindViewHolder
方法中,您必须根据项目中的这个值来评估颜色。
if (orderItem.isSelected()){
viewHolder.orderItem.setBackgroundColor(xxxx);
} else {
viewHolder.orderItem.setBackgroundColor(xxxx);
}
这是一个很常见的错误,有一个简单的解决方法。
快速回答:在您的 onBindViewHolder
方法中添加此行:
if (orderItem.isSelected()){
viewHolder.orderItem.setBackgroundColor(Color.parseColor(SELECTED_COLOR));
} else {
viewHolder.orderItem.setBackgroundColor(Color.parseColor(DEFAULT_COLOR));
}
(DEFAULT_COLOR
viewholder 默认的颜色)
答案解释:当系统回收一个viewholder时,它只是调用onBindViewHolder
方法,所以如果你改变了那个viewholder的任何东西,你必须重新设置它。如果您更改背景、项目的位置等,就会发生这种情况。任何与内容本身无关的更改都应在该方法中重置
问题是 recyclerView
回收行为,它将屏幕外的 ViewHolder
项目分配给即将显示在屏幕上的新项目。
我不建议您像上述所有答案那样基于 ViewHolder
对象绑定您的逻辑。它真的会给你带来麻烦。
您应该根据数据对象的状态构建逻辑,而不是 ViewHolder
对象,因为您永远不知道它何时被回收。
假设你存了一个
state boolean isSelected in ViewHolder
进行检查,但是如果它是 true,那么当这个 viewHolder
将被回收时,新项目将有相同的状态。
更好的方法是在 DataModel 对象中保存任何状态。在你的情况下,只有一个 boolean isSelected.
示例
package chhimwal.mahendra.multipleviewrecyclerproject;
import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.support.v7.widget.CardView;
import android.widget.TextView;
import java.util.List;
/**
* Created by mahendra.chhimwal on 12/10/2015.
*/
public class MyRecyclerViewAdapter extends RecyclerView.Adapter<MyRecyclerViewAdapter.ViewHolder> {
private Context mContext;
private List<DataModel> mRViewDataList;
public MyRecyclerViewAdapter(Context context, List<DataModel> rViewDataList) {
this.mContext = context;
this.mRViewDataList = rViewDataList;
}
@Override
public MyRecyclerViewAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
View view = inflater.inflate(R.layout.item_recycler_view, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
holder.bindDataWithViewHolder(mRViewDataList.get(position));
}
@Override
public int getItemCount() {
return mRViewDataList != null ? mRViewDataList.size() : 0;
}
public class ViewHolder extends RecyclerView.ViewHolder {
private TextView textView;
private LinearLayout llView;
private DataModel mDataItem=null;
public ViewHolder(View itemView) {
super(itemView);
llView=(LinearLayout)itemView.findViewById(R.id.ll_root_view);
textView = (TextView) itemView.findViewById(R.id.tvItemName);
cvItemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// One should handle onclick of event here based on the dataItem i.e. mDataItem in this case.
// something like that..
/* Intent intent = new Intent(mContext,ResultActivity.class);
intent.putExtra("MY_DATA",mDataItem); //If you want to pass data.
intent.putExtra("CLICKED_ITEM_POSTION",getAdapterPosition()); // If one want to get selected item position
startActivity(intent);*/
Toast.makeText(mContext,"You clicked item number "+ViewHolder.this.getAdapterPosition(),Toast.LENTH_SHORT).show();
}
});
}
//This is clean method to bind data with viewHolder. Do all dirty things on View based on dataItem.
//Must be called from onBindViewHolder(),with dataItem. In our case dataItem is String object.
public void bindDataWithViewHolder(DataModel dataItem){
this.mDataItem=dataItem;
if(mDataItem.isSelected()){
llView.setBackgroundColor(Color.ParseColor(SELCTED_COLOR);
}else{
llView.setBackgroundColor(Color.ParseColor(DEFAULT_COLOR);
}
//other View binding logics like setting text , loading image etc.
textView.setText(mDataItem);
}
}
}
正如@Gabriel 在评论中所问,
what if one want to select a single item at time?
在那种情况下,同样不应将所选项目状态保存在 ViewHolder
对象中,因为它会被回收并给您带来问题。更好的方法是在 Adapter
class 而不是 ViewHolder
中有一个字段 int selectedItemPosition
。
下面的代码片段展示了它。
public class MyRecyclerViewAdapter extends RecyclerView.Adapter<MyRecyclerViewAdapter.ViewHolder> {
private Context mContext;
private List<DataModel> mRViewDataList;
//variable to hold selected Item position
private int mSelectedItemPosition = -1;
public MyRecyclerViewAdapter(Context context, List<DataModel> rViewDataList) {
this.mContext = context;
this.mRViewDataList = rViewDataList;
}
@Override
public MyRecyclerViewAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
View view = inflater.inflate(R.layout.item_recycler_view, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
holder.bindDataWithViewHolder(mRViewDataList.get(position),position);
}
@Override
public int getItemCount() {
return mRViewDataList != null ? mRViewDataList.size() : 0;
}
public class ViewHolder extends RecyclerView.ViewHolder {
private TextView textView;
private LinearLayout llView;
private DataModel mDataItem=null;
public ViewHolder(View itemView) {
super(itemView);
llView=(LinearLayout)itemView.findViewById(R.id.ll_root_view);
textView = (TextView) itemView.findViewById(R.id.tvItemName);
cvItemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//Handling for background selection state changed
int previousSelectState=mSelectedItemPosition;
mSelectedItemPosition = getAdapterPosition();
//notify previous selected item
notifyItemChanged(previousSelectState);
//notify new selected Item
notifyItemChanged(mSelectedItemPosition);
//Your other handling in onclick
}
});
}
//This is clean method to bind data with viewHolder. Do all dirty things on View based on dataItem.
//Must be called from onBindViewHolder(),with dataItem. In our case dataItem is String object.
public void bindDataWithViewHolder(DataModel dataItem, int currentPosition){
this.mDataItem=dataItem;
//Handle selection state in object View.
if(currentPosition == mSelectedItemPosition){
llView.setBackgroundColor(Color.ParseColor(SELCTED_COLOR);
}else{
llView.setBackgroundColor(Color.ParseColor(DEFAULT_COLOR);
}
//other View binding logics like setting text , loading image etc.
textView.setText(mDataItem);
}
}
}
如果您只需要维护选定的项目状态,我强烈反对使用适配器 class 的 notifyDataSetChanged() 方法,因为 RecyclerView 为这些情况提供了更多的灵活性.
我有一个使用 RecyclerView
创建的项目列表。当用户单击其中一个时,我会更改该选定项目的背景颜色。
问题是,当我滚动浏览我的项目并且它们被回收时,一些项目获得了所选项目的背景颜色(这是错误的)。
在这里你可以看到我的 Adapter
的代码:
public class OrderAdapter extends RecyclerView.Adapter<OrderAdapter.ViewHolder> {
private static final String SELECTED_COLOR = "#ffedcc";
private List<OrderModel> mOrders;
public OrderAdapter() {
this.mOrders = new ArrayList<>();
}
public void setOrders(List<OrderModel> orders) {
mOrders = orders;
}
public void addOrders(List<OrderModel> orders) {
mOrders.addAll(0, orders);
}
public void addOrder(OrderModel order) {
mOrders.add(0, order);
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
Context context = parent.getContext();
LayoutInflater inflater = LayoutInflater.from(context);
// Inflate the custom layout
View contactView = inflater.inflate(R.layout.order_main_item, parent, false);
// Return a new holder instance
ViewHolder viewHolder = new ViewHolder(contactView);
return viewHolder;
}
@Override
public void onBindViewHolder(final ViewHolder viewHolder, final int position) {
final OrderModel orderModel = mOrders.get(position);
// Set item views based on the data model
TextView customerName = viewHolder.customerNameText;
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("MM/dd/yyyy' 'HH:mm:ss:S");
String time = simpleDateFormat.format(orderModel.getOrderTime());
customerName.setText(time);
TextView orderNumber = viewHolder.orderNumberText;
orderNumber.setText("Order No: " + orderModel.getOrderNumber());
Button button = viewHolder.acceptButton;
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
viewHolder.userActions.acceptButtonClicked(position);
}
});
final LinearLayout orderItem = viewHolder.orderItem;
orderItem.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
viewHolder.userActions.itemClicked(orderModel);
viewHolder.orderItem.setBackgroundColor(Color.parseColor(SELECTED_COLOR));
}
});
}
@Override
public int getItemCount() {
return mOrders.size();
}
public static class ViewHolder extends RecyclerView.ViewHolder implements OrderContract.View {
public TextView customerNameText;
public Button acceptButton;
public TextView orderNumberText;
public OrderContract.UserActions userActions;
public LinearLayout orderItem;
public ViewHolder(View itemView) {
super(itemView);
userActions = new OrderPresenter(this);
customerNameText = (TextView) itemView.findViewById(R.id.customer_name);
acceptButton = (Button) itemView.findViewById(R.id.accept_button);
orderNumberText = (TextView) itemView.findViewById(R.id.order_number);
orderItem = (LinearLayout) itemView.findViewById(R.id.order_item_selection);
}
@Override
public void removeItem() {
}
}
您应该修改您的逻辑,在项目(对象)而不是视图中分配值:
orderItem.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
orderItem.setSelected(xxxx);
}
});
然后在您的 onBindViewHolder
方法中,您必须根据项目中的这个值来评估颜色。
if (orderItem.isSelected()){
viewHolder.orderItem.setBackgroundColor(xxxx);
} else {
viewHolder.orderItem.setBackgroundColor(xxxx);
}
这是一个很常见的错误,有一个简单的解决方法。
快速回答:在您的 onBindViewHolder
方法中添加此行:
if (orderItem.isSelected()){
viewHolder.orderItem.setBackgroundColor(Color.parseColor(SELECTED_COLOR));
} else {
viewHolder.orderItem.setBackgroundColor(Color.parseColor(DEFAULT_COLOR));
}
(DEFAULT_COLOR
viewholder 默认的颜色)
答案解释:当系统回收一个viewholder时,它只是调用onBindViewHolder
方法,所以如果你改变了那个viewholder的任何东西,你必须重新设置它。如果您更改背景、项目的位置等,就会发生这种情况。任何与内容本身无关的更改都应在该方法中重置
问题是 recyclerView
回收行为,它将屏幕外的 ViewHolder
项目分配给即将显示在屏幕上的新项目。
我不建议您像上述所有答案那样基于 ViewHolder
对象绑定您的逻辑。它真的会给你带来麻烦。
您应该根据数据对象的状态构建逻辑,而不是 ViewHolder
对象,因为您永远不知道它何时被回收。
假设你存了一个
state boolean isSelected in ViewHolder
进行检查,但是如果它是 true,那么当这个 viewHolder
将被回收时,新项目将有相同的状态。
更好的方法是在 DataModel 对象中保存任何状态。在你的情况下,只有一个 boolean isSelected.
示例
package chhimwal.mahendra.multipleviewrecyclerproject;
import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.support.v7.widget.CardView;
import android.widget.TextView;
import java.util.List;
/**
* Created by mahendra.chhimwal on 12/10/2015.
*/
public class MyRecyclerViewAdapter extends RecyclerView.Adapter<MyRecyclerViewAdapter.ViewHolder> {
private Context mContext;
private List<DataModel> mRViewDataList;
public MyRecyclerViewAdapter(Context context, List<DataModel> rViewDataList) {
this.mContext = context;
this.mRViewDataList = rViewDataList;
}
@Override
public MyRecyclerViewAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
View view = inflater.inflate(R.layout.item_recycler_view, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
holder.bindDataWithViewHolder(mRViewDataList.get(position));
}
@Override
public int getItemCount() {
return mRViewDataList != null ? mRViewDataList.size() : 0;
}
public class ViewHolder extends RecyclerView.ViewHolder {
private TextView textView;
private LinearLayout llView;
private DataModel mDataItem=null;
public ViewHolder(View itemView) {
super(itemView);
llView=(LinearLayout)itemView.findViewById(R.id.ll_root_view);
textView = (TextView) itemView.findViewById(R.id.tvItemName);
cvItemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// One should handle onclick of event here based on the dataItem i.e. mDataItem in this case.
// something like that..
/* Intent intent = new Intent(mContext,ResultActivity.class);
intent.putExtra("MY_DATA",mDataItem); //If you want to pass data.
intent.putExtra("CLICKED_ITEM_POSTION",getAdapterPosition()); // If one want to get selected item position
startActivity(intent);*/
Toast.makeText(mContext,"You clicked item number "+ViewHolder.this.getAdapterPosition(),Toast.LENTH_SHORT).show();
}
});
}
//This is clean method to bind data with viewHolder. Do all dirty things on View based on dataItem.
//Must be called from onBindViewHolder(),with dataItem. In our case dataItem is String object.
public void bindDataWithViewHolder(DataModel dataItem){
this.mDataItem=dataItem;
if(mDataItem.isSelected()){
llView.setBackgroundColor(Color.ParseColor(SELCTED_COLOR);
}else{
llView.setBackgroundColor(Color.ParseColor(DEFAULT_COLOR);
}
//other View binding logics like setting text , loading image etc.
textView.setText(mDataItem);
}
}
}
正如@Gabriel 在评论中所问,
what if one want to select a single item at time?
在那种情况下,同样不应将所选项目状态保存在 ViewHolder
对象中,因为它会被回收并给您带来问题。更好的方法是在 Adapter
class 而不是 ViewHolder
中有一个字段 int selectedItemPosition
。
下面的代码片段展示了它。
public class MyRecyclerViewAdapter extends RecyclerView.Adapter<MyRecyclerViewAdapter.ViewHolder> {
private Context mContext;
private List<DataModel> mRViewDataList;
//variable to hold selected Item position
private int mSelectedItemPosition = -1;
public MyRecyclerViewAdapter(Context context, List<DataModel> rViewDataList) {
this.mContext = context;
this.mRViewDataList = rViewDataList;
}
@Override
public MyRecyclerViewAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
View view = inflater.inflate(R.layout.item_recycler_view, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
holder.bindDataWithViewHolder(mRViewDataList.get(position),position);
}
@Override
public int getItemCount() {
return mRViewDataList != null ? mRViewDataList.size() : 0;
}
public class ViewHolder extends RecyclerView.ViewHolder {
private TextView textView;
private LinearLayout llView;
private DataModel mDataItem=null;
public ViewHolder(View itemView) {
super(itemView);
llView=(LinearLayout)itemView.findViewById(R.id.ll_root_view);
textView = (TextView) itemView.findViewById(R.id.tvItemName);
cvItemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//Handling for background selection state changed
int previousSelectState=mSelectedItemPosition;
mSelectedItemPosition = getAdapterPosition();
//notify previous selected item
notifyItemChanged(previousSelectState);
//notify new selected Item
notifyItemChanged(mSelectedItemPosition);
//Your other handling in onclick
}
});
}
//This is clean method to bind data with viewHolder. Do all dirty things on View based on dataItem.
//Must be called from onBindViewHolder(),with dataItem. In our case dataItem is String object.
public void bindDataWithViewHolder(DataModel dataItem, int currentPosition){
this.mDataItem=dataItem;
//Handle selection state in object View.
if(currentPosition == mSelectedItemPosition){
llView.setBackgroundColor(Color.ParseColor(SELCTED_COLOR);
}else{
llView.setBackgroundColor(Color.ParseColor(DEFAULT_COLOR);
}
//other View binding logics like setting text , loading image etc.
textView.setText(mDataItem);
}
}
}
如果您只需要维护选定的项目状态,我强烈反对使用适配器 class 的 notifyDataSetChanged() 方法,因为 RecyclerView 为这些情况提供了更多的灵活性.