Potrzebujemy wyświetlić listę zawierającą elementy różnego typu. Dla przykładu niech to będzie lista polecanych produktów sklepu na której mogą się znaleźć np. płyta DVD, książka i tp. Lista może też zawierać elementy nie będące produktami jak nagłówek listy czy separator sekcji. Każdy z tych elementów ma swój własny widok z layoutem różnym od pozostałych. Elementy te mają swoje modele danych, reprezentowane przez obiekty różnych klas. Lista może być długa, więc potrzebujemy zarządzania pamięcią jaki oferuje RecyclerView
.
1. Główna aktywność aplikacji i jej layout.
Tworzymy nowy projekt w Android Studio wybierając puste Activity. Do swtorzonej aktywności z layoutem dołączamy kontrolkę typuRecyclerView
res/layout/activity_main.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" tools:context="com.mycorp.multielementslist.MainActivity android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <android.support.v7.widget.RecyclerView android:id="@+id/list_recycler" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_below="@id/title_text" android:scrollbarStyle="outsideOverlay" android:scrollbars="vertical" /> </LinearLayout>
MainActivity.java
public class MainActivity extends AppCompatActivity { private RecyclerView listRecyclerView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Fresco.initialize(this); setContentView(R.layout.activity_main); listRecyclerView = (RecyclerView) findViewById(R.id.list_recycler); } }
Nie zapomnij dodać do pliku app/build.gradle w sekcji dependencies linijki:
implementation 'com.android.support:recyclerview-v7:+'
Do ładowania zdjęć okładek z sieci użyjemy biblioteki Fresco. Dzięki temu będziemy mieć obsłużone ładowanie, cache’owanie obrazków i wydajne zarządzanie pamięcią. Inicjalizację biblioteki należy umieścić przed wywołaniem setContentView
inaczej otrzymamy błąd "Error inflating class com.facebook.drawee.view.SimpleDraweeView"
. Do pliku app/build.gradle dodajemy oczywiście zależność od tej biblioteki:
compile 'com.facebook.fresco:fresco:1.7.1'
2. Klasy elementów.
Stwórzmy teraz modele reprezentujące dane potrzebnych elementów, czyli książki, płyty DVD oraz nagłówka listy:
model/Book.java
public class Book { private String title; private String author; private String coverUrl; public Book(String title, String author, String coverUrl) { this.title = title; this.author = author; this.coverUrl = coverUrl; } // here getters and setters // ... }
model/DVD.java
public class Dvd { private String title; private String description; private String coverUrl; public Dvd(String title, String description, String coverUrl) { this.title = title; this.description = description; this.coverUrl = coverUrl; } // ... }
model/DVD.java
public class Header { private String title; private String subTitle; public Header(String title, String subTitle) { this.title = title; this.subTitle = subTitle; } // ... }
3. Klasa reprezentująca element listy.
Klasa reprezentująca element listy będzie niejako kontenerem na obiekty modelów z poprzedniego punktu. Będzie zawierać typ przechowywanego obiektu zdefiniowany w enumeratorze ItemType. Na potrzeby naszego przykładu powstały trzy typy: HEADER, BOOK, DVD. Dzięki temu adapter będzie wiedzieć, jakiego widoku użyć do wyrenderowania danego elementu.
RecycledListItem.java
public class RecycledListItem { public enum ItemType { HEADER, BOOK, DVD } private ItemType type; private Header header; private Book book; private Dvd dvd; public RecycledListItem(Header haeder) { this.header = header; this.type = ItemType.HEADER; } public RecycledListItem(Book book) { this.book = book; this.type = ItemType.BOOK; } public RecycledListItem(Dvd book) { this.dvd = book; this.type = ItemType.DVD; } public ItemType getType() { return type; } public Object getItem() { if (ItemType.HEADER.equals(this.type)) { return this.header; } else if (ItemType.BOOK.equals(this.type)) { return this.book; } else if (ItemType.DVD.equals(this.type)) { return this.dvd; } else { return null; } } }
Przeciążyliśmy konstruktor, aby można było tworzyć kontener dla różnych klas modeli. Metodą getItem
pobierzemy obiekt właściwego typu.
4. Widoki elementów
Zaprojektujemy teraz layouty dla poszczególnych elementów.
res/layout/header_item_layout.xml
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@drawable/box_with_shadow" android:padding="10dp" android:layout_margin="10dp"> <TextView android:id="@+id/dvd_title_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="25sp" android:textAllCaps="true" android:textStyle="bold" android:text="Section header" /> </FrameLayout>
res/layout/dvd_item_layout.xml
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:fresco="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@drawable/box_with_shadow" android:padding="15dp" android:layout_margin="10dp"> <RelativeLayout android:layout_width="match_parent" android:layout_height="match_parent"> <com.facebook.drawee.view.SimpleDraweeView android:id="@+id/dvd_cover_drawee" android:layout_width="120dp" android:layout_height="160dp" fresco:placeholderImage="@drawable/dvd" android:layout_marginRight="10dp" android:scaleType="centerCrop" /> <TextView android:id="@+id/dvd_title_text" android:layout_toRightOf="@+id/dvd_cover_drawee" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="25sp" android:layout_marginBottom="3dp" android:text="Star Wars ep. IV" /> <TextView android:id="@+id/dvd_description_text" android:layout_toRightOf="@+id/dvd_cover_drawee" android:layout_below="@+id/dvd_title_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="10dp" android:textSize="12sp" android:text="A young boy from Tatooine sets out on an adventure with an old Jedi named Obi-Wan Kenobi as his mentor to save Princess Leia." /> <Button android:id="@+id/book_download_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_toRightOf="@+id/dvd_cover_drawee" android:layout_below="@+id/dvd_description_text" android:textAllCaps="false" android:text="Watch trailer"/> </RelativeLayout> </FrameLayout>
res/layout/book_item_layout.xml
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:fresco="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@drawable/box_with_shadow" android:padding="15dp" android:layout_margin="10dp"> <RelativeLayout android:layout_width="match_parent" android:layout_height="match_parent"> <com.facebook.drawee.view.SimpleDraweeView android:id="@+id/book_cover_drawee" android:layout_width="120dp" android:layout_height="160dp" fresco:placeholderImage="@drawable/book" android:layout_marginLeft="10dp" android:layout_alignParentRight="true" android:scaleType="centerCrop" /> <TextView android:id="@+id/book_title_text" android:layout_toLeftOf="@+id/book_cover_drawee" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="25sp" android:layout_marginBottom="3dp" android:layout_alignParentLeft="true" android:text="Lord of The Rings" /> <TextView android:id="@+id/book_author_text" android:layout_toLeftOf="@+id/book_cover_drawee" android:layout_below="@+id/book_title_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="10dp" android:layout_alignParentLeft="true" android:textSize="18sp" android:text="J. R. R. Tolkien" /> <TextView android:id="@+id/book_description_text" android:layout_toLeftOf="@+id/book_cover_drawee" android:layout_below="@+id/book_author_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="10dp" android:layout_alignParentLeft="true" android:textSize="12sp" android:text="A young Hobbit named Frodo is thrown on an amazing adventure, when he is appointed the job of destroying the one ring which was created by the dark lord Sauron." /> </RelativeLayout> </FrameLayout>
Elementy będą miały wygląd kafelków z zaokrąglonymi rogami oraz cieniem, należy więc zdefiniować ich kształt:
res/drawable/box_with_shadow/item_layout.xml
<?xml version="1.0" encoding="utf-8"?> <layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <item> <shape android:shape="rectangle"> <solid android:color="#CABBBBBB"/> <corners android:radius="5dp" /> </shape> </item> <item android:left="0dp" android:right="0dp" android:top="0dp" android:bottom="2dp"> <shape android:shape="rectangle"> <solid android:color="@android:color/white"/> <corners android:radius="5dp" /> </shape> </item> </layer-list>
Umieśćmy też testowo obrazki okładek w res/drawable: book.jpg oraz dvd.jpg. Podczas docelowego ładowania obrazków z sieci powinna wyświetlać się też zaślepka: placeholder.png. Docelowo podmień w tagach SimpleDraweeView testowe obrazki na tę zaślepkę:
<com.facebook.drawee.view.SimpleDraweeView ... fresco:placeholderImage="@drawable/book" ... />
Sprawdźmy, jak będą prezentować się nasze elementy. Dodajmy je tymczasowo do głównego layoutu, powyżej tagu
... <include layout="@layout/header_item_layout"/> <include layout="@layout/dvd_item_layout"/> <include layout="@layout/book_item_layout"/> <android.support.v7.widget.RecyclerView ...
Powinniśmy otrzymać następujący ekran:
Na ten moment mamy zdefiniowane elementy, ich modele i layouty. W następnej części artykułu dowiemy się czym jest ViewHolder
, oraz jak stworzyć listę naszych elementów aby renderowały się w kontrolce RecyclerView
.
Cały kod źródłowy dostępny jest na GitHubie.