MójDroid.pl

#5 Twórz aplikacje na Androida z Mojdroid.pl - Listy i Adaptery

2012-09-16
|
Damian P.

W kolejnych poradnikach będę chciał pokazać wam większość funkcji i możliwości, jakie Android oferuje swoim programistom. Poszczególnych Activity będzie zatem sporo, a żeby nie wprowadzać zbędnego bałaganu, od razu spróbujemy wszystko to posegregować w zgrabną listę. Zatem czas jej się nauczyć, i o tym wam dziś opowiem.
Informacja: Nie radzisz sobie z kodem? Coś Ci nie wychodzi lub nie działa? Zgubiłeś się? W tym miejscu zobaczysz pełen kod aplikacji, a tutaj go pobierzesz na dysk.

1. Typy list

Listy są najbardziej rozpowszechnionym elementem interfejsu w Androidzie. Praktycznie nie ma aplikacji, w której nie było by chociaż jednego elementu tego typu. Do plusów tego rozwiązania należą zalety takie jak łatwość optymalizacji, jeden kod dla każdego elementu na liście, łatwe tworzenie własnych, modyfikowalnych rozwiązań i przejrzysty wygląd. Wiele mówić nie trzeba. Wyróżniamy dwa główne typy list: ListView, gdzie elementy są położone pionowo (jeden element pod drugim), oraz GridView, który jest siatką elementów (podzieloną na kolumny i wiersze). Każdy z typów możemy również rozdzielić na dwa mniejsze (nieoficjalnie, ja to tak rozdzielam): na listę prostą, czyli taką jaką oferuje nam Android od początku bez definiowania własnych szablonów, i rozbudowaną, czyli taką gdzie sami tworzymy wygląd każdego elementu. Jakie są między nimi różnice? Prostą listę możemy dużo szybciej zbudować, ale musimy dostosować się do jej wyglądu i wymagań. Z rozbudowaną jest na odwrót - poświęcamy na nią więcej czasu, ale za to lepiej się prezentuje i sprawdza.

2. Tworzymy prostą listę

Zacznijmy od prostej listy. Od razu na wstępie powiem wam, jak nasza aplikacja będzie wyglądać. Główny widok będzie prostą listą, gdzie każdy z elementów będzie odsyłał nas do jakiegoś podpunktu w kolejnych poradnikach. Przez to wszytko będzie pogrupowane i w razie czego, chcąc ponownie przeanalizować kod, wystarczy że uruchomimy daną klasę i zobaczymy co w niej się dzieje. Zaletą takiego rozwiązania jest również prostota dodawania kolejnych elementów. Ale o tym za chwilę. Co musimy zrobić - z pewnością stworzyć View listy (ListView) i zdefiniować kilka elementów (coś lista musi w końcu przedstawiać), po czym ustawić to wszystko do naszego ListView i uruchomić aplikację. Zatem przechodzimy do naszego layoutu głównego Activity w folderze res/layout, włączamy tryb graficzny, klikamy na napis (TextView) i go usuwamy, po czym z palety po lewej stronie wybieramy zakładkę Composite, a wniej ListView. Element przenosimy na prawą stronę, do layoutu. Widzimy coś mniej więcej takiego: Lista już jest, ale nie prezentuje się ona zbyt dobrze przez duże odstępy po bokach. To wina RelativeLayoutu, który mogliście zauważyć już wcześniej w poradnikach. Póki co nie będę tłumaczył czym on jest, bo chcę to zostawić na inną okazję, więc jedyne co zróbcie to kliknijcie 2x na listę w graficznym edytorze lub przejdźcie do edytora tekstowego. W tym miejscu zobaczycie coś takiego:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <ListView
        android:id="@+id/lv_prostalista"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_alignParentTop="true"
        android:layout_marginLeft="102dp"
        android:layout_marginTop="112dp" >
    </ListView>

</RelativeLayout>
Jest to definicja naszego ListView w layoucie XML. Naszą listę musimy rozszerzyć do maksymalnych granic wyświetlacza, więc usuwamy atrybuty "android:layout_marginLeft", "android:layout_marginTop", "android:layout_alignParentTop" oraz "android:layout_alignParentLeft" z naszego listowego View. Jak pewnie się domyślacie, są to właśnie odstępy od lewej i prawej strony oraz wyrównanie do wyższego elementu. Nam to tutaj oczywiście nie potrzebne. Po wszystkim lista wygląda tak: A jej definicja tak:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <ListView
        android:id="@+id/lv_prostalista"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" >
    </ListView>

