RecyclerView мигает после notifyDatasetChanged ()

114

У меня есть RecyclerView, который загружает некоторые данные из API, включает URL-адрес изображения и некоторые данные, и я использую networkImageView для ленивой загрузки изображения.

@Override
public void onResponse(List<Item> response) {
   mItems.clear();
   for (Item item : response) {
      mItems.add(item);
   }
   mAdapter.notifyDataSetChanged();
   mSwipeRefreshLayout.setRefreshing(false);
}

Вот реализация адаптера:

public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, final int position) {
        if (isHeader(position)) {
            return;
        }
        // - get element from your dataset at this position
        // - replace the contents of the view with that element
        MyViewHolder holder = (MyViewHolder) viewHolder;
        final Item item = mItems.get(position - 1); // Subtract 1 for header
        holder.title.setText(item.getTitle());
        holder.image.setImageUrl(item.getImg_url(), VolleyClient.getInstance(mCtx).getImageLoader());
        holder.image.setErrorImageResId(android.R.drawable.ic_dialog_alert);
        holder.origin.setText(item.getOrigin());
    }

Проблема в том, что когда у нас есть обновление в recyclerView, оно вначале мигает очень короткое время, что выглядит странно.

Вместо этого я просто использовал GridView / ListView, и он работал так, как я ожидал. Сияния не было.

конфигурация для RecycleView в onViewCreated of my Fragment:

mRecyclerView = (RecyclerView) view.findViewById(R.id.recyclerView);
        // use this setting to improve performance if you know that changes
        // in content do not change the layout size of the RecyclerView
        mRecyclerView.setHasFixedSize(true);

        mGridLayoutManager = (GridLayoutManager) mRecyclerView.getLayoutManager();
        mGridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
            @Override
            public int getSpanSize(int position) {
                return mAdapter.isHeader(position) ? mGridLayoutManager.getSpanCount() : 1;
            }
        });

        mRecyclerView.setAdapter(mAdapter);

Кто-нибудь сталкивался с такой проблемой? Что может быть причиной?

Али
источник
Возможный дубликат анимации Disable onChange на ItemAnimator для RecyclerView
Apfelsaft23,

Ответы:

127

Попробуйте использовать стабильные идентификаторы в своем RecyclerView.Adapter

setHasStableIds(true)и переопределить getItemId(int position).

Без стабильных идентификаторов после notifyDataSetChanged()этого ViewHolders обычно назначаются не на одни и те же позиции. В моем случае это было причиной моргания.

Здесь вы можете найти хорошее объяснение.

Анатолий Вдовичев
источник
1
Я добавил setHasStableIds (true), который решил мерцание, но в моем GridLayout элементы все еще меняют место при прокрутке. Что я должен переопределить в getItemId ()? Спасибо
Balázs Orbán
3
Есть идеи, как нам сгенерировать идентификатор?
Mauker
Ты обалденный! Google НЕ. Google либо не знает об этом, либо они знают и не хотят, чтобы мы знали! СПАСИБО, МУЖЧИНА
MBH
1
испортить позиции предметов, предметы поступают из базы данных firebase в правильном порядке.
MuhammadAliJr
1
@Mauker Если у вашего объекта есть уникальный номер, вы можете использовать его, или если у вас есть уникальная строка, вы можете использовать object.hashCode (). Для меня это прекрасно работает,
Саймон Шуберт
106

Согласно этой странице проблемы .... это анимация изменения элемента recycleview по умолчанию ... Вы можете отключить ее ... попробуйте это

recyclerView.getItemAnimator().setSupportsChangeAnimations(false);

Изменить в последней версии

Цитата из блога разработчиков Android :

Обратите внимание, что этот новый API не имеет обратной совместимости. Если вы ранее реализовали ItemAnimator, вы можете вместо этого расширить SimpleItemAnimator, который предоставляет старый API путем обертывания нового API. Вы также заметите, что некоторые методы были полностью удалены из ItemAnimator. Например, если вы вызывали recyclerView.getItemAnimator (). SetSupportsChangeAnimations (false), этот код больше не будет компилироваться. Вы можете заменить его:

