Actualiser les données dans RecyclerView et conserver sa position de défilement

96

Comment actualiser les données affichées dans RecyclerView(en appelant notifyDataSetChangedson adaptateur) et s'assurer que la position de défilement est réinitialisée exactement là où elle était?

En cas de bon vieux, ListViewil suffit de récupérer getChildAt(0), de vérifier getTop()et d'appeler setSelectionFromTopavec les mêmes données exactes par la suite.

Cela ne semble pas possible en cas de RecyclerView.

Je suppose que je suis censé utiliser son LayoutManagerqui fournit en effet scrollToPositionWithOffset(int position, int offset), mais quelle est la bonne façon de récupérer la position et le décalage?

layoutManager.findFirstVisibleItemPosition()et layoutManager.getChildAt(0).getTop()?

Ou y a-t-il une manière plus élégante de faire le travail?

Konrad Morawski
la source
Il s'agit des valeurs de largeur et de hauteur de votre RecyclerView ou du LayoutManager. Vérifiez cette solution: stackoverflow.com/questions/28993640/…
serefakyuz
@Konard, Dans mon cas, j'ai la liste des fichiers audio avec l'implémentation de Seekbar et fonctionnant avec le problème suivant: 1) Pour le dernier élément indexé dès qu'il a déclenché notifyItemChanged (pos), il pousse la vue en bas automatique. 2) Tout en restant sur notifyItemChanged (pos), il a collé la liste et ne permet pas de faire défiler facilement. Bien que je pose une question, mais faites-moi savoir s'il vous plaît si vous avez une meilleure approche pour résoudre ce problème.
CoDe du

Réponses:

139

J'utilise celui-ci. ^ _ ^

// Save state
private Parcelable recyclerViewState;
recyclerViewState = recyclerView.getLayoutManager().onSaveInstanceState();

// Restore state
recyclerView.getLayoutManager().onRestoreInstanceState(recyclerViewState);

C'est plus simple, j'espère que cela vous aidera!

DawnYu
la source
1
C'est en fait la bonne réponse imo. Cela devient délicat uniquement à cause du chargement asynchrone des données, mais vous pouvez différer la restauration.
EpicPandaForce
perfect +2 exactement ce que je recherchais
Adeel Turk
@BubbleTree paste.ofcode.org/EEdjSLQbLrcQyxXpBpEQ4m J'essaie de mettre en œuvre cela mais cela ne fonctionne pas, qu'est-ce que je fais de mal?
Kaustubh Bhagwat
@KaustubhBhagwat Vous devriez peut-être vérifier "recyclerViewState! = Null" avant de l'utiliser.
DawnYu
4
Où dois-je utiliser exactement ce code? dans la méthode onResume?
Lucky_girl
47

J'ai un problème assez similaire. Et j'ai trouvé la solution suivante.

L'utilisation notifyDataSetChangedest une mauvaise idée. Vous devriez être plus précis, puis RecyclerViewenregistrera l'état de défilement pour vous.

Par exemple, si vous avez seulement besoin d'actualiser, ou en d'autres termes, vous voulez que chaque vue soit reliée, procédez comme suit:

adapter.notifyItemRangeChanged(0, adapter.getItemCount());
Kaerdan
la source
2
J'ai essayé adapter.notifyItemRangeChanged (0, adapter.getItemCount ());, cela fonctionne aussi comme .notifyDataSetChanged ()
Mani
4
Cela a aidé dans mon cas. J'étais en notifyDataSetChangedtrain d' insérer plusieurs éléments à la position 0 et avec la position de défilement changerait montrant les nouveaux éléments. Cependant, si vous utilisez notifyItemRangeInserted(0, newItems.size() - 1)le, RecyclerViewconserve l'état de défilement.
Carlosph
très très bonne réponse, je cherche la solution de cette réponse entière un jour. merci beaucoup ..
Gundu Bandgar
adapter.notifyItemRangeChanged (0, adapter.getItemCount ()); devrait être évité, comme indiqué dans la dernière Google I / O (regardez la présentation sur recyclerview in et out), il serait préférable (si vous avez les connaissances) de dire exactement à recyclerview quels éléments ont changé au lieu d'un rafraîchissement plat fourre-tout de toutes les vues.
A. Steenbergen
3
son ne fonctionne pas, recyclerview rafraîchissant au sommet même si j'ai ajouté
Shaahul
21