</RelativeLayout>
Czyli tak jak chcemy (a przynajmniej ja chcę ;)). Zapisujemy. Teraz wracamy do naszego Activity w src. Wcześniej pozbyliśmy się elementu TextView, więc kasujemy go z logiki naszego programu, bo inaczej wyrzuci on nam błąd. W jego miejsce wstawiamy definicję ListView oraz prostą tablicę zdań, które będą elementami na naszej liście:
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_view__main);

        String[] elementy_listy = {"Element numer 1", "Drugi element", "A tutaj trzeci!"};
        ListView prosta_lista = (ListView) findViewById(R.id.lV_prostalista);

    }
Póki co nic trudnego, same definicje. Czas wszystko to połączyć i wyświetlić naszą listę. Do tego służyć będzie element systemu, który nazywa się Adapter. Jest to nic innego jak "most", który łączy nasze dane z wybranym widokiem. Wprowadza on dostęp do pojedynczych elementów i odpowiada za tworzenie następnych w odpowiedniej kolejności. Jako, że posiadamy jedynie prostą listę, nie musimy tworzyć osobnej klasy do jego zarządzania (tak jest najwygodniej i najefektywniej, bo później bezproblemowo Adapter listy możemy używać w innej części aplikacji). Zdefiniujmy więc całość, aby miało to ręce i nogi:
	String[] elementy_listy = { "Własna lista" };
	ListView prosta_lista = (ListView) findViewById(R.id.lV_prostalista);

	ArrayAdapter adapter_listy = new ArrayAdapter(this,
		android.R.layout.simple_list_item_1, elementy_listy);
	prosta_lista.setAdapter(adapter_listy);
Co tutaj robimy?  Idąc po kolei:
  • definiujemy ArrayAdapter, czyli adapter z tablicą elementów, gdzie każde jedno pole to String (łańcuch, wyraz). Adapter sam w sobie wymaga kilku elementów do działania, więc te umieszczamy w nawiasie przy tworzeniu nowego obiektu. Jest to Context, czyli łącznik z zasobami Androida, Restource, czyli layout dla każdego elementu (tutaj standardowy Androida, zostawmy tak jak jest, zaraz stworzymy swój) oraz tablica z danymi
  • łączymy adapter z listą przez funkcję setAdapter, który przyjmuje oczywiście nasz ArrayAdapter jako atrybut.
I to wszystko. Uruchamiamy aplikację i widzimy naszą pierwszą listę :)

3. Tworzymy listę z własnymi elementami

Teraz stworzymy listę, w której każdy element będzie naszym zdefiniowanym wcześniej layoutem. Nie jest to nic trudnego i całość działa bardzo podobnie do poprzedniego przykładu, z tą różnicą, że wszystko jest nieco bardziej rozbudowane. Ale efekt jest za to świetny. W tym podpunkcie stworzymy: nowe Activity, layout dla pojedynczego elementu, podepniemy Activity pod naszą prostą listę, a po kliknięciu w element - obsłużymy przeniesienie do naszej rozbudowanej listy. Stwórzmy więc na początek pojedynczy element. Przejdź do folderu res/layout, kliknij na niego prawym przyciskiem myszy, wybierz zakładkę New->Other, znajdź kategorię Android a w niej pole "Android XML Layout File". Kliknij dwa razy lub zatwierdź, wpisz nazwę taką, abyś wiedział co gdzie jest (np. list_item_view_custom_list) i zakończ. Czas na stworzenie wyglądu, tutaj niech zatryumfuje wam się własna twórczość (w rozsądnym obszarze) :) Oczywiście zapamiętajcie co tworzycie i nadajcie tym elementom swoje unikalne ID (atrybut android:id). Ja stworzyłem coś takiego:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical" >

    <ImageView
        android:id="@+id/imageView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/ic_launcher" />

    <TextView
        android:id="@+id/textView_item_custom"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:layout_marginLeft="15dp"
        android:layout_toRightOf="@+id/imageView1"
        android:text="TextView" />

</RelativeLayout>
Jedna uwaga - przy głównym, najwyższym elemencie w naszym layoucie (dla pojedynczego elementu) ustawcie jego wysokość na "wrap_content", czyli niech jego wysokość będzie zależna od wysokości Views użytych w nim. Teraz przechodzimy do folderu src, gdzie tworzymy nowe Activity (robimy to podobnie jak z Layoutem, przez PPM->New->Class i ustawiając nazwę - u mnie niech będzie to View_Custom_List). Rozszerzamy klasę o nie [Activity] oraz dodajemy znaną nam już funkcję OnCreate ze zdefiniowanym ListView (ustawmy layout dla Activity taki sam jak w klasie View_Main). Jak to szybko zrobić? Ręcznie dopisujemy extends Activity (zapisujemy) obok nazwy po czym między nawiasami klamrowymi klikamy PPM i wybieramy Source->Overrride/Implement Methods..., gdzie szukamy onCreate i potwierdzamy. Obecnie całość wygląda tak:
package com.example.moja.pierwsza.aplikacja;

import android.app.Activity;
import android.os.Bundle;
import android.widget.ListView;

