Какова позиция адаптера RecyclerView, связанная с индексом его набора данных?

Я думал, что они такие же, но это не так. Следующий код дает исключение indexOutOfBounds, когда я пытаюсь получить доступ к индексу позиции моего набора данных, в этом случае список созданной мной модели называется 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);
}
}

Удаление из моего recyclerview иногда дает неожиданные результаты. Иногда элемент перед одним нажатым удаляется, а иногда исключение indexOutOfBounds происходит в "taskList.get(position)".

Чтение https://developer.android.com/reference/android/support/v7/widget/RecyclerView.Adapter.html и https://developer.android.com/training/material/lists-cards.html больше не давало мне понять, почему это было и как это исправить.

Похоже, RecyclerView перерабатывает строки, но я бы не ожидал исключения indexoutofbounds с использованием меньшего подмножества чисел для индексации моего списка.

Ответ 1

RecyclerView не восстанавливает представления, когда их позиции меняются (по очевидным причинам производительности). Например, если ваш набор данных выглядит следующим образом:

A B C D

и вы добавляете элемент X через

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

чтобы получить

A X B C D

RecyclerView свяжет только X и запустит анимацию.

В ViewHolder существует метод getPosition, но это может не соответствовать позиции адаптера, если вы вызываете его в середине анимации.

Если вам нужна позиция адаптера, ваш самый безопасный вариант получает позицию от адаптера.

обновление для вашего комментария

Добавить поле задачи в ViewHolder.

Измените onCreateViewHolder следующим образом, чтобы избежать создания объекта-слушателя при каждой перегруппировке.

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

поэтому в методе привязки on вы выполняете

// 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

Как и yigit, RecyclerView работает так:

A B C D

и вы добавляете элемент X через

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

вы получаете

A X B C D

Использование holder.getAdapterPosition() в onClickListener() даст вам нужный элемент из набора данных, который нужно удалить, а не "статическую" позицию представления. Здесь документ об этом onBindViewHolder

Ответ 3

Почему вы не используете открытый интерфейс для нажатия кнопки и не контролируете действие в MainActivity.

В вашем адаптере добавьте:

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

и

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

плюс вызов в классе ViewHolder

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

В вашем MainActivity измените адаптер для обработки вызова

// 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

Лично мне не нравится эта концепция RecyclerViews. Похоже, он не думал полностью.

Как было сказано при удалении элемента, представление Recycler просто скрывает элемент. Но обычно вы не хотите оставлять этот элемент в своей коллекции. При удалении элемента из коллекции "он сдвигает свои элементы на 0", тогда как recyclerView сохраняет одинаковый размер.

Если вы вызываете taskList.remove(position);, ваша позиция должна быть снова оценена:

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

Ответ 5

Благодаря @yigit для его ответа, его решение в основном работало, я немного изменил его немного, чтобы избежать использования vh.getTask(), который я не был уверен, как реализовать.

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

        }
    });

Обратите внимание, вместо этого, чтобы получить обновленную позицию, вы можете вызвать vh.getAdapterPosition(), которая является строкой, которая даст вам обновленную позицию из базового набора данных, а не поддельного представления.

Теперь это работает для меня, если кто-то знает о недостатке использования этого, пожалуйста, дайте мне знать. Надеюсь, это поможет кому-то.