Android RecyclerView: co to jest, ucz się na prostych przykładach

W czym jest RecyclerView Android?

Kurs RecyklerWidok to widget, który jest bardziej elastyczną i zaawansowaną wersją GridView i ListView. Jest to kontener do wyświetlania dużych zbiorów danych, które można efektywnie przewijać przy zachowaniu ograniczonej liczby wyświetleń. Możesz użyć widgetu RecyclerView, jeśli masz zbiory danych, których elementy zmieniają się w czasie wykonywania w zależności od zdarzenia sieciowego lub działania użytkownika.

odwiedzajacy

Kurs Android platforma używa klas View i ViewGroup do rysowania elementów na ekranie. Klasy te są abstrakcyjne i można je rozszerzyć na różne implementacje w zależności od przypadku użycia. Na przykład TextView ma prosty cel wyświetlania treści tekstowej na ekranie. EditText rozszerza tę samą klasę View i dodaje więcej funkcjonalności, aby umożliwić użytkownikowi wprowadzanie danych.

Możliwe jest tworzenie własnych, niestandardowych widoków, aby móc osiągnąć większą elastyczność podczas opracowywania interfejsów użytkownika. Klasa View udostępnia metody, które możemy zastąpić, aby rysować na ekranie, a także umożliwia przekazywanie parametrów, takich jak szerokość, wysokość i nasze własne atrybuty niestandardowe, które chcielibyśmy dodać do naszego widoku, aby zachowywał się zgodnie z oczekiwaniami.

Wyświetl grupy

Klasa ViewGroup jest rodzajem widoku, ale w przeciwieństwie do prostej klasy View, której zadaniem jest po prostu wyświetlanie, ViewGroup daje nam możliwość umieszczenia wielu widoków w jednym widoku, do którego możemy się odwoływać jako całość. W tym przypadku widok utworzony na najwyższym poziomie, do którego dodajemy inne proste widoki (możemy również dodać grupy widoków), nazywany jest „rodzicem”, a widoki dodawane wewnątrz to „dzieci”.

Możemy zobrazować widok jako tablicę i grupę widoków jako tablicę tablic. Biorąc pod uwagę, że tablica tablic sama w sobie jest tablicą, możemy zobaczyć, jak grupę widoków można traktować jak widok.

var arr1 = [1,2,3] //imagine a simple View as an Array
//we can imagine this as a NumberTextView which doesn't really exist
//but we could imagine there's one that makes it easy to use numbers
var arr2 = ["a","b","c"] // We can imagine this as another simple view

var nestedArr = [arr1,arr2] //in our anology, we can now group views 
//together and the structure that would hold that would be what we call the ViewGroup

ViewGroup pozwala nam również zdefiniować, jak dzieci są zorganizowane wewnątrz widoku, na przykład, czy są ułożone pionowo lub poziomo. Możemy mieć różne reguły interakcji wewnątrz widoku. Na przykład, TextViews następujące po sobie powinny mieć odległość 12dp, podczas gdy ImageViews, po których następuje TextView, powinny mieć odległość 5dp.

Miałoby to miejsce, gdybyśmy od podstaw opracowywali własną grupę ViewGroup. Aby ułatwić te konfiguracje, Android udostępnia klasę o nazwie LayoutParams, której możemy użyć do wprowadzenia tych konfiguracji.

Android dokumentacja zapewnia pewne domyślne parametry, które zaimplementowalibyśmy podczas konfigurowania naszego własnego ViewGroup. Niektóre typowe parametry dotyczą szerokości, wysokości i marginesu. Domyślnie te konfiguracje mają strukturę android:layout_height dla wysokości, na przykład android:layout_width W związku z tym, gdy tworzysz swój ViewGroup, możesz dalej tworzyć LayoutParams specyficzne dla sposobu, w jaki chcesz, aby zachowywał się Twój ViewGroup.

Android zawiera domyślne widoki i grupy widoków, których możemy używać do wykonywania wielu typowych zadań, których potrzebujemy. Jednym z przykładów, o którym wspominaliśmy, jest TextView. Jest to prosty widok z konfigurowalnymi aspektami, takimi jak wysokość, szerokość, rozmiar tekstu i tym podobne. Mamy ImageView do wyświetlania obrazów i EditText, jak wspomnieliśmy, między innymi. Android ma również niestandardowe grupy widoków, do których możemy dodać nasze widoki i uzyskać oczekiwane zachowanie.

Układ liniowy