EDIT: Pour restaurer exactement la même position apparente , comme dans, la faire ressembler exactement à ce qu'elle était, nous devons faire quelque chose d'un peu différent (voir ci-dessous comment restaurer la valeur exacte scrollY):

Enregistrez la position et le décalage comme ceci:

LinearLayoutManager manager = (LinearLayoutManager) mRecycler.getLayoutManager();
int firstItem = manager.findFirstVisibleItemPosition();
View firstItemView = manager.findViewByPosition(firstItem);
float topOffset = firstItemView.getTop();

outState.putInt(ARGS_SCROLL_POS, firstItem);
outState.putFloat(ARGS_SCROLL_OFFSET, topOffset);

Et puis restaurez le parchemin comme ceci:

LinearLayoutManager manager = (LinearLayoutManager) mRecycler.getLayoutManager();
manager.scrollToPositionWithOffset(mStatePos, (int) mStateOffset);

Cela rétablit la liste à sa position apparente exacte . Apparent car il aura le même aspect pour l'utilisateur, mais il n'aura pas la même valeur scrollY (en raison des différences possibles dans les dimensions de la mise en page paysage / portrait).

Notez que cela ne fonctionne qu'avec LinearLayoutManager.

--- Ci-dessous comment restaurer le scrollY exact, ce qui rendra probablement la liste différente ---

  1. Appliquez un OnScrollListener comme ceci:

    private int mScrollY;
    private RecyclerView.OnScrollListener mTotalScrollListener = new RecyclerView.OnScrollListener() {
        @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
            super.onScrolled(recyclerView, dx, dy);
            mScrollY += dy;
        }
    };
    

Cela stockera la position de défilement exacte à tout moment dans mScrollY.

  1. Stockez cette variable dans votre Bundle et restaurez-la dans la restauration de l'état vers une variable différente , nous l'appellerons mStateScrollY.

  2. Après la restauration de l'état et après que votre RecyclerView a réinitialisé toutes ses données, réinitialisez le défilement avec ceci:

    mRecyclerView.scrollBy(0, mStateScrollY);

C'est tout.

Attention, vous restaurez le scroll vers une variable différente, c'est important, car le OnScrollListener sera appelé avec .scrollBy () et par la suite affectera mScrollY à la valeur stockée dans mStateScrollY. Si vous ne le faites pas, mScrollY aura le double de la valeur de défilement (car OnScrollListener fonctionne avec des deltas, pas des défilements absolus).

L'économie d'état dans les activités peut être réalisée comme ceci:

@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    outState.putInt(ARGS_SCROLL_Y, mScrollY);
}

Et pour restaurer, appelez ceci dans votre onCreate ():

if(savedState != null){
    mStateScrollY = savedState.getInt(ARGS_SCROLL_Y, 0);
}

La sauvegarde d'état dans les fragments fonctionne de la même manière, mais la sauvegarde d'état réelle nécessite un peu de travail supplémentaire, mais il y a beaucoup d'articles traitant de cela, vous ne devriez donc pas avoir de problème pour savoir comment, les principes de sauvegarde du parcheminY et le restaurer restent les mêmes.

A. Steenbergen
la source
Je ne comprends pas que vous pouvez poster un exemple complet?
C'est en fait aussi proche d'un exemple complet que vous pouvez obtenir si vous ne voulez pas que je poste toute mon activité
A. Steenbergen
Je n'ai rien. S'il y avait des éléments ajoutés au début de la liste, ne sera-t-il pas faux de rester sur la même position de défilement, car vous allez maintenant rechercher différents éléments qui ont envahi cette zone?
développeur android
Oui, dans ce cas, vous devez ajuster la position lors de la restauration, cependant, cela ne fait pas partie de la question, car généralement lors de la rotation, vous n'ajoutez ni ne supprimez d'éléments. Ce serait un comportement étrange et probablement pas dans l'intérêt de l'utilisateur, sauf pour certains cas particuliers qui pourraient exister, que je ne peux pas imaginer, honnêtement.
A. Steenbergen
9

