Kurs Android (16)

Autor: Damian Chodorek • Opublikowany: 6 stycznia 2016 • Ostatnia aktualizacja: 25 marca 2016 • Kategoria: android, kursy

Wyświetlanie danych w postaci listy lub siatki elementów. Wykorzystanie CardView oraz RecyclerView.

Prezentowanie danych w postaci listy elementów to bardzo popularny wzorzec aplikacji mobilnych. Wystarczy otworzyć kilka aplikacji od Google, aby się o tym przekonać. Google Play wyświetla aplikacje w postaci listy. Gmail wyświetla e-maile w postaci listy. Google+ również wyświetla zawartość w postaci listy elementów.

Do niedawna, programiści mieli do wyboru dwie klasy, które umożliwiały wyświetlanie list: ListView oraz GridView. Pierwsza klasa wyświetlała elementy pionowo, jeden pod drugim. Klasa druga prezentowała treść w postaci siatki, np. po trzy elementy w poziomie.

W zasadzie, do najprostszych zastosowań, te dwie klasy są wystarczające. Z czasem okazało się, że programiści potrzebowali nowych sposobów układania elementów oraz łatwiejszego animowania ich. Chcieli, aby np. dodawanie lub usuwanie elementów odbywało się w sposób animowany, a nie natychmiastowy. Z tego też powodu, w raz z Androidem L, wprowadzono RecyclerView. W ramach tej lekcji to właśnie ją omówimy bardziej szczegółowo, ponieważ jest bardziej na czasie, a dwie starsze klasy są trochę prostsze w użyciu, więc załatwimy wszystko za jednym zamachem :).

Wymagane komponenty

Czego będziemy potrzebować? Po pierwsze klasy, która będzie wyświetlać elementy w postaci listy lub siatki, np. RecyclerView, ListView lub GridView.

Potrzebujemy również layoutu dla pojedynczego elementu listy. Taki layout możemy napisać sami i mamy tu pełną dowolność. Może to być LinearLayout lub RelativeLayout. My jednak wykorzystamy modny CardView, który jest prostokątem z zaokrąglonymi rogami oraz z cieniem wokół.

Kolejnym elementem jest adapter, czyli klasa, która przechowuje i zarządza danymi do wyświetlenia. Lista tylko wyświetla elementy, które dostaje właśnie od adaptera. Dla ListView oraz GridView możemy rozszerzyć klasę BaseAdapter. Dla RecyclerView musimy rozszerzyć klasę RecyclerView.Adapter

Jako, że RecyclerView jest trochę bardziej zaawansowaną klasą, to aby z niej skorzystać potrzebujemy dodatkowego komponentu, którym jest layout manager, czyli klasa, która odpowiada za rozmieszczenie elementów na ekranie. Do wyboru mamy trzy rodzaje:

  • LinearLayoutManager – elementy pojawiają się wertykalnie (jak w ListView) lub horyzontalnie,
  • GridLayoutManager – elementy są rozmieszczone na regularnej siatce (jak w GridView),
  • StaggeredGridLayoutManager – elementy rozłożone są na nieregularnej siatce, tzn. wysokości poszczególnych elementów mogą być różne.

Cały proces tworzenia i wyświetlania listy wygląda tak jak na poniższym rysunku.

Zaczynamy

Utwórz projekt com.damianchodorek.kurs.android16. Zanim zaczniemy pisać jakikolwiek kod, do naszego pliku /app/build.gradle dodamy w dependencies dwie linijki, tak aby całość wyglądał jak poniżej.

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:23.0.1'
    compile 'com.android.support:recyclerview-v7:23.+'
    compile 'com.android.support:cardview-v7:23.+'
}

RecyclerView oraz CardView są zawarte w bibliotece androidowej support library dzięki czemu możemy korzystać z tych komponentów również w starszych wersjach systemu (mamy kompatybilność wsteczną).

Tworzymy listę

