Listy są wszędzie i nieuniknione. Twoje wiadomości (e-mail, SMSy itp.) są listami, odtwarzacz muzyki również korzysta z list do wyświetlania playlist. Baa nawet lista aplikacji zainstalowanych w Twoim telefonie też jest listą.

Listy są uważane za najwygodniejszy sposób wyświetlania danych. Sprawiają, że wyświetlanie zbioru danych użytkownikom to bezproblemowe zadanie o ile jest miejsce na ekranie.

Podczas gdy listy są świetne w wyświetlaniu informacji, mogą nie być tak dobre w zużyciu zasobów, ponieważ za każdym razem, gdy lista jest przewijana, jest tworzony nowy widok, który jest kosztownym zadaniem. W związku z tym im więcej elementów do wyświetlenia tym większe zapotrzebowanie na zasoby. Był to problem na Androidzie od kilku lat, zanim pojawił się RecyclerView.

Firma Google w 2014 roku wraz z premierą Androida Lollipop wydała bibliotekę RecyclerView. Od tego czasu wiele się zmieniło. Pomysł na stworzenie listy był prosty — zamiast tworzyć widoki za każdym razem, gdy użytkownik przewija listę, stwórz raz widoki i przetwarzaj je ponownie, jeśli zajdzie taka potrzeba.

RecyclerView jest biblioteką, która tworzy listę(y). Zasadniczo zapewnia okno o ustalonym rozmiarze do załadowania dużego zbioru danych. Na początku tworzy widoki, z których będzie korzystać, gdy widoki wychodzą poza zakres (okno) i istnieje taka potrzeba, wykorzystuje je ponownie.

Atrybuty

RecyclerView to nic innego jak ViewGroup, który implementuje ScrollView. Podstawowe atrybuty RecyclerView są takie same jak w każdej innej grupie widoków. Oczywiście istnieją specjalne atrybuty dla tego widżetu. Będziesz je poznawał wraz z kolejnymi sekcjami tego kursu.

Layout Manager

Menedżer układu jest odpowiedzialny za to, aby RecyclerView wiedział, kiedy należy odzyskać widok potomny, gdy zniknie z zasięgu. Bez niego RecyclerView nie może określić, jak widoki mają być układane na ekranie oraz czy będzie to układ liniowy, czy też siatkowy.

Dostępne są 3 menedżery układów dla RecyclerView:

Adapter Recycler

Najważniejsza część całej architektury RecyclerView. Adapter jest odpowiedzialny za pobieranie danych z zestawu i generowanie obiektów View opartych o te dane. Ogólnie mówiąc adapter jest iteratorem, który uzyskuje limit od metody getCount() (zwraca rozmiar zestawu danych). Następnie „tworzy nowe widoki" i wiąże z nimi dane z tego samego zbioru danych.

Adapter działa jako połączenie między zestawem danych a widokiem. Zestaw danych może być wszystkim, co reprezentuje dane w zorganizowany sposób. Na przykład tablice, listy.

Adapter Recycler, zamiast tworzyć widoki, tworzy elementy ViewHoldera, które z kolei zawierają widoki. ViewHolder jest umieszczany w pamięci podręcznej i może być ponownie użyty, jeśli jest to wymagane.

Adaptery mogą bardzo wydajnie wyświetlać duże zestawy danych przy minimalnym opóźnieniu oraz przy zachowaniu niskiego zużycia pamięci i procesora. Renderuje tylko te obiekty View, które albo znajdują się na ekranie albo te, które pojawić się na ekranie za chwile. W ten sposób pamięć zużyta przez adaptera może być stała i niezależna od rozmiaru zestawu danych.

ViewHolder

Wzorzec ViewHolder ma na celu zmniejszenie liczby wywołań view.findViewById(), a co za tym idzie, zwiększeniu wydajności podczas przewijania listy. Metoda findViewById jest kosztowną operacją.

Wystarczy utworzyć klasę ViewHolder, w której zaimplementujesz całą logikę wiązania danych z widokiem. Warto wspomnieć, że ViewHoldery są tworzone i odwoływane na podstawie typu widoku, a nie na podstawie pozycji, więc możemy dodać nowe typy widoków do naszego RecyclerView.

Widżet RecyclerView jest bardziej zaawansowaną wersją ListView — wyświetla elementy na ekranie, ale używa innego podejścia do tego celu.

RecyclerView - jak sama nazwa wskazuje, odtwarza widoki, gdy znikną one z zasięgu ekranu za pomocą wzorca ViewHolder. Oczywiście ListView może mieć również ViewHoldery, ale domyślnie nie jest dostępny, co zmusza programistę do pisania więcej kodu. Jest to jedna z głównych różnic między tymi bytami.

W ListView mogliśmy tworzyć tylko pionowe listy. Nie ma możliwości implementacji poziomej listy. Korzystając z RecyclerView masz taką możliwość.

Przechwytywanie kliknięć pozycji na ListView było proste dzięki interfejsowi AdapterView.OnItemClickListener. Jednak RecyclerView zapewnia większe możliwości i kontrolę dzięki RecyclerView.OnItemTouchListener.

Inne korzyści to LayoutManager, Dekoracja elementów i animator wiersza. W kursie poznasz te możliwości dokładniej.

Aby dodać bibliotekę RecyclerView do swojej aplikacji na system Android musisz dodać zależność w pliku Gradle (Module.app)

dependencies {
    implementation 'com.android.support:recyclerview-v7:28.0.0'
}

Następnie synchronizować projekt.

Mamy za sobą podstawowe informacje o RecyclerView. Wraz z postępem nauki będziesz poznawał bardziej zaawansowane rzeczy. Zobaczmy teraz jak może wyglądać prosta lista stworzona za pomocą RecyclerView.

Krok 1

W pierwszej kolejności musimy dodać widżet RecyclerView do pliku z wyglądem aktywności lub fragmentu.

<android.support.v7.widget.RecyclerView
    android:id="@+id/myRC"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

Krok 2

W kolejnym kroku tworzymy wygląd pojedynczego wiersza. W tym celu musimy utworzyć osobny plik XML Na początku wyświetlimy tylko imię. Nasz kod będzie wyglądał następująco.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/name"
        android:textSize="20sp"
        android:padding="20dp"
        />
</LinearLayout>

Krok 3

W kroku 3 przygotowujemy dane oraz kod w aktywności do obsługi widoku RecyclerView.

myRC.layoutManager = LinearLayoutManager(this)
val adapter = MyRecyclerViewAdapter(listName )
myRC.adapter = adapter

Przykładowe dane będą generowane za pomocą biblioteki faker.

myRC - to identyfikator naszego RecyclerView w układzie XML Ustawiamy LayoutManager na LinearLayoutManager. Dalej inicjujemy adapter i go podpinamy do RecyclerView.

Możesz zmienić oś przewijania listy. Domyślnie ustawiona jest jako pionowa. Jeśli chcesz zmienić przewijanie w poziomie, musisz zmienić tylko jedną linię kodu:

myRC.layoutManager = LinearLayoutManager(
        this,
        LinearLayoutManager.HORIZONTAL, 
        false)

Jak wspomniano wcześniej możemy zmienić menedżer układu naszej listy na GridLayout. W tym celu zamień:

myRC.layoutManager = LinearLayoutManager(this)

na:

myRC.layoutManager = GridLayoutManager(this, 3)

Liczba 3 oznacza liczbę kolumn. Możesz dać także

myRC.layoutManager = StaggeredGridLayoutManager(2,StaggeredGridLayoutManager.VERTICAL)

Jeżeli chcesz skorzystać z StaggeredGridLayout.

Krok 4

Przyglądnijmy się teraz naszemu adapterowi. Stworzymy jeden plik, a w nim dwie klasy.

class MyRecyclerViewAdapter(val items : ArrayList<String> )
        : RecyclerView.Adapter<MyViewHolder>() {
...
}
class MyViewHolder (view: View) : RecyclerView.ViewHolder(view) {
...
}

Klasa MyRecyclerViewAdapter jest rozszerzona o RecyclerView.Adapter<> i przekazujemy do tej klasy nasze dane (imiona).

Jeśli używasz Android Studio zauważysz czerwoną linię, która informuje nas, że istnieje podstawowy zestaw metod, które będą musiały zostać dodane do adaptera, aby działał poprawnie. Metody które musimy nadpisać to:

Przyglądnijmy się teraz tym metodom dokładniej. Metoda getItemCount() zwraca rozmiar zestawu danych używanego przez adapter. Oczywiście rozmiar może zmieniać się dynamicznie jak okaże się w kolejnych sekcjach.

override fun getItemCount(): Int {
    return items.size
}

Kolejna metoda to onCreateViewHolder W tej metodzie podpinamy nasz plik XML z widokiem wiersza, którego będziemy używać do przechowywania każdego elementu listy.

override fun onCreateViewHolder(parent: ViewGroup, 
                                viewType: Int): MyViewHolder {
   
    val view = LayoutInflater.from(parent.context)
            .inflate(R.layout.recyclerview_item, parent, false)
        
    return MyViewHolder(view)

}

Ta metoda jest wywoływana bezpośrednio po utworzeniu adaptera i jest używana do inicjowania ViewHoldera(ów).

Ostatnią funkcją, która jest wymagana to onBindViewHolder. Jak sama nazwa wskazuje, służy do wiązania danych z układem.

override fun onBindViewHolder(myHolder: MyViewHolder, 
                              position: Int) {
    myHolder.name.text = items.get(position)
}

Teraz kiedy już mamy adapter musimy jeszcze stworzyć uchwyt widoku.

Związku z tym, w naszym projekcie korzystamy tylko z jednego widoku i jest nim TextView. Dlatego tworzymy tylko jeden obiekt, gdybyśmy mieli więcej obiektów musimy je utworzyć. Nasza klasa będzie wyglądać następująco:

class MyViewHolder (view: View) : RecyclerView.ViewHolder(view) {
    val name = view.name
}

Tak mało kodu? Tak ponieważ pracujemy z językiem Kotlin i jego dodatkiem.

Więcej o Kotlin Extensions możesz przeczytać tutaj.