Oui, vous pouvez résoudre ce problème en créant le constructeur de l'adaptateur une seule fois, j'explique la partie de codage ici:

if (appointmentListAdapter == null) {
        appointmentListAdapter = new AppointmentListAdapter(AppointmentsActivity.this);
        appointmentListAdapter.addAppointmentListData(appointmentList);
        appointmentListAdapter.setOnStatusChangeListener(onStatusChangeListener);
        appointmentRecyclerView.setAdapter(appointmentListAdapter);

    } else {
        appointmentListAdapter.addAppointmentListData(appointmentList);
        appointmentListAdapter.notifyDataSetChanged();
    }

Vous pouvez maintenant voir que j'ai vérifié que l'adaptateur est nul ou non et que je ne l'initialise que lorsqu'il est nul.

Si l'adaptateur n'est pas nul, je suis assuré que j'ai initialisé mon adaptateur au moins une fois.

Je vais donc simplement ajouter la liste à l'adaptateur et appeler notifydatasetchanged.

RecyclerView maintient toujours la dernière position défilée, vous n'avez donc pas à stocker la dernière position, appelez simplement notifydatasetchanged, la vue recycleur actualise toujours les données sans aller en haut.

Merci Happy Coding

Amit Singh Tomar
la source
Je pense que cela va à la réponse acceptée @Konrad Morawski
Jithin Jude
5

Gardez la position de défilement en utilisant la réponse @DawnYu pour envelopper notifyDataSetChanged()comme ceci:

val recyclerViewState = recyclerView.layoutManager?.onSaveInstanceState() 
adapter.notifyDataSetChanged() 
recyclerView.layoutManager?.onRestoreInstanceState(recyclerViewState)
Morten Holmgaard
la source
parfait ... la meilleure réponse
jlandyr
Je cherchais cette réponse depuis longtemps. Merci beaucoup.
Alaa AbuZarifa le
1

La première réponse de @DawnYu fonctionne, mais la vue de recyclage défilera d'abord vers le haut, puis retournera à la position de défilement voulue, provoquant une réaction de type «scintillement» qui n'est pas agréable.

Pour actualiser recyclerView, en particulier après être issu d'une autre activité, sans scintillement et en conservant la position de défilement, vous devez effectuer les opérations suivantes.

  1. Assurez-vous de mettre à jour votre vue recycleur à l'aide de DiffUtil. En savoir plus à ce sujet ici: https://www.journaldev.com/20873/android-recyclerview-diffutil
  2. Lors de la reprise de votre activité, ou au moment où vous souhaitez mettre à jour votre activité, chargez les données dans votre vue de recyclage. En utilisant le diffUtil, seules les mises à jour seront effectuées sur la vue de recyclage tout en la maintenant en position.

J'espère que cela t'aides.

Moïse Mwongela
la source
0

Je n'ai pas utilisé Recyclerview mais je l'ai fait sur ListView. Exemple de code dans Recyclerview:

setOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
        rowPos = mLayoutManager.findFirstVisibleItemPosition();

C'est l'auditeur lorsque l'utilisateur fait défiler. La surcharge de performance n'est pas significative. Et la première position visible est précise de cette façon.

L'Android d'origine
la source
OK, findFirstVisibleItemPositionrenvoie "la position de l'adaptateur de la première vue visible", mais qu'en est-il du décalage.
Konrad Morawski
1
Existe-t-il une méthode similaire pour Recyclerview à la méthode setSelection de ListView pour la position visible? C'est ce que j'ai fait pour ListView.
L'Android d'origine
1
Que diriez-vous d'utiliser la méthode setScrollY avec le paramètre dy, une valeur que vous sauvegarderez? Si cela fonctionne, je mettrai à jour ma réponse. Je n'ai pas utilisé Recyclerview mais je le souhaite dans ma future application.
L'Android d'origine
Veuillez voir ma réponse ci-dessous pour savoir comment enregistrer la position de défilement.
A. Steenbergen
1
D'accord, je garderai cela à l'esprit la prochaine fois, je ne vous ai pas voté avec malveillance. Cependant, je ne suis pas d'accord avec les réponses courtes et incomplètes car lorsque j'ai commencé, les réponses courtes où les autres développeurs supposaient beaucoup d'informations de fond ne m'ont pas vraiment aidé, car elles étaient souvent trop incomplètes et j'ai dû poser beaucoup de questions de clarification, quelque chose que vous ne peut généralement pas faire sur les anciens messages stackoverflow. Notez également qu'aucune réponse n'a été acceptée par Konrad, ce qui indique que ces réponses n'ont pas résolu son problème. Bien que je vois votre point général.
A. Steenbergen
-1
 mMessageAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
        @Override
        public void onChanged() {
            mLayoutManager.smoothScrollToPosition(mMessageRecycler, null, mMessageAdapter.getItemCount());
        }
    });

La solution ici est de continuer à faire défiler la vue de recyclage lorsqu'un nouveau message arrive.

La méthode onChanged () détecte l'action effectuée sur recyclerview.

Abhishek Chudekar
la source
-1

Cela fonctionne pour moi à Kotlin.

  1. Créez l'adaptateur et remettez vos données dans le constructeur
class LEDRecyclerAdapter (var currentPole: Pole): RecyclerView.Adapter<RecyclerView.ViewHolder>()  { ... }
  1. changez cette propriété et appelez notifyDataSetChanged ()
adapter.currentPole = pole
adapter.notifyDataSetChanged()

Le décalage de défilement ne change pas.

Chris8447
la source
-2

J'ai eu ce problème avec une liste d'articles qui avaient chacun un temps en minutes jusqu'à ce qu'ils soient «dus» et avaient besoin d'être mis à jour. Je mettrais à jour les données et ensuite, j'appellerais

orderAdapter.notifyDataSetChanged();

et il défilerait vers le haut à chaque fois. Je l'ai remplacé par

 for(int i = 0; i < orderArrayList.size(); i++){
                orderAdapter.notifyItemChanged(i);
            }

et c'était bien. Aucune des autres méthodes de ce fil n'a fonctionné pour moi. Cependant, en utilisant cette méthode, chaque élément individuel a clignoté lors de sa mise à jour, je devais donc également le mettre dans onCreateView du fragment parent.

RecyclerView.ItemAnimator animator = orderRecycler.getItemAnimator();
    if (animator instanceof SimpleItemAnimator) {
        ((SimpleItemAnimator) animator).setSupportsChangeAnimations(false);
    }
Mat
la source
-2

Si vous avez un ou plusieurs EditTexts à l'intérieur des éléments d'un recyclerview, désactivez l'autofocus de ceux-ci, en plaçant cette configuration dans la vue parente de recyclerview:

android:focusable="true"
android:focusableInTouchMode="true"

J'ai eu ce problème lorsque j'ai commencé une autre activité lancée à partir d'un élément recyclerview, lorsque je suis revenu et j'ai défini une mise à jour d'un champ dans un élément avec notifyItemChanged (position) le défilement des mouvements de RV, et ma conclusion était que, l'autofocus d'EditText Articles, le code ci-dessus a résolu mon problème.

meilleur.

Akhha8
la source
-3

Renvoyez simplement si l'ancienne position et la position sont identiques;

private int oldPosition = -1;

public void notifyItemSetChanged(int position, boolean hasDownloaded) {
    if (oldPosition == position) {
        return;
    }
    oldPosition = position;
    RLog.d(TAG, " notifyItemSetChanged :: " + position);
    DBMessageModel m = mMessages.get(position);
    m.setVideoHasDownloaded(hasDownloaded);
    notifyItemChanged(position, m);
}
raditya gumay
la source