RecyclerView miga po notifyDatasetChanged ()


114

Mam RecyclerView, który ładuje niektóre dane z API, zawiera adres URL obrazu i niektóre dane, i używam networkImageView do leniwego ładowania obrazu.

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

Oto implementacja dla adaptera:

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

Problem polega na tym, że gdy mamy odświeżenie w RecyklerView, na początku świeci przez bardzo krótki czas, co wygląda dziwnie.

Zamiast tego użyłem GridView / ListView i działało zgodnie z oczekiwaniami. Nie było mrugnięcia okiem.

konfiguracja RecycleView w 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);

Czy ktoś miał taki problem? jaki może być powód?


Odpowiedzi:


126

Spróbuj użyć stabilnych identyfikatorów w swoim RecyclerView.Adapter

setHasStableIds(true)i zastąp getItemId(int position).

Bez stabilnych identyfikatorów, po notifyDataSetChanged()ViewHolders zwykle przypisane do różnych pozycji. To był powód mrugania w moim przypadku.

Tutaj znajdziesz dobre wyjaśnienie.


1
Dodałem setHasStableIds (true), co rozwiązało problem migotania, ale w moim GridLayout elementy nadal zmieniają miejsce podczas przewijania. Co powinienem zastąpić w getItemId ()? Dzięki
Balázs Orbán,

3
Masz pomysł, jak powinniśmy wygenerować identyfikator?
Mauker,

Jesteś niesamowity! Google NIE JEST. Google albo o tym nie wie LUB wiedzą i nie chcą, abyśmy wiedzieli! DZIĘKUJĘ MĘŻCZYZN
MBH

1
zepsuć pozycje przedmiotów, elementy pochodzą z bazy danych Firebase w odpowiedniej kolejności.
MuhammadAliJr

1
@Mauker Jeśli twój obiekt ma unikalny numer, możesz go użyć lub jeśli masz unikalny ciąg znaków, możesz użyć object.hashCode (). To działa idealnie dla mnie
Simon Schubert,

106

Zgodnie z tą stroną wydania … jest to domyślna animacja zmiany przedmiotu z recyklingu… Możesz ją wyłączyć… spróbuj tego

recyclerView.getItemAnimator().setSupportsChangeAnimations(false);

Zmiana w najnowszej wersji

Cytat z bloga programistów Androida :

Należy pamiętać, że ten nowy interfejs API nie jest wstecznie zgodny. Jeśli wcześniej zaimplementowałeś ItemAnimator, możesz zamiast tego rozszerzyć SimpleItemAnimator, który udostępnia stary interfejs API, opakowując nowy interfejs API. Zauważysz również, że niektóre metody zostały całkowicie usunięte z ItemAnimator. Na przykład, jeśli wywołujesz recyklerView.getItemAnimator (). SetSupportsChangeAnimations (false), ten kod nie będzie się już kompilować. Możesz go zastąpić:

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

5
@ Sabeer wciąż ten sam problem. To nie rozwiązuje problemu.
Shreyash Mahajan

3
@delive, daj mi znać, jeśli znajdziesz jakieś rozwiązanie tego
problemu

Używam Picassa, to coś, nie wydaje się mrugać.

8
Kotlin: (recyklerView.itemAnimator as? SimpleItemAnimator) ?. supportChangeAnimations = false
Pedro Paulo Amorim

Działa, ale nadchodzi inny problem (NPE) java.lang.NullPointerException: Próba odczytu z pola 'int android.support.v7.widget.RecyclerView $ ItemAnimator $ ItemHolderInfo.left' w odwołaniu do obiektu o wartości zerowej Czy jest jakikolwiek sposób rozwiązać ten problem?
Navas pk

46

To po prostu zadziałało:

recyclerView.getItemAnimator().setChangeDuration(0);

jest dobrą alternatywą dla codeItemAnimator animator = recyclingView.getItemAnimator (); if (instancja animatora SimpleItemAnimator) {((SimpleItemAnimator) animator) .setSupportsChangeAnimations (false); }code
ziniestro

1
zatrzymuje wszystkie animacje DODAJ i USUŃ
MBH

11

Mam ten sam problem z ładowaniem obrazu z niektórych adresów URL, a następnie miga imageView. Rozwiązany za pomocą

notifyItemRangeInserted()    

zamiast

notifyDataSetChanged()

co pozwala uniknąć ponownego załadowania tych niezmienionych starych danych.


9

spróbuj tego, aby wyłączyć domyślną animację

ItemAnimator animator = recyclerView.getItemAnimator();

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

To nowy sposób na wyłączenie animacji od czasu obsługi Androida 23

