【问题标题】:RecyclerView causes issue when recyclingRecyclerView 在回收时导致问题
【发布时间】:2016-02-08 09:12:14
【问题描述】:

我有一个使用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() {

    }
}

【问题讨论】:

  • 您应该根据设置的背景颜色维护选择了哪个项目

标签: android android-recyclerview recyclerview-layout


【解决方案1】:

问题是recyclerView 回收行为将您的屏幕外ViewHolder 项目分配给即将在屏幕上显示的新项目。 我不建议您像以上所有答案一样基于 ViewHolder 对象绑定您的逻辑。它真的会给你带来麻烦。 您应该根据数据对象的状态而不是 ViewHolder Object 来构建逻辑,因为您永远不知道它何时被回收。

假设你保存一个 在 ViewHolder 中声明 boolean isSelected 进行检查,但如果为真,那么当这个 viewHolder 被回收时,新 Item 将处于相同的状态。

上述更好的方法是在 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 在评论中问的那样,

如果想一次选择一个项目怎么办?

在这种情况下,再次不应将选定项目状态保存在 ViewHolder 对象中,因为它会被回收并导致您出现问题。对于那种更好的方法是在 Adapter 类而不是 ViewHolder 中有一个字段 int selectedItemPosition 。 下面的代码 sn -p 显示它。

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);
            }
        }
    }

如果您只需要维护选定的 Item 状态,我强烈反对使用 Adapter 类的 notifyDataSetChanged() 方法,因为 RecyclerView 为这些情况提供了更大的灵活性。

【讨论】:

  • 我把你的答案标记为正确的,我做的时候没有充分考虑。但这有一个问题,在您检查 mDataItem.isSelected() 是否为真的 if 语句中,情况总是如此,一旦您选择了一个项目,它将永远被选中。但是一旦选择了另一个项目,您就需要取消选择它!
  • @Gabriel ,如果你想像导航抽屉一样一次只选择一个项目,那是另一种情况。问题不要求一次选择单选。不过,您可以非常优雅地处理它。请查看我的更新答案。
  • @Gabriel 让我知道它是否解决了您的问题?
  • 谢谢,我想第二个解决方案会起作用,但我觉得它有点脏,因为适配器处于位置状态!不是吗?
  • 嗨@Gabriel,真的吗?我不这么认为,因为它的适配器负责保存与列表或 RecyclerView 相关的信息、状态和任何其他值,就像它保存我们的 DataItems 一样。它可以保存全局到整个 List 或 RecyclerView 的任何状态信息,在我们的例子中,它是整个 recyclerView 中的当前选定项。但是把它放在 ViewHolder 对象中确实会导致回收时出现问题。
【解决方案2】:

您应该修改您的逻辑,分配项目(对象)内的值而不是视图:

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);
}

【讨论】:

  • 我给你的答案加了一个,我做的时候没有充分考虑。但这有一个问题,在您检查 mDataItem.isSelected() 是否为真的 if 语句中,情况总是如此,一旦您选择了一个项目,它将永远被选中。但是一旦选择了另一个项目,您需要取消选择它!
  • @Gabriel 答案解释了滚动时出现问题的原因。当然它不能解决你所有的情况。如果您需要取消选择该项目,只需在 clickListener 上更改您的登录名。禁用其他项目,或者只是存储所选项目的 id(而不是位置)。
【解决方案3】:

这是一个很常见的错误,但有一个简单的解决方案。

快速回答:在您的 onBindViewHolder 方法中添加这一行:

if (orderItem.isSelected()){
    viewHolder.orderItem.setBackgroundColor(Color.parseColor(SELECTED_COLOR));
} else {
    viewHolder.orderItem.setBackgroundColor(Color.parseColor(DEFAULT_COLOR));
}

(DEFAULT_COLOR是viewholder默认的颜色)

解释的答案:当系统回收一个视图时,它只是调用onBindViewHolder 方法,所以如果您更改了该视图的任何内容,则必须重置它。如果您更改背景、项目的位置等,就会发生这种情况。任何与内容本身无关的更改都应在该方法中重置

【讨论】:

  • 非常感谢!!我犯了同样的错误,无法解决问题。我很高兴找到你的帖子,你拯救了我的一天。
猜你喜欢
  • 2015-04-30
  • 2014-11-30
  • 1970-01-01
  • 2017-01-08
  • 1970-01-01
  • 2015-04-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多