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.