06

Uspořádání prvků v okně
(Layouts)

Grafické prvky v okně (tlačítka, textová pole atd.) v Androidu vkládáme do komponent, kterým se říká layout. Layout grafické prvky shromažduje (funguje jako kontejner grafických prvků) a především určuje uspořádání prvků v okně a jejich vzájemnou polohu (funguje jako správce rozložení). V českém textu budeme používat buď slovo kontejner, nebo anglické slovo layout tam, kde by překlad byl zavádějící. Termín layout se běžně používá i v českých textech o programování a tvorbě grafického rozhraní.


Běžně používané layouty

Zatím jsme prvky v okně skládali pod sebe nebo vedle sebe a používali k jejich uspořádání kontejner LinearLayout. Tento kontejner je pro naše účely nejjednodušší za předpokladu, že nám stačí skládat prvky pod sebe nebo vedle sebe. Pro složitější rozložení se sice kontejnery dají vnořovat, ale to vede k výpočetně náročnějšímu a méně spolehlivému kódu.


Výchozí volbou v Android Studiu je RelativeLayout. Ten určuje pozici grafických prvků ve vztahu k ostatním grafickým prvkům. Pro komplexnější rozložení je tak méně výpočetně náročný než vnořované kontejnery LinearLayout, ale zase je obtížnější si rozložení prvků představit a upravovat.


Dalším základním kontejnerem je WebView, který slouží k jednoduchému zobrazení dokumentu zapsaného v jazyce HTML.


Existují ale i další kontejnery (layouts). Zmiňme GridView, který uspořádává prvky do pravidelné mřížky a hodí se tedy typicky pro náhledy obrázků a další podobné situace, a ListView, který umožňuje jednoduše řešit dynamické přidávání textových položek. Tyto kontejnery používají třídu Adapter k získání položek, které mají zobrazovat. Kontejnerem ListView se budeme podrobněji zabývat v této kapitole.


Layout  ListView

ListView slouží k zobrazení několika textových položek, mezi kterými můžeme skrolovat. Je tak vhodný zejména tehdy, kdy počet položek předem neznáme a přidáváme položky teprve za běhu aplikace.


Umístění ListView do okna

Kontejner vkládáme stejně jako ostatní kontejnery v XML popisu vzhledu okna. Používáme element <ListView>.


Vkládání dat do kontejneru ListView

Ke vkládání položek používá layout ListView instance tříd, implementujících rozhraní (interface) Adapter. Nejběžnější je použití třídy ArrayAdapter, která umožňuje vkládat položky z pole či Listu (například z kontejneru ArrayList), nebo použití třídy Cursor a SimpleCursorAdapter. My se omezíme na použití ArrayAdapteru.


ListView lvSeznam = (ListView) this.findViewById(R.id.prehled_zaznamu);
lvSeznam.setAdapter(new ArrayAdapter<Zaznam>(this, android.R.layout.simple_list_item_1, zaznamy));

Adaptér nastavíme metodou setAdapter(). Při vytváření ArrayAdapteru zadáváme parametry:

  • Context, tedy aktivitu, ve které bude Adapter použit

  • způsob formátování položky – můžeme použít
    android.R.layout.simple_list_item_1

  • a konečně List či pole s daty.

Data navíc musí mít vhodně překrytu metodu toString(), která bude použita pro nastavení textů jednotlivých položek ListView.


Aktualizace dat

Pokud dojde ke změně dat v ArrayListu, musíme o tom informovat ArrayAdapter tak, že zavoláme jeho metodu notifyDataSetChange(). ArrayAdapter zařídí aktualizaci zobrazených dat.


((ArrayAdapter) seznam.getAdapter()).notifyDataSetChanged();

Reakce na volbu položky

Reakci na stisk některé z položek ListView nastavujeme pomocí mechanismu posluchačů (Listener). V našem případě použijeme metodu setOnClickListener() třídy ListView a předáme jí jako parametry instanci potomka třídy OnItemClickListener.


Při použití anonymního posluchače (Listener) může kód vypadat například takto:


lvSeznam.setOnItemClickListener(new AdapterView.OnItemClickListener() {
    @Override
    public void onItemClick(AdapterView<?> adapterView, View itemView, int poradiPolozkyVAdapteru, long cisloRadkuVListView) {
        uprav(adapterView, itemView, poradiPolozkyVAdapteru,
                cisloRadkuVListView);
    }
});