ItemAnimator animator = recyclerView.getItemAnimator();
if (animator instanceof SimpleItemAnimator) {
  ((SimpleItemAnimator) animator).setSupportsChangeAnimations(false);
}
Сабир Мохаммед
источник
5
@sabeer все еще такая же проблема. Это не решает проблему.
Shreyash Mahajan
3
@delive дайте мне знать, если вы нашли решение этой
проблемы
Я использую Picasso, это что-то, а не моргание.
8
Котлин: (recyclerView.itemAnimator как? SimpleItemAnimator) ?. supportsChangeAnimations = false
Педро Пауло Аморим,
Он работает, но возникает другая проблема (NPE) java.lang.NullPointerException: попытка чтения из поля 'int android.support.v7.widget.RecyclerView $ ItemAnimator $ ItemHolderInfo.left' при ссылке на нулевой объект Есть ли способ решить это?
Navas pk
46

Это просто сработало:

recyclerView.getItemAnimator().setChangeDuration(0);
Хамзе Собох
источник
это хорошая альтернатива codeItemAnimator animator = recyclerView.getItemAnimator (); if (экземпляр аниматора SimpleItemAnimator) {((SimpleItemAnimator) аниматор) .setSupportsChangeAnimations (false); }code
ziniestro
1
останавливает все анимации ДОБАВИТЬ и УДАЛИТЬ
MBH
11

У меня такая же проблема с загрузкой изображения с некоторых URL-адресов, а затем мигает imageView. Решено с помощью

notifyItemRangeInserted()    

вместо того

notifyDataSetChanged()

что позволяет избежать перезагрузки неизмененных старых данных.

Wesely
источник
9

попробуйте это, чтобы отключить анимацию по умолчанию

ItemAnimator animator = recyclerView.getItemAnimator();

if (animator instanceof SimpleItemAnimator) {
  ((SimpleItemAnimator) animator).setSupportsChangeAnimations(false);
}

это новый способ отключить анимацию, так как поддержка Android 23

этот старый способ будет работать для более старой версии библиотеки поддержки

recyclerView.getItemAnimator().setSupportsChangeAnimations(false)
Мохамед Фарук
источник
4

Предполагая, mItemsчто это коллекция, которая поддерживает вашAdapter , почему вы удаляете все и снова добавляете? Вы в основном говорите ему, что все изменилось, поэтому RecyclerView повторно связывает все представления, чем я предполагаю, библиотека изображений не обрабатывает его должным образом, когда он по-прежнему сбрасывает представление, даже если это тот же URL-адрес изображения. Возможно, у них было какое-то запеченное решение для AdapterView, чтобы оно отлично работало в GridView.

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

Yigit
источник
3
А может это просто "баг" в RecyclerView? Очевидно, что если последние 6 лет он работал нормально с AbsListView, а теперь - нет с RecyclerView, значит, что-то не так с RecyclerView, верно? :) Быстрый взгляд на это показывает, что когда вы обновляете данные в ListView и GridView, они отслеживают вид + положение, поэтому при обновлении вы получите точно такой же держатель просмотра. Пока RecyclerView перемешивает держатели просмотра, что приводит к мерцанию.
вовкаб
Работа с ListView не означает, что это правильно для RecyclerView. Эти компоненты имеют разную архитектуру.
yigit
1
Согласитесь, но иногда действительно сложно узнать, какие элементы были изменены, например, если вы используете курсоры или просто обновляете все данные. Так что recyclerview должен правильно обрабатывать и этот случай.
вовкаб
4

Recyclerview использует DefaultItemAnimator в качестве аниматора по умолчанию. Как видно из приведенного ниже кода, они меняют альфа-канал держателя представления при изменении элемента:

@Override
public boolean animateChange(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder, int fromX, int fromY, int toX, int toY) {
    ...
    final float prevAlpha = ViewCompat.getAlpha(oldHolder.itemView);
    ...
    ViewCompat.setAlpha(oldHolder.itemView, prevAlpha);
    if (newHolder != null) {
        ....
        ViewCompat.setAlpha(newHolder.itemView, 0);
    }
    ...
    return true;
}

Я хотел сохранить остальную анимацию, но убрать «мерцание», поэтому я клонировал DefaultItemAnimator и удалил 3 альфа-строки выше.

Чтобы использовать новый аниматор, просто вызовите setItemAnimator () в своем RecyclerView:

mRecyclerView.setItemAnimator(new MyItemAnimator());
Питер Файл
источник
Он убрал мигание, но по какой-то причине вызвал эффект мерцания.
Мохамед Медхат,
4

В Kotlin вы можете использовать расширение класса для RecyclerView:

fun RecyclerView.disableItemAnimator() {
    (itemAnimator as? SimpleItemAnimator)?.supportsChangeAnimations = false
}

