OnBindViewHolder() никогда не вызывается при просмотре в позиции, хотя RecyclerView.findViewHolderForAdapterPosition() возвращает значение null в этой позиции

У меня есть список из 13 элементов (хотя элементы могут быть добавлены или удалены), позиции 0-12. Когда первый фрагмент, содержащий RecyclerView, сначала отображается, только позиции с 0 по 7 видны пользователю (позиция 7 отображается только наполовину). В моем адаптере я Log каждый раз, когда держатель вида привязан/привязан (idk, если здесь применяется грамматика) и записывайте его положение.

Адаптер

@Override
public void onBindViewHolder(final ViewHolder holder, final int position) {
    Log.d(TAG, "onBindViewHolder() position: " + position);
    ...
}

Из моего Log я вижу, что позиции 0-7 связаны:

Вход из адаптера

У меня есть метод selectAll(), который получает каждую позицию ViewHolder по позиции адаптера. Если возвращаемый holder НЕ null, я использую возвращенный holder, чтобы обновить представление, чтобы отобразить его. Если возвращенный держатель IS null, я вызываю selectOnBind() метод, который помещает представление в этом обновлении позиции, чтобы отображать его, когда он привязан, а не в реальном времени, так как в настоящее время он не отображается:

public void selectAll() {
    for (int i = 0; i < numberOfItemsInList; i++) {
        MyAdapter.ViewHolder holder = (MyAdapter.ViewHolder)
                mRecyclerView.findViewHolderForAdapterPosition(i);

        Log.d(TAG, "holder at position " + i + " is " + holder);

        if (holder != null) {
            select(holder);
        } else {
            selectOnBind(i);
        }
    }
}

В этом методе я Log holder вместе с его положением:

Вход из selectAll()

Итак, до этого момента все кажется нормальным. У нас есть позиции 0-7, и в соответствии с Log они связаны с позициями. Когда я ударил selectAll(), не изменяя видимые виды (прокрутка), я вижу, что позиции 0-7 определены, а 8-12 - null. Пока все хорошо.

Здесь, где это становится интересным. Если после вызова selectAll() я прокручиваю дальше вниз, позиции 8 и 9 списка не показывают, что они выбраны.

При проверке Log я вижу, что это потому, что они никогда не связаны, хотя они были сообщены как null:

Журнал адаптеров после прокрутки

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

РЕДАКТИРОВАТЬ (6-29-16)
После обновления AndroidStudio я не могу воспроизвести ошибку. Он работает так, как я ожидал, привязывая нулевые представления. Если эта проблема возникнет, я вернусь к этому сообщению.

Ответы

Ответ 1

Это происходит потому, что:

  • Представления не добавляются в recyclerview (getChildAt не будет работать и вернет null для этой позиции)
  • Они также кэшируются (onBind не будет вызываться)

Вызов recyclerView.setItemViewCacheSize(0) устранит эту "проблему".

Поскольку значение по умолчанию равно 2 (private static final int DEFAULT_CACHE_SIZE = 2; в RecyclerView.Recycler), вы всегда получите 2 представления, которые не будут вызывать onBind, но которые не добавляются в recycler

Ответ 2

В вашем случае вид на позиции 8 и 9 не перерабатывается, они отсоединяются от окна и снова будут прикреплены. И для этого снятого вида onBindViewHolder не вызывается, вызывается только onViewAttachedToWindow. Если вы переопределите эти функции в своем адаптере, вы можете видеть, что я говорю.

@Override
    public void onViewRecycled(ViewHolder vh){
        Log.wtf(TAG,"onViewRecycled "+vh);
    }

    @Override
    public void onViewDetachedFromWindow(ViewHolder viewHolder){
        Log.wtf(TAG,"onViewDetachedFromWindow "+viewHolder);
    }

Теперь, чтобы решить вашу проблему, вам нужно отслеживать просмотры, которые должны были быть переработаны, но отсоединиться, а затем сделать процесс раздела на

@Override
    public void onViewAttachedToWindow(ViewHolder viewHolder){
        Log.wtf(TAG,"onViewAttachedToWindow "+viewHolder);
    }

Ответ 3

Ответы Педро Оливейры и Зарты отлично подходят для понимания проблемы, но я не вижу никаких решений, которым я доволен.

