RecyclerView clignote après notifyDatasetChanged ()

114

J'ai un RecyclerView qui charge certaines données de l'API, comprend une URL d'image et des données, et j'utilise networkImageView pour charger l'image paresseux.

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

Voici l'implémentation pour 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());
    }

Le problème est que lorsque nous avons une actualisation dans recyclerView, cela clignote pendant très peu de temps au début, ce qui semble étrange.

Je viens d'utiliser GridView / ListView à la place et cela a fonctionné comme prévu. Il n'y avait aucun blincking.

configuration pour RecycleView dans 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);

Quelqu'un confronté à un tel problème? Quelle pourrait être la raison?

Ali
la source

Réponses:

126

Essayez d'utiliser des ID stables dans votre RecyclerView.

setHasStableIds(true) et remplacer getItemId(int position) .

Sans ID stables, après notifyDataSetChanged() , les ViewHolders sont généralement affectés à des positions différentes. C'était la raison de cligner des yeux dans mon cas.

Vous pouvez trouver une bonne explication ici.

Anatoly Vdovichev
la source
1
J'ai ajouté setHasStableIds (true), qui a résolu le scintillement, mais dans mon GridLayout, les éléments changent toujours de place lors du défilement. Que dois-je remplacer dans getItemId ()? Thanks
Balázs Orbán
3
Une idée sur la façon dont nous devons générer l'ID?
Mauker
Tu es incroyable! Google n'est PAS. Google ne le sait pas OU il le sait et ne veut pas que nous le sachions! MERCI HOMME
MBH
1
gâcher les positions des éléments, les éléments proviennent de la base de données Firebase dans le bon ordre.
MuhammadAliJr
1
@Mauker Si votre objet a un numéro unique, vous pouvez l'utiliser ou si vous avez une chaîne unique, vous pouvez utiliser object.hashCode (). Cela fonctionne parfaitement bien pour moi
Simon Schubert
106

Selon cette page de problème ... il s'agit de l'animation de changement d'élément de recycleview par défaut ... Vous pouvez la désactiver ... essayez ceci

recyclerView.getItemAnimator().setSupportsChangeAnimations(false);

Changement dans la dernière version

Cité du blog des développeurs Android :

Notez que cette nouvelle API n'est pas rétrocompatible. Si vous avez précédemment implémenté un ItemAnimator, vous pouvez à la place étendre SimpleItemAnimator, qui fournit l'ancienne API en encapsulant la nouvelle API. Vous remarquerez également que certaines méthodes ont été entièrement supprimées de ItemAnimator. Par exemple, si vous appeliez recyclerView.getItemAnimator (). SetSupportsChangeAnimations (false), ce code ne se compilera plus. Vous pouvez le remplacer par:

ItemAnimator animator = recyclerView.getItemAnimator();
if (animator instanceof SimpleItemAnimator) {
  ((SimpleItemAnimator) animator).setSupportsChangeAnimations(false);
}
Sabeer Mohammed
la source
5
@sabeer toujours le même problème. Cela ne résout pas le problème.
Shreyash Mahajan
3
@delive laissez-moi savoir si vous avez trouvé une solution à ce
problème
J'utilise Picasso, c'est quelque chose, ne semble pas cligner des yeux.
8
Kotlin: (recyclerView.itemAnimator as? SimpleItemAnimator)?. SupportsChangeAnimations = false
Pedro Paulo Amorim
Cela fonctionne, mais un autre problème arrive (NPE) java.lang.NullPointerException: Tentative de lecture à partir du champ 'int android.support.v7.widget.RecyclerView $ ItemAnimator $ ItemHolderInfo.left' sur une référence d'objet nulle Y a-t-il un moyen résoudre ça?
Navas pk
46

Cela a simplement fonctionné:

recyclerView.getItemAnimator().setChangeDuration(0);
Hamzeh Soboh
la source
c'est une bonne alternative à codeItemAnimator animator = recyclerView.getItemAnimator (); if (instance d'animation de SimpleItemAnimator) {((SimpleItemAnimator) animateur) .setSupportsChangeAnimations (false); }code
ziniestro
1
arrête toutes les animations AJOUTER et SUPPRIMER
MBH
11

J'ai le même problème de chargement d'image à partir de certaines URL, puis imageView clignote. Résolu en utilisant

notifyItemRangeInserted()    

au lieu de

notifyDataSetChanged()

ce qui évite de recharger ces anciennes données inchangées.

Wesely
la source
9

essayez ceci pour désactiver l'animation par défaut

ItemAnimator animator = recyclerView.getItemAnimator();

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

c'est la nouvelle façon de désactiver l'animation depuis le support Android 23

