RecyclerView блимає після notifyDatasetChanged ()


113

У мене є 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());
    }

Проблема полягає в тому, що коли ми оновимо у recilerView, воно блимає дуже коротко, але на початку це виглядає дивно.

Я замість цього використовував 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);

Хтось стикався з такою проблемою? що може бути причиною?


Відповіді:


126

Спробуйте використовувати стабільні ідентифікатори у своєму RecyclerView.Adapter

setHasStableIds(true)і перекрити getItemId(int position).

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

Ви можете знайти хороше пояснення тут.


1
Я додав setHasStableIds (true), який вирішив мерехтіння, але в моєму GridLayout елементи все ще змінюють місце при прокрутці. Що я повинен перекрити в getItemId ()? Спасибі
Balázs Orbán

3
Будь-яка ідея про те, як нам створити ідентифікатор?
Маукер

Ти надзвичайний! Google НЕ. Google або не знає про це АБО вони знають і не хочуть, щоб ми знали! ДЯКУЄМО ЧОЛОВУ
MBH

1
зіпсувати позиції елементів, елементи надходять із бази даних firebase у правильному порядку.
MuhammadAliJr

1
@Mauker Якщо ваш об’єкт має унікальний номер, ви можете використовувати його або якщо у вас є унікальна рядок, ви можете використовувати object.hashCode (). Це прекрасно працює для мене
Саймон Шуберт,

106

Згідно з цією сторінкою випуску .... анімація про зміну елемента рециркуляції за замовчуванням є анімацією ... Ви можете вимкнути її. Спробуйте це

recyclerView.getItemAnimator().setSupportsChangeAnimations(false);

Зміна в останній версії

Цитується з блогу розробників Android :

Зауважте, що цей новий API не сумісний із зворотним. Якщо ви раніше реалізували ItemAnimator, ви можете замість цього розширити SimpleItemAnimator, який надає старий API, загортаючи новий API. Ви також помітите, що деякі способи повністю видалено з ItemAnimator. Наприклад, якщо ви викликали recilerView.getItemAnimator (). SetSupportsChangeAnimations (false), цей код більше не буде компілюватися. Ви можете замінити його на:

ItemAnimator animator = recyclerView.getItemAnimator();
if (animator instanceof SimpleItemAnimator) {
  ((SimpleItemAnimator) animator).setSupportsChangeAnimations(false);
}

5
@sabeer все одно та сама проблема. Це не вирішує питання.
Шреяш Махаджан

3
@delive Повідомте мене, якщо ви знайшли рішення для цього
Шреяш Махаджан

Я використовую пікассо, його щось, а не моргати.

8
Kotlin: (recilerView.itemAnimator as? 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 = recilerView.getItemAnimator (); if (animator instanceof SimpleItemAnimator) {((SimpleItemAnimator) animator) .setSupportsChangeAnimations (false); }code
ziniestro

1
зупиняє всі анімації ADD and REMOVE
MBH

11

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

notifyItemRangeInserted()    

замість

notifyDataSetChanged()

що дозволяє уникнути перезавантаження цих незмінних старих даних.


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 відновлював лише необхідні перегляди, і нічого не мерехтіло.


3
А може, це просто "помилка" в RecyclerView? Очевидно, якщо він працював нормально протягом останніх 6 років з AbsListView, а зараз він не з RecyclerView, це означає, що з RecyclerView щось не в порядку, правда? :) Швидкий перегляд показує, що при оновленні даних у ListView та GridView вони відстежують перегляд + позицію, тож при оновленні ви отримаєте абсолютно той самий переглядач. У той час як RecyclerView перетасовує власників перегляду, що призводить до мерехтіння.
вовкаб

Робота над ListView не означає, що це правильно для RecyclerView. Ці компоненти мають різні архітектури.
yigit

1
Погодьтеся, але іноді дуже важко дізнатися, які елементи змінюються, наприклад, якщо ви використовуєте курсори або просто оновлюєте цілі дані. Тож рециркулятор повинен також правильно поводитися з цим випадком.
вовкаб

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

У Котліні ви можете використовувати "клас розширення" для 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;
}

Сподіваюся, що це допоможе вам.


Дякую за вашу відповідь. Ось що я точно зробив у своєму VollyClient.java. Подивіться на: VolleyClient.java
Алі

Просто перевірте один раз за допомогою класу LruCache <String, Bitmap>, я думаю, що це вирішить вашу проблему. Погляньте на LruCache
Android учень

Ви перевіряли один раз код, яким я поділився з вами в коментарі? що ти маєш у своєму класі, що я там пропустив?
Алі

Вам не вистачає розширення класу LruCache <String, Bitmap> та переосмислення методу sizeOf (), як, як я це зробив, залишилися всі мені здаються нормальними.
Android учень

Гаразд, я спробую це дуже скоро, але ви могли б пояснити мені, що ви там зробили, що зробило магію для вас і вирішило проблему? вихідний код Для мене це здається, що ваш переопределений розмірOf повинен бути у вихідному коді.
Алі


1

У моєму випадку не працювали ані вище, ані відповіді з інших запитань щодо потокового потоку, що мали однакові проблеми.

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

Зауважте, у RecyclerView Adapter доступні два методи onBindViewHolder (...). Метод onBindViewHolder (...), який має 3 параметра, завжди буде викликаний перед методом onBindViewHolder (...), який має 2 параметри.

Як правило, ми завжди перекриваємо метод 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 of viewHolder''s 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's 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;
}

0

для моєї програми я змінив деякі дані, але я не хотів, щоб весь погляд блимав.

Я вирішив це, лише зменшивши старий перегляд на 0,5 альфа і запустивши нову альфа-версію на 0,5. Це створило більш м'який згасаючий перехід, не змушуючи погляд повністю зникати.

На жаль, через приватні реалізації я не зміг підкласифікувати DefaultItemAnimator для того, щоб внести цю зміну, тому мені довелося клонувати код і внести наступні зміни

в animateЗмінити:

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

в animateChangeImpl:

oldViewAnim.alpha(0).setListener(new VpaListenerAdapter() { //change 0 to 0.5f

0

Використання відповідних методів рециркуляції для оновлення поглядів вирішить цю проблему

Спочатку внесіть зміни у список

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


Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.