Я считаю, что у вас есть 2 отличных варианта в зависимости от того, что вы делаете:

Вариант 1

Если вы хотите, чтобы onBindViewHolder() вызывался в режиме вне экрана, независимо от того, был ли он кэширован/отсоединен или нет, вы можете сделать:

RecyclerView.ViewHolder view_holder = recycler_view.findViewHolderForAdapterPosition( some_position );

if ( view_holder != null )
{
    //manipulate the attached view
}
else //view is either non-existant or detached waiting to be reattached
    notifyItemChanged( some_position );

Идея состоит в том, что если представление кэшировано/отсоединено, то notifyItemChanged() сообщит адаптеру, что представление недопустимо, что приведет к вызову onBindViewHolder().

Вариант 2

Если вы хотите выполнить частичное изменение (а не все внутри onBindViewHolder()), то внутри onBindViewHolder( ViewHolder view_holder, int position ) вам нужно сохранить position в view_holder и выполнить требуемое изменение onViewAttachedToWindow( ViewHolder view_holder ).

Я рекомендую вариант 1 для простоты, если ваш onBindViewHolder() не делает что-то интенсивное, как возиться с растровыми изображениями.

Ответ 4

Я думаю, что играть с видом - это не очень хорошая идея в recyclerview. Подход, который я всегда использую, чтобы просто ввести флаг модели, используемой для RecyclerView. Пусть предположим, что ваша модель похожа -

class MyModel{
    String name;
    int age;
}

Если вы отслеживаете выбранный вид или нет, введите в модель один логический элемент. Теперь это будет выглядеть как

class MyModel{
    String name;
    int age;
    boolean isSelected;
}

Теперь ваш флажок будет выбран/не выбран на основе нового флага isSelected (в onBindViewHolder()). При каждом выборе в представлении изменится значение соответствующего выбранного значения модели на значение true, а при невыбранном изменении - на false. В вашем случае просто запустите цикл для изменения всей модели isSelected на значение true, а затем вызовите notifyDataSetChanged().

Например, предположим, что ваш список

ArrayList<MyModel> recyclerList;
private void selectAll(){
    for(MyModel myModel:recyclerList)
        myModel.isSelected = true;
    notifyDataSetChanged();
}

Мое предложение, используя recyclerView или ListView, чтобы меньше пытаться играть с представлениями.

Итак, в вашем случае -

@Override
public void onBindViewHolder(final ViewHolder holder, final int position) {
   holder.clickableView.setTag(position);
   holder.selectableView.setTag(position);
   holder.checkedView.setChecked(recyclerList.get(position).isSelected);
    Log.d(TAG, "onBindViewHolder() position: " + position);
    ...
}

@Override
public void onClick(View view){
    int position = (int)view.getTag();
    recyclerList.get(position).isSelected = !recyclerList.get(position).isSelected;
}

@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
      int position = (int)buttonView.getTag();
      recyclerList.get(position).isSelected = isChecked;
}

Надеюсь, это поможет вам, пожалуйста, дайте мне знать, если вам нужно дальнейшее объяснение:)

Ответ 5

Итак, я думаю, что на ваш вопрос ответил @Pedro Oliveira. Основной смысл RecycleView заключается в том, что он использует специальные алгоритмы кэширования ViewHolder в любое время. Поэтому следующий onBindViewHolder (...) может не работать, например. если представление статично или что-то еще.

И о вашем вопросе, который вы думаете использовать RecycleView для динамических изменений Views. НЕ ДЕЛАЙТЕ ЭТО!. Поскольку RecycleView делает недействительными представления и имеет систему кеширования, у вас будет много проблем.

Используйте LinkedListView для этой задачи!

Ответ 6

Когда у вас есть большое количество элементов в списке, который вы перешли на recyclerview adapter onBindViewHolder() вы не столкнетесь с проблемой onBindViewHolder() не выполняется во время прокрутки.

Но если в списке меньше предметов (я проверил размер списка 5), вы можете столкнуться с этой проблемой.

Лучшее решение - проверить list size.

Пожалуйста, найдите пример кода ниже.

private void setupAdapter(){
    if (list.size() <= 10){
        recycler.setItemViewCacheSize(0);
     }
     recycler.setAdapter(adapter);
     recycler.setLayoutManager(linearLayoutManager);
}