cette ancienne méthode fonctionnera pour l'ancienne version de la bibliothèque de support

recyclerView.getItemAnimator().setSupportsChangeAnimations(false)
Mohamed Farouk
la source
4

En supposant que mItemsla collection qui soutient votreAdapter , pourquoi supprimez-vous tout et rajoutez-vous? Vous lui dites essentiellement que tout a changé, donc RecyclerView relie toutes les vues que je suppose que la bibliothèque d'images ne la gère pas correctement où elle réinitialise toujours la vue même s'il s'agit de la même URL d'image. Peut-être qu'ils avaient une solution intégrée pour AdapterView afin que cela fonctionne correctement dans GridView.

Au lieu d'appeler, notifyDataSetChangedce qui entraînera la ré-liaison de toutes les vues, appelez les événements de notification granulaire (notification ajoutée / supprimée / déplacée / mise à jour) afin que RecyclerView ne relie que les vues nécessaires et que rien ne clignote.

yigit
la source
3
Ou peut-être est-ce juste un "bogue" dans RecyclerView? Évidemment, si cela a bien fonctionné pendant les 6 dernières années avec AbsListView et maintenant ce n'est pas le cas avec RecyclerView, cela signifie que quelque chose ne va pas avec RecyclerView, non? :) Un examen rapide montre que lorsque vous actualisez les données dans ListView et GridView, ils gardent une trace de vue + position, donc lorsque vous actualisez, vous obtiendrez exactement le même visualiseur. Alors que RecyclerView mélange les supports de vue, ce qui entraîne un scintillement.
vovkab
Travailler sur ListView ne signifie pas qu'il est correct pour RecyclerView. Ces composants ont des architectures différentes.
yigit
1
D'accord, mais il est parfois très difficile de savoir quels éléments sont modifiés, par exemple si vous utilisez des curseurs ou si vous actualisez simplement vos données entières. Donc, recyclerview devrait également gérer ce cas correctement.
vovkab
4

Recyclerview utilise DefaultItemAnimator comme animateur par défaut. Comme vous pouvez le voir dans le code ci-dessous, ils modifient l'alpha du support de vue lors du changement d'élément:

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

Je voulais conserver le reste des animations mais supprimer le "scintillement" alors j'ai cloné DefaultItemAnimator et supprimé les 3 lignes alpha ci-dessus.

Pour utiliser le nouvel animateur, appelez simplement setItemAnimator () sur votre RecyclerView:

mRecyclerView.setItemAnimator(new MyItemAnimator());
Fichier Peter
la source
Il a supprimé le clignotement mais cela a provoqué un effet de scintillement pour une raison quelconque.
Mohamed Medhat le
4

Dans Kotlin, vous pouvez utiliser une `` extension de classe '' pour 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()
    // ...
}
Grégory
la source
1

Hé @Ali, ça pourrait être une relecture tardive. J'ai également rencontré ce problème et résolu avec la solution ci-dessous, cela peut vous aider à vérifier.

La classe LruBitmapCache.java est créée pour obtenir la taille du cache d'image

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

La classe de singleton VolleyClient.java [ extend Application] a été ajoutée sous le code

dans le constructeur de classe singleton VolleyClient, ajoutez ci-dessous l'extrait de code pour initialiser ImageLoader

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

J'ai créé la méthode getLruBitmapCache () pour renvoyer LruBitmapCache

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

J'espère que ça va vous aider.

Apprenant Android
la source
Merci pour votre réponse. C'est exactement ce que j'ai fait dans mon VollyClient.java. Jetez un oeil à: VolleyClient.java
Ali
Vérifiez simplement une fois avec la classe LruCache <String, Bitmap>, je pense que cela va résoudre votre problème. Jetez un oeil à LruCache
Apprenant Android
Avez-vous vérifié une fois le code que j'ai partagé avec vous en commentaire? qu'est-ce que tu as dans ta classe que j'ai manqué là-bas?
Ali
Il vous manque l'extension de la classe LruCache <String, Bitmap> et la substitution de la méthode sizeOf () comme je l'ai fait, rester tout cela me semble bien.
Apprenant Android
Ok, je vais essayer très bientôt, mais pourriez-vous m'expliquer ce que vous avez fait là-bas qui a fait la magie pour vous et a résolu le problème? sourcecode Pour moi, il semble que votre méthode sizeOf remplacée devrait être dans le code source.
Ali
1

pour moi recyclerView.setHasFixedSize(true);travaillé

Pramod
la source
Je ne pense pas que les articles d'OP aient une taille fixe
Irfandi D. Vendy
cela m'a aidé dans mon cas
bst91
1