ten stary sposób zadziała dla starszej wersji biblioteki obsługi

recyclerView.getItemAnimator().setSupportsChangeAnimations(false)

4

Zakładając, że mItemsjest to kolekcja, która wspiera Twoją Adapter, dlaczego usuwasz wszystko i ponownie dodajesz? Zasadniczo mówisz mu, że wszystko się zmieniło, więc RecyclerView ponownie wiąże wszystkie widoki, niż zakładam, że biblioteka obrazów nie obsługuje go poprawnie, gdy nadal resetuje widok, mimo że jest to ten sam adres URL obrazu. Może przygotowali jakieś rozwiązanie dla AdapterView, aby działało dobrze w GridView.

Zamiast wywoływać, notifyDataSetChangedco spowoduje ponowne powiązanie wszystkich widoków, wywołaj szczegółowe zdarzenia powiadomień (powiadomienie dodane / usunięte / przeniesione / zaktualizowane), aby RecyclerView ponownie powiązał tylko niezbędne widoki i nic nie będzie migotać.


3
A może to tylko „błąd” w RecyclerView? Oczywiście, jeśli działało dobrze przez ostatnie 6 lat z AbsListView, a teraz nie z RecyclerView, oznacza to, że coś nie jest w porządku z RecyclerView, prawda? :) Szybkie spojrzenie na to pokazuje, że kiedy odświeżasz dane w ListView i GridView, śledzą one widok + położenie, więc kiedy odświeżysz, otrzymasz dokładnie ten sam viewholder. Podczas gdy RecyclerView tasuje posiadaczy widoku, co prowadzi do migotania.
vovkab

Praca na ListView nie oznacza, że ​​jest poprawna dla RecyclerView. Te komponenty mają różne architektury.
yigit

1
Zgadzam się, ale czasami naprawdę trudno jest wiedzieć, które elementy zostały zmienione, na przykład jeśli używasz kursorów lub po prostu odświeżasz całe dane. Dlatego recykling powinien również poprawnie obsługiwać ten przypadek.
vovkab

4

Recyclerview używa DefaultItemAnimator jako domyślnego animatora. Jak widać z poniższego kodu, zmieniają one alfa posiadacza widoku po zmianie przedmiotu:

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

Chciałem zachować resztę animacji, ale usunąć „migotanie”, więc sklonowałem DefaultItemAnimator i usunąłem powyższe 3 linie alfa.

Aby użyć nowego animatora, po prostu wywołaj setItemAnimator () w swoim RecyclerView:

mRecyclerView.setItemAnimator(new MyItemAnimator());

Usunęło miganie, ale z jakiegoś powodu spowodowało efekt migotania.
Mohamed Medhat

4

W Kotlinie możesz użyć `` rozszerzenia klasy '' dla 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

Hej @Ali, może być późna powtórka. Również napotkałem ten problem i rozwiązałem za pomocą poniższego rozwiązania, może pomóc ci to sprawdzić.

Klasa LruBitmapCache.java jest tworzona w celu pobrania rozmiaru pamięci podręcznej obrazu

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

Klasa singleton VolleyClient.java [rozszerza aplikację] dodana poniżej kodu

w konstruktorze klasy singleton VolleyClient dodaj poniższy fragment kodu, aby zainicjować ImageLoader

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

Utworzyłem metodę getLruBitmapCache (), aby zwracała LruBitmapCache

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

Mam nadzieję, że ci to pomoże.


Dziękuję za odpowiedź. Właśnie to zrobiłem w moim VollyClient.java. Spójrz na: VolleyClient.java
Ali

Wystarczy raz sprawdzić, używając klasy LruCache <String, Bitmap>, myślę, że rozwiąże to Twój problem. Spójrz na LruCache
Android learner,

Czy sprawdziłeś kiedyś kod, który ci udostępniłem w komentarzu? co masz w swojej klasie, czego tam brakowało?
Ali

Brakuje ci rozszerzenia klasy LruCache <String, Bitmap> i nadpisywania metody sizeOf (), tak jak to zrobiłem, pozostałe wszystko wydaje mi się w porządku.
Uczący się Androida

Ok, spróbuję wkrótce, ale czy możesz mi wyjaśnić, co zrobiłeś, co zrobiło za Ciebie magię i rozwiązało problem? sourcecode Dla mnie wygląda na to, że twoja nadpisana metoda sizeOf powinna znajdować się w kodzie źródłowym.
Ali

1

dla mnie recyclerView.setHasFixedSize(true);pracował


Nie sądzę, żeby przedmioty OP
miały

to pomogło mi w moim przypadku
bst91

1