Nasza aplikacja będzie zawierać listę krótkich artykułów. Każdy z nich będzie posiadał tytuł oraz treść. Zdefiniujemy layout dla pojedynczego artykułu. Wykorzystamy tutaj właśnie CardView. Utwórz plik layout/article_layout.xml i uzupełnij go zgodnie z kodem poniżej.

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:card_view="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="5dp"
    card_view:cardBackgroundColor="#FEFEFE"
    card_view:cardCornerRadius="5dp"
    card_view:cardElevation="5dp">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:padding="5dp">

        <TextView
            android:id="@+id/article_title"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Title"
            android:textColor="#333"
            android:textSize="20sp" />

        <TextView
            android:id="@+id/article_subtitle"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="8dp"
            android:text="Subtitle"
            android:textColor="#888"
            android:textSize="14sp" />

    </LinearLayout>

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

Layout dla pojedynczego elementu listy gotów. Teraz w pliku layout/activity_main.xml zdefiniujmy listę RecyclerView.

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

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

</RelativeLayout>

Spinamy wszystko w całość

Oto metoda onCreate() w MainActivity.

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        RecyclerView recyclerView = (RecyclerView) findViewById(R.id.articles);
        // w celach optymalizacji
        recyclerView.setHasFixedSize(true);

        // ustawiamy LayoutManagera
        recyclerView.setLayoutManager(new LinearLayoutManager(this));

        // ustawiamy animatora, który odpowiada za animację dodania/usunięcia elementów listy
        recyclerView.setItemAnimator(new DefaultItemAnimator());

        // tworzymy źródło danych - tablicę z artykułami
        ArrayList<Article> articles = new ArrayList<>();
        for (int i = 0; i < 20; ++i)
            articles.add(new Article());

        // tworzymy adapter oraz łączymy go z RecyclerView
        recyclerView.setAdapter(new MyAdapter(articles, recyclerView));
    }
}

Powyższy kod został opisany komentarzami, ale podsumujmy to co się dzieje. Tworzymy adaptera i przypisujemy mu dane. Potem przypisujemy go do listy RecyclerView i gotowe. Oczywiście żeby wszystko zadziałało, potrzebujemy kilku klas. Zacznijmy od najprostszej – klasy Article.

public class Article {
    private String mTitle;
    private String mContent;

    // statyczne tablice, na podstawie których zostaną uzupełnione obiekty artykułów
    private static String[] sTitles = {"Lorem ipsum dolor sit amet",
            "Etiam sit", "Cras vel lorem",
            "Cras suscipit, urna at aliquam rhoncus",
            "Phasellus congue lacus eget neque",
            "Phasellus pharetra nulla ac diam"};

    private static String[] sContents = {"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin nibh augue, suscipit a, scelerisque sed, lacinia in, mi. Cras vel lorem. Etiam pellentesque aliquet tellus. Phasellus pharetra nulla ac diam.",
            "Quisque semper justo at risus. Donec venenatis, turpis vel hendrerit interdum, dui ligula ultricies purus, sed posuere libero dui id orci",
            "Nam congue, pede vitae dapibus aliquet, elit magna vulputate arcu, vel tempus metus leo non est. Etiam sit amet lectus quis est congue mollis.",
            "Phasellus congue lacus eget neque. Phasellus ornare, ante vitae consectetuer consequat, purus sapien ultricies dolor, et mollis pede metus eget nisi.",
            "Praesent sodales velit quis augue. Cras suscipit, urna at aliquam rhoncus, urna quam viverra nisi, in interdum massa nibh nec erat."};

    public Article() {
        Random random = new Random();

        // ustawiamy losowy tytuł i treść artykułu
        mTitle = sTitles[random.nextInt(sTitles.length)];
        mContent = sContents[random.nextInt(sContents.length)];
    }

    public String getTitle() {
        return mTitle;
    }

    public String getContent() {
        return mContent;
    }
}