Dans mon cas, ni l'un ni l'autre des éléments ci-dessus ni les réponses d'autres questions de stackoverflow ayant les mêmes problèmes n'ont fonctionné.

Eh bien, j'utilisais une animation personnalisée chaque fois que l'on cliquait sur l'élément, pour lequel j'appelais notifyItemChanged (position int, charge utile de l'objet) pour transmettre la charge utile à ma classe CustomAnimator.

Remarquez qu'il existe 2 méthodes onBindViewHolder (...) disponibles dans l'adaptateur RecyclerView. La méthode onBindViewHolder (...) ayant 3 paramètres sera toujours appelée avant la méthode onBindViewHolder (...) ayant 2 paramètres.

Généralement, nous remplaçons toujours la méthode onBindViewHolder (...) ayant 2 paramètres et la principale racine du problème était que je faisais la même chose, car chaque fois que notifyItemChanged (...) est appelé, notre méthode onBindViewHolder (...) être appelé, dans lequel je chargeais mon image dans ImageView en utilisant Picasso, et c'était la raison pour laquelle il se chargeait à nouveau indépendamment de sa mémoire ou d'Internet. Jusqu'à ce qu'il soit chargé, il me montrait l'image de l'espace réservé, ce qui était la raison pour laquelle je clignotais pendant 1 seconde chaque fois que je cliquais sur la vue de l'élément.

Plus tard, je remplace également une autre méthode onBindViewHolder (...) ayant 3 paramètres. Ici, je vérifie si la liste des charges utiles est vide, puis je renvoie l'implémentation de la super classe de cette méthode, sinon s'il y a des charges utiles, je mets simplement la valeur alpha de l'itemView du support à 1.

Et oui, j'ai eu la solution à mon problème après avoir malheureusement perdu une journée complète!

Voici mon code pour les méthodes onBindViewHolder (...):

onBindViewHolder (...) avec 2 paramètres:

@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 (...) avec 3 paramètres:

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

Voici le code de la méthode que j'appelais dans onClickListener de l'itemView de viewHolder dans 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);
    }

Remarque: vous pouvez obtenir cette position en appelant la méthode getAdapterPosition () de votre viewHolder à partir de onCreateViewHolder (...).

J'ai également remplacé la méthode getItemId (int position) comme suit:

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

et setHasStableIds(true);j'ai appelé mon objet adaptateur en activité.

J'espère que cela aidera si aucune des réponses ci-dessus ne fonctionne!

Parth Bhanushali
la source
1

Dans mon cas, il y avait un problème beaucoup plus simple, mais il peut ressembler beaucoup au problème ci-dessus. J'avais converti un ExpandableListView en un RecylerView avec Groupie (en utilisant la fonctionnalité ExpandableGroup de Groupie). Ma mise en page initiale avait une section comme celle-ci:

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

Avec layout_height réglé sur "wrap_content", l'animation du groupe développé au groupe réduit semblait clignoter, mais c'était en réalité une animation à partir de la "mauvaise" position (même après avoir essayé la plupart des recommandations de ce fil).

Quoi qu'il en soit, changer simplement layout_height en match_parent comme ceci a résolu le problème.

<androidx.recyclerview.widget.RecyclerView
  android:id="@+id/hint_list"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:background="@android:color/white" />
À M
la source
0

J'ai eu un problème similaire et cela a fonctionné pour moi Vous pouvez appeler cette méthode pour définir la taille du cache d'image

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
la source
0

pour mon application, certaines données ont été modifiées mais je ne voulais pas que la vue entière clignote.

Je l'ai résolu en atténuant uniquement l'ancienne vue vers le bas de 0,5 alpha et en commençant la newview alpha à 0,5. Cela a créé une transition de fondu plus douce sans faire disparaître complètement la vue.

Malheureusement, à cause d'implémentations privées, je ne pouvais pas sous-classer le DefaultItemAnimator pour effectuer cette modification, j'ai donc dû cloner le code et apporter les modifications suivantes

dans animateChange:

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

dans animateChangeImpl:

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

L'utilisation de méthodes de recyclage appropriées pour mettre à jour les vues résoudra ce problème

Commencez par apporter des modifications à la liste

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

Puis notifiez en utilisant

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

J'espère que cela aidera !!

Sreedhu Madhu
la source
Absolument pas. Si vous effectuez plusieurs mises à jour en quelques secondes (analyse BLE dans mon cas), cela ne fonctionne pas du tout. J'ai passé une journée à mettre à jour cette merde RecyclerAdapter ... Mieux vaut garder ArrayAdapter. C'est dommage de ne pas utiliser le modèle MVC, mais au moins c'est presque utilisable.
Gojir4
0

Solution Kotlin:

(recyclerViewIdFromXML.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
Robert Pal
la source