// sample of using in Activity:
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                          savedInstanceState: Bundle?): View? {
    // ...
    myRecyclerView.disableItemAnimator()
    // ...
}
Грегори
источник
1

Привет, @Ali, возможно, уже поздно. Я также столкнулся с этой проблемой и решил с помощью решения ниже, это может помочь вам, пожалуйста, проверьте.

Класс LruBitmapCache.java создан для получения размера кеша изображений

import android.graphics.Bitmap;
import android.support.v4.util.LruCache;
import com.android.volley.toolbox.ImageLoader.ImageCache;

public class LruBitmapCache extends LruCache<String, Bitmap> implements
        ImageCache {
    public static int getDefaultLruCacheSize() {
        final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
        final int cacheSize = maxMemory / 8;

        return cacheSize;
    }

    public LruBitmapCache() {
        this(getDefaultLruCacheSize());
    }

    public LruBitmapCache(int sizeInKiloBytes) {
        super(sizeInKiloBytes);
    }

    @Override
    protected int sizeOf(String key, Bitmap value) {
        return value.getRowBytes() * value.getHeight() / 1024;
    }

    @Override
    public Bitmap getBitmap(String url) {
        return get(url);
    }

    @Override
    public void putBitmap(String url, Bitmap bitmap) {
        put(url, bitmap);
    }
}

Одноэлементный класс VolleyClient.java [расширяет приложение], добавленный под кодом

в конструкторе одноэлементного класса VolleyClient добавьте ниже фрагмент для инициализации ImageLoader

private VolleyClient(Context context)
    {
     mCtx = context;
     mRequestQueue = getRequestQueue();
     mImageLoader = new ImageLoader(mRequestQueue,getLruBitmapCache());
}

Я создал метод getLruBitmapCache () для возврата LruBitmapCache

public LruBitmapCache getLruBitmapCache() {
        if (mLruBitmapCache == null)
            mLruBitmapCache = new LruBitmapCache();
        return this.mLruBitmapCache;
}

Надеюсь, это поможет вам.

Ученик Android
источник
Спасибо за Ваш ответ. Именно это я и сделал в своем VollyClient.java. Взгляните на: VolleyClient.java
Али
Просто проверьте один раз с использованием класса LruCache <String, Bitmap>, я думаю, это решит вашу проблему. Взгляните на LruCache
Android
Вы когда-то проверяли код, которым я поделился с вами в комментариях? что у вас в классе, чего я там пропустил?
Али
Вам не хватает расширения класса LruCache <String, Bitmap> и переопределения метода sizeOf (), как я сделал, но все остальное кажется мне нормальным.
Ученик Android,
Хорошо, я очень скоро попробую, но не могли бы вы объяснить мне, что вы там сделали, что помогло вам и решило проблему? исходный код Для меня это звучит так, будто ваш переопределенный метод sizeOf должен быть в исходном коде.
Али
1

для меня recyclerView.setHasFixedSize(true);работал

Pramod
источник
Я не думаю, что предметы OP имеют фиксированный размер
Ирфанди Д. Венди
это помогло мне в моем случае
bst91
1

В моем случае ни одно из вышеперечисленных, ни ответы на другие вопросы о стеке с такими же проблемами не помогли.

Ну, я использовал настраиваемую анимацию каждый раз, когда щелкали элемент, для чего я вызывал notifyItemChanged (int position, Object Payload), чтобы передать полезные данные моему классу CustomAnimator.

Обратите внимание, что в адаптере RecyclerView доступно 2 метода onBindViewHolder (...). Метод onBindViewHolder (...) с тремя параметрами всегда будет вызываться перед методом onBindViewHolder (...) с двумя параметрами.

Как правило, мы всегда переопределяем метод onBindViewHolder (...), имеющий 2 параметра, и основная причина проблемы заключалась в том, что я делал то же самое, поскольку каждый раз, когда вызывается notifyItemChanged (...), наш метод onBindViewHolder (...) будет быть вызванным, в котором я загружал свое изображение в ImageView с помощью Picasso, и это было причиной его повторной загрузки, независимо от того, из памяти или из Интернета. До загрузки он показывал мне изображение заполнителя, которое было причиной мигания в течение 1 секунды, когда я нажимаю на просмотр элемента.

Позже я также переопределил другой метод onBindViewHolder (...), имеющий 3 параметра. Здесь я проверяю, пуст ли список полезных нагрузок, затем я возвращаю реализацию суперкласса этого метода, иначе, если есть полезные нагрузки, я просто устанавливаю альфа-значение itemView держателя на 1.

