How is the position of a RecyclerView adapter related to the index of its dataset?

风格不统一 提交于 2019-12-17 19:43:00

问题


I thought they were the same, but they're not. The following code gives an indexOutOfBounds exception when I try to access the "position" index of my dataset, in this case a list of a model I created called Task:

public class TaskAdapter extends RecyclerView.Adapter<TaskAdapter.TaskViewHolder>   {

private List<Task> taskList;
private TaskAdapter thisAdapter = this;

// cache of views to reduce number of findViewById calls
public static class TaskViewHolder extends RecyclerView.ViewHolder {
    protected TextView taskTV;
    protected ImageView closeBtn;

    public TaskViewHolder(View v) {
        super(v);
        taskTV = (TextView)v.findViewById(R.id.taskDesc);
        closeBtn = (ImageView)v.findViewById(R.id.xImg);
    }
}


public TaskAdapter(List<Task> tasks) {
    if(tasks == null)
        throw new IllegalArgumentException("tasks cannot be null");
    taskList = tasks;
}


// onBindViewHolder binds a model to a viewholder
@Override
public void onBindViewHolder(TaskViewHolder taskViewHolder, int pos) {
    final int position = pos;
    Task currTask = taskList.get(pos);
    taskViewHolder.taskTV.setText(currTask.getDescription());

    **taskViewHolder.closeBtn.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Log.d("TRACE", "Closing task at position " + position);
            // delete from SQLite DB
            Task taskToDel = taskList.get(position);
            taskToDel.delete();
            // updating UI
            taskList.remove(position);
            thisAdapter.notifyItemRemoved(position);
        }
    });**
}

@Override
public int getItemCount() {
    //Log.d("TRACE", taskList.size() + " tasks in DB");
    return taskList.size();
}


// inflates row to create a viewHolder
@Override
public TaskViewHolder onCreateViewHolder(ViewGroup parent, int pos) {
    View itemView = LayoutInflater.from(parent.getContext()).
                                   inflate(R.layout.list_item, parent, false);
    Task currTask = taskList.get(pos);

    //itemView.setBackgroundColor(Color.parseColor(currTask.getColor()));
    return new TaskViewHolder(itemView);
}
}

Deleting from my recyclerview gives unexpected results sometimes. Sometimes the element ahead of the one clicked is deleted, other times an indexOutOfBounds exception occurs at "taskList.get(position)".

Reading https://developer.android.com/reference/android/support/v7/widget/RecyclerView.Adapter.html and https://developer.android.com/training/material/lists-cards.html did not give me any more insight into why this was happening and how to fix it.

It looks like RecyclerView recycles the rows, but I wouldn't expect an indexoutofbounds exception using a smaller subset of numbers to index my list.


回答1:


RecyclerView does not rebind views when their positions change (for obvious performance reasons). For example, if your data set looks like this:

A B C D

and you add item X via

mItems.add(1, X);
notifyItemInserted(1, 1);

to get

A X B C D

RecyclerView will only bind X and run the animation.

There is a getPosition method in ViewHolder but that may not match adapter position if you call it in the middle of an animation.

If you need the adapter position, your safest option is getting the position from the Adapter.

update for your comment

Add a Task field to the ViewHolder.

Change onCreateViewHolder as follows to avoid creating a listener object on each rebind.

// inflates row to create a viewHolder
@Override
public TaskViewHolder onCreateViewHolder(ViewGroup parent, int type) {
    View itemView = LayoutInflater.from(parent.getContext()).
                               inflate(R.layout.list_item, parent, false);

    final TaskViewHolder vh = new TaskViewHolder(itemView);
    taskViewHolder.closeBtn.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            // delete from SQLite DB
            Task taskToDel = vh.getTask();
            final int pos = taskList.indexOf(taskToDel);
            if (pos == -1) return;
            taskToDel.delete();
            // updating UI
            taskList.remove(pos);
            thisAdapter.notifyItemRemoved(pos);
        }
    });
}

so in your on bind method, you do