Jak się domyślasz – utworzenie nowego obiektu spowoduje uzupełnienie go losowymi danymi z naszych statycznych tablic. Czas na klasę adaptera. Wykorzystuje on wzorzec ViewHolder, o którym więcej przeczytasz np. tutaj. Ogólnie chodzi o to, aby ograniczyć wywołanie metody findViewById() podczas uzupełniania listy elementów.

Zamiast tworzyć tyle elementów article_layout co artykułów w tablicy, tworzymy tylko tyle ile zmieści się na ekranie. Podczas przewijania listy podmieniamy dane elementów, np. zmieniamy tytuł i treść artykułu. Jest to spora optymalizacja ze względu na ilość tworzonych elementów listy.

Gorąco zachęcam Cię do zapoznania się z tym wzorcem zanim przejdziesz do dalszych akapitów.

Najważniejsze w adapterze są trzy metody:

  • onCreateViewHolder() – tutaj tworzymy obiekt layoutu elementu listy oraz na jego podstawie tworzymy obiekt ViewHolder,
  • onBindViewHolder() – uzupełniamy element listy odpowiednimi danymi,
  • getItemCount() – zwracamy ilość wszystkich elementów.
public class MyAdapter extends RecyclerView.Adapter {

    // źródło danych
    private ArrayList<Article> mArticles = new ArrayList<>();

    // obiekt listy artykułów
    private RecyclerView mRecyclerView;

    // implementacja wzorca ViewHolder
    // każdy obiekt tej klasy przechowuje odniesienie do layoutu elementu listy
    // dzięki temu wywołujemy findViewById() tylko raz dla każdego elementu
    private class MyViewHolder extends RecyclerView.ViewHolder {
        public TextView mTitle;
        public TextView mContent;

        public MyViewHolder(View pItem) {
            super(pItem);
            mTitle = (TextView) pItem.findViewById(R.id.article_title);
            mContent = (TextView) pItem.findViewById(R.id.article_content);
        }
    }

    // konstruktor adaptera
    public MyAdapter(ArrayList<Article> pArticles, RecyclerView pRecyclerView){
        mArticles = pArticles;
        mRecyclerView = pRecyclerView;
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup viewGroup, final int i) {
        // tworzymy layout artykułu oraz obiekt ViewHoldera
        View view = LayoutInflater.from(viewGroup.getContext())
                       .inflate(R.layout.article_layout, viewGroup, false);

        // dla elementu listy ustawiamy obiekt OnClickListener,
        // który usunie element z listy po kliknięciu na niego
        view.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // odnajdujemy indeks klikniętego elementu
                int positionToDelete = mRecyclerView.getChildAdapterPosition(v);
                // usuwamy element ze źródła danych
                mArticles.remove(positionToDelete);
                // poniższa metoda w animowany sposób usunie element z listy
                notifyItemRemoved(positionToDelete);
            }
        });

        // tworzymy i zwracamy obiekt ViewHolder
        return new MyViewHolder(view);
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, final int i) {
        // uzupełniamy layout artykułu
        Article article = mArticles.get(i);
        ((MyViewHolder) viewHolder).mTitle.setText(article.getTitle());
        ((MyViewHolder) viewHolder).mContent.setText(article.getContent());
    }

    @Override
    public int getItemCount() {
        return mArticles.size();
    }
}

Gotowe. Stworzyliśmy aplikację, która wyświetla listę artykułów. Po kliknięciu na jeden z nich zostanie on usunięty w animowany sposób.

Tworzymy siatkę

Oto najlepszy dowód, że RecyclerView to potężne narzędzie. Aby naszą listę zamienić na siatkę elementów wystarczy zamienić jedną linijkę w klasie MainActivity.

// linijkę:

recyclerView.setLayoutManager(new LinearLayoutManager(this));

// zamień na:

recyclerView.setLayoutManager(new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL));

Siatka gotowa! Usuń element i zobacz jak teraz wygląda animacja. Parametry konstruktora StaggeredGridLayoutManager to liczba kolumn lub wierszy w zależności od orientacji oraz orientacja: pionowa lub pozioma.

część 17