Metoda uprav(…) je v tomto příkladě metoda třídy, která reprezentuje okno, ve kterém je ListView vnořen. V této metodě realizujeme samotnou reakci na stisk položky.


Jaké parametry dostaneme k dispozici:

  • adapterView je v našem případě instance ListView, ve které byla položka zvolena. Je tady proto, abychom mohli mít listener společný pro více ListView.

  • itemView je položka v ListView, která byla zvolena.

  • Číslo poradiPolozkyVAdapteru v parametru metody onItemClick(…) udává číslo položky, na kterou uživatel klikl. Jedná se o pořadí položky v adaptéru, tedy v našem případě v ArrayListu. Položky jsou číslovány od 0. Pod tímto číslem lze položku získat z ArrayListu metodou get(…).

  • Číslo cisloRadkuVListView udává číslo řádku v komponentě ListView, který byl zvolen. Řádky jsou také číslovány od 0 a v našem případě to tedy bude stejná hodnota jako poradiPolozkyVAdapteru.

Pokud tedy chceme získat záznam, který odpovídá zvolené položce, použijeme metody getItemAtPosition() třídy ListView (zděděnou ze třídy AdapterView, která je nepřímým předkem ListView):


Zaznam zaznam = (Zaznam) zvolenyView.getItemAtPosition(poziceVAdapteru);

Příklad: Můj pestrý život

Vytvoříme jednoduchou aplikaci, která bude umožňovat zaznamenat, co nového jsme zažili/naučili se/vyzkoušeli si. Budeme zaznamenávat jednotlivé poznámky. U každé poznámky uvedeme datum, co zajímavého se stalo a případně další údaje, komentáře atd. Při kliknutí na poznámku se poznámka zvýrazní jako důležitá, druhé kliknutí příznak důležitosti zruší.



 Aplikaci si můžete snadno upravit na jednoduchý tréninkový deník, ...



Jednotlivé položky/záznamy budou v aplikaci načteny do komponenty ListView. V dalších kapitolách doplníme možnost přidávat záznamy, upravovat je a mazat.


Celá aplikace bude ve výsledku vypadat takto:


Postup – implementace ListView:

  1. Vytvoříme nový projekt již známým způsobem. V XML návrhu vytvoříme komponentu ListView, která umožňuje zobrazit více položek najednou:

  2. <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        ...
        android:orientation="vertical">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Uložené záznamy:" />
        <ListView
            android:id="@+id/lv_seznam"
            android:layout_width="match_parent"
            android:layout_height="match_parent">
        </ListView>
    </LinearLayout>
    

  3. Přichystáme si třídu Zaznam s atributy nazev a popis (oba budou textové). Přidáme atribut dulezite (logická hodnota) a případně další atributy dle potřeby. Překryjeme metodu toString(), čímž umožníme správné zobrazení v ListView.

  4. public class Zaznam {
        String nazev;
        String popis;
        boolean dulezite;
        
        public Zaznam(String nazev, String popis, boolean dulezite) {
            this.nazev = nazev;
            this.popis = popis;
            this.dulezite = false;
        }
        
        public String toString() {
            String vysledek = this.nazev + "\n" + this.popis.substring(0,25) + "...";
            if (this.dulezite) vysledek = "[!] " + vysledek;
            return vysledek;
        }
    }
    

  5. Nachystáme si třídu Data, která bude zastřešovat operace nad kontejnerem jednotlivých záznamů. Zároveň ji v dalších kapitolách doplníme tak, aby zasílala informace o změnách v záznamech komponentě ListView. K tomu použijeme implementaci návrhového vzoru Pozorovatel (observer) v Javě. Třída Data zatím vytvoří několik záznamů „ručně“, později ji ale doplníme tak, aby data četla ze souboru.

  6. public class Data {
        ArrayList<Zaznam> data = new ArrayList<Zaznam>();
        protected void nactiData() {
            this.data = new ArrayList();
            Zaznam z;
            z = new Zaznam("Programování pro Android", "Tvorba aplikací pro Android.", true);
            this.data.add(z);
            z= new Zaznam("Hra na kytaru", "Základní akordy na kytaru.", false);
            this.data.add(z);
            z = new Zaznam("Terra Mystica", "Naučil jsem se novou deskovou hru.", false);
            this.data.add(z);
        }
        public ArrayList<Zaznam> getData() {
           return this.data;
        }
    }
    

  7. V hlavní aktivitě si vytvoříme instanci třídy Data a inicializujeme ji.

  8. Abychom mohli načíst jednotlivé záznamy do ListView, bude nejprve potřeba vytvořit datový adaptér, který se nám bude starat o data v ListView. Dále našemu seznamu (ListView) tento adaptér přiřadíme.
    Celý kód hlavní aktivity vypadá následovně:

  9. public class MainActivity extends AppCompatActivity {
        Data zaznamy;
        ListView seznam;
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            // Vytvoření a inicializace třídy Data:
            this.zaznamy = new Data();
            this.zaznamy.nactiData();
            
            // Vytvoření ListView a propojení s daty:
            this.seznam = (ListView) this.findViewById(R.id.lv_seznam);
            this.seznam.setAdapter(new ArrayAdapter<Zaznam>(this, android.R.layout.simple_list_item_1, this.zaznamy.getData()));
        }
    }
    

  10. V hlavní aktivitě také připravíme metodu aktualizuj(), která zařídí aktualizaci obsahu ListView při změně obsahu.
    K tomu použijeme metodu notifyDataSetChanged() adaptéru ArrayAdapter:

  11. private void aktualizuj() {
        ListView seznam = (ListView) this.findViewById(R.id.lv_seznam);
        ArrayAdapter<Zaznam> adapter = (ArrayAdapter<Zaznam>) seznam.getAdapter();
        adapter.notifyDataSetChanged();
    }
    

  12. Vyzkoušejte funkčnost komponenty ListView. Zatím nereaguje na volbu položky, ale položky by se již měly zobrazovat.