public class View_Custom_List extends Activity {

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

	ListView rozbudowana_lista = (ListView) findViewById(R.id.lV_prostalista);
    }

}
Czas na definicję Adaptera. Znowu tworzymy nową klasę, nazwijmy ją List_Custom_List. Po stworzeniu rozszerzmy ją o BaseAdapter (extends BaseAdapter) i dodajemy wymagane metody (najedź na czerwone podkreślenie lub kliknij na kółeczko z krzyżykiem po lewej stronie, poczekaj i kliknij Add unimplemented methods). Podstawy same się stworzą. Widzimy coś takiego:
package com.example.moja.pierwsza.aplikacja;

import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;

public class List_Custom_List extends BaseAdapter {

    public int getCount() {
	return 0;
    }

    public Object getItem(int position) {
	return null;
    }

    public long getItemId(int position) {
	return 0;
    }

    public View getView(int position, View convertView, ViewGroup parent) {
	return null;
    }

}
Czas zadbać o kilka rzeczy: ustawienie wyglądu elementów, przekazanie danych, wyliczenie liczby elementów oraz optymalizację listy. W tym celu wprowadzimy konstruktor oraz zmienne, pod którymi ukryjemy nasze dane. Po zmianach całość wygląda tak:
package com.example.moja.pierwsza.aplikacja;

import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;

public class List_Custom_List extends BaseAdapter {

    private String[] data;
	private Context ctx;

    public List_Custom_List(String[] importeddata) {
	this.ctx = ctx;
	this.data = importeddata;
    }

    public int getCount() {
	return data.length;
    }

    public Object getItem(int position) {
	return null;
    }

    public long getItemId(int position) {
	return 0;
    }

    public View getView(int position, View convertView, ViewGroup parent) {
	return null;
    }

}
Nic trudnego i niezrozumiałego, tak myślę. Będziemy przekazywać do listy dwa parametry - łącznik Context oraz tablicę zdań lub wyrazów, które będą tworzyć elementy. Z tej tablicy bierzemy jej długość i ustawiamy długość ListView. Teraz czas na ustawienie wyglądu. Robimy to w funkcji "getView" (która jest wykonywana za każdym razem, gdy generowany jest widok elementu), gdzie całość zapisujemy w ten sposób:
package com.example.moja.pierwsza.aplikacja;

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;

public class List_Custom_List extends BaseAdapter {

    private Context ctx;
    private String[] data;

    public List_Custom_List(Context ctx, String[] importeddata) {
	this.data = importeddata;
	this.ctx = ctx;
    }

    public int getCount() {
	return data.length;
    }

    public Object getItem(int position) {
	return null;
    }

    public long getItemId(int position) {
	return 0;
    }

    public View getView(int position, View convertView, ViewGroup parent) {

	LayoutInflater inflater = (LayoutInflater) ctx.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

	convertView = inflater.inflate(R.layout.list_item_view_custom_list, parent, false);

	return convertView;
    }

}
Wygląd będzie ustawiony, dane będą się zmieniać, ale całość będzie posiadała jeden minus - przy dużej liczbie elementów będzie strasznie wolna i zasobożerna. Czas na optymalizację. BaseAdapter akurat ma taką zaletę, że od początku umożliwia nam cachowanie wyglądu elementów. Działa to w ten sposób, że Android ostatni element widoczny na liście używa ponownie, zmieniając w nim tylko dane. Proste i bardzo skuteczne, o czym się za chwilę przekonacie. Ten re-używany widok jest dostępny pod zmienną "convertView" i po prostu sprawdzamy, czy jest on pusty czy nie (jak nie jest, to używamy widoku ponownie).
package com.example.moja.pierwsza.aplikacja;

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.TextView;

public class List_Custom_List extends BaseAdapter {

    private Context ctx;
    private String[] data;

    public List_Custom_List(Context ctx, String[] importeddata) {
	this.data = importeddata;
	this.ctx = ctx;
    }

    public int getCount() {
	return data.length;
    }

    public Object getItem(int position) {
	return null;
    }

    public long getItemId(int position) {
	return 0;
    }

    public View getView(int position, View convertView, ViewGroup parent) {

	if (convertView == null) {
	    LayoutInflater inflater = (LayoutInflater) ctx.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
	    convertView = inflater.inflate(R.layout.list_item_view_custom_list, parent, false);
	}

	TextView tekst_w_layoucie = (TextView) convertView.findViewById(R.id.textView_item_custom);
	tekst_w_layoucie.setText(data[position]);

	return convertView;
    }

}
Całość będzie działała już o wiele lepiej, ale to nadal nie wszystko. Nadal na marne musimy ustalać textView. Poprawmy to używając prostej sztuczki:
package com.example.moja.pierwsza.aplikacja;

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.TextView;

public class List_Custom_List extends BaseAdapter {

