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

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


person HukeLau_DABA    schedule 02.11.2014    source источник
comment
Извините, я не прочитал весь ваш вопрос, но я думаю, что в вашем коде второй параметр onCreateViewHolder не относится к положению элемента в адаптере, а будет представлен ViewType, который создается ViewHolder. Подробнее читайте в ссылке :)   -  person Nguyễn Hoài Nam    schedule 02.11.2014
comment
developer.android.com/reference/android/support/v7/widget/ Здесь определяется связанный с ним метод. Кстати, вы должны получить элемент задачи только в методе onBindViewHolder :-?   -  person Nguyễn Hoài Nam    schedule 02.11.2014


Ответы (5)


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

A B C D

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

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

получить

A X B C D

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

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

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

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

Добавьте поле Task в 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);
        }
    });
}

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

// 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());
}
person yigit    schedule 02.11.2014
comment
Я хочу удалить элемент моего адаптера, когда я нажимаю на него, как показано в моем адаптере. pos не является используемой переменной, какую переменную следует использовать? - person HukeLau_DABA; 04.11.2014
comment
поместите задачу в свой TaskViewHolder, чтобы вы могли получить к ней доступ в прослушивателе кликов. Кроме того, не создавайте прослушиватель кликов каждый раз при восстановлении View, вместо этого назначайте его при создании. Соответственно обновлю свой ответ. - person yigit; 04.11.2014
comment
спасибо, но я добавил геттер и сеттер для задачи защищенного поля во вьюхолдере, скопировал и вставил код в этот пост, а удаление ничего не делает - person HukeLau_DABA; 04.11.2014
comment
ну, я бы сказал, отладьте событие onClick и посмотрите, что происходит. sth small должен отсутствовать. - person yigit; 04.11.2014
comment
@yigit извините, я не понимаю vh.getTask(); Как это выглядит внутри класса TaskViewHolder? Откуда он знает, какая задача? Я борюсь за получение должности. - person Script Kitty; 17.04.2016

Как сказал yigit, RecyclerView работает так:

A B C D

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

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

Вы получаете

A X B C D

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

person Ely Dantas    schedule 13.01.2016

Почему бы вам не использовать общедоступный интерфейс для нажатия кнопки и управления действием в 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();
            } 
        }
    });
person Hugo Mulder    schedule 27.06.2015

Лично мне не нравится эта концепция RecyclerViews. Кажется, это не было продумано до конца.

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

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

int position = recyclerView.getChildAdapterPosition(taskViewHolder.itemView);
person Amio.io    schedule 15.05.2015

Благодаря @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(), которая представляет собой строку, которая даст вам обновленную позицию из базового набора данных, а не поддельное представление.

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

person Script Kitty    schedule 17.04.2016