RecyclerView lampeggia dopo notifyDatasetChanged ()


113

Ho un RecyclerView che carica alcuni dati dall'API, include l'URL di un'immagine e alcuni dati e utilizzo networkImageView per caricare l'immagine lazy.

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

Ecco l'implementazione per Adapter:

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

Il problema è che quando abbiamo l'aggiornamento in recyclerView, all'inizio lampeggia per un brevissimo tempo, il che sembra strano.

Ho appena usato GridView / ListView invece e ha funzionato come mi aspettavo. Non ci furono blincking.

configurazione per RecycleView in 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);

Qualcuno ha dovuto affrontare un problema del genere? quale potrebbe essere il motivo?


Risposte:


126

Prova a utilizzare ID stabili nel tuo RecyclerView.Adapter

setHasStableIds(true)e sovrascrivi getItemId(int position).

Senza ID stabili, dopo notifyDataSetChanged(), i ViewHolder di solito non vengono assegnati alle stesse posizioni. Questo è stato il motivo per cui ho battuto le palpebre nel mio caso.

Puoi trovare una buona spiegazione qui.


1
Ho aggiunto setHasStableIds (true), che ha risolto lo sfarfallio, ma nel mio GridLayout, gli elementi cambiano ancora posizione durante lo scorrimento. Cosa dovrei sovrascrivere in getItemId ()? Grazie
Balázs Orbán

3
Qualche idea su come dovremmo generare l'ID?
Mauker

Sei fantastico! Google NON lo è. Google o non lo sa O lo sanno e non vogliono che lo sappiamo! GRAZIE UOMO
MBH

1
rovinare le posizioni degli oggetti, gli oggetti provengono dal database Firebase nell'ordine corretto.
MuhammadAliJr

1
@Mauker Se il tuo oggetto ha un numero univoco puoi usarlo o se hai una stringa univoca puoi usare object.hashCode (). Per me funziona perfettamente
Simon Schubert,

106

Secondo questa pagina del problema .... è l'animazione predefinita per la modifica dell'elemento di visualizzazione del riciclo ... Puoi disattivarla .. prova questo

recyclerView.getItemAnimator().setSupportsChangeAnimations(false);

Modifica nell'ultima versione

Citato dal blog degli sviluppatori Android :

Tieni presente che questa nuova API non è compatibile con le versioni precedenti. Se in precedenza hai implementato un ItemAnimator, puoi invece estendere SimpleItemAnimator, che fornisce la vecchia API avvolgendo la nuova API. Noterai anche che alcuni metodi sono stati completamente rimossi da ItemAnimator. Ad esempio, se stavi chiamando recyclerView.getItemAnimator (). SetSupportsChangeAnimations (false), questo codice non verrà più compilato. Puoi sostituirlo con:

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

5
@sabeer ancora lo stesso problema. Non risolve il problema.
Shreyash Mahajan

3
@delive fammi sapere se hai trovato una soluzione a questo
Shreyash Mahajan

Io uso Picasso, è qualcosa, non sembra lampeggiare.

8
Kotlin: (recyclerView.itemAnimator as? SimpleItemAnimator) ?. supportsChangeAnimations = false
Pedro Paulo Amorim

Funziona, ma è in arrivo un altro problema (NPE) java.lang.NullPointerException: tentativo di leggere dal campo 'int android.support.v7.widget.RecyclerView $ ItemAnimator $ ItemHolderInfo.left' su un riferimento a un oggetto nullo C'è un modo per risolvere questo problema?
Navas pk

46

Questo ha semplicemente funzionato:

recyclerView.getItemAnimator().setChangeDuration(0);

è una buona alternativa a codeItemAnimator animator = recyclerView.getItemAnimator (); if (istanza animatore di SimpleItemAnimator) {((SimpleItemAnimator) animatore) .setSupportsChangeAnimations (false); }code
ziniestro

1
interrompe tutte le animazioni AGGIUNGI e RIMUOVI
MBH

11

Ho lo stesso problema durante il caricamento dell'immagine da alcuni URL e poi imageView lampeggia. Risolto usando

notifyItemRangeInserted()    

invece di

notifyDataSetChanged()

che evita di ricaricare quei vecchi dati invariati.


9

prova questo per disabilitare l'animazione predefinita

ItemAnimator animator = recyclerView.getItemAnimator();

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