И, к сожалению, я получил решение своей проблемы, потратив на это целый день!

Вот мой код для методов onBindViewHolder (...):

onBindViewHolder (...) с двумя параметрами:

@Override
public void onBindViewHolder(@NonNull RecyclerAdapter.ViewHolder viewHolder, int position) {
            Movie movie = movies.get(position);

            Picasso.with(context)
                    .load(movie.getImageLink())
                    .into(viewHolder.itemView.posterImageView);
    }

onBindViewHolder (...) с 3 параметрами:

@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position, @NonNull List<Object> payloads) {
        if (payloads.isEmpty()) {
            super.onBindViewHolder(holder, position, payloads);
        } else {
            holder.itemView.setAlpha(1);
        }
    }

Вот код метода, который я вызывал в onClickListener объекта viewHolder itemView в onCreateViewHolder (...):

private void onMovieClick(int position, Movie movie) {
        Bundle data = new Bundle();
        data.putParcelable("movie", movie);

        // This data(bundle) will be passed as payload for ItemHolderInfo in our animator class
        notifyItemChanged(position, data);
    }

Примечание. Вы можете получить эту позицию, вызвав метод getAdapterPosition () вашего viewHolder из onCreateViewHolder (...).

Я также переопределил метод getItemId (int position) следующим образом:

@Override
public long getItemId(int position) {
    Movie movie = movies.get(position);
    return movie.getId();
}

и позвонил setHasStableIds(true); мой объект адаптера в действии.

Надеюсь, это поможет, если ни один из приведенных выше ответов не работает!

Парт Бханушали
источник
1

В моем случае была гораздо более простая проблема, но она может выглядеть / ощущаться очень похоже на проблему выше. Я преобразовал ExpandableListView в RecylerView с помощью Groupie (используя функцию Groupie ExpandableGroup). В моем первоначальном макете был такой раздел:

<androidx.recyclerview.widget.RecyclerView
  android:id="@+id/hint_list"
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:background="@android:color/white" />

Если для layout_height установлено значение «wrap_content», анимация из расширенной группы в свернутую выглядела так, как будто она будет мигать, но на самом деле это просто анимация из «неправильной» позиции (даже после того, как вы попробовали большинство рекомендаций в этом потоке).

В любом случае, простое изменение layout_height на match_parent решило проблему.

<androidx.recyclerview.widget.RecyclerView
  android:id="@+id/hint_list"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:background="@android:color/white" />
Том
источник
0

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

private int getCacheSize(Context context) {

    final DisplayMetrics displayMetrics = context.getResources().
            getDisplayMetrics();
    final int screenWidth = displayMetrics.widthPixels;
    final int screenHeight = displayMetrics.heightPixels;
    // 4 bytes per pixel
    final int screenBytes = screenWidth * screenHeight * 4;

    return screenBytes * 3;
}
developer_android
источник
0

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

Я решил это, только уменьшив альфа-канал oldview до 0,5 и начав альфа-канал newview с 0,5. Это создало более мягкий переход затухания без полного исчезновения изображения.

К сожалению, из-за частных реализаций я не смог создать подкласс DefaultItemAnimator, чтобы внести это изменение, поэтому мне пришлось клонировать код и внести следующие изменения

в animateChange:

ViewCompat.setAlpha(newHolder.itemView, 0);  //change 0 to 0.5f

в animateChangeImpl:

oldViewAnim.alpha(0).setListener(new VpaListenerAdapter() { //change 0 to 0.5f
Deefer
источник
0

Использование соответствующих методов recyclerview для обновления представлений решит эту проблему.

Сначала внесите изменения в список

mList.add(item);
or mList.addAll(itemList);
or mList.remove(index);

Затем сообщите, используя

notifyItemInserted(addedItemIndex);
or
notifyItemRemoved(removedItemIndex);
or
notifyItemRangeChanged(fromIndex, newUpdatedItemCount);

Надеюсь, это поможет !!

Сридху Мадху
источник
Точно нет. Если вы делаете несколько обновлений по секундам (в моем случае сканирование BLE), это вообще не работает. Я потратил один день на обновление до этого говна RecyclerAdapter ... Лучше оставить ArrayAdapter. Жаль, что не используется шаблон MVC, но, по крайней мере, он почти применим.
Gojir4 04
0

Котлинское решение:

(recyclerViewIdFromXML.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
Роберт Пал
источник