// onBindViewHolder binds a model to a viewholder
@Override
public void onBindViewHolder(TaskViewHolder taskViewHolder, int pos) {
    Task currTask = taskList.get(pos);
    taskViewHolder.setTask(currTask);
    taskViewHolder.taskTV.setText(currTask.getDescription());
}



回答2:


Like yigit said, RecyclerView works like that:

A B C D

and you add item X via

mItems.add(1, X);
notifyItemInserted(1, 1);

you get

A X B C D

Using holder.getAdapterPosition() in onClickListener() will give you the right item from dataset to be removed, not the "static" view position. Here's the doc about it onBindViewHolder




回答3:


Why dont you use a public interface for the button click and controle the action in the MainActivity.

In your adapter add:

public interface OnItemClickListener {
    void onItemClick(View view, int position, List<Task> mTaskList);
}

and

public OnItemClickListener mItemClickListener;

// Provide a suitable constructor (depends on the kind of dataset)
public TaskAdapter (List<Task> myDataset, OnItemClickListener mItemClickListener) {
    this.mItemClickListener = mItemClickListener;
    this.mDataset = mDataset;
}

plus the call in the ViewHolder class

 public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {

    public ViewHolder(View v) {
        super(v);
        ...
        closeBtn = (ImageView)v.findViewById(R.id.xImg);
        closeBtn.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        // If not long clicked, pass last variable as false.
        mItemClickListener.onItemClick(v, getAdapterPosition(), mDataset);
    }
}

In your MainActivity change your adapter to handle the call

// set Adapter
    mAdapter = new TaskAdapter(taskList, new TaskAdapter.OnItemClickListener() {

        @Override
        public void onItemClick(View v, int position) {
            if (v.getId() == R.id.xImg) {
                Task taskToDel = taskList.get(position);
                // updating UI
                taskList.remove(position);
                thisAdapter.notifyItemRemoved(position);
                // remove from db with unique id to use delete query
                // dont use the position but something like taskToDel.getId() 
                taskToDel.delete();
            } 
        }
    });



回答4:


Personally, I don't like this concept of RecyclerViews. Seems like it wasn't thought of completely.

As it was said when removing an item the Recycler view just hides an item. But usually you don't want to leave that item in your collection. When deleting an item from the collection "it shifts its elements towards 0" whereas recyclerView keeps the same size.

If you are calling taskList.remove(position); your position must be evaluated again:

int position = recyclerView.getChildAdapterPosition(taskViewHolder.itemView);



回答5:


Thanks to @yigit for his answer, his solution mainly worked, I just tweaked it a little bit so as to avoid using vh.getTask() which I was not sure how to implement.

    final ViewHolder vh = new ViewHolder(customView);
    final KittyAdapter final_copy_of_this = this;

    // We attach a CheckChange Listener here instead of onBindViewHolder
    // to avoid creating a listener object on each rebind
    // Note Rebind is only called if animation must be called on view (for efficiency)
    // It does not call on the removed if the last item is checked
    vh.done.setChecked(false);
    vh.done.setOnCheckedChangeListener(null);
    vh.done.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
        @Override
        public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
            buttonView.setEnabled(false);
            final int pos2 = vh.getAdapterPosition(); // THIS IS HOW TO GET THE UPDATED POSITION

            // YOU MUST UPDATE THE DATABASE, removed by Title
            DatabaseHandler db = new DatabaseHandler(mContext);
            db.remove(mDataSet.get(pos2).getTitle(), fp);
            db.close();
            // Update UI
            mDataSet.remove(pos2);
            final_copy_of_this.notifyItemRemoved(pos2);

        }
    });

Notice instead to get the updated position, you can call vh.getAdapterPosition(), which is the line that will give you the updated position from the underlying dataset rather than the fake view.

This is working for me as of now, if someone knows of a drawback to using this please let me know. Hope this helps someone.



来源:https://stackoverflow.com/questions/26695229/how-is-the-position-of-a-recyclerview-adapter-related-to-the-index-of-its-datase

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!