questo è il nuovo modo per disabilitare l'animazione poiché supporta Android 23

questo vecchio modo funzionerà per la versione precedente della libreria di supporto

recyclerView.getItemAnimator().setSupportsChangeAnimations(false)

4

Supponendo che mItemssia la collezione che sostiene il tuoAdapter , perché rimuovi tutto e aggiungi di nuovo? In pratica gli stai dicendo che tutto è cambiato, quindi RecyclerView ricollega tutte le visualizzazioni di quanto presumo che la libreria di immagini non la gestisca correttamente dove reimposta ancora la vista anche se è lo stesso URL dell'immagine. Forse avevano una soluzione integrata per AdapterView in modo che funzionasse bene in GridView.

Invece di chiamare notifyDataSetChangedche causerà il riassociazione di tutte le viste, chiama eventi di notifica granulari (notifica aggiunta / rimossa / spostata / aggiornata) in modo che RecyclerView rileghi solo le viste necessarie e nulla sfarfallerà.


3
O forse è solo un "bug" in RecyclerView? Ovviamente se ha funzionato bene negli ultimi 6 anni con AbsListView e ora non funziona con RecyclerView, significa che qualcosa non va bene con RecyclerView, giusto? :) Una rapida occhiata mostra che quando aggiorni i dati in ListView e GridView, tengono traccia della vista + posizione, quindi quando aggiorni otterrai esattamente lo stesso viewholder. Mentre RecyclerView mescola i titolari delle visualizzazioni, il che porta allo sfarfallio.
vovkab

Lavorare su ListView non significa che sia corretto per RecyclerView. Questi componenti hanno architetture differenti.
yigit

1
D'accordo, ma a volte è davvero difficile sapere quali elementi vengono modificati, ad esempio se usi i cursori o semplicemente aggiorni tutti i dati. Quindi recyclerview dovrebbe gestire correttamente anche questo caso.
vovkab

4

Recyclerview usa DefaultItemAnimator come animatore predefinito. Come puoi vedere dal codice sottostante, cambiano l'alfa del titolare della vista al cambio di elemento:

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

Volevo mantenere il resto delle animazioni ma rimuovere lo "sfarfallio", quindi ho clonato DefaultItemAnimator e rimosso le 3 linee alfa sopra.

Per usare il nuovo animatore basta chiamare setItemAnimator () sul tuo RecyclerView:

mRecyclerView.setItemAnimator(new MyItemAnimator());

Ha rimosso il lampeggiamento ma ha causato uno sfarfallio per qualche motivo.
Mohamed Medhat

4

In Kotlin puoi usare 'estensione di classe' per 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

Hey @Ali, potrebbe essere la riproduzione in ritardo. Ho anche affrontato questo problema e l'ho risolto con la soluzione di seguito, potrebbe aiutarti a controllare.