Postup – reakce na volbu položky:

Položky tedy máme načtené v komponentě ListView. Nyní budeme chtít zařídit, aby se při volbě položky změnila její důležitost. Projeví se to změnou popisu položky.

  1. Připravíme metodu zvolenaPolozka() do hlavní aktivity. Ta získá zvolený záznam z ListView a zavolá jeho metodu invertujDulezite() (tu jsme si již ve třídě Zaznam vytvořili):

  2. private void zvolenaPolozka(AdapterView<?> zvolenyView, View polozka, int poziceVAdapteru, long cisloRadkuUvnitrView) {
        Zaznam zaznam = (Zaznam) zvolenyView.getItemAtPosition(poziceVAdapteru);
        zaznam.invertujDulezite();
    }
    

  3. Přidáme v metodě onCreate() hlavní aktivity položku ListView OnClickListener.
    Použijeme anonymní listener, který zavolá metodu zvolenaPolozka():

  4. protected void onCreate(Bundle savedInstanceState) {
        ...
        this.seznam.setAdapter(new ArrayAdapter<Zaznam>(this, android.R.layout.simple_list_item_1, this.zaznamy.getData()));
        this.seznam.setOnItemClickListener(
            new AdapterView.OnItemClickListener() {
                    @Override
                    public void onItemClick(
                            AdapterView<?> zvolenyView, View polozka, 
                            int poziceVAdapteru, long cisloRadkuUvnitrView) {
                                zvolenaPolozka(zvolenyView, polozka, 
                                poziceVAdapteru, cisloRadkuUvnitrView);
            }
        });
    }
    

Shrnutí

  1. Uspořádání grafických prvků v okně mají na starosti komponenty souhrnně označované jako layouts.

  2. Dosud jsme používali především LinearLayout, který je vhodný pro jednoduché uspořádání prvků pod sebou nebo vedle sebe.

  3. Pro složitější návrhy je vhodné použít RelativeLayout, nejlépe s grafickým návrhářem vzhledu – například tím, který je součástí vývojového prostředí Android Studio.

  4. V této kapitole jsme ukázali a vyzkoušeli si layout ListView, který je vhodný pro zobrazení většího počtu položek, které se navíc mohou dynamicky měnit.

  5. Pro naplnění ListView položkami jsme použili ArrayAdapter, který bere položky z ArrayListu.

  6. Pokud se změní data v ArrayListu, informujeme ArrayAdapter metodou notifyDataSetChanged() a ten zajistí aktualizaci ListView.

  7. Reakci na volbu položky v ListView zajistí OnClickListener. Konkrétní zvolenou položku získáme metodou getItemAtPosition() třídy ListView.