W moim przypadku żadne z powyższych ani odpowiedzi z innych pytań dotyczących stackoverflow, które mają te same problemy, nie zadziałały.

Cóż, korzystałem z niestandardowej animacji za każdym razem, gdy element został kliknięty, dla którego wywoływałem notifyItemChanged (pozycja int, Object Payload), aby przekazać ładunek do mojej klasy CustomAnimator.

Zauważ, że w adapterze RecyclerView dostępne są 2 metody onBindViewHolder (...). Metoda onBindViewHolder (...) mająca 3 parametry będzie zawsze wywoływana przed metodą onBindViewHolder (...) mającą 2 parametry.

Generalnie zawsze nadpisujemy metodę onBindViewHolder (...) mającą 2 parametry, a głównym źródłem problemu było to, że robiłem to samo, ponieważ za każdym razem, gdy wywoływana jest funkcja notifyItemChanged (...), nasza metoda onBindViewHolder (...) być wywołane, w którym ładowałem mój obraz w ImageView za pomocą Picassa i to był powód, dla którego ładował się ponownie, niezależnie od tego, czy pochodzi z pamięci, czy z Internetu. Aż do załadowania pokazywał mi obraz zastępczy, który był powodem migania przez 1 sekundę za każdym razem, gdy klikałem widok przedmiotu.

Później nadpisuję też inną metodę onBindViewHolder (...) mającą 3 parametry. Tutaj sprawdzam, czy lista ładunków jest pusta, następnie zwracam implementację superklasy tej metody, w przeciwnym razie, jeśli są ładunki, ustawiam tylko wartość alfa posiadacza itemView na 1.

I tak, znalazłem rozwiązanie mojego problemu po niestety zmarnowaniu jednego pełnego dnia!

Oto mój kod dla metod onBindViewHolder (...):

onBindViewHolder (...) z 2 parametrami:

@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 (...) z 3 parametrami:

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

Oto kod metody, którą wywołałem w onClickListener elementu viewHolder itemView w 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);
    }

Uwaga: Możesz uzyskać tę pozycję, wywołując metodę getAdapterPosition () elementu viewHolder z onCreateViewHolder (...).

Zastąpiłem również metodę getItemId (pozycja int) w następujący sposób:

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

i zadzwonił setHasStableIds(true); mój obiekt adaptera w działaniu.

Mam nadzieję, że to pomoże, jeśli żadna z powyższych odpowiedzi nie zadziała!


1

W moim przypadku był znacznie prostszy problem, ale może wyglądać / odczuwać bardzo podobnie do powyższego problemu. Przekonwertowałem ExpandableListView na RecylerView z Groupie (używając funkcji ExpandableGroup Groupie). Mój początkowy układ miał następującą sekcję:

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

Gdy layout_height ustawiono na „wrap_content”, animacja z grupy rozwiniętej do grupy zwiniętej wyglądała tak, jakby migała, ale tak naprawdę była to po prostu animacja z „niewłaściwej” pozycji (nawet po wypróbowaniu większości zaleceń w tym wątku).

W każdym razie zwykła zmiana layout_height na match_parent w ten sposób rozwiązała problem.

<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

Miałem podobny problem i to zadziałało dla mnie Możesz wywołać tę metodę, aby ustawić rozmiar pamięci podręcznej obrazu

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

w przypadku mojej aplikacji niektóre dane uległy zmianie, ale nie chciałem, aby cały widok migał.

Rozwiązałem to tylko wygaszając alfa oldview w dół o 0,5 alfa i uruchamiając alfa newview na 0,5. Stworzyło to łagodniejsze przejście bez całkowitego znikania widoku.

Niestety z powodu prywatnych implementacji nie mogłem wykonać podklasy DefaultItemAnimator w celu wprowadzenia tej zmiany, więc musiałem sklonować kod i wprowadzić następujące zmiany

w animateChange:

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

w animateChangeImpl:

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

0

Użycie odpowiednich metod podglądu recyklingu do aktualizacji widoków rozwiąże ten problem

Najpierw wprowadź zmiany na liście

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

Następnie powiadom za pomocą

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

Mam nadzieję, że to pomoże !!


Absolutnie nie. Jeśli wykonujesz kilka aktualizacji w ciągu kilku sekund (w moim przypadku skanowanie BLE), to w ogóle nie działa. Spędziłem jeden dzień, aby zaktualizować ten gówniany RecyclerAdapter ... Lepiej zachować ArrayAdapter. Szkoda, że ​​nie używa wzorca MVC, ale przynajmniej jest prawie użyteczny.
Gojir4


0

Rozwiązanie Kotlin:

(recyclerViewIdFromXML.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.