La classe LruBitmapCache.java viene creata per ottenere la dimensione della cache dell'immagine

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 classe singleton [estende l'applicazione] aggiunta sotto il codice

nel costruttore della classe singleton VolleyClient aggiungere il frammento di seguito per inizializzare ImageLoader

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

Ho creato il metodo getLruBitmapCache () per restituire LruBitmapCache

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

Spero che ti possa aiutare.


Grazie per la tua risposta. Questo è quello che ho fatto esattamente nel mio VollyClient.java. Dai un'occhiata a: VolleyClient.java
Ali

Controlla una volta usando la classe LruCache <String, Bitmap>, penso che risolverà il tuo problema. Dai un'occhiata a LruCache
Android il

Hai controllato una volta il codice che ho condiviso con te nel commento? cosa hai nella tua classe che mi sono perso lì?
Ali

Ti manca l'estensione della classe LruCache <String, Bitmap> e l'override del metodo sizeOf () come ho fatto, rimanendo tutto a me sembra a posto.
Studente Android il

Ok, ci provo molto presto, ma potresti spiegarmi cosa hai fatto lì che ha fatto la magia per te e ha risolto il problema? codice sorgente Per me sembra che il tuo metodo sizeOf sovrascritto dovrebbe essere nel codice sorgente.
Ali

1

per me ha recyclerView.setHasFixedSize(true);funzionato


Non credo che gli articoli di OP abbiano dimensioni fisse
Irfandi D.Vendy,

questo mi ha aiutato nel mio caso
bst91

1

Nel mio caso, nessuna delle risposte precedenti né le risposte di altre domande di stackoverflow con gli stessi problemi hanno funzionato.

Bene, stavo usando un'animazione personalizzata ogni volta che l'elemento viene cliccato, per cui stavo chiamando notifyItemChanged (int position, Object Payload) per passare il payload alla mia classe CustomAnimator.

Si noti che sono disponibili 2 metodi onBindViewHolder (...) in RecyclerView Adapter. Il metodo onBindViewHolder (...) con 3 parametri verrà sempre chiamato prima del metodo onBindViewHolder (...) con 2 parametri.

In generale, sovrascriviamo sempre il metodo onBindViewHolder (...) con 2 parametri e la radice principale del problema era che stavo facendo lo stesso, poiché ogni volta che viene chiamato notifyItemChanged (...), il nostro metodo onBindViewHolder (...) lo farà essere chiamato, in cui stavo caricando la mia immagine in ImageView usando Picasso, e questo era il motivo per cui si caricava di nuovo indipendentemente dalla sua memoria o da Internet. Fino al caricamento, mi mostrava l'immagine segnaposto, motivo per cui lampeggiava per 1 secondo ogni volta che facevo clic sulla visualizzazione dell'elemento.

Successivamente, sovrascrivo anche un altro metodo onBindViewHolder (...) con 3 parametri. Qui controllo se l'elenco dei payload è vuoto, quindi restituisco l'implementazione della super classe di questo metodo, altrimenti se ci sono payload, sto solo impostando il valore alfa di itemView del titolare su 1.

E sì, ho avuto la soluzione al mio problema dopo aver perso un giorno intero purtroppo!

Ecco il mio codice per i metodi onBindViewHolder (...):

onBindViewHolder (...) con 2 parametri:

@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 (...) con 3 parametri:

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

Ecco il codice del metodo che stavo chiamando in onClickListener di itemView di viewHolder in 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);
    }

Nota: puoi ottenere questa posizione chiamando il metodo getAdapterPosition () del tuo viewHolder da onCreateViewHolder (...).

Ho anche sovrascritto il metodo getItemId (int position) come segue:

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

e chiamato setHasStableIds(true); il mio oggetto adattatore in attività.

Spero che questo aiuti se nessuna delle risposte sopra funziona!


1

Nel mio caso si è verificato un problema molto più semplice, ma può sembrare molto simile al problema sopra. Avevo convertito un ExpandableListView in un RecylerView con Groupie (utilizzando la funzione ExpandableGroup di Groupie). Il mio layout iniziale aveva una sezione come questa:

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

Con layout_height impostato su "wrap_content", l'animazione dal gruppo espanso al gruppo compresso sembrava lampeggiare, ma in realtà si stava solo animando dalla posizione "sbagliata" (anche dopo aver provato la maggior parte dei consigli in questo thread).

Ad ogni modo, semplicemente cambiando layout_height in match_parent in questo modo ha risolto il problema.

<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

Ho avuto un problema simile e questo ha funzionato per me Puoi chiamare questo metodo per impostare la dimensione per la cache delle immagini

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

per la mia applicazione, ho dovuto modificare alcuni dati ma non volevo che l'intera vista lampeggiasse.

L'ho risolto dissolvendo solo il oldview verso il basso di 0,5 alpha e avviando il newview alpha a 0,5. Ciò ha creato una transizione in dissolvenza più morbida senza far scomparire completamente la vista.

Sfortunatamente a causa di implementazioni private, non ho potuto sottoclassare DefaultItemAnimator per apportare questa modifica, quindi ho dovuto clonare il codice e apportare le seguenti modifiche

in animateChange:

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

in animateChangeImpl:

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

0

L'utilizzo di metodi recyclerview appropriati per aggiornare le viste risolverà questo problema

Innanzitutto, apporta le modifiche nell'elenco

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

Quindi avvisare utilizzando

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

Spero che questo ti aiuti !!


Assolutamente no. Se stai effettuando diversi aggiornamenti per secondi (scansione BLE nel mio caso) non funziona affatto. Ho passato un giorno ad aggiornare a questa merda RecyclerAdapter ... Meglio mantenere ArrayAdapter. È un peccato che non utilizzi il pattern MVC, ma almeno è quasi utilizzabile.
Gojir4


0

Soluzione Kotlin:

(recyclerViewIdFromXML.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.