Odświeżanie danych w RecyclerView i utrzymywanie pozycji przewijania

98

W jaki sposób można odświeżyć dane wyświetlane w programie RecyclerView(wywołując notifyDataSetChangedjego adapter) i upewnić się, że pozycja przewijania jest resetowana dokładnie do miejsca, w którym się znajdowała?

W przypadku starego dobrego ListViewwystarczy pobrać getChildAt(0), sprawdzić getTop()i wywołać setSelectionFromToppóźniej te same dokładne dane.

Wydaje się, że nie jest to możliwe w przypadku RecyclerView.

Myślę, że powinienem użyć tego, LayoutManagerco rzeczywiście zapewnia scrollToPositionWithOffset(int position, int offset), ale jaki jest właściwy sposób na odzyskanie pozycji i przesunięcia?

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

A może istnieje bardziej elegancki sposób wykonania pracy?

Konrad Morawski
źródło
Chodzi o wartości szerokości i wysokości w RecyclerView lub LayoutManager. Sprawdź to rozwiązanie: stackoverflow.com/questions/28993640/ ...
serefakyuz
@Konard, w moim przypadku mam listę plików audio z implementacją Seekbara i działającym z następującym problemem: 1) Dla kilku ostatnio zindeksowanych elementów, gdy tylko uruchomił notifyItemChanged (pos), wyświetla widok push na dole automatycznie. 2) Kontynuuj notifyItemChanged (pos), ale zablokował listę i nie pozwala na łatwe przewijanie. Chociaż zadam pytanie, ale daj mi znać, jeśli masz lepsze podejście do rozwiązania tego problemu.
CoDe

Odpowiedzi:

144

Używam tego. ^ _ ^

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

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

To jest prostsze, mam nadzieję, że ci pomoże!

DawnYu
źródło
1
To jest właściwie właściwa odpowiedź imo. Jest to trudne tylko z powodu asynchronicznego ładowania danych, ale możesz odroczyć przywracanie.
EpicPandaForce
doskonały +2 dokładnie to, czego szukałem
Adeel Turk
@BubbleTree paste.ofcode.org/EEdjSLQbLrcQyxXpBpEQ4m Próbuję to zaimplementować, ale nie działa, co robię źle?
Kaustubh Bhagwat
@KaustubhBhagwat Może powinieneś sprawdzić "recyclingViewState! = Null" przed użyciem.
DawnYu
4
Gdzie dokładnie powinienem użyć tego kodu? w metodzie onResume?
Lucky_girl
47

Mam dość podobny problem. I wymyśliłem następujące rozwiązanie.

Używanie notifyDataSetChangedto zły pomysł. Powinieneś być bardziej szczegółowy, a następnie RecyclerViewzapiszesz stan przewijania.

Na przykład, jeśli potrzebujesz tylko odświeżyć, lub innymi słowy, chcesz, aby każdy widok został ponownie powiązany, po prostu zrób to:

adapter.notifyItemRangeChanged(0, adapter.getItemCount());
Kaerdan
źródło
2
Próbowałem adapter.notifyItemRangeChanged (0, adapter.getItemCount ());, to też działa jako .notifyDataSetChanged ()
Mani
4
To pomogło w moim przypadku. Wstawiałem kilka elementów na pozycji 0 i wraz notifyDataSetChangedz pozycją przewijania zmieniłem się, pokazując nowe elementy. Jednak, jeśli używać utrzymuje stan przewijania. notifyItemRangeInserted(0, newItems.size() - 1)RecyclerView
Carlosph
bardzo dobra odpowiedź, szukam rozwiązania tej odpowiedzi cały dzień. dziękuję bardzo ...
Gundu Bandgar
adapter.notifyItemRangeChanged (0, adapter.getItemCount ()); należy unikać, jak wskazano w najnowszym Google I / O (obejrzyj prezentację na temat recyklingu wejść i wyjść), lepiej (jeśli masz wiedzę), aby dokładnie powiedzieć przeglądowi recyklingu, które elementy uległy zmianie zamiast płaskiego odświeżania typu catch-all wszystkich widoków.
A. Steenbergen
3
to nie działa, widok z recyklingu odświeżający się do góry, mimo że dodałem
Shaahul
21

EDYCJA: Aby przywrócić dokładnie tę samą widoczną pozycję, jak w programie, aby wyglądała dokładnie tak, jak była, musimy zrobić coś innego (zobacz poniżej, jak przywrócić dokładną wartość scrollY):

Zapisz pozycję i przesunięcie w następujący sposób:

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

A następnie przywróć zwój w ten sposób:

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

Spowoduje to przywrócenie listy do dokładnej widocznej pozycji. Pozorny, ponieważ będzie wyglądał tak samo dla użytkownika, ale nie będzie miał takiej samej wartości scrollY (z powodu możliwych różnic w wymiarach układu poziomego / pionowego).