LinearLayout pozwala nam dodawać do niego elementy widoku. LinearLayout to atrybut orientacji, który dyktuje, jak będzie on rozłożony na ekranie. Ma również LinearLayout.LayoutParams, które dyktują reguły dla widoków wewnątrz, na przykład atrybut android:center_horizontal centrowałby widoki wzdłuż osi poziomej, podczas gdy `android:center_vertical centrowałby zawartość widoku wzdłuż osi pionowej.

Oto kilka obrazów ułatwiających zrozumienie centrowania. Uznalibyśmy to za prosty widok tekstowy w przestrzeni o wymiarach 200 na 200 pikseli, a atrybuty centrujące sprawiłyby, że zachowywałby się w następujący sposób.

android:środek_poziomy

Treść wyśrodkowana poziomo
Treść wyśrodkowana poziomo

android:środek_pionowy

Treść wyśrodkowana pionowo
treść wyśrodkowana pionowo

android:środek

Wyśrodkowana treść
Wyśrodkowana treść

Podstawowe składniki RecyclerView

Podstawowe składniki RecyclerView
Podstawowe składniki RecyclerView

Poniżej przedstawiono najważniejsze komponenty RecyclerView:

RecyclerView.Adapter

Prace przygotowawcze nr 1 – Wzór adaptera

Adapter to urządzenie, które przekształca atrybuty systemu lub urządzenia na atrybuty w inny sposób niekompatybilnego urządzenia lub systemu. Niektóre z nich modyfikują atrybuty sygnału lub zasilania, podczas gdy inne po prostu dostosowują fizyczną formę jednego złącza do drugiego.

Prostym przykładem wyjaśnienia adaptera jest sytuacja, w której musimy połączyć ze sobą urządzenia, ale mają one porty połączeniowe, które nie pasują do siebie. Może się tak zdarzyć, gdy odwiedzasz inny kraj, w którym używane są różne rodzaje gniazdek. Jeśli nosisz przy sobie ładowarkę do telefonu lub laptopa, nie będzie możliwości podłączenia jej do gniazdka elektrycznego. Jednak nie poddajesz się i po prostu kupujesz adapter, który można umieścić pomiędzy gniazdkiem elektrycznym a ładowarką i umożliwić ładowanie.

Dzieje się tak w programowaniu, gdy chcemy połączyć ze sobą dwie struktury danych, aby wykonać zadanie, ale ich domyślne porty nie mają możliwości komunikowania się ze sobą.

Posłużymy się prostym przykładem urządzenia i ładowarki. Będziemy mieli dwa przypadki ładowarek. Amerykański i brytyjski

class AmericanCharger() {
    var chargingPower = 10
}
class BritishCharger(){
    var charginPower = 5
}

Następnie utworzymy dwa urządzenia

class AmericanDevice() 
class BritishDevice()

Możemy na przykład utworzyć kilka instancji urządzeń do wspólnej zabawy.

var myAmericanPhone = new AmericanDevice()
var myBritishPhone = new BritishDevice()

Następnie przedstawimy koncepcję ładowania obu urządzeń, dodając w urządzeniach metodę o nazwie Charge() .

Metoda wykorzystuje jako dane wejściowe odpowiednią ładowarkę i na jej podstawie wykonuje ładowanie.

sealed trait Device
class AmericanDevice : Device{
    fun charge(charger:AmericanCharger){
        //Do some American charging
    }
}
class BritishDevice: Device{
    fun charge(charger:BritishCharger){
        //Do some British charging
    }
}

W tym przypadku, bazując na naszej analogii, z tego czy innego powodu potrzebowalibyśmy użycia brytyjskiej ładowarki podczas korzystania z urządzenia amerykańskiego i odwrotnie.

W świecie programowania ma to zazwyczaj miejsce podczas mieszania bibliotek oferujących tę samą funkcjonalność (w naszym kontekście nasza wspólna funkcjonalność jest płatna). Musielibyśmy znaleźć sposób, aby to umożliwić.

Jeśli podążymy za analogią, będziemy musieli pójść do sklepu z elektroniką i kupić adapter, który umożliwi nam ładowanie AmericanDevices za pomocą BritishChargers. Z perspektywy programowania, to my będziemy producentem adaptera.

Zrobimy adapter dla jednego, który będzie odpowiadał dokładnie wzorowi potrzebnemu do stworzenia drugiego. Zaimplementujemy to jako klasę w następujący sposób. Nie musi to koniecznie być klasa. Może to być funkcja podkreślająca ogólne działanie wzorca adaptera. Użyjemy klasy, która pasuje do większości zastosowań Android.

class AmericanToBritishChargerAdapter(theAmericanCharger:AmericanCharger){
    fun returnNewCharger(): BritishCharger{
            //convert the American charger to a BritishCharger
            //we would change the American charging functionality
            //to British charging functionality to make sure the
            //adapter doesn't destroy the device. The adapter could 
            //, for example, control the power output by dividing by 2
            //our adapter could encompass this functionality in here
           
           var charingPower:Int = charger.chargingPower / 2
           var newBritishCharger = new BritishCharger()
           newBritishCharger.chargingPower = theAmericanCharger.chargingPower/2
        
        return newBritishCharger
    }
}

W świecie programowania różnica w gniazdach jest analogiczna do różnicy w metodach ładowania stosowanych wewnątrz. Ładowarki posiadające różne metody uniemożliwiałyby korzystanie z ładowarek.

 var myBritishDevice = new BritishDevice()
var americanChargerIFound = new AmericanCharger()

Próba wywołania metody ładowania() w myBritishDevice za pomocą americanChargerIFound nie zadziała, ponieważ AmericanDevice akceptuje tylko AmericanCharger

Więc nie da się tego zrobić

 var myBritishDevice = new BritishDevice()
var americanChargerIFound = new AmericanCharger()
myBritishDevice.charge(americanChargerIFound)

W tym scenariuszu stworzony przez nas adapter

AmericanToBritishChargerAdapter może się teraz przydać. Za pomocą metody returnNewCharger() możemy utworzyć nową ładowarkę BritishCharger, którą będziemy mogli ładować. Wszystko, czego potrzebujemy, to utworzyć instancję naszego adaptera i zasilić ją posiadaną ładowarką AmericanCharger, a utworzy się brytyjska ładowarka, której możemy użyć

var myBritishDevice = new BritishDevice()
var americanChargerIFound = new AmericanCharger()
//We create the adapter and feed it the americanCharger
var myAdapter =  AmericanToBritishChargerAdapter(theAmericanCharger)
//calling returnNewCharger from myAdapter would return a BritishCharger
var britishChargerFromAdapter = myAdapter.returnNewCharger()
//and once we have the britishCharger we can now use it
myBritishDevice.charge(britishChargerFromAdapter)

RecyclerView.LayoutManager

W przypadku grupy widoków umieścilibyśmy w niej widoki. To LayoutManager miałby za zadanie opisać, w jaki sposób widoki są ułożone wewnątrz.

Dla celów porównawczych, podczas pracy z Linearlayout ViewGroup, pożądanym przypadkiem użycia jest możliwość umieszczenia elementów w pionie lub w poziomie. Można to łatwo zaimplementować, dodając atrybut orientacji, który mówi nam, w jaki sposób układ liniowy zostanie umieszczony na ekranie. Możemy to zrobić za pomocą android:orientation=VERTICAL|HORIZONTAL atrybutów.

Mamy także inną grupę widoków o nazwie GridLayout. Jej przypadek użycia ma miejsce wtedy, gdy chcielibyśmy umieścić widoki w prostokątnej strukturze siatki. Może to wynikać z takich powodów, jak ułatwienie wykorzystania danych, które prezentujemy użytkownikowi aplikacji. Z założenia GridLayout umożliwia konfiguracje pomagające w osiągnięciu tego celu poprzez konfiguracje, w których możemy zdefiniować wymiary siatki, na przykład możemy mieć siatkę 4 × 4, siatkę 3 x 2.

RecyclerView.ViewHolder

ViewHolder to klasa abstrakcyjna, którą również rozszerzamy z RecyclerView. ViewHolder zapewnia nam typowe metody, które pomagają nam odwołać się do widoku, który umieściliśmy w RecyclerView, nawet po tym, jak maszyny do recyklingu w RecyclerView zmieniły różne odniesienia, o których nie wiemy.

Duże listy

RecyclerViews stosujemy wtedy, gdy chcemy zaprezentować użytkownikowi naprawdę duży zestaw Widoków, a jednocześnie nie wyczerpuje to naszych RAM na naszym urządzeniu dla każdego wystąpienia utworzonego widoku.

Gdybyśmy wzięli przypadek Contact-List, mielibyśmy ogólne pojęcie, jak jeden kontakt wyglądałby na liście. Następnie utworzylibyśmy układ szablonu – który jest w rzeczywistości widokiem – z polami, w których będą wypełniane różne dane z naszej listy kontaktów. Poniżej znajduje się pseudokod, który wyjaśnia cały cel:

//OneContactView
<OneContact>
<TextView>{{PlaceHolderForName}}</TextView>
<TextView>{{PlaceHolderForAddress}}</TextView>
<ImageView>{{PlaceHolderForProfilePicture}}</ImageView>
<TextView>{{PlaceHolderForPhoneNumber}}</TextView>
</OneContact>

Mielibyśmy wtedy listę kontaktów tego rodzaju

 <ContactList>
</ContactList>

Gdyby tak było, zakodowaliśmy zawartość na stałe i nie mielibyśmy programowego sposobu dodawania nowej zawartości do listy bez przepisywania aplikacji. Na szczęście dla nas. Dodawanie widoku do grupy widoków jest obsługiwane przez addView(view:View) Metoda.

Nawet jeśli tak jest, nie jest to sposób, w jaki RecyclerView dodaje do niego widoki dzieci.

W naszym przypadku mielibyśmy długą listę kontaktów. Dla każdego kontaktu na liście musielibyśmy utworzyć OneContactView i wypełnić dane w widoku, aby dopasować je do pól w naszej klasie Contact. Następnie, gdy już będziemy mieć widok, będziemy musieli dodać go do RecyclerView, aby wyświetlić listę.

data  class Contact(var name:String, var address:String, var pic:String, var phoneNumber:Int)

var contact1 = Contact("Guru","Guru97", "SomePic1.jpg", 991)
var contact2 = Contact("Guru","Guru98", "SomePic2.jpg", 992)
var contact3 = Contact("Guru","Guru99", "SomePic3.jpg", 993)

var myContacts:ArrayList<Contact> = arrayListOf<Contact>(contact1,contact2,contact3)

Mamy szereg kontaktów o nazwie OneContactView. Zawiera miejsca do pobierania treści z klasy Contact i wyświetlania ich. W RecyclerView musimy dodać do niego widoki, aby pomóc nam w możliwości recyklingu.

RecyclerView tak naprawdę nie pozwala nam dodawać widoku, ale umożliwia nam dodanie ViewHolder. Zatem w tym scenariuszu mamy dwie rzeczy, które chcemy połączyć, ale do siebie nie pasują. W tym miejscu z pomocą przychodzi nasz Adapter. RecyclerView zapewnia nam Adapter podobny do naszego AmericanToBritishChargerAdapter() z wcześniejszych rozwiązań, co umożliwiło nam przekształcenie naszej amerykańskiej ładowarki, która nie nadawała się do użytku z naszym brytyjskim urządzeniem, w coś użytecznego, podobnego do zasilacza w prawdziwym życiu.

W tym scenariuszu adapter pobierze naszą tablicę kontaktów i nasz widok, a następnie wygeneruje ViewHoldery, które RecyclerView jest skłonny zaakceptować.

RecyclerView zapewnia interfejs, który możemy rozszerzyć, aby utworzyć nasz adapter za pomocą klasy RecyclerView.Adapter. Wewnątrz tego adaptera znajduje się sposób na utworzenie klasy ViewHolder, z którą RecyclerView chce pracować. Mamy więc tę samą sytuację, co poprzednio, ale z jedną dodatkową rzeczą, czyli adapterem.

Mamy tablicę kontaktów, widok umożliwiający wyświetlenie jednego kontaktu OneContactView. RecyclerView to lista widoków, które świadczą usługi recyklingu, ale chcą przyjmować tylko ViewHolders

Jednak w tym scenariuszu mamy teraz klasę RecyclerView.Adapter, która zawiera metodę tworzenia elementów ViewHolders.

fun createViewHolder(@NonNull parent: ViewGroup, viewType: Int): ViewHolder

RecyclerView.ViewHolder to klasa abstrakcyjna, która przyjmuje nasz widok jako argument i konwertuje go na ViewHolder.

Używa wzorca opakowania, który jest używany do rozszerzania możliwości klas.

Prace przygotowawcze #2 – Wzór owijania

Użyjemy prostego przykładu, aby zademonstrować, w jaki sposób możemy zmusić Zwierzęta do mówienia.

sealed trait Animal{
    fun sound():String
}

data class Cat(name:String):Animal{
    fun sound(){
        "Meow"
        }
}
data class Dog(name:String):Animal{
    fun sound(){
        "Woof"
        }
}

var cat1 = Cat("Tubby")
var dog1 = Dog("Scooby")
cat1.sound() //meow
dog1.sound() //woof

W powyższym przykładzie mamy dwa zwierzęta. Jeśli przez przypadek chcielibyśmy dodać metodę, aby przemówić, ale autor biblioteki nie był zabawny, wciąż moglibyśmy znaleźć sposób. Potrzebujemy opakowania dla naszej klasy Animal. Zrobilibyśmy to, przyjmując Animal jako konstruktora naszej klasy

 class SpeechPoweredAnimalByWrapper(var myAnimal:Animal){

    fun sound(){
        myAnimal.sound()
    }

    speak(){
        println("Hello, my name is ${myAnimal.name}")
    }
}

Teraz możemy przekazać instancję zwierzęcia do SpeechPoweredAnimalByWrapper. Wywołanie na nim metody sound() wywołałoby metodę przekazaną w zwierzęcym dźwięku(). Mamy też dodatkową metodę mówienia(), która liczy się jako nowa funkcjonalność, którą dodajemy do przekazywanych zwierząt. Możemy z niej skorzystać w następujący sposób:

var cat1 =  Cat("Garfield")
cat1.sound()//"meow"
cat1.speak()// doesn't work as it isn't implemented
var talkingCat = new SpeechPoweredAnimalByWrapper(cat1)
talkingCat.sound() //"meow" the sound method calls the one defined for cat1
talkingCat.speak() //"Hello, my name is Garfield"

Korzystając z tego wzorca, możemy brać udział w zajęciach i dodawać funkcjonalności. Wszystko, czego potrzebowalibyśmy, to przekazać instancję klasy i nowe metody zdefiniowane przez naszą klasę opakowującą.

W naszym przypadku powyżej użyliśmy klasy konkretnej. To samo można również zaimplementować w klasie abstrakcyjnej. Musielibyśmy dodać zmianę klasy SpeechPoweredAnimalByWrapper na abstrakcyjną i gotowe. Zmienimy nazwę klasy na coś krótszego, aby była bardziej czytelna.

abstract class SpeechPowered(var myAnimal:Animal){

    fun sound(){
        myAnimal.sound()
    }

    speak(){
        println("Hello, my name is ${myAnimal.name}")
    }
}

Jest tak samo jak wcześniej, ale oznaczałoby to coś innego. W normalnej klasie możemy mieć instancję klasy w taki sam sposób, w jaki stworzyliśmy cat1 i dog1. Klasy abstrakcyjne nie są jednak przeznaczone do tworzenia instancji, ale mają na celu rozszerzanie innych klas. Jak więc użylibyśmy nowej klasy abstrakcyjnej SpeechPowered(var myAnimal:Animal). Możemy go wykorzystać tworząc nowe klasy, które będą go rozszerzać i zyskiwać na funkcjonalności.

W naszym przykładzie utworzymy klasę SpeechPoweredAnimal, która rozszerza tę klasę

class SpeechPoweredAnimal(var myAnimal:Animal):SpeechPowered(myAnimal)
var cat1 =  Cat("Tubby")
var speakingKitty = SpeechPoweredAnimal(cat1)
speakingKitty.speak() //"Hello, my name is Tubby"

Jest to ten sam wzór, który jest używany w ViewHolder. Klasa RecyclerView.ViewHolder jest klasą abstrakcyjną, która dodaje funkcjonalność do widoku, podobnie jak dodaliśmy metodę mówienia do zwierząt. Dodatkowa funkcjonalność sprawia, że ​​działa ona w przypadku RecyclerView.

W ten sposób utworzymy OneContactViewHolder z OneContactView

//The View argument we pass is converted to a ViewHolder which uses the View to give it more abilities and in turn work with the RecyclerView
class OneContactViewHolder(ourContactView: View) : RecyclerView.ViewHolder(ourContactView)

RecyclerView ma adapter, który pozwala nam połączyć naszą tablicę Contacts z ContactsView za pomocą RecyclerView

Dodawanie widoku

ViewGroup nie odświeża ViewGroup automatycznie, ale podąża za określonym harmonogramem. Może się zdarzyć, że na Twoim urządzeniu odświeża się co 10 ms lub 100 ms, lub jeśli wybierzemy absurdalną liczbę, powiedzmy 1 minutę, gdy dodamy View do ViewGroup, zobaczysz zmiany 1 minutę później, gdy ViewGroup się „odświeży”.

RecyclerView.Recycler

Prace przygotowawcze nr 3. Buforowanie

Jednym z najlepszych przykładów regularnego odświeżania jest przeglądarka. Załóżmy na przykład, że witryna, którą odwiedzamy, jest statyczna i nie wysyła treści dynamicznie. Aby zobaczyć zmiany, musielibyśmy ją odświeżać.

W tym przykładzie wyobraźmy sobie, że witryną, o której mowa, jest Twitter. Na liście byłaby seria statycznych tweetów, a jedynym sposobem, aby zobaczyć nowe tweety, byłoby kliknięcie przycisku odświeżania w celu ponownego pobrania treści.

Przemalowanie całego ekranu jest oczywiście kosztowną rzeczą. Jeśli mielibyśmy sobie wyobrazić, że tak jest, u naszego operatora telefonicznego mieliśmy ograniczoną przepustowość. A nasza lista tweetów zawierała wiele zdjęć i filmów, ponowne pobieranie całej zawartości strony przy każdym odświeżeniu byłoby kosztowne.

Potrzebowalibyśmy sposobu na przechowywanie już załadowanych tweetów i upewnienia się, że nasze następne żądanie będzie miało możliwość wypowiedzenia tweetów, które już posiada. Dlatego nie pobiera wszystkiego ponownie i pobiera tylko nowe tweety, które posiada, a także sprawdza, czy jakiś tweet, który został zapisany lokalnie, już nie istnieje, aby mógł go usunąć lokalnie. To, co opisujemy, nazywa się buforowaniem.

Informacje, które wysyłamy do witryny internetowej na temat znajdujących się w niej treści, nazywane są metadanymi. Tak więc w prawdziwym sensie nie mówimy tylko „chcemy załadować Twoją witrynę”, mówimy „chcemy załadować Twoją witrynę, a oto część treści, którą już zapisaliśmy podczas ostatniego ładowania. Użyj jej, aby Wysyłaj nam tylko to, czego tam nie ma, abyśmy nie zużywali dużej przepustowości, ponieważ nie mamy zbyt wielu zasobów.

Połączenia dotyczące układu – lista tweetów musi być szalona

Przykładem wywołania układu jest scrollToPosition

Jest to typowy przykład występujący w aplikacjach do czatowania. Jeśli ktoś w wątku czatu odpowie na wcześniejszy dymek czatu, niektóre aplikacje do czatowania dołączają odpowiedź i link do dymka czatu, który po kliknięciu przenosi Cię do miejsca, w którym znajdowała się Twoja pierwotna wiadomość.

W tym przypadku wywołujemy tę metodę, zanim dodamy LayoutManager do naszego RecyclerView i zanim będziemy mieć RecyclerView.Adapter, scrollToPosition(n:Int) jest po prostu ignorowane.

Komunikacja pomiędzy komponentami RecyclerView

Prace ziemne #4. Oddzwonienia

RecyclerView podczas wykonywania swojej pracy ma wiele ruchomych części. Musi sobie poradzić z LayoutManagerem, który mówi nam, jak organizować widoki, liniowo lub w siatce. Musi sobie poradzić z adapterem, który konwertuje nasze elementy contactList na Views OneContactView, a następnie na ViewHolders OneContactViewHolder, z którym RecyclerView chce pracować w ramach metod, które nam udostępnia.

Surowcem dla RecyclerView są nasze widoki, np. OneContactView i źródło danych.

contactList:Array<Contact>

Jako punkt wyjścia wykorzystaliśmy prosty scenariusz, aby zorientować się, co próbuje osiągnąć RecyclerView.

Podstawowy przypadek, w którym mielibyśmy statyczną tablicę 1000 kontaktów, które chcemy pokazać użytkownikowi, jest łatwy do zrozumienia.

Maszyny RecyclerView naprawdę zaczynają żyć, gdy lista nie jest już statyczna.

W przypadku listy dynamicznej musimy pomyśleć o tym, co stanie się z widokiem na ekranie, gdy dodamy element do listy lub usuniemy element z listy.

RecyclerView.LayoutManager

Oprócz decydowania, jak nasze widoki mają być rozłożone liniowo lub w siatce, LayoutManager wykonuje wiele pracy pod maską, pomagając Recyclerowi wiedzieć, kiedy wykonać Recycling.

Odpowiada za śledzenie widoków aktualnie widocznych na ekranie i przekazywanie tych informacji do mechanizmu recyklingu. Gdy użytkownik przewija w dół, menedżer układu jest odpowiedzialny za poinformowanie systemu recyklingu o widokach, które wychodzą z widoku u góry, aby można je było ponownie wykorzystać, zamiast pozostać tam i zużywać pamięć lub zamiast je niszczyć i tworzyć nowe.

Oznacza to, że LayoutManager musi śledzić, gdzie znajduje się użytkownik podczas przewijania naszej listy. Robi to poprzez posiadanie listy pozycji będących bazą indeksu, tj. pierwszy element zaczyna się od 0 i zwiększa się w celu dopasowania liczby pozycji na naszej liście.

Jeśli możemy wyświetlić 10 pozycji na naszej liście, powiedzmy 100, na początku LayoutManager jest świadomy, że ma aktywny widok-0 aż do Widok-9. Podczas przewijania LayoutManager jest w stanie obliczyć wyświetlane widoki skupienia.

LayoutManager może zwolnić te widoki do mechanizmu Recycling, aby można je było ponownie wykorzystać (można z nimi powiązać nowe dane, np. dane kontaktowe widoku można usunąć, a nowe dane kontaktowe z następnego segmentu mogą zastąpić elementy zastępcze).

Byłby to szczęśliwy przypadek, gdyby lista, którą mamy, była statyczna, ale jednym z najczęstszych przypadków użycia RecyclerView są listy dynamiczne, w przypadku których dane mogą pochodzić z punktu końcowego online, a nawet z czujnika. Nie tylko dane są dodawane, ale dane na naszej Liście są czasami usuwane lub aktualizowane.

Dynamiczny stan naszych danych może bardzo utrudniać ocenę LayoutManager. Z tego powodu LayoutManager utrzymuje własną listę dotyczącą elementów i pozycji, która jest odrębna od listy używanej przez komponent Recycling. To gwarantuje, że poprawnie wykona swoje zadanie związane z układem.

Jednocześnie LayoutManager RecyclerView nie chce błędnie przedstawiać danych, które posiada. Aby działać poprawnie, LayoutManager synchronizuje się z RecyclerView.Adapter w określonych odstępach czasu (60 ms), udostępniając informacje o elementach naszej listy. tj. elementy dodane, zaktualizowane, usunięte, przeniesione z jednej pozycji do drugiej). LayoutManager, po otrzymaniu tych informacji, reorganizuje zawartość na ekranie, aby dopasować ją do zmian, gdy jest to konieczne.

Wiele podstawowych operacji związanych z RecylerView opiera się na komunikacji między RecyclerView.LayoutManager i RecyclerView.Adapter, w których przechowywane są nasze listy danych, czasami statycznych, a czasami dynamicznych.

Co więcej, RecyclerView udostępnia nam metody, których możemy użyć do nasłuchiwania zdarzeń, takich jak onBindViewHolder, gdy nasz RecyclerView.Adapter wiąże zawartość z naszej listy, np. Kontakt z ViewHolder, dzięki czemu teraz przyzwyczai się do wyświetlania informacji na ekranie.

Innym jest onCreateViewHolder, który informuje nas, kiedy RecyclerView. Adapter pobiera zwykły widok, taki jak OneContactView, i konwertuje go na element ViewHolder, z którym RecyclerView może pracować. Z naszego ViewHolder do wykorzystania przez RecyclerView. onViewOdłączony

Oprócz podstawowych mechanizmów umożliwiających recykling. RecyclerView zapewnia sposoby dostosowywania zachowania bez wpływu na recykling.

Ponowne wykorzystanie widoków utrudnia wykonywanie typowych czynności, do których jesteśmy przyzwyczajeni w przypadku widoków statycznych, takich jak reagowanie na zdarzenia onClick.

Jak mamy świadomość, że RecyclerView.LayoutManager który prezentuje widoki użytkownikowi, może przez chwilę mieć inną listę elementów niż RecyclerView.Adapter który zawiera listę, którą przechowujemy w bazie danych lub przesyłamy strumieniowo ze źródła. Umieszczenie zdarzeń OnClick bezpośrednio w Widokach może prowadzić do nieoczekiwanych zachowań, takich jak usunięcie niewłaściwego kontaktu lub zmiana.

Gradle

Jeśli chcemy użyć RecyclerView, musimy dodać go jako zależność w naszym pliku .gradle kompilacji.

W poniższym przykładzie użyliśmy implementacji „androidx.recyclerview:recyclerview:1.1.0”, która jest najnowszą wersją zgodnie z tym artykułem.

Po dodaniu zależności do naszego Gradle plik, zostaniemy poproszeni o Android Studio do Syncchronizuj zmiany,

Tak wygląda nasz Gradle plik będzie wyglądał tak, jak po dodaniu RecyclerView w pustym projekcie z ustawieniami domyślnymi.

apply plugin: 'com.android.application'

apply plugin: 'kotlin-android'

apply plugin: 'kotlin-android-extensions'

android {
    compileSdkVersion 29
    buildToolsVersion "29.0.2"
    defaultConfig {
        applicationId "com.guru99.learnrecycler"
        minSdkVersion 17
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),
             'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.core:core-ktx:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'

    implementation "androidx.recyclerview:recyclerview:1.1.0"
}

W tej chwili mamy tylko jeden plik układu. Zaczniemy od prostego przykładu, w którym użyjemy RecyclerView do wyświetlenia listy nazw owoców na ekranie.

Lista rzeczy

Przejdziemy do naszego pliku MainActivity i utworzymy tablicę zawierającą nazwy owoców tuż przed metodą onCreate() wygenerowaną podczas konfiguracji.

package com.guru99.learnrecycler

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle

class MainActivity : AppCompatActivity() {

    var fruitNames:Array<String> = arrayOf<String>("Banana", "Mango", "Passion fruit", "Orange", "Grape")

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}

Naszym następnym celem będzie zaprezentowanie tej listy na ekranie za pomocą RecyclerView.

Aby to zrobić, przejdziemy do katalogu układów, w którym znajdują się nasze układy, i utworzymy widok, który będzie odpowiedzialny za wyświetlanie jednego owocu.

Układ, który ma być używany dla każdego elementu na naszej liście

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/fruitName"
    />
</TextView>

W powyższym TextView dodaliśmy pole id, które będzie używane do identyfikacji widoku.

Nie jest generowany domyślnie. Mamy w TextView identyfikator FruitName pasujący do danych, które zostaną z nim powiązane.

Dodanie RecyclerView do głównego układu

W tym samym działaniu znajduje się domyślnie wygenerowany dla nas plik układu main_layout.xml.

Jeśli wybierzemy pusty projekt. Wygeneruje XML zawierający ConstraintLayout, a wewnątrz będzie TextView z tekstem „Hello”.

Usuniemy całą zawartość, a układ będzie zawierał tylko widok RecyclerView, jak poniżej:

<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
     android:id="@+id/fruitRecyclerView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity" />

Dodaliśmy także atrybut id dla RecyclerView, którego będziemy używać do odwoływania się do niego w naszym kodzie.

 android:id="@+id/fruitRecyclerView"

Następnie powrócimy do naszego pliku MainActivity. Korzystając z utworzonych przez nas identyfikatorów, będziemy mogli odwoływać się do właśnie utworzonych widoków.

Zaczniemy od odniesienia się do RecyclerView za pomocą metody findViewById() dostarczonej przez Android. Zrobimy to w naszej metodzie onCreate().

Nasza metoda onCreate() będzie wyglądać następująco.

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        var myFruitRecyclerView: RecyclerView = findViewById(R.id.fruitRecyclerView)
    }

Utwórz ViewHolder

Następnie utworzymy obiekt RecyclerView.ViewHolder, który będzie odpowiedzialny za pobranie naszego widoku i przekonwertowanie go na ViewHolder, którego RecyclerView używa do wyświetlania naszych elementów.

Zrobimy to zaraz po naszej zabawnej metodzie onCreate().

package com.guru99.learnrecycler

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import androidx.recyclerview.widget.RecyclerView

class MainActivity : AppCompatActivity() {

    var fruitNames:Array<String> = arrayOf<String>("Banana", "Mango", "Passion fruit", "Orange", "Grape")

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        var myFruitRecyclerView: RecyclerView = findViewById(R.id.fruitRecyclerView)
    }
    
    class FruitViewHolder(fruitView: View): RecyclerView.ViewHolder(fruitView)
    
}

Utwórz element RecyclerViewAdapter

Następnie utworzymy klasę FruitArrayAdapter, która stanowi rozszerzenie klasy RecyclerView.Adapter.

Utworzony przez nas obiekt FruitArrayAdapter będzie odpowiedzialny za wykonanie następujących zadań.

Przyjmie nazwy owoców z tablicy Fruit. Utworzy ViewHolder przy użyciu naszego widoku one_fruit_view.xml. Następnie powiąże owoc z ViewHolder i dynamicznie powiąże zawartość z utworzonym przez nas widokiem one_fruit_view.xml.

package com.guru99.learnrecycler

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import androidx.recyclerview.widget.RecyclerView

class MainActivity : AppCompatActivity() {

    var fruitNames:Array<String> = arrayOf<String>("Banana", "Mango", "Passion fruit", "Orange", "Grape")

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        var myFruitRecyclerView: RecyclerView = findViewById(R.id.fruitRecyclerView)
    }
    
    class FruitViewHolder(fruitView: View): RecyclerView.ViewHolder(fruitView)
    
    class FruitArrayAdapter(var fruitArray: Array<String>) : RecyclerView.Adapter<FruitViewHolder>()
}

Android Studio doda czerwone zygzaki do naszego FruitArrayAdapter, informując nas, że musimy zaimplementować metodę, której może użyć RecyclerView do połączenia naszej tablicy z ViewHolderem, którego może użyć RecyclerView.

   class FruitListAdapter(var fruitArray: Array<String>) : RecyclerView.Adapter<FruitViewHolder>() {
        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FruitViewHolder {
            
        }

        override fun getItemCount(): Int {
         
        }

        override fun onBindViewHolder(holder: FruitViewHolder, position: Int) {
          
        }
    }

Zaczniemy od najłatwiejszego fragmentu wygenerowanego kodu, czyli metody getItemCount(). Wiemy, jak uzyskać liczbę elementów w naszej tablicy, wywołując metodę array.length.

class FruitListAdapter(var fruitArray: Array<String>) : RecyclerView.Adapter<FruitViewHolder>() {
        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FruitViewHolder {
            
        }

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

        override fun onBindViewHolder(holder: FruitViewHolder, position: Int) {
          
        }
    }

Następnie zaimplementujemy metodę override fun onCreateViewHolder.

W tym miejscu RecyclerView prosi nas o pomoc w skonstruowaniu dla niego FruitHolder.

Jeśli sobie przypomnimy, tak wyglądała nasza klasa FruitViewHolder:

class FruitViewHolder(fruitView: View): RecyclerView.ViewHolder(fruitView)

Wymaga naszego FruitView, który utworzyliśmy jako plik xml one_fruit_view.xml

Możemy utworzyć odniesienie do tego pliku XML i przekonwertować je na widok w następujący sposób.

class FruitListAdapter(var fruitArray: Array<String>) : RecyclerView.Adapter<FruitViewHolder>() {

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

            var fruitView = LayoutInflater.from(parent.context).inflate(R.layout.one_fruit_view, parent, false)

            var fruitViewHolder = FruitViewHolder(fruitView)

            return fruitViewHolder

        }

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

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

        }
    }

Pozostały bit to nadpisanie

fun onBindViewHolder(holder: FruitViewHolder, position: Int)

RecyclerView.Adapter pyta o liczbę całkowitą pozycji, której użyjemy do pobrania elementu z naszej listy. Zapewnia nam także uchwyt, dzięki któremu możemy powiązać element otrzymany z obiektu FruitArray z widokiem przechowywanym w uchwycie widoku.

Widok przechowywany w ViewHoder jest dostępny poprzez pole ViewHolder.itemView. Kiedy już uzyskamy widok, możemy użyć utworzonego wcześniej identyfikatora FruitName, aby ustawić zawartość.

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

            var ourFruitTextView = holder.itemView.findViewById<TextView>(R.id.fruitName)

            var aFruitName = fruitArray.get(position)

            ourFruitTextView.setText(aFruitName)
        }

Dzięki temu nasz FruitArrayAdapter jest kompletny i wygląda następująco.

class FruitListAdapter(var fruitArray: Array<String>) : RecyclerView.Adapter<FruitViewHolder>() {

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

            var fruitView = LayoutInflater.from(parent.context).inflate(R.layout.one_fruit_view, parent, false)

            var fruitViewHolder = FruitViewHolder(fruitView)

            return fruitViewHolder
        }

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

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

            var ourFruitTextView = holder.itemView.findViewById<TextView>(R.id.fruitName)

            var aFruitName = fruitArray.get(position)

            ourFruitTextView.setText(aFruitName)
        }
    }

Wreszcie jesteśmy gotowi podłączyć pozostałe elementy naszego RecyclerView. Które tworzą LayoutManager, który powie RecyclerView, jak wyświetlić zawartość listy. Określa, czy wyświetlać w sposób liniowy przy użyciu LinearLayoutManager, czy w siatce przy użyciu GridLayoutManager lub StaggeredGridLayoutManager.

Utwórz Menedżera układu

Wrócimy do naszej funkcji onCreate i dodamy LayoutManager

 override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        var myFruitRecyclerView: RecyclerView = findViewById(R.id.fruitRecyclerView)
        
        var fruitLinearLayout = LinearLayoutManager(this)
        
        myFruitRecyclerView.layoutManager =fruitLinearLayout
        
    }

Podłącz nasz adapter do przedmiotów i ustaw go na RecyclerView

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        var myFruitRecyclerView: RecyclerView = findViewById(R.id.fruitRecyclerView)
        
        var fruitLinearLayout = LinearLayoutManager(this)
        
        myFruitRecyclerView.layoutManager =fruitLinearLayout
        
        var fruitListAdapter = FruitListAdapter(fruitNames)
        
        myFruitRecyclerView.adapter =fruitListAdapter
    }

Stworzyliśmy także instancję FruitListAdapter i podaliśmy jej tablicę nazw owoców.

I w zasadzie już wszystko skończyliśmy.

Kompletny plik MainActivity.kt wygląda następująco.

package com.guru99.learnrecycler

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView

class MainActivity : AppCompatActivity() {

    var fruitNames:Array<String> = arrayOf<String>("Banana", "Mango", "Passion fruit", "Orange", "Grape")

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        var myFruitRecyclerView: RecyclerView = findViewById(R.id.fruitRecyclerView)

        var fruitLinearLayout = LinearLayoutManager(this)

        myFruitRecyclerView.layoutManager =fruitLinearLayout

        var fruitListAdapter = FruitListAdapter(fruitNames)

        myFruitRecyclerView.adapter =fruitListAdapter
    }

    class FruitViewHolder(fruitView: View): RecyclerView.ViewHolder(fruitView)

    class FruitListAdapter(var fruitArray: Array<String>) : RecyclerView.Adapter<FruitViewHolder>() {

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

            var fruitView = LayoutInflater.from(parent.context).inflate(R.layout.one_fruit_view, parent, false)

            var fruitViewHolder = FruitViewHolder(fruitView)

            return fruitViewHolder
        }

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

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

            var ourFruitTextView = holder.itemView.findViewById<TextView>(R.id.fruitName)

            var aFruitName = fruitArray.get(position)

            ourFruitTextView.setText(aFruitName)
        }
    }
}

Pobierz projekt