W powyższym kodzie klasa MyviewHolder jest osobną klasą. Jest to celowy zabieg. Zaleca się używanie (statycznych) klas wewnętrznych, aby uniknąć wycieków pamięci. Temat dotyczący wycieków pamięci jest obszerny, więc nie będzie poruszany w tym poradniku.

Jeśli chcesz użyć jednego posiadacza widoku (ViewHolder) w wielu miejscach, zaleca się wtedy utworzenie oddzielnych klas. W przeciwnym wypadku, utwórz zagnieżdżoną klasę. W kolejnych rozdziałach zobaczysz zagnieżdżone klasy ViewHolder.

To cały kod dla naszego prostego przykładu. Mamy kompletny projekt, który implementuje wzorzec RecyclerView, Adapter i ViewHolder. Poniżej znajduje się efekt naszej pracy.

Wiesz jak już tworzyć prostą listę. Teraz skupimy się na dodatkowych funkcjach.

Więcej pól

W naszym podstawowym przykładzie lista składała się tylko z imion. Korzystaliśmy z ArrayList<String>, ale nic nie stoi na przeszkodzie abyśmy stworzyli własny model danych z większa ilością pól.

data class Profile(var name: String,
                   var city: String,
                   var color: String

)

Teraz przeróbmy nasz wygląd wiersza.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="16dp">

    <android.support.v7.widget.CardView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:id="@+id/cv">

        <RelativeLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:padding="16dp">

            <View
                    android:layout_width="36dp"
                    android:layout_height="34dp"
                    android:id="@+id/color"
                    android:layout_alignParentLeft="true"
                    android:layout_alignParentTop="true"
                    android:layout_marginRight="16dp" />

            <TextView
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:id="@+id/name"
                    android:layout_toRightOf="@+id/color"
                    android:layout_alignParentTop="true"
                    android:textSize="30sp" />

            <TextView
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:id="@+id/city"
                    android:layout_toRightOf="@+id/color"
                    android:layout_below="@+id/name" />

        </RelativeLayout>

    </android.support.v7.widget.CardView>

</LinearLayout>

Często RecyclerView współpracuje z CardView. Dzięki temu listy mają ładny wygląd. Musimy także zmienić konstruktor naszego adaptera

class MyRecyclerViewAdapter( val items: ArrayList<Profile>)
        : RecyclerView.Adapter<MyViewHolder>() {
...
}

Oraz modyfikujemy kod metody onBindViewHolder()

override fun onBindViewHolder(myHolder: MyViewHolder, position: Int) {
        myHolder.name.text = items.get(position).name
        myHolder.city.text = items.get(position).city
        myHolder.color.setBackgroundColor( Color.parseColor(items.get(position).color) )
}

Jak widzisz nie tylko możemy ustawiać tekst, ale też przypisywać inne atrybuty takie jak kolor tła. Jeżeli chcesz wstawić obraz, także możesz to zrobić.

Została jeszcze nam klasa ViewHolder.

class MyViewHolder (view: View) : RecyclerView.ViewHolder(view) {
    val name = view.name
    val city = view.city
    val color = view.color
}

Wynikiem zmiany będzie taki efekt

Gdy korzystamy z ListView mamy łatwy dostęp do setOnItemClickListener. Niestety w RecyclerView nie mamy tak już prosto. Musimy stworzyć własny sposób na obsługę zdarzeń noClick(). Jest na to wiele sposobów. Przyjrzyjmy się jak najprościej możemy to zrobić.

Jeżeli chcemy nasłuchiwać na każdy wiersz (niezależnie od tego jaki widok klikasz) to naszą metodę onCreateViewHolder() musimy zmodyfikować w następujący sposób:

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {

    val view = LayoutInflater.from(parent.context)                              
                      .inflate(R.layout.recyclerview_item, 
                       parent, 
                       false)

        val myViewHolder = MyViewHolder(view)

        view.setOnClickListener(
           { v -> Log.d("TAG ",
                  myViewHolder.adapterPosition.toString() ) 
           })

        return myViewHolder
}

W powyższym kodzie wyświetlamy pozycję klikniętego wiersza.

Aby obsłużyć kliknięcie na konkretnym elemencie, skorzystamy z klasy VewHolder.

Uważaj! W naszym poprzednim przykładzie nie mieliśmy bezpośredniego dostępu do danych. Rozwiązanie jest dość proste, zrobić klasę ViewHolder jako klasę wewnętrzną adaptera. Z wiązku z tym kod przyjmie postać:

inner class MyViewHolder (view: View) : RecyclerView.ViewHolder(view) {
    val name = view.name
    val city = view.city
    val color = view.color

    init {
        color.setOnClickListener { v ->  Log.d("TAG color ",
            items.get( layoutPosition).color ) }
    }
}

W tym przypadku, po naciśnięciu widoku z kolorem, dostaniemy informację zwrotną o pozycji.

Przy większych projektach warto rozbudować kod o interfejsy. RecyclerView dostarcza także addOnItemTouchListener, który złapie wszystkie zdarzenia dotykowe w widoku. Jest to kolejny sposób na wykonanie akcji po kliknięciu.

Gdy dodajesz lub usuwasz elementy ze zbioru danych powinieneś poinformować o tym fakcie RecyclerView. W jaki sposób możemy to zrobić?

Proces modyfikowania danych obejmuje dwa główne kroki za każdym razem. Pierwszy krok to zaktualizowanie zestawu danych, a następnie poinformowanie adaptera o zmianie. W RecyclerView.Adapter dostępnych jest kilka metod powiadamiania o zmianach. Najczęściej będziesz korzystał z:

Możemy ich użyć w aktywności lub fragmencie.

items.add(0, new Profile("Maciej", "Kraków"));
adapter.notifyItemInserted(0);

lub bezpośrednio w adapterze, jeśli tego wymaga sytuacja.

Warto pamiętać o tym, że notifyDataSetChanged() spowoduje większy narzut, ponieważ zmusi LayouManagers do pełnego ponownego przerysowania widoków oraz ich ponownego powiązania z ViewHolderem

Wstaw pojedynczy element

val insertIndex = 2
items.add(insertIndex, item)
adapter.notifyItemInserted(insertIndex)

Wstaw wiele elementów

var items: ArrayList<Profile> = ArrayList()
items.add("Maciej","Kraków")
...
val insertIndex = 2
data.addAll(insertIndex, items)
adapter.notifyItemRangeInserted(insertIndex, items.size())

Usuń pojedynczy element

val removeIndex = 2
data.remove(removeIndex)
adapter.notifyItemRemoved(removeIndex)

Usuń wiele elementów

val startIndex = 2
val endIndex = 5
val count = endIndex - startIndex  // 3
list.subList(startIndex, endIndex).clear()
adapter.notifyItemRangeRemoved(startIndex, count)

Usuń wszystko

data.clear()
adapter.notifyDataSetChanged()

Zastąp starą listę nową listą

items.clear()
// create new list
var newList: ArrayList<Profile> = ArrayList()
newList.add("Maciej","Kraków")
...
items.addAll(newList)
// notify adapter
adapter.notifyDataSetChanged()

Zaktualizuj pojedynczy element

val newValue = "Adam"
val updateIndex = 3
items.set(updateIndex, newValue)
adapter.notifyItemChanged(updateIndex)

Przenieś pojedynczy element na inną pozycję

val fromPosition = 10
val toPosition = 5
// update data array
val item = items.get(fromPosition)
items.remove(fromPosition)
items.add(toPosition, item)
// notify adapter
adapter.notifyItemMoved(fromPosition, toPosition)

DiffUtil

Gdy Twoja lista przybiera dynamiczny kształt. Podczas dodawania lub usuwania elementów z listy, metoda notifyDataSetChanged może nie być najlepszym rozwiązaniem. Warto sięgnąć po coś bardziej efektywniejszego.

Począwszy od wersji 24.2.0 bibliotekia oferuje przydatne narzędzie o nazwie DiffUtil. Ta klasa znajduje różnicę między dwiema listami i dostarcza zaktualizowaną listę jako wynik.

Polega w dużej mierze na równości elementów, co oznacza, że ​​dwie rzeczy są takie same, gdy ich zawartość jest taka sama. Za każdym razem, gdy używasz RecyclerView, powinieneś używać DiffUtil, ponieważ ​​potrzebujesz wydajnego sposobu porównania.

DiffUtil.Callback jest klasą abstrakcyjną i jest używana jako klasa wywołania zwrotnego przez DiffUtil podczas obliczania różnicy między dwiema listami. Posiada cztery metody abstrakcyjne i jedną metodę bez abstrakcji.

Jak używać? Zobacz poniższy kod:

class MyDiffCallback(
    val oldList: ArrayList<Profile>,
    val newList: ArrayList<Profile>
) : DiffUtil.Callback() {

    override fun getOldListSize() = oldList.size
    override fun getNewListSize() = newList.size
   
    override fun areItemsTheSame(oldItemPosition: Int,              
                                 newItemPosition: Int): Boolean {
        return oldList[oldItemPosition].id == newList[newItemPosition].id
    }

    override fun areContentsTheSame(oldItemPosition: Int,
                                    newItemPosition: Int): Boolean {
        return oldList[oldItemPosition].name == newList[newItemPosition].name
    }
}

Zauważ, że w metodzie areItemsTheSame() porównujemy za pomocą identyfikatora. W związku z tym, do naszego modelu, który został wcześniej przedstawiony musimy dodać pole "id".

Teraz zmodyfikujemy listę, a dokładnie posortujmy ją. Kod aktualizacji może wyglądać następująco.

var sortedList = profiles.sortedWith(compareBy { it.name })
val newlist = arrayListOf<Profile>().addAll(sortedList) // Convert to ArrayList

val diffCallback = MyDiffCallback(profiles, newlist)
val diffResult = DiffUtil.calculateDiff(diffCallback)

profiles.clear()
profiles.addAll(newlist)
diffResult.dispatchUpdatesTo(adapter)

Metoda dispatchUpdatesTo() automatycznie informuje adapter o zmianie danych. To wszystko!

Domyślnie lista jest wyświetlana od początku. Jeżeli masz 40 rekordów, użytkownik musi przewinąć listę do samego końca, aby zobaczyć ostatni wpis. W niektórych przypadkach (np. w wiadomościach SMS) potrzebujemy przewinąć listę do samego końca zaraz po jej utworzeniu.

W tym celu mozemy skorzystać z właściwości setStackFromEnd lub setReverseLayout.

Różnica między nimi polega na tym, że setStackFromEnd pokaże ostatni element zaraz po utworzeniu listy. Kierunek układu pozostanie bez zmian. W układzie pionowym będzie przewijanie z góry do dołu. W osi poziomej - od lewej do prawej, a przewijanie w lewo spowoduje wyświetlenie wcześniejszych elementów.

W przypadku setReverseLayout zmieni się kolejność dodawania elementów przez adapter. Układanie wierszy rozpocznie się od ostatniego rekordu. Czyli na górze będziemy mieć ostatni wpis ze zbioru danych, a na dole pierwszy element.

val layoutMgr = LinearLayoutManager(this)
layoutMgr.stackFromEnd = true
layoutMgr.reverseLayout = false
myRC.layoutManager = layoutMgr

Przewijanie do nowych elementów

Jeśli wstawiamy elementy z przodu listy i chcemy zobaczyć ten element, możemy ustawić pozycję przewijania na pierwszy element:

val index = 0
adapter.notifyItemInserted(index)
myRC.scrollToPosition(index)  

Jeśli dodajemy dane na końcu listy i chcemy przewinąć do dołu, skorzystaj z poniższego kodu.

val index = profiles+1
adapter.notifyItemInserted(index)
myRC.scrollToPosition(index)  

Istnieje również funkcja smoothScrollToPosition(). Dzięki której możesz także przewijać do konkretnej wartości. Metoda ta dodatkowo animuje przewijanie listy. Jeśli występuje duża różnica między elementem widocznym a docelowym, użytkownik może czekać dość długo, aż pojawi się odpowiednia pozycja na ekranie. Także uważnie korzystaj z tej metody

Zapisywanie i przywracanie pozycji

Podczas korzystania z RecyclerView w aplikacji na Androida, zwłaszcza z wieloma lub nieskończonymi elementami, użytkownicy mogą być sfrustrowani, jeśli nie zapamiętasz ich pozycji.

Aby zapisać pozycję pierwszego w pełni widocznego elementu korzystamy z findFirstVisibleItemPosition(). Dane te możesz zapisać w SharedPreferences.

private val recyclerScrollKey: String = "MY_RC_POSITION"
val position = (myRC.layoutManager as LinearLayoutManager).findFirstCompletelyVisibleItemPosition()
       
PreferenceManager.getDefaultSharedPreferences(this)
            .edit()
            .putInt(recyclerScrollKey, position)
            .apply()

Powyższy kod można dodać do onPause().

W celu przywrócenia pozycji skorzystaj z następującego kodu

val position = PreferenceManager
                   .getDefaultSharedPreferences(this)
                   .getInt(recyclerScrollKey, 0)  
myRC.scrollToPosition(position)

Tego kodu nie umieszczaj w onResume() ponieważ RecyclerView może nie być jeszcze zainicjalizowany.

Szybkie przewijanie

Gdy posiadamy bardzo duży zbiór danych warto włączyć szybkie przewijanie. W pierwszej kolejności musisz dodać do pliku layoutu następujące atrybuty::

<android.support.v7.widget.RecyclerView
...
app:fastScrollEnabled="true"
app:fastScrollHorizontalThumbDrawable="@drawable/thumb_drawable"
app:fastScrollHorizontalTrackDrawable="@drawable/track_drawable"
app:fastScrollVerticalThumbDrawable="@drawable/thumb_drawable"
app:fastScrollVerticalTrackDrawable="@drawable/track_drawable"
... />

fastScrollEnabled:- aktywuje lub dezaktywuje szybkie przewijanie. Ustawienie tego na true wymagać będzie podania czterech poniższych atrybutów.

thumb_drawable i track_drawable mogą przybrać następującą postać:

<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/line"/>
</selector>

Kształty możemy dowolnie formować. Przykładowy kształt będzie tak wyglądał:

<shape xmlns:android="http://schemas.android.com/apk/res/android"
        android:shape="rectangle">
    <solid android:color="@color/colorPrimary"/>
</shape>

val postion = 50
val y = myRC.getY() + myRC.getChildAt( postion ).getY()              
scrollView.smoothScrollTo(0, y.toInt() )

Dodatkowe biblioteki

Możesz także skorzystać z bibliotek. Dzięki nim będziesz mógł zbudować listę RecyclerView z indeksem alfabetycznym. Spójrz na te dwa projekty, które pomogą Ci to osiągnąć:

Przesuwanie wiersza w poziomie

Wyobraź sobie, że mamy listę użytkowników. Gdy będziemy chcieli zadzwonić do konkretnego użytkownika wykonujemy gest w prawo. Natomiast gdy chcemy wysłać mu wiadomośc przesuwamy wiersz w lewo. Prosta nawigacja :) Zobaczmy jak ją wdrożyć.

Na początku tworzymy klasę SwipeController, którą rozszerzamy o Callback z pakietu android.support.v7.widget.helper.ItemTouchHelper.Callback, a następnie nadpisujemy kilka metod.

override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
        return makeMovementFlags(0, RIGHT or LEFT)
}

override fun onMove(
        recyclerView: RecyclerView,
        viewHolder: RecyclerView.ViewHolder,
        target: RecyclerView.ViewHolder
    ): Boolean {
        return false
}

W metodzie getMovementFlags() wyznaczamy w jaki sposób elementy mogą się poruszać. W naszym przypadku na prawo i lewo.

Metoda onMove() wykrywa przenoszenie elementu na inną pozycję. Teraz tym nie będziemy się zajmować. Dlatego zwracamy false.

override fun getSwipeThreshold(viewHolder: RecyclerView.ViewHolder): Float {
    return 0.7F
}

Metoda getSwipeThreshold() zwraca wartość float. Wartość 0.7f oznacza, że ​​przesunięcie o 70% wiersza oznacza wykonanie akcji z metody onSwiped()

override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
    val position = viewHolder.adapterPosition
    val phoneNumber = adapter.items.get(position).tel
    val intent: Intent

     //Action
    if (direction == ItemTouchHelper.LEFT) {
        intent = Intent(Intent.ACTION_VIEW, Uri.parse("sms:$phoneNumber"))
        intent.putExtra("sms_body", "Hello!")
    }
    else {
        intent = Intent(Intent.ACTION_DIAL, Uri.parse("tel:$phoneNumber"))
    }
    context.startActivity(intent)
    adapter.notifyItemChanged(viewHolder.adapterPosition)
}

Wewnątrz onSwiped(), korzystamy z adapterPosition do określenia indeksu elementu listy, który został przesunięty. Pobieramy nr tel. Na końcu wykonujemy akcję w zależności od kierunku gestu.

override fun onChildDraw(c: Canvas,
                         recyclerView: RecyclerView,
                         viewHolder: RecyclerView.ViewHolder,
                         dX: Float, dY: Float,
                         actionState: Int,
                         isCurrentlyActive: Boolean ) {

    if (actionState == ACTION_STATE_SWIPE)
        drawButton(c, viewHolder, dX);

    super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
}

W funkcji onChildDraw() będziemy rysować ikonę gdy wiersz będzie przesuwany. Ważne jest wywołanie metody super(). Metoda rysowania ikony może wyglądać następująco:

private val iconCall: Bitmap = BitmapFactory.decodeResource(context.resources, R.drawable.ic_action_call)
private val iconSMS: Bitmap = BitmapFactory.decodeResource(context.resources, R.drawable.ic_action_sms)

private val paint = Paint()

private val colorCall = Color.GREEN
private val colorSNS = Color.BLUE

private val heightIconCall = iconCall.height.toFloat()
private val heightIconSMS = iconSMS.height.toFloat()

private fun drawButton(c: Canvas,
                        viewHolder: RecyclerView.ViewHolder,
                        dX: Float ) {

    val itemView = viewHolder.itemView
    val heightItem = itemView.bottom - itemView.top
    val padding = 30f

    val paddingHeightCall = (heightItem - heightIconCall) / 2
    val paddingHeightSNS = (heightItem - heightIconSMS) / 2

    var background: RectF? = null
    var icon: Bitmap? = null
    var iconDest: RectF? = null

    // Swipe Right
    // set icon
    if (dX > 10) {
        icon = iconCall
        iconDest = RectF(
            itemView.left + padding,
            itemView.top + paddingHeightCall,
            itemView.left + padding + iconCall.width,
            itemView.bottom - paddingHeightCall
        )
    }

    // set background
    if (dX > 200) {
        paint.color = colorCall
        background = RectF(0f,
            itemView.top.toFloat(),
            itemView.left + 2 * padding + iconCall.width,
            itemView.bottom.toFloat())
    }

    // Swipe Left
    // set icon
    if (dX < -10) {
        icon = iconSMS
        iconDest = RectF(
            itemView.right - padding - iconSMS.width,
            itemView.top + paddingHeightSNS,
            itemView.right - padding,
            itemView.bottom - paddingHeightSNS)
    }

    // set background
    if (dX < -200) {
        paint.color = colorSNS
        background = RectF(itemView.right - 2 * padding - iconSMS.width,
            itemView.top.toFloat(),
            itemView.right.toFloat(),
            itemView.bottom.toFloat()
        )

    }

    if (background != null )
        c.drawRect(background, paint)

    if ( icon != null && iconDest != null  )
        c.drawBitmap(icon, null, iconDest, paint)

}

Kod powinien być zrozumiały. Może być mylące to, że na początku rysujemy tło, a następnie ikonę. Jeżeli zrobimy to w odwrotnej kolejności wtedy ikona zostanie zamalowana przez kolor. Tło Ikony jest przezroczyste. Dlatego kolor będzie widoczny.

Aby wszystko działało poprawnie musimy podłączyć naszą klasę do RecyclerView w aktywności.

val swipeController = SwipeController(this, adapter)
val itemTouchhelper = ItemTouchHelper(swipeController)
itemTouchhelper.attachToRecyclerView(myRC)

Efekt możesz zobaczyć na tym filmie

Przenoszenie elementów w pionie

Za pomocą całego wiersza

Wiesz już jak wykonywać akcję po przesunięciu elementu na bok. Teraz przyglądnij się w jaki sposób przenosić elementy na inna pozycję w liście. Wykorzystamy do tego celu tą samą klasę co w poprzedniej sekcji.

Cała magia polega na tym, że będziemy musieli nadpisać tylko dwie metody w następujący sposób:

override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
    return makeMovementFlags(UP or DOWN, 0)
}

override fun onMove(
    recyclerView: RecyclerView,
    viewHolder: RecyclerView.ViewHolder,
    target: RecyclerView.ViewHolder
): Boolean {

    val fromPosition = viewHolder.adapterPosition
    val toPosition = target.adapterPosition

    swap(adapter.items, fromPosition, toPosition);
    adapter.notifyItemMoved(fromPosition, toPosition)

    return true
}

Oczywiście możemy także nadpisać metodę isLongPressDragEnabled(). Jej celem jest aktywowanie akcji dopiero po długim kliknięciu na wierszu. Metoda ta zwraca wartość logiczną — true albo false.

Możemy także wyróżnić element, który jest przesuwany. W poniższym przykładzie zmieniamy kolor przenoszonego wiersza na szary, a po opuszczeniu go, usuwamy kolor.

override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {

    if (actionState != ACTION_STATE_IDLE) {
        if (viewHolder != null) {
            viewHolder.itemView.setBackgroundColor(Color.GRAY)
        }
    }
    super.onSelectedChanged(viewHolder, actionState)
}

override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
    if (viewHolder != null) {
        viewHolder.itemView.setBackgroundColor(Color.TRANSPARENT)
    }

    super.clearView(recyclerView, viewHolder)
}

Za pomocą konkretnego widoku

Istnieje także możliwość przesuwania wierszy za pomocą konkretnego widoku w wierszu. Aby użyć określonego widoku (uchwytu) do przeciągania i upuszczania, musimy wykonać następujące czynności:

Ustaw isLongPressDragEnabled na false, aby wyłączyć domyślne przeciąganie i upuszczanie.

W pliku xml z wyglądem wiersza musimy dodać widok ImageView

<ImageView
        android:layout_width="24dp"
        android:layout_height="24dp"
        android:id="@+id/dragHandle"
        android:src="@drawable/ic_drag_handle_24dp"
        android:layout_alignParentEnd="true"
        android:layout_alignParentTop="true"
        android:layout_marginStart="16dp"
/>

Kolejnym krokiem jest utworzenie słuchacza w adapterze RecyclerView

private lateinit var listener: adapterListener

interface adapterListener {
    fun StartDragListener(viewHolder: RecyclerView.ViewHolder)
}

fun setListener (adapterListener: adapterListener) {
    listener = adapterListener
}

Natomiast w klasie ViewHolder musimy dodać:

init {
    dragHandle.setOnTouchListener { v, event ->
        if (event.action == MotionEvent.ACTION_DOWN) {
            listener.StartDragListener(this)
        }
        false
    }
}

Zaimplementuj teraz interfejs w aktywności i przekaż go do adaptera.

val dragDropController = DragDropController(this, adapter)

itemTouchHelper = ItemTouchHelper(dragDropController)
itemTouchHelper.attachToRecyclerView(myRC)

val adapterListener =  object :MyRecyclerViewAdapter.adapterListener {
    override fun StartDragListener(viewHolder: RecyclerView.ViewHolder) {
        itemTouchHelper.startDrag(viewHolder)
    }
}
adapter.setListener(adapterListener)

Gest typu: Swipe-to-Refresh

Gest „przeciągnij, aby odświeżyć" (przesuwanie palcem po ekranie w dół), stał się tak popularny w dzisiejszych czasach, że Google stworzyło do tego dedykowany komponent.

Aby wdrożyć tą funkcjonalność musisz uczynić RecyclerView dzieckiem widżetu SwipeRefreshLayout. Otwórz plik z layoutem i przenieś tag <RecyclerView> do <SwipeRefreshLayout>. Po wykonaniu tej czynności zawartość pliku powinna wyglądać tak:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout ...>

    <android.support.v4.widget.SwipeRefreshLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">

        <android.support.v7.widget.RecyclerView
          ... />

    </android.support.v4.widget.SwipeRefreshLayout>
</RelativeLayout>

Natomiast po stronie kodu w aktywności dodajemy taki wpis:

swipeRefresh.setOnRefreshListener {
            val list = DataProfile(10).getUsers()
            adapter.items.clear()
            adapter.items.addAll( list )
            adapter.notifyDataSetChanged()
            swipeRefresh.isRefreshing = false  
        }

Wewnątrz słuchacza możesz modyfikować dane wyświetlane na liście w zależności od Twoich wymagań. Ponieważ pracujemy z fałszywymi danymi, po prostu czyścimy listę i generujemy nową. Także informujemy adaptera o zmianach.

Domyślnie, gdy tylko użytkownik wykona gest odświeżenia, widżet SwipeRefreshLayout wyświetli animowany wskaźnik postępu. Dlatego po zaktualizowaniu listy należy pamiętać o usunięciu wskaźnika, ustawiając właściwość isRefreshing na false.

W tej sekcji zobaczysz, jak korzystać z dodatkowej biblioteki, która oferuje intuicyjny interfejs do wybierania wielu elementów na liście.

W pierwszej kolejności musisz dodać zależności w pliku gradle.

implementation 'com.android.support:recyclerview-selection:28.0.0'

Aby RecyclerView Selection działał poprawnie, za każdym razem, gdy użytkownik dotknie listę, musisz przetłumaczyć współrzędne dotyku na obiekt ItemDetails. W związku z tym utwórz nową klasę, którą rozszerzysz o ItemDetailsLookup. Dodaj do niej konstruktor, gdzie jako argument podajesz obiekt RecyclerView.

class MyItemDetailsLookup(private val rv: RecyclerView)
    : ItemDetailsLookup<Long>() {

    override fun getItemDetails(event: MotionEvent)
            : ItemDetails<Long>? {

        val view = rv.findChildViewUnder(event.x, event.y)
        if(view != null) {
            return (rv.getChildViewHolder(view) as MyRecyclerViewAdapter.MyViewHolder)
                .getItemDetails()
        }
        return null
    }
}

Jest to klasa, która zapewni bibliotece informacje o elementach powiązanych z wyborem użytkownika. Metoda getItemDetails() otrzymuje obiekt MotionEvent. Przekazuje współrzędne X i Y do findChildViewUnder(). Ta funkcja określa, który widok dotknął użytkownik. W rezultacie dowiemy się, w którym dziecku listy nastąpiło zdarzenie i zwróci szczegóły tego elementu.

Przejdź teraz do adaptera. Ważne jest, aby każdy element miał unikalny identyfikator, który jest typu Long. Dlatego w bloku init dodaj:

init {
    setHasStableIds(true)
}

Ustawienie tej opcji na true, powoduje, że każdy element w zbiorze danych ma być reprezentowany przez unikalny identyfikator typu Long.

Dodatkowo, aby móc pobrać pozycję unikalnego identyfikatora, musisz zastąpić metodę getItemId().

override fun getItemId(position: Int): Long {
    return position.toLong()
}

Ponadto biblioteka RecyclerView Selection wymaga metody, która będzie identyfikować wybrane elementy listy. Tą metodę należy dodać do ViewHoldera. Musi zwrócić instancję klasy ItemDetailsLookup.ItemDetails. W tym celu dodaj następujący kod w klasie MyViewHolder:

fun getItemDetails(): ItemDetailsLookup.ItemDetails<Long> =
    object: ItemDetailsLookup.ItemDetails<Long>() {
        override fun getPosition(): Int = adapterPosition
        override fun getSelectionKey(): Long? = itemId
    }

Nadpisujemy dwie abstrakcyjne metody obecne w klasie ItemDetails. Pierwsza z nich getPosition() zwraca indeks elementu listy.

Metoda getSelectionKey() musi zwrócić klucz, który może być użyty do jednoznacznej identyfikacji elementu listy. Aby zachować prostotę, po prostu zwróćmy itemId posiadacza widoku.

Nic nie stoi na przeszkodzie, aby użyć dowolnej innej techniki do generowania klucza wyboru, o ile generuje unikalne wartości.

Aby włączyć wybór wielu elementów, będziesz potrzebować obiektu SelectionTracker w aktywności.

val tracker: SelectionTracker<Long> = SelectionTracker.Builder<Long>(
    "selected",
    myRC,
    StableIdKeyProvider(myRC),
    MyItemDetailsLookup(myRC),
    StorageStrategy.createLongStorage()
).withSelectionPredicate(SelectionPredicates.createSelectAnything())
    .build()

Możesz zainicjować tracker za pomocą SelectionTracker.Builder. Do jego konstruktora należy przekazać:

Po utworzeniu konstruktora Builder możesz wywołać withSelectionPredicate(), aby określić, ile elementów użytkownik może wybrać. Jeżeli chcesz mieć opcję wyboru kilku wierszy to nie musisz wywoływać tej metody. Domyślnie jest ustawione aby obsłużyć wybór wielu elementów. Z drugiej strony możesz przekazać argument jako obiekt SelectionPredicate zwrócony przez metodę createSelectAnything().

Podczas gdy Twój projekt zakłada, że ma być tylko jeden wybór skorzystaj z SelectionPredicates.createSelectSingleAnything().

Możesz także tworzyć własne SelectionPredicatei, a następnie wdrożyć abstrakcyjne metody. Dzięki temu można robić takie rzeczy, jak wybieranie elementów, które są w pozycjach parzystych.

Tracker wyboru nie jest zbyt przydatny, chyba że jest powiązany z adapterem. Dlatego należy przekazać go do adaptera, wywołując setTracker().

adapter.setTracker(tracker)

Natomiast w adapterze dodaj

private var tracker: SelectionTracker<Long>? = null

fun setTracker(tracker: SelectionTracker<Long>?) {
    this.tracker = tracker
}

W tym momencie powinieneś być w stanie wybrać wiele elementów w RecyclerView. Aby rozpocząć wybieranie elementów, musimy najpierw aktywować tryb wielokrotnego wyboru przez długie naciśnięcie dowolnego elementu.

Powyższy kod zadziała, ale nie będziesz widział jakie wiersze zaznaczasz. Sposobem wyróżniania wybranych elementów jest zmiana ich koloru tła. Dlatego wystarczy zmienić kolor tła widgetu LinearLayout, który jest obecny w pliku XML układu wiersza.

W tym celu w adapterze, w metodzie onBindViewHolder(), dodaj:

if(tracker!!.isSelected(position.toLong()) )
    parent.background = ColorDrawable(Color.GRAY)
else
    parent.background = ColorDrawable(Color.TRANSPARENT)

Oprócz tego możemy dodać obserwatora (w aktywności lub fragmencie), aby obserwować bieżący wybór i coś z tym zrobić.

tracker.addObserver(object: SelectionTracker.SelectionObserver<Long>() {
    override fun onSelectionChanged() {
        val nItems:Int? = tracker?.selection?.size()
        val colorToolbar :ColorDrawable
        val titleToolbar :String

        if(nItems!=null && nItems > 0) {
            titleToolbar = "$nItems items selected"
            colorToolbar = ColorDrawable(Color.BLUE)
        } else {
            titleToolbar = getString(R.string.recyclerview_linearlayout)
            colorToolbar = ColorDrawable(getColor(R.color.colorPrimary))
        }
        supportActionBar?.title = titleToolbar
        supportActionBar?.setBackgroundDrawable(colorToolbar)
    }
})

W powyższym kodzie zmieniamy zachowanie ActionBar'a. Gdy mamy zaznaczone elementy dostaniemy informacje ile wierszy jest zaznaczonych oraz zmieni się kolor na niebieski.

Dodanie wyszukiwania jest bardzo prostym zadaniem, użyjemy do tego celu widżetu SearchView, który umieścimy na pasku narzędzi. W przykładzie będziemy szukać nazwy użytkownika lub miasta.

W pierwszej kolejności musimy rozszerzyć nasz adapter o Filterable.

class MyRecyclerViewAdapter(...) : RecyclerView.Adapter<MyRecyclerViewAdapter.MyViewHolder>(), Filterable {

    private var itemsCopy: ArrayList<Profile> = ArrayList()

    init {
        itemsCopy.addAll(items)
    }

Tworzymy kopie naszej listy. Jak się za chwilę przekonasz jest to spowodowane tym, że podczas szukania chcemy znaleźć elementy z oryginalnej listy, a nie już przefiltrowanej.

Kolejnym krokiem jest nadpisanie metody getFilter().

var itemsFiltered: ArrayList<Profile> = ArrayList()

override fun getFilter(): Filter {
    return object : Filter() {
        override fun performFiltering(charSequence: CharSequence): Filter.FilterResults {
            val charString = charSequence.toString()
            if (charString.isEmpty()) {
                itemsFiltered = itemsCopy
            } else {
                val filteredList = ArrayList<Profile>()
                for (row in itemsCopy) {
                    if (row.name.toLowerCase().contains(charString.toLowerCase())
                        || row.city.contains(charSequence) )
                        filteredList.add(row)
                }
                itemsFiltered = filteredList
            }
            val filterResults = FilterResults()
            filterResults.values = itemsFiltered
            return filterResults
        }

        override fun publishResults(charSequence: CharSequence, filterResults: Filter.FilterResults) {
            itemsFiltered = filterResults.values as ArrayList<Profile>
            // refresh the list with filtered data
            items.clear()
            items.addAll(itemsFiltered)
            notifyDataSetChanged()
        }
    }
}

Przekazujemy do niej ciąg znaków, który chcemy przeszukać. Tutaj właśnie stawiamy warunek. Jeżeli użytkownik wprowadził tekst do przeszukiwania to szukamy. Natomiast gdy użytkownik usunie zapytanie, zostanie wyświetlona lista pierwotna.

Dodaj teraz ikonę szukania w menu (plik XML)

<menu ... >
       <item
        android:id="@+id/action_search"
        android:icon="@android:drawable/ic_menu_search"
        android:orderInCategory="100"
        android:title="Search"
        app:showAsAction="always"
        app:actionViewClass="android.support.v7.widget.SearchView" />
</menu>

Następnie w aktywności lub fragmencie nadpisujemy metodę onCreateOptionsMenu()

override fun onCreateOptionsMenu(menu: Menu): Boolean {
    val findMenuItems = menuInflater
    findMenuItems.inflate(net.myenv.recyclerview.R.menu.menu_main, menu)
    super.onCreateOptionsMenu(menu)
    
    val searchView = menu.findItem(net.myenv.recyclerview.R.id.action_search).actionView as SearchView

    // listening to search query text change
    searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
        override fun onQueryTextSubmit(query: String): Boolean {
            // filter recycler view when query submitted
            adapter.getFilter().filter(query)
            return false
        }
    
        override fun onQueryTextChange(query: String): Boolean {
            // filter recycler view when text is changed
            adapter.getFilter().filter(query)
            return false
        }
    })

    return true
}

We wcześniejszym rozdziale zostało wspomniane, że można korzystać z biblioteki CardView w celu ładniejszego wyglądu wierszy. Co, jeśli nie chcemy z niej korzystać, a mimo to chcemy zrobić jakiś odstęp między rekordami?

Oczywiście możemy w pliku układu wiersza zrobić ładne odstępy. A to nie zawsze może być wydajne. Dobrym pomysłem jest skorzystanie z ItemDecoration.

ItemDecoration umożliwia aplikacji dodanie specjalnego "rysunku" i przesunięcia układu do określonych wartości. Może to być przydatne do rysowania dzielników między elementami.

Nie możemy po prostu powiedzieć, że ItemDecoration to po prostu dzielnik. Dzielnik, jak sugeruje nazwa, można rysować tylko między elementami. Natomiast dekoracja elementów może zostać narysowana na wszystkich czterech stronach wiersza. ItemDecoration daje pełną kontrolę programistom w mierzeniu i rysowaniu dekoracji.

Podstawowy dzielnik

Najprostszym sposobem dodania dzielnika do listy jest skorzystanie z klasy DividerItemDecoration

val itemDecoration = DividerItemDecoration(this,
                          DividerItemDecoration.VERTICAL)
myRC.addItemDecoration(itemDecoration)

Poniżej znajduje się efekt bez CardView.

Jest to bardzo delikatna linia, która oddziela wiersze od siebie. Istnieje również możliwość zmiany koloru tego przerywnika. W pliku ze stylami dodaj odpowiedni atrybut:

<item name="android:listDivider">@color/divider_bg</item>

Własny dzielnik

Jeżeli potrzebujesz innego wyglądu musisz napisać własną dekoracje. Może wydawać się to trudne lecz tak nie jest. Wystarczy utworzyć klasę, która rozszerza się o ItemDecoration.

Następnie trzeba nadpisać metody w zależności od tego co chcemy osiągnąć. Mamy do dyspozycji:

Aby narysować dzielnik zanim widoki elementów zostaną utworzone wykorzystaj onDraw(). W przeciwnym wypadku użyj onDrawOver().

Powyżej przedstawiono różnice między metodami onDraw a onDrawOver. Po lewej stronie znajduje się efekt metody onDrawOver a po prawej onDraw. Zwróć uwagę na to czy kwadrat z kolorem jest nad obramowaniem lub pod nim.

Poniżej przedstawiono przykładowy kod metody onDrawOver(), która rysuje linie.

class MyItemDecoration(
    val mDivider: Drawable
) : RecyclerView.ItemDecoration() {
    
    override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
        super.onDraw(c, parent, state)

        for (i in 0 until parent.childCount) {

            val child : View = parent.getChildAt(i)
            val params : RecyclerView.LayoutParams = child.layoutParams as RecyclerView.LayoutParams;

            val left = parent.getPaddingLeft()
            val right = parent.getWidth() - parent.getPaddingRight()
            val top = child.getBottom() + params.bottomMargin;
            val bottom = top + mDivider.getIntrinsicHeight();

            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(c);

        }
    }
}

Musisz także stworzyć plik xml w folderze drawable, o następującej treści:

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="@android:color/holo_green_light" />
    <size
        android:height="1dp"
        android:width="1dp"/>
</shape>

W tym pliku tworzymy kształt naszego dzielnika.

Metoda getItemOffsets() pozwala na przesunięcie elementu. Musisz zmodyfikować obiekt Rect i dodać przesunięcia do układu wiersza. Można powiedzieć, że to taki odstęp do granicy rekordów.

override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
    super.getItemOffsets(outRect, view, parent, state)
    outRect.set(100, 50, 200, 150)
}

Jeżeli nie chcesz na pierwszym elemencie przesunięcia, musimy uzupełnić kod o:

if (parent.getChildAdapterPosition(view)==0 ) return

Wstaw ten kod zaraz po metodzie super().

Parametr outRect może wyglądać nieco dziwnie na początku. Dlaczego metoda nie zwraca tego obkietu? Ponieważ pozwala RecyclerView na ponowne wykorzystanie obiektu Rect dla wszystkich dzieci, a tym samym oszczędza zasoby.

Jeszcze trzeba podłączyć nasz dekorator do RecyclerView.

val mDivider = getDrawable( R.drawable.my_divider)
val myItemDecoration = MyItemDecoration(mDivider)
myRC.addItemDecoration( myItemDecoration )

Tryb poziomy

Może też się tak zdarzyć, że Twoja lista będzie przesuwana w poziomie. Wtedy Twój kod do rysowania linii musi wyglądać inaczej. Dokładniej mówiąc, musisz narysować linie po prawej stronie. Metody mogą wyglądać następująco:

fun setOrientation(orientation: Int) {
    if (orientation != HORIZONTAL && orientation != VERTICAL) {
        throw IllegalArgumentException("invalid orientation")
    }
    mOrientation = orientation
}

override fun onDraw(c: Canvas, parent: RecyclerView) {
    if (mOrientation == VERTICAL) {
        drawVertical(c, parent)
    } else {
        drawHorizontal(c, parent)
    }
}


fun drawHorizontal(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
    super.onDraw(c, parent, state)

    for (i in 0 until parent.childCount) {
        
        val child = parent.getChildAt(i)
        val params = child.layoutParams as RecyclerView.LayoutParams

        val left = child.right + params.rightMargin
        val right = left + mDivider.intrinsicHeight
        val top = parent.paddingTop
        val bottom = parent.height - parent.paddingBottom

        mDivider.setBounds(left, top, right, bottom)
        mDivider.draw(c)
    }
}

Linia po lewej stronie

Jeżeli chcesz aby linia pojawiała się tylko po lewej stronie. To obliczenia muszą być następujące:

val left = child.left
val right = left + mDivider.intrinsicHeight
val top = child.top
val bottom = top + child.height

Gradient

Nie ma ograniczeń co do dzielnika. Możesz także dodać gradient:

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <gradient
            android:centerColor="#ff00ff00"
            android:endColor="#ff0000ff"
            android:startColor="#ffff0000"
            android:type="linear" />
    <size android:height="4dp"/>
</shape>

Obramowanie wokół wiersza

Jeśli sama linia nie wystarcza i chciałbyś otoczyć cały element linią. Metodę onDrawOver() musisz przerobić w następujący sposób:

val paint: Paint
val offset: Int = 10

init {
    paint = Paint(Paint.ANTI_ALIAS_FLAG)
    paint.color = Color.CYAN
    paint.style = Paint.Style.STROKE
    paint.strokeWidth = 5F
}

override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
    super.onDrawOver(c, parent, state)

    val layoutManager = parent.layoutManager

    for (i in 0 until parent.childCount) {

        val child = parent.getChildAt(i)

        if (layoutManager != null) {
            c.drawRoundRect(
                (layoutManager.getDecoratedLeft(child) + offset).toFloat(),
                (layoutManager.getDecoratedTop(child) + offset).toFloat(),
                (layoutManager.getDecoratedRight(child) - offset).toFloat(),
                (layoutManager.getDecoratedBottom(child) - offset).toFloat(),
                15F,                 15F,
                paint
            )
        }
}

Możesz także skorzystać z metody drawRect(), dzięki której narysujesz obramowanie bez zaokrąglonych rogów. Inne metody Canvas to:

Stwórzmy teraz listę, która zawiera listę użytkowników oraz będzie podzielona na sekcje według alfabetu. Efekt będzie podobny do tego:

W pierwszej kolejności musisz dodać wygląd wiersza, w którym będa prezentowane litery alfbatetu.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginBottom="5dp"
        android:paddingLeft="10dp">

    <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:id="@+id/header"
            android:background="@color/colorPrimaryDark"
            android:gravity="center"
            android:textSize="25sp"
            android:textStyle="bold"
    />


</LinearLayout>

Dodaj do modelu danych pole header, które będzie nas informować czy dany wiersz listy jest nagłówkiem. Domyślnie ustawiamy go na false.

var header: Boolean = false

Teraz przygotujmy listę, która będzie posortowana oraz znajdą się w niej odpowiednie nagłówki.

private fun getHeaderList(list: ArrayList<Profile>): ArrayList<Profile> {

    val listWithHeader: ArrayList<Profile> = ArrayList()

    val usersList = list.sortedWith(compareBy { it.name[0].toUpperCase() })
    var lastHeader = ""

    for (i in 0 until usersList.size) {

        val user = usersList[i]
        val header = user.name[0].toUpperCase()

        if (!lastHeader.equals( header.toString())) {
            lastHeader = header.toString()
            listWithHeader.add(Profile(i, header.toString(), "", "", "", true))
        }
        listWithHeader.add(user)
    }
    return listWithHeader
}

Po stronie adaptera musimy zmienić kilka rzeczy. Po pierwsze, klasę adaptera rozszerzamy o RecyclerView.Adapter<RecyclerView.ViewHolder>().

Musisz nadpisać metodę getItemViewType(). Sprawdzamy w niej czy dana pozycja jest nagłówkiem czy nie.

override fun getItemViewType(position: Int): Int {
    val type = when (items[position].header) {
        false -> 0
        // other types...
        else -> 1
    }
    return type
}

W metodzie onCreateViewHolder() jak wiesz tworzymy ViewHoldery z odpowiednimi widokami wierszy. Podobnie jak wyżej sprawdzamy typ widoku i zwracamy odpowiedni ViewHolder

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {

    val item = LayoutInflater.from(parent.context).inflate(R.layout.recyclerview_item, parent, false)

    val header = LayoutInflater.from(parent.context).inflate(R.layout.recyclerview_header, parent, false)

    val viewHolder : RecyclerView.ViewHolder =
    when (viewType) {
        0 -> ItemViewHolder(item)
        // other viewType...
        else ->  HeadernViewHolder(header)
    }

    return viewHolder
}

Metoda onBindViewHolder():

override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {

    when (holder.itemViewType) {
        0 -> {
            holder as ItemViewHolder
            holder.name.text = items.get(position).name
            holder.city.text = items.get(position).city
            holder.color.setBackgroundColor(Color.parseColor(items.get(position).color))
            holder.color.text = position.toString()
        }
        // other holder...
        else -> {
            holder as HeadernViewHolder
            holder.header.text = items.get(position).name

        }
    }
}

Pozostaje nam jeszcze ViewHolder

inner class ItemViewHolder (view: View) : RecyclerView.ViewHolder(view) {
    val name = view.name
    val city = view.city
    val color = view.color
}

inner class HeadernViewHolder (view: View) : RecyclerView.ViewHolder(view) {
    val header = view.header
}

Prawda, że proste? Chcesz więcej typów widoków? Nic nie stoi na przeszkodzie, aby dodać.

Może się zdarzyć, że będziesz potrzebował dodać do listy nagłówek lub stopkę. Nie ma prostej metody aby dodać taką funkcję. Warto tutaj wspomóc się bibliotekami. Poniżej znajdują się przykładowe projekty z których możesz korzystać lub się wzorować

Możesz także uzyskać podobne efekty po zastosowaniu multi widoków, które zostały omówione.

RecyclerView obsługuje animacje dla wierszy, które są dodawane, przenoszone lub usuwane za pomocą ItemAnimator. Domyślne efekty animacji są obsługiwane przez DefaultItemAnimator, pod warunkiem, że elementy dodajesz lub usuwasz, tak jak zostało pokazane w sekcji "Powiadomienia o zmianach w liście".

Istnieje też możliwość utworzenia własnej animacji. W tym celu musisz utworzyć nową klasę i rozszerzyć ją o SimpleItemAnimator i nadpisać poszczególne metody.

override fun animateRemove(holder: RecyclerView.ViewHolder): Boolean {
        val anim = AnimatorInflater.loadAnimator(
            context,
            R.animator.fade_out
        )
        anim.duration = 1000
        anim.setTarget(holder.itemView)
        anim.start()

        return true
    }

    override fun animateAdd(holder: RecyclerView.ViewHolder): Boolean {

        val anim = AnimatorInflater.loadAnimator(
            context,
            R.animator.fade_in
        )
        anim.duration = 1000
        anim.setTarget(holder.itemView)
        anim.start()

        return true
    }

    override fun animateMove(holder: RecyclerView.ViewHolder, fromX: Int, fromY: Int, toX: Int, toY: Int): Boolean {
        return false
    }

    override fun animateChange(
        oldHolder: RecyclerView.ViewHolder,
        newHolder: RecyclerView.ViewHolder,
        fromLeft: Int,
        fromTop: Int,
        toLeft: Int,
        toTop: Int
    ): Boolean {
        return false
    }

    override fun runPendingAnimations() {}

    override fun endAnimation(item: RecyclerView.ViewHolder) {}

    override fun endAnimations() {}

    override fun isRunning(): Boolean {
        return false
    }
}

W powyższym przykładzie ustawiamy animacje dla elementów, które są dodawane i usuwane z listy. Jeżeli nie chcesz wykonać animacji dla danej czynności zwróć false.

Animacja podczas wiązania danych

Jeżeli chciałbyś wykonać animację podczas tworzenia listy możesz zastosować poniższy kod.

verride fun onBindViewHolder(holder: ItemViewHolderAnimation, position: Int) {
    ...
    setAnimation(holder.itemView, position)
}

private fun setAnimation(view :View, position :Int){

    val animation :Animation = AnimationUtils.loadAnimation(context,
                android.R.anim.slide_in_left)
    animation.duration = 1000 // in ms
    view.startAnimation(animation)
}

override fun onViewDetachedFromWindow(holder: ItemViewHolderAnimation) {
    holder.clearAnimation()
}

inner class ItemViewHolderAnimation (view: View) : RecyclerView.ViewHolder(view) {
    ...
    fun clearAnimation() {
        itemView.clearAnimation()
    }
}

Po co metoda clearAnimation()? Korzystanie z metody setAnimation() może powodować problemy z szybkim przewijaniem. Widok może zostać ponownie użyty podczas trwania animacji. Aby tego uniknąć, zaleca się usunięcie animacji po odłączeniu widoku wiersza.

Biblioteki i dodatkowe animatory

Tworzenie niestandardowej animacji nie jest prostym procesem. Dlatego najszybszym sposobem implementacji animacji w RecyclerView jest korzystanie z bibliotek.

Przykładowe biblioteki:

Sprawdź też następujące niestandardowe animatory:

Zagnieżdżone lista to taka, w której mamy dwie listy. Jedna lista jest przewijana pionowa a w niej znajduje się druga lista, która jest przesuwana w poziomie.

Zagnieżdżona pozioma lista wewnątrz pionowego RecyclerView'a jest bardzo popularnym wzorcem projektowym. Taki przykład można zobaczyć w samej aplikacji Google Play.

Poniżej znajduje się przykład, w którym pokażę listę miast wraz z jego mieszkańcami. Będzie to wyglądać tak:

Interfejs użytkownika

W wyglądzie aktywności lub fragmentu, umieszczamy tylko widżet RecyclerView. Tak jak robiliśmy to w poprzednich rozdziałach. Układ ten będzie przewijany w osi pionowej..

Teraz przygotujmy układ widoku, który będzie wyświetlał nazwę miejscowości. W tym układzie umieszczamy także blok RecyclerView.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="10dp"
        android:orientation="vertical">

        <TextView
                android:id="@+id/city_item"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:textSize="20sp"
                android:textStyle="bold"
                android:layout_marginBottom="10dp"
        />

        <androidx.recyclerview.widget.RecyclerView
                android:id="@+id/recycler_view_nested"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="horizontal"
        />


</LinearLayout>

Teraz stwórzmy drugi układ, w którym wyświetlimy nazwę użytkownika oraz jego zdjęcie. Będzie to widok, który będzie przesuwany w osi poziomej. Skorzystamy w tym układzie z CardView, aby wyświetlić kartę użytkownika. Dlatego nie zapomnij dodać odpowiednich zależności w pliku gradle

<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="150dp"
        android:layout_height="200dp"
        android:layout_margin="5dp"
        app:cardCornerRadius="8dp"
        app:cardElevation="10dp">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:layout_margin="10dp">

        <TextView
                android:id="@+id/user_name"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginBottom="10dp"
                android:layout_gravity="center_horizontal"
        />

        <ImageView
                android:id="@+id/user_avatar"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:scaleType="centerCrop"
        />
    </LinearLayout>

</androidx.cardview.widget.CardView>

Model danych

Musimy stworzyć odpowiednie klasy, które będą reprezentować nasze listy. W pierwszej kolejności stworzymy klasę City, a następnie Citizens. Odpowiedni kod dla tych klas znajduje się poniżej.

data class City(val name: String,
                val citizens: List<Citizens>
)
data class Citizens(var name: String,
                var avatar : String
)

Tworzymy adaptery

Kolejnym krokiem jest stworzenie dwóch adapterów. Kolejno dla listy z miejscowościami oraz z użytkownikami. Adapter dla pierwszej listy będzie wyglądał tak:

class CityAdapter(private val context : Context,
                  private val citylist : List<City>) : RecyclerView.Adapter<CityAdapter.ViewHolder>(){

    private val viewPool = RecyclerView.RecycledViewPool()

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val v = LayoutInflater.from(parent.context).inflate(R.layout.city_recycler,parent,false)
        return ViewHolder(v)
    }

    override fun getItemCount(): Int  = citylist.size

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.cityName.text = citylist[position].name
        holder.recyclerView.apply {
            layoutManager = LinearLayoutManager(holder.recyclerView.context, LinearLayout.HORIZONTAL, false)
            adapter = CitizensAdapter(context, citylist[position].citizens)
            setRecycledViewPool(viewPool)
        }
    }


    inner class ViewHolder(itemView : View) : RecyclerView.ViewHolder(itemView){
        val cityName:TextView = itemView.city_item
        val recyclerView : RecyclerView = itemView.recycler_view_nested
    }
}

Na chwilę tutaj się zatrzymamy. Jak widać powyżej w onBindViewHolder, konfigurujemy widok recyklera potomnego. Przekazujemy menedżer układu, adapter oraz ustawiamy recycledViewPool dla każdego widoku recyklera potomnego na tę samą pulę widoków, która została utworzona wcześniej.

Pule widoku umożliwiają listom, współużytkowanie wspólnej puli widoków. Może to być przydatne, jeśli masz wiele widoków RecyclerView, które używają tych samych typów widoków. Musisz upewnić się, że ten sam ViewType jest używany w RecyclerView, który współdzieli wspólny ViewPool.

Podczas przewijania zagnieżdżonych list poziomych, przewijanie będzie płynne, ale przewijanie listy pionowej może spowolnić. Możemy skorygować tę sytuację, dzieląc jedną pulę ViewHolderów wśród wszystkich list.

Teraz, gdy wszystkie wewnętrzne widoki RecyclerView mają tę samą pulę widoków, zyskujemy lepszą wydajność przewijania.

RecycledViewPool nie ma kontekstu, ale przechowywane w nim widoki posiadają kontekst aktywności, fragmentu. Dlatego mogą pojawić się wycieki pamięci, jeśli RecycledViewPool będzie działać dłużej niż aktywność, fragment. Z wiązku z tym udostępnione widoki powinny znajdować się w tej samej aktywności.

Przewijanie można zoptymalizować jeszcze bardziej. Dla określonego typu widoku, możesz ustawić maksymalną liczbę ViewHolderów do przechowywania w puli:

myRC.recycledViewPool.setMaxRecycledViews(INT_VIEW_TYPE,INT_MAX_POOL_CAPACITY)

Po drugie, mamy możliwość określenia ile widoków należy przygotować przed pojawieniem się na ekranie:

layoutManager.initialPrefetchItemCount = 5

Dzięki temu wewnętrzny RecyclerView może tworzyć swój widok na wczesnym etapie, co poprawia wydajność podczas przewijania zewnętrznej listy.

Inną nowością w kodzie jest funkcja apply. Używaj tej funkcji dla bloków kodu, które nie zwracają wartości i działają głównie na elementach obiektu. Czyli apply posłuży do konfiguracji obiektu. Inaczej mówiąc, pozwala nam przypisać wartości na danym obiekcie.

Adapter dla listy z użytkownikami będzie wyglądał w następujący sposób:

class CitizensAdapter(private val context : Context,
                   private val user : List<Citizens>)

    : RecyclerView.Adapter<CitizensAdapter.ViewHolder>(){

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val v =  LayoutInflater.from(parent.context)
                      .inflate(R.layout.citizens_item_recyclerview,parent,false)
        return ViewHolder(v)
    }

    override fun getItemCount(): Int = user.size

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        Glide.with(context)
                .load(user[position].avatar)
                .into(holder.userAvatar);

        holder.userName.text = user[position].name
    }


    inner class ViewHolder(view : View) : RecyclerView.ViewHolder(view){

        val userName : TextView = view.user_name
        val userAvatar: ImageView = view.user_avatar

    }
}

Kod jest jasny i nie ma nic nowego czego byś nie wiedział z poprzednich rozdziałów. Kod w aktywności lub fragmencie może wyglądać tak:

myRC.apply {
            layoutManager = LinearLayoutManager(activity, LinearLayout.VERTICAL, false)
            adapter = CityAdapter(this.context ,list )
            addItemDecoration(DividerItemDecoration(context,DividerItemDecoration.VERTICAL))
        }

Listy utworzyłem w ten sposób:

fun generateCityWithUsers(sizeCity : Int ): ArrayList<City> {
        val cityList : ArrayList<City> = ArrayList()

        for (x in 0 until sizeCity) {

            // Generate User
            val random = (1..10).shuffled().first()

            val listUser : ArrayList<Citizens> = ArrayList()
            for (y in 0 until random) {
                val randomAvatar = (1..50).shuffled().first()
                listUser.add(User(
                            faker.name.name(),
                            "https://i.pravatar.cc/150?img="+randomAvatar
                        )
                    )
            }

            cityList.add(City(
                    faker.address.city(),
                    listUser)
            )
        }

        return cityList
    }

Mamy już za sobą duża dawkę wiedzy, ale to nie koniec. Zastanawiałeś się czy jest możliwość pracowania na elementach, które są niewidoczne na ekranie? Jeśli tak, to mam dla Ciebie dobrą wiadomość. Da się i właśnie w tej części to zrobimy.

Wyobraź sobie, że mamy listę książek. Jeżeli zainteresuje nas jakaś książka to po przytrzymaniu (longClick) na danej pozycji, dodajemy ją do listy, którą nazwiemy "przeczytaj później". Natomiast pozycje, które nas nie interesują będą automatycznie oznaczane, jako przeczytane.

Domyślnie będziemy mieć listę, która będzie przesuwać się w pionie. Książkę będziemy wtedy odznaczać jeśli zniknie z ekranu. Czyli jeśli mamy widocznych 10 elementów (0-10) i przesuniemy listę o 2 elementy to książka 0 będzie oznaczona jako przeczytana.

Nasz adapter będzie prezentował się w następujący sposób:

class DetectInvisibilityItemsAdapter(private val bookList: List<Book>) : RecyclerView.Adapter<DetectInvisibilityItemsAdapter.ViewHolder>(){

    lateinit var orgColors : ColorStateList

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val v = LayoutInflater.from(parent.context).inflate(R.layout.book_item,parent,false)
        orgColors = v.title.getTextColors() //save original colors
        return ViewHolder(v)
    }

    override fun getItemCount(): Int  = bookList.size

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.title.text = bookList[position].title
        if (bookList[position].readLeter)
            holder.title.setTextColor(Color.GREEN)
        else
            holder.title.setTextColor(orgColors)

        if (bookList[position].read)
            holder.title.setTextColor(Color.GRAY)

    }

    fun markAsRead(pos: Int) {
        if (!bookList[pos].read)
            bookList[pos].read = true
        notifyItemChanged(pos)
    }


    inner class ViewHolder(itemView : View) : RecyclerView.ViewHolder(itemView){
        val title :TextView = itemView.title

        init {
            title.setOnLongClickListener {
                bookList[layoutPosition].readLeter = bookList[layoutPosition].readLeter != true
                notifyItemChanged(adapterPosition)
                true
            }
        }
    }
}

Jest on oczywiście dość jasny, więc nie ma potrzeby go tłumaczenia. Cały magia się dzieje w poniższym kodzie, który znajduje się w aktywności lub fragmencie.

private fun initRecycler(list: List<Book>) {

    myRC.apply {
        addItemDecoration(DividerItemDecoration(context,DividerItemDecoration.VERTICAL))
    }

    val adapter = DetectInvisibilityItemsAdapter(list)
    myRC.adapter = adapter

    val layoutManager = LinearLayoutManager(activity, LinearLayout.VERTICAL, false)
    myRC.layoutManager = layoutManager
    myRC.addOnScrollListener(object : RecyclerView.OnScrollListener() {
        override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
            super.onScrolled(recyclerView, dx, dy)
            val last = layoutManager.findFirstVisibleItemPosition()

            if ( last >= 1) {
                myRC.post {
                    adapter.markAsRead(last-1)
                }
            }
        }
    })
}

Dodajemy słuchacza przy przewijaniu listy - myRC.addOnScrollListener. Niestety na ma metody, która by pobierała wiersz, który właśnie został ukryty. Dlatego korzystamy z findFirstVisibleItemPosition(). Jak sama nazwa wskazuje, pobiera pozycję pierwszego elementu, który jest widoczny.

Mamy także do dyspozycję jeszczę metodę findFirstCompletelyVisibleItemPosition. Ta metoda w naszym przypadku nie sprawdzi się, ponieważ jeżeli widok zostanie ukryty przynajmniej 1% to manager układu uznaje go za już niewidoczny element. Zaś z metodą pierwszą nie ma takiego problemu.

Gdy potrzebujesz skorzystać z informacji o ostatnim widocznym elemencie, skorzystaj z metody findLastVisibleItemPosition().

W dalszej części robimy warunek. To tak naprawdę taki bufor, aby pierwsza książka widoczna na ekranie nie była od razu odznaczana jako przeczytana pozycja. Oczywiście tą wartość możesz zwiększyć, ale nie polecam ze względów wydajnościowych. Chyba, że zwiększasz także, pamięć podręczną i pulę widoków.

Metoda post(), także jest dodana w celach optymalizacyjnych. Wysyłamy w osobnym wątku informacje do adaptera, że dana pozycja została zmodyfikowana. Gdybyśmy tego nie zrobili, to menedżer układu rysowałby na nowo widok przy minimalnym przesunięciu się listy. A tego nie chcemy.

Niestety funkcja onScrolled() jest bardzo gadatliwa, dlatego staraj się tam nie zamieszczać dużych i skomplikowanych partii kodu.

I to wszystko, odpal teraz i sprawdź jak to działa :)

Istnieje kilka sposobów, aby dodać dodatkowy zbiór danych do istniejącej listy.

Pierwszą możliwością jest użycie metody adaptera onBindViewHolder.

override fun onBindViewHolder(holder: ViewHolder, position: Int) {

    if (position == mData.length - 1) {
        // load more data here.
    }

    // ...
}

Może być konieczne dodanie dodatkowych zabezpieczeń do warunku, aby upewnić się, że nie uruchomisz ładowania danych więcej niż raz, ponieważ jest to możliwe.

Inna opcja wymaga edycji obiektu LayoutManagera. Pomysł polega na porównaniu czy ostatnia widoczna pozycja jest równa ostatniej pozycji zestawu danych. Jeśli tak to rozpocznij ładowanie nowych danych. To wywołanie powinno być wykonane w metodzie onLayoutChildren() lub scrollVerticallyBy() (zakładając, że używasz przewijania pionowego):

myRC.layoutManager = object : LinearLayoutManager(getContext()) {

    override fun scrollVerticallyBy(
        dy: Int,
        recycler: RecyclerView.Recycler?,
        state: RecyclerView.State?
    ): Int {
        if (findLastVisibleItemPosition() == mData.length - 1) {
            //loadMoreData()
        }
        return super.scrollVerticallyBy(dy, recycler, state)
    }
    // or
    override fun onLayoutChildren(recycler: RecyclerView.Recycler?, state: RecyclerView.State?) {
        super.onLayoutChildren(recycler, state)
        if (findLastVisibleItemPosition() == mData.length - 1) {
            loadMoreData()
        }
    }
}

Jeszcze innym sposobem, jest wykorzystanie do tego celu AsyncListUtil. Możesz także wykorzystać metodę onScrolled(), która została przedstawiona w poprzednim rozdziale.

Jeśli twój RecyclerView odbiera aktualizacje stopniowo, np. element jest wstawiony lub usuwany, możesz użyć SortedList do zarządzania listą.

Według mnie najlepszym pomysłem jest skorzystanie z biblioteki Paging, o której możesz przeczytać tutaj.

W tym wpisie na blogu przedstawiłem w jaki sposób tworzyć listy, w których można zwijać i rozwijać elementy. Kod był oparty na typach widoków. Poniżej przedstawię Ci inną technikę, dzięki której możesz osiągnąć ten sam efekt co na blogu..

Wszystko będzie się działo w adapterze, a dokładniej w klasie ViewHolder. Nasz adapter będzie wyglądał następująco:

class ExpandAdapter(private val list: List<Profile>
    ) : RecyclerView.Adapter<ExpandAdapter.ViewHolder>()  {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ExpandAdapter.ViewHolder {
        val viev = LayoutInflater
                .from(parent.context)
                .inflate(net.myenv.recyclerview.R.layout.expand_item, parent, false)

        return ViewHolder(viev)
    }

    override fun getItemCount(): Int = list.size

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val profile = list[position]
        holder.bind(profile)
    }

    inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), View.OnClickListener {
        var name = itemView.name
        var city = itemView.city
        var tel = itemView.tel
        var expandArea = itemView.expandArea

        init {
            name.setOnClickListener(this)
        }

        internal fun bind(profile: Profile) {

            name.text = profile.name

            if (profile.expanded) {
                expandArea.visibility = View.VISIBLE
                city.text = profile.city
                tel.text = profile.tel
            }
            else
                expandArea.visibility = View.GONE
        }

        override fun onClick(view: View?) {

            val expanded = list[adapterPosition].expanded
            list[adapterPosition].expanded = !expanded
            notifyItemChanged(adapterPosition)

        }
    }
}

W metodzie bind() klasy ViewHolder, wypełniamy cały wiersz danymi, w zależności od tego czy pole expanded jest ustawione na false lub true. Właściwość expanded informuje nas czy dana pozycja ma być rozwinięta.

Także zastosowałem inną technikę na kliknięcie elementu. Rozszerzamy klasę ViewHolder o View.OnClickListener. Nadpisujemy metodę onClick(). W niej ustawiamy pole expanded na true jeśli dany element jest zwinięty, w przeciwnym wypadku ustawiamy na false.

Natomiast plik XML z układem wiersza listy będzie wyglądał tak:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/llCardBack"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:animateLayoutChanges="true"
        android:padding="4dp"
        android:orientation="vertical">

        <TextView
                android:id="@+id/name"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_gravity="center|fill_horizontal"
                android:padding="10dp"
                android:gravity="center"
                android:textSize="30sp"
                />

        <LinearLayout
                android:id="@+id/expandArea"
                android:visibility="gone"
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                android:orientation="horizontal">

                <TextView
                        android:id="@+id/city"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_weight="1"
                        android:textSize="20sp"
                        android:drawableLeft="@drawable/ic_home"/>

                <TextView
                        android:id="@+id/tel"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_weight="1"
                        android:textSize="20sp"
                        android:drawableLeft="@drawable/ic_phone_forwarded"/>

        </LinearLayout>

</LinearLayout>

Efekt będzie następujący:

Gdy projekt zakłada, że tylko jeden wiersz może być rozwinięty, wtedy listener będzie wyglądał tak:

private var prev_expanded = -1

inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), View.OnClickListener {
   
    ... 
    
    override fun onClick(view: View?) {

         if (prev_expanded != -1 && prev_expanded != adapterPosition) {
            list[prev_expanded].expanded = false
            notifyItemChanged(prev_expanded)
        }
        prev_expanded = adapterPosition;

        val expanded = list[adapterPosition].expanded
        list[adapterPosition].expanded = !expanded
        notifyItemChanged(adapterPosition)

    }
}

Dokładamy zmienną prev_expanded, w której zapisujemy ostatnią pozycję rozwiniętą. Również doklejamy warunek. W warunku sprawdzamy, czy kliknięty element jest różny od wartości domyślnej (-1) oraz czy jest różny od poprzedniego elementu, który był wysunięty. Jeśli warunek jest prawdziwy to ustawiamy odpowiednie wartości na liście i informujemy o tym adapter.

Pamiętaj o tym, aby pole prev_expanded było poza klasą ViewHolder, w przeciwnym wypadku kod nie zadziała poprawnie. Dzieje się tak dlatego, poniewaz widok ląduje w pamięci podręcznej i warunek sprawdzający będzie fałszywy. Co za tym idzie, wiele elementów będziesz mógł rozwijać i zwijać zamiast jednego.

Pasek przewijania

Jeżeli chcesz dodać pasek przewijania w swojej liście, dodaj te atrybuty do widźetu RecyclerView:

<androidx.recyclerview.widget.RecyclerView
    ...
    android:scrollbars="vertical"
    android:scrollbarThumbVertical="@color/colorPrimary"
 />

Efekt kończący przewijanie

Gdy przewijasz listę i kończy się ona, na końcu ekranu lub na początku pojawia się poświata. Informuje, że nie da się dalej przewijać. Gdy chcesz wyłączyć lub zmienić kolor efektu, zrób to w ten sposób:

myRC.overScrollMode = View.OVER_SCROLL_NEVER // DISABLE
myRC.edgeEffectFactory = object : RecyclerView.EdgeEffectFactory() {
    override fun createEdgeEffect(view: RecyclerView, direction: Int): EdgeEffect {
        return EdgeEffect(view.context).apply {
            color = Color.RED
        }
    }
}

Asymetryczny układ GridLayoutManager

Przykład galerii asymetrycznej opartej na RecylerView i GridLayoutManager możesz znaleźć tutaj.

Oczywiście temat jest niewyczerpany. Dlatego tutaj prośba do Ciebie. Jeżeli brakuje Ci jakiegoś zagadnienia w kursie lub widzisz błąd daj mi znać o tym fakcie. Dzięki temu pomożemy innym bardziej zrozumieć bibliotekę RecyclerView :)

Prosze Cię również o informacje zwrotną jak Ci się podobał kurs. Czy forma podobała Ci się. Co byś dodał, a co zmienił.

Skontaktować się możesz ze mną pod tym adresem.

Nie zapomnij również podzielić się z informacją o tym kursie ze swoimi znajomymi :)