    private Context ctx;
    private String[] data;

    public List_Custom_List(Context ctx, String[] importeddata) {
	this.data = importeddata;
	this.ctx = ctx;
    }

    public int getCount() {
	return data.length;
    }

    public Object getItem(int position) {
	return null;
    }

    public long getItemId(int position) {
	return 0;
    }

    private class ViewHolderPattern {
	TextView tekst_w_layoucie;
    }

    public View getView(int position, View convertView, ViewGroup parent) {

	ViewHolderPattern view_holder;

	if (convertView == null) {
	    LayoutInflater inflater = (LayoutInflater) ctx.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
	    convertView = inflater.inflate(R.layout.list_item_view_custom_list, parent, false);

	    view_holder = new ViewHolderPattern();
	    view_holder.tekst_w_layoucie = (TextView) convertView.findViewById(R.id.textView_item_custom);

	    convertView.setTag(view_holder);
	} else {
	    view_holder = (ViewHolderPattern) convertView.getTag();
	}

	view_holder.tekst_w_layoucie.setText(data[position]);

	return convertView;
    }

}
Co tutaj robimy? Spychamy na margines każdorazowe szukanie View bo ID, które jest pamięciożerne po pewnym czasie. Całość sprowadzamy do zapisywania prostego tagu, który zapamięta naszemu elementowi nasze TextView. Przez to dostając się ponownie do elementu przez ConvertView dostajemy wcześniej wygenerowane wiązania i nie musimy ich ponownie używać. Wystarczy jedynie zamienić kontent i gotowe! Nasz Adapter jest gotowy, czas połączyć całość. Wracamy do View_Custom_List i generujemy jakieś dane do wyświetlenia. Później je łączymy z ListView.
package com.example.moja.pierwsza.aplikacja;

import android.app.Activity;
import android.os.Bundle;
import android.widget.ListView;

public class View_Custom_List extends Activity {

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

	ListView rozbudowana_lista = (ListView) findViewById(R.id.lV_prostalista);
	String[] przykladowe_dane = {"Test 1", "Test 2", "Test 3", "Test 4", "Test 5", "Test 6", "Test 7", "Test 8", "Test 9"};
	List_Custom_List adapter_listy = new List_Custom_List(this, przykladowe_dane);

	rozbudowana_lista.setAdapter(adapter_listy);

    }

}

4. Reakcja na kliknięcie w prostej liście

Teraz czas na ostatni krok - połączenie naszej prostej listy i uruchomienie nowego Activity z rozbudowaną listą. Do AndroidManifest dodajemy deklarację nowego Activity (<activity android:name=".View_Custom_List"></activity>), po czym przechodzimy do View_Main. Tutaj musimy zareagować na kliknięcie elementu. Najpierw jednak oczyśćmy naszą listę elementów w "elementy_listy" i zostawmy tam jedno pole - np. Własna lista. Jak wykryć dotknięcie elementu? Tą część podzielę na dwie części, z czego teraz pokażę wam jak to wykonać na prostej liście, a w następnym poradniku jak reagować na wszystkie dotknięcia. A więc w tym wypadku po prostu należy ustawić "czujkę" (Listener) na kliknięcie (ClickListener) elementu z listy (ItemClickListener). A robi się to tak:
package com.example.moja.pierwsza.aplikacja;

import android.os.Bundle;
import android.app.Activity;
import android.content.Intent;
import android.view.Menu;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;
import android.widget.ListView;

public class View_Main extends Activity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
	super.onCreate(savedInstanceState);
	setContentView(R.layout.activity_view__main);

	String[] elementy_listy = { "Własna lista" };
	ListView prosta_lista = (ListView) findViewById(R.id.lV_prostalista);

	ArrayAdapter adapter_listy = new ArrayAdapter(this,
		android.R.layout.simple_list_item_1, elementy_listy);
	prosta_lista.setAdapter(adapter_listy);

	prosta_lista.setOnItemClickListener(new OnItemClickListener() {

	    public void onItemClick(AdapterView arg0, View arg1, int pos,
		    long arg3) {

		switch (pos) {
		case 0:
		    Intent startActivityCustomList = new Intent(View_Main.this,
			    View_Custom_List.class);
		    startActivity(startActivityCustomList);
		    break;
		}

	    }

	});

    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
	getMenuInflater().inflate(R.menu.activity_view__main, menu);
	return true;
    }
}
Listener w tym przypadku ustawia się na elemencie przez zastosowanie setOnItemClickListener wraz z jego definicją (new OnItemClickListener()). Musimy przy tym dodać jedną wymaganą metodę, którą jest reakcja na kliknięcie i wykryć, który element wybraliśmy (zmienna pos). To wszytko, po wybraniu 0 elementu na liście wystartuje nam nowe Activity z naszą własną listą.

I to cała magia na dzisiaj! :)