Zauważ, że działa to tylko z LinearLayoutManager.

--- Poniżej jak przywrócić dokładne scrollY, co prawdopodobnie sprawi, że lista będzie wyglądać inaczej ---

  1. Zastosuj OnScrollListener w następujący sposób:

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

Spowoduje to zapisanie dokładnej pozycji przewijania przez cały czas w mScrollY.

  1. Przechowuj tę zmienną w swoim pakiecie i przywróć ją podczas przywracania stanu do innej zmiennej , nazwiemy ją mStateScrollY.

  2. Po przywróceniu stanu i po zresetowaniu wszystkich danych przez RecyclerView zresetuj przewijanie za pomocą tego:

    mRecyclerView.scrollBy(0, mStateScrollY);
    

Otóż ​​to.

Uważaj, przywracasz przewijanie do innej zmiennej, jest to ważne, ponieważ OnScrollListener zostanie wywołany z .scrollBy (), a następnie ustawi mScrollY na wartość przechowywaną w mStateScrollY. Jeśli tego nie zrobisz, mScrollY będzie miał podwójną wartość przewijania (ponieważ OnScrollListener działa z deltami, a nie bezwzględnymi przewijaniami).

Oszczędności państwa w działaniach można osiągnąć w następujący sposób:

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

Aby przywrócić, wywołaj to w swoim onCreate ():

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

Zapisywanie stanu we fragmentach działa w podobny sposób, ale faktyczne zapisywanie stanu wymaga trochę dodatkowej pracy, ale jest wiele artykułów na ten temat, więc nie powinieneś mieć problemu z ustaleniem jak, zasady zapisywania scrollY a przywrócenie pozostaje takie samo.

A. Steenbergen
źródło
nie rozumiem, czy możesz zamieścić pełny przykład?
To jest właściwie tak zbliżone do pełnego przykładu, jaki możesz uzyskać, jeśli nie chcesz, żebym publikował całą moją aktywność
A. Steenbergen
Czegoś nie rozumiem. Jeśli na początku listy były dodane elementy, czy pozostawanie na tej samej pozycji przewijania nie będzie błędne, ponieważ teraz będziesz patrzeć na różne elementy, które przejęły ten obszar?
programista Androida
Tak, w takim przypadku musisz dopasować pozycję podczas przywracania, jednak nie o to chodzi, ponieważ zwykle podczas obracania nie dodajesz ani nie usuwasz elementów. Byłoby to dziwne i zachowanie i prawdopodobnie nie leżałoby w interesie użytkownika, z wyjątkiem pewnych specjalnych przypadków, które mogą istnieć, których nie mogę sobie wyobrazić, szczerze.
A. Steenbergen
9

Tak, możesz rozwiązać ten problem, tworząc konstruktor adaptera tylko raz, wyjaśniam część dotyczącą kodowania tutaj:

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

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

Teraz widać, że sprawdziłem, czy adapter jest pusty, czy nie, i inicjuję tylko wtedy, gdy jest pusty.

Jeśli karta nie jest zerowa, mam pewność, że zainicjowałem swój adapter przynajmniej raz.

Więc po prostu dodam listę do adaptera i wywołam notifydatasetchanged.

RecyclerView zawsze przechowuje przewijaną ostatnią pozycję, dlatego nie musisz przechowywać ostatniej pozycji, po prostu wywołaj notifydatasetchanged, widok recyklera zawsze odświeża dane bez przechodzenia do góry.

Dzięki, szczęśliwego kodowania

Amit Singh Tomar
źródło
Myślę, że to idzie do zaakceptowanej odpowiedzi @Konrad Morawski
Jithin Jude
6

Zachowaj pozycję przewijania, używając odpowiedzi @DawnYu, aby zawijać w notifyDataSetChanged()ten sposób:

val recyclerViewState = recyclerView.layoutManager?.onSaveInstanceState() 
adapter.notifyDataSetChanged() 
recyclerView.layoutManager?.onRestoreInstanceState(recyclerViewState)
Morten Holmgaard
źródło
doskonały ... najlepsza odpowiedź
jlandyr
Długo szukałem tej odpowiedzi. Dziękuję Ci bardzo.
Alaa AbuZarifa
2

Najlepsza odpowiedź od @DawnYu działa, ale widok recyklingu najpierw przewinie się do góry, a następnie wróci do zamierzonej pozycji przewijania, powodując reakcję „migotania”, która nie jest przyjemna.

Aby odświeżyć widok recyklingu, szczególnie po przejściu z innego działania, bez migotania i utrzymywania pozycji przewijania, należy wykonać następujące czynności.

  1. Upewnij się, że aktualizujesz widok recyklera za pomocą DiffUtil. Przeczytaj więcej na ten temat tutaj: https://www.journaldev.com/20873/android-recyclerview-diffutil
  2. Po wznowieniu swojej działalności lub w momencie, w którym chcesz zaktualizować swoją aktywność, załaduj dane do widoku recyklingu. Używając diffUtil, tylko aktualizacje będą dokonywane w widoku recyklingu, zachowując jego pozycję.

Mam nadzieję że to pomoże.

Moses Mwongela
źródło
1

Nie korzystałem z Recyclerview, ale zrobiłem to na ListView. Przykładowy kod w Recyclerview:

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

Jest to słuchacz, gdy użytkownik przewija. Narzut wydajności nie jest znaczący. I w ten sposób pierwsza widoczna pozycja jest dokładna.

Oryginalny system Android
źródło
OK, findFirstVisibleItemPositionzwraca „pozycję adaptera pierwszego widocznego widoku”, ale co z przesunięciem.
Konrad Morawski
1
Czy istnieje podobna metoda dla Recyclerview do setSelection metody ListView dla widocznej pozycji? Oto, co zrobiłem dla ListView.
Oryginalny Android
1
A co powiesz na użycie metody setScrollY z parametrem dy, wartością, którą zapiszesz? Jeśli to zadziała, zaktualizuję odpowiedź. Nie korzystałem z Recyclerview, ale chcę tego w mojej przyszłej aplikacji.
Oryginalny Android
Zobacz moją odpowiedź poniżej, jak zapisać pozycję przewijania.
A. Steenbergen
1
W porządku, będę o tym pamiętać następnym razem, nie przegłosowałem cię złośliwie. Jednak nie zgadzam się z krótkimi i niekompletnymi odpowiedziami, ponieważ kiedy zaczynałem krótkie odpowiedzi, w których inni programiści założyli, że wiele podstawowych informacji tak naprawdę mi nie pomogło, ponieważ często były zbyt niekompletne i musiałem zadać wiele wyjaśniających pytań, coś, co ty zwykle nie można tego zrobić na starych postach typu stackoverflow. Zwróć też uwagę, jak żadna odpowiedź nie została przyjęta przez Konrada, co wskazuje, że odpowiedzi te nie rozwiązały jego problemu. Chociaż rozumiem twój ogólny punkt widzenia.
A. Steenbergen
-1
 mMessageAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
        @Override
        public void onChanged() {
            mLayoutManager.smoothScrollToPosition(mMessageRecycler, null, mMessageAdapter.getItemCount());
        }
    });

Rozwiązaniem jest tutaj przewijanie widoku recyklingu, gdy nadejdzie nowa wiadomość.

Metoda onChanged () wykrywa akcję wykonywaną na recyclinglerview.

Abhishek Chudekar
źródło
-1

To działa dla mnie w Kotlinie.

  1. Utwórz adapter i przekaż swoje dane w konstruktorze
class LEDRecyclerAdapter (var currentPole: Pole): RecyclerView.Adapter<RecyclerView.ViewHolder>()  { ... }
  1. zmień tę właściwość i wywołaj notifyDataSetChanged ()
adapter.currentPole = pole
adapter.notifyDataSetChanged()

Przesunięcie przewijania nie zmienia się.

Chris8447
źródło
-2

Miałem ten problem z listą pozycji, z których każda miała czas w minutach, aż były „należne” i wymagały aktualizacji. Zaktualizowałem dane, a potem zadzwoniłem

orderAdapter.notifyDataSetChanged();

i za każdym razem przewijał się do góry. Zastąpiłem to

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

i było dobrze. Żadna z innych metod w tym wątku nie zadziałała. Korzystając z tej metody, sprawiał jednak, że każdy pojedynczy element migał po aktualizacji, więc musiałem również umieścić to w onCreateView fragmentu nadrzędnego

RecyclerView.ItemAnimator animator = orderRecycler.getItemAnimator();
    if (animator instanceof SimpleItemAnimator) {
        ((SimpleItemAnimator) animator).setSupportsChangeAnimations(false);
    }
Matt
źródło
-2

Jeśli masz jeden lub więcej EditTextów wewnątrz elementu widoku recyklingu , wyłącz ich autofokus, umieszczając tę ​​konfigurację w widoku nadrzędnym widoku recyklingu:

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

Miałem ten problem, gdy zacząłem inną czynność uruchomioną z przedmiotu z recyklingu, kiedy wróciłem i ustawiłem aktualizację jednego pola w jednym elemencie z notifyItemChanged (pozycja) przewijanie ruchu RV, a mój wniosek był taki, że autofokus EditText Pozycje, powyższy kod rozwiązał mój problem.

Najlepsza.

Achha8
źródło
-3

Po prostu wróć, jeśli stara pozycja i pozycja są takie same;

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
źródło