Vyvíjíme pro Android: Bližší pohled na pohledy – 2. díl

Jak bychom mohli vyvíjet aplikace pro Android, kdybychom neuměli používat vestavěná View? Jak bychom tvořili uživatelské rozhraní, kdybychom neuměli pracovat s Layouty? V dnešním dvojčlánku si všechny důležité představíme a povíme si něco o jejich atributech a metodách.

Seriál: Vyvíjíme pro Android (14 dílů)

  1. Vyvíjíme pro Android: Začínáme 15.6.2012
  2. Vyvíjíme pro Android: První krůčky 22.6.2012
  3. Vyvíjíme pro Android: Suroviny, Intenty a jednotky 29.6.2012
  4. Vyvíjíme pro Android: Bližší pohled na pohledy – 1. díl 13.7.2012
  5. Vyvíjíme pro Android: Bližší pohled na pohledy – 2. díl 13.7.2012
  6. Vyvíjíme pro Android: Fragmenty a SQLite databáze 20.7.2012
  7. Vyvíjíme pro Android: Preference, menu a vlastní Adapter 27.7.2012
  8. Vyvíjíme pro Android: Intenty, intent filtry a permissions 3.8.2012
  9. Vyvíjíme pro Android: Content providery 10.8.2012
  10. Vyvíjíme pro Android: Dialogy a activity 17.8.2012
  11. Vyvíjíme pro Android: Stylování a design 24.8.2012
  12. Vyvíjíme pro Android: Notifikace, broadcast receivery a Internet 31.8.2012
  13. Vyvíjíme pro Android: Nahraváme aplikaci na Google Play Store 7.9.2012
  14. Vyvíjíme pro Android: Epilog 14.9.2012

Nevíte, kde jste? Možná si chcete nejprve přečíst první díl.

ViewGroup

Jak už jsme si několikrát řekli, ViewGroup dokáže na rozdíl od View jedno nebo více view obsahovat. Stará se potom o jejich rozmístění a vykreslení. A hlavně tím, jak jednotlivá view rozmisťují se liší potomci ViewGroup. Ale nejdříve ke vlastnostem samotné  ViewGroup.

Kromě atributů a metod zděděných od ViewViewGroup samozřejmě i nějaké vlastní. Atributy pro nás nejsou zajímavé žádné, z metod pouze ty, které nějakým způsobem manipulují s view, která má daná ViewGroup obsáhnout, a to (seznam bude přehlednější než tabulka):

  • addView se všemi svými signaturami, která přidá nové view. Nejjednodušší přebírá pouze objekt View, jejž umístí na poslední místo seznamu svých view,
  • View getChildAt(int index), která vrátí View na určené pozici (čísluje se od nuly) či null, pokud na té pozici žádné view není. Pozice se určují podle času přidání – které view bylo přidání dříve, to má pozici s nižším číslem. Nejprve se přidají view, která byla definovaná v layoutovém XML souboru společně s ViewGroup, potom se přidají ta z kódu,
  • int getChildCount(), jež vrátí počet obsažených view (chci se vyhnout slovu potomek, neboť to používám pro dědění v OOP a nerad bych omylem mátl už trochu zmateného čtenáře, takže budu raději používat obsažená view a tak podobně),
  • int indexOfChild(View child), která zjistí, na jaké pozici se nachází předané view,
  • void removeAllViews(), jež odebere všechny potomky,
  • void removeView(View view), která odebere předané view,
  • void removeViewAt(int index), jež odebere view na určené pozici
  • a void removeViews(int start, int count), která odebere count view počínaje tím na pozici  index.

Tím máme hotovou třídu ViewGroup a žádný ukázkový kód s ní nebude, neboť je abstraktní. Ale ještě než se pohneme dál, zodpovím otázku, která vás už určitě napadla: Jak můžu já předat nějaké ViewGroup informace o tom, jak přesně chci uspořádat jí obsažené view? To je výborná otázka a určitě vás potěší, že odpověď už vlastně skoro znáte.

LayoutParams

Třídy pojmenované jako LayoutParams jsou nested classes (vnitřní třídy?) ViewGroup či jejího potomka, které definují takzvané layout atributy, což jsou atributy s předponou layout_, které se nastaví každému obsaženému view, ale pracuje s nimi právě obalující ViewGroup.

Takže android:layout_width a android:layout_height, které musíme používat už od začátku, nejsou žádná speciální magie, ale jsou to (mimochodem jediné) atributy definované v ViewGroup.LayoutParams. Tamtéž jsou definovány tři speciální konstanty MATCH_PARENT, FILL_PARENT a WRAP_CONTENT (z nichž první dvě mají stejnou hodnotu, což dokazuje, že jsou to opravdu aliasy, pokud mi někdo nevěřil), které se použijí, potřebujete-li layout atributy nastavit nějakému view vytvořenému za běhu. U většiny Layoutů dokonce stačí nastavit právě tyto dvě základní hodnoty. Jak se může takové úplně jednoduché view vytvořit a vložit do nějaké skupiny?

View v = new View(this);
v.setBackgroundColor(Color.BLUE);
v.setLayoutParams(new ViewGroup.LayoutParams(
        ViewGroup.LayoutParams.MATCH_PARENT, 30));

((ViewGroup) findViewById(R.id.view_group)).addView(v);

Na jeden problém ale narážíme hned na začátku. Při objekt LayoutParams totiž v jednom ze svých konstruktorů přijímá dva integery, sířku a výšku. Ale v pixelech. Jak to převést?

public float dpToPixels(int dp, Context ctx) {
    Resources r = ctx.getResources();
    return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,
            r.getDisplayMetrics());
}

Nemyslím, že je až tak stěžejní tomu přesně rozumět, pokud vás to zajímá, odkazuji na Android Developers.

Třída ViewGroup má na rozdíl od svých potomků dokonce dvě třídy definující layoutové atributy. Výše jmenovanou ViewGroup.LayoutParams a potom ViewGroup.MarginLayoutParams, která od prvně jmenované třídy dědí a od níž dědí všechny třídy definující layoutové parametry těch potomků ViewGroup, které si dnes představíme (jinak řečeno u všech potomků ViewGroup, o nichž budeme dnes mluvit, můžete použít jak rozměrové layoutové parametry, tak marginy. Výjimku tvoří třída, o níž budeme mluvit jako o poslední, a která se od těch ostatních liší).

ViewGroup.MarginLayoutParams definuje čtyři nové layoutové parametry: android:layout_marginTop, android:layout_marginLeft, android:layout_marginTopBottom a android:layout_marginRight. Každý z nich přijímá jako hodnotu nějaký rozměr (nebo odkaz na rozměr z resources). Tyto atributy nastavují vnější okraje. Jsou víceméně totožné s CSS margin. Zatímco android:padding je atribut definovaný na View a určuje vnitřní okraje, view zabírá stejně místa s jakýmkoli paddingem a místo se ubírá jenom obsahu toho view, nastavením marginu zvětšíte místo, které view zabírá, zatímco view vůbec nebude vědět o tom, že má nějaké vnější okraje. Nejlépe to asi vysvětlí obrázek a kód.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <TextView
        android:layout_width="match_parent"
        android:layout_height="40dp"
        android:background="@color/blue"
        android:text="Bez okrajů" />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="40dp"
        android:background="@color/red"
        android:padding="12dp"
        android:text="S paddingem" />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="40dp"
        android:background="@color/blue"
        android:text="Bez okrajů" />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="40dp"
        android:background="@color/red"
        android:layout_marginTop="30dp"
        android:layout_marginLeft="10dp"
        android:text="S horním a levým marginem" />

</LinearLayout> 

Text u view s paddingem je dole trochu useknutý, neboť u něj pro samé okraje nezbylo místo.

Než se vrhneme na potomky ViewGroup, ještě vás upozorním na to, že layout atributy se vždycky týkají přímého jen toho ViewGroup, který to určité view přímo obaluje, a žádného jiného (předpokládejme, že máme strom, v němž se vyskytuje více ViewGroup). Krásně to ilustruje obrázek v Podkapitole Layout Parameters článku Layouts v dokumentaci.

Potomky ViewGroup, kteří jsou nějak určení pro uspořádávání dalších view, nazývám Layouty (s velkým L), neboť všechny na Layout končí. A těm se budeme věnovat teď. Jinak má ale ViewGroup mnoho přímých či nepřímých potomků, z nichž naprostou většinu v praxi budete používat stejně jako různá view, o nichž jsme už mluvili, tzn. tak, že nebudou obalovat žádná další view. Mezi ně patří například DatePicker a TimePicker, které budete používat ve formě dialogů (o dialozích si povíme už brzy), NumberPicker, RadioGroup, Spinner a úplně speciálně mimo stojí WebView, které je sice mocné, ale často se nesprávně používá pro zobrazení nějaké webové stránky v rámci aplikace, přestože by bylo uživatelsky mnohem přívětivější nechat stránku Intentem otevřít v prohlížeči. Dalším speciálním případem jsou potomci AdapterView (mezi něž patří i ListView, o němž se dnes zmíníme), což jsou view umožňující zobrazit libovolné množství typově stejných dat. Zní to složitě, v příštím či popříštím dílu se k tomu dostaneme detailně. A nakonec je to třída ScrollView, což je potomek FrameLayoutu a umožňuje zobrazit view větší jež jsou rozměry displeje a to view různě scrollovat. Mnoho dalších potomků můžete najít v dokumentaci.

FrameLayout

FrameLayout je nejjednodušší a nejhloupější z Layoutů. Je určen k tomu, aby zabral místo na displeji pro zobrazení jednoho view. Lze jich do něj sice umístit nekonečně mnoho, ale pokud nepoužijeme layoutový atribut layout_gravity, k němuž se dostaneme za chvilku, budou všechny v horním levém rohu a budou se překrývat tak, že navrchu bude poslední přidané view.

FrameLayout.LayoutParams

Stejně jako všechny ostatní LayoutParams, o nichž se dnes budeme bavit, dědí od ViewGroup.MarginLayoutParams a tím pádem můžete obsaženým view nastavit velikosti a marginy. Kromě toho přidává jen jeden parametr, a to android:layout_gravity, jehož hodnoty jsou shodné s android:gravity  u TextView, akorát místo pozice textu v rámci view určují pozici view v rámci FrameLayoutu. Výchozí hodnota je top|left, vlevo nahoře.

Ukázka

<?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="match_parent" >

    <View
        android:layout_width="250dp"
        android:layout_height="250dp"
        android:background="@color/white" />

    <View
        android:layout_width="150dp"
        android:layout_height="150dp"
        android:background="@color/green" />

    <View
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:background="@color/blue" />

    <View
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:layout_gravity="bottom|right"
        android:background="@color/red" />


</FrameLayout>

Tři view vlevo nahoře se překrývají.

LinearLayout

LinearLayout už známe. Na rozdíl od FrameLayoutu je přímo určen k tomu, aby obsahoval více view. Ta skládá buď pod sebe, anebo vedle sebe, v závislosti na hodnotě atributu android:orientation (či metodě setOrientation(int orientation)), který může nabývat hodnot horizontal a vertical. Dále je tu už nám známý atribut android:gravity, který určuje, k jaké straně budou „přitahována” obsažená view. Tím přehled atributů a metod skončíme, ostatní už tak kriticky důležité nejsou.

LinearLayout.LayoutParams

Třída LinearLayout.LayoutParams opět dědí od ViewGroup.MarginLayoutParams, takže můžeme nastavit rozměrové (ty dokonce musíme) a marginové atributy. Dále nabízí atribut android:layout_gravity, což je opět náš známý gravitační atribut, ale umožňuje nastavit pro každé obsažené view různou hodnotu.

Tady se na chvíli pozastavím. layout_gravity jsem nikdy nepoužil jiným způsobem než nastavit vertikální umístění u horizontálního Layoutu anebo opačně, horizontální umístění u vertikálního layoutu. Když jsem teď chvíli experimentoval, vykazovalo to zvláštní chování, které jsem ale nikde zdokumentované neviděl. Na studium zdrojových kódů nemám čas, takže vzhledem k tomu, že pozicování pomocí layout_gravity je stejně nepohodlné a za chvíli si řekneme o mnohem lepší variantě, doporučím vám nepoužívat gravitační atributy složitě. Přidávat všechna view odspoda, prosím. Zarovnat je na střed či doleva, pohodička. Mít jedno view vlevo, druhé vpravo (u vertikálního Layoutu), ještě dejme tomu. Ale snažit se mít jedno nahoře a druhé dole, to je cesta do pekel.

Poslední layoutový atribut, který LinearLayout nabízí, je velmi zajímavý. android:layout_weight umožňuje nastavit jednotlivým view poměrnou velikost tak, aby dohromady zabraly celý LinearLayout. Co to znamená? Jsou dvě možnosti, jak to použít.

Mějme formulář skládající se ze čtyř prvků – dva jednořádkové EditTexty, potom jeden EditText, do kterého se má uživatel rozepsat, a tlačítko na odeslání. Chceme-li třetí vstupní políčko roztáhnout tak, aby dohromady formulář zabírat celou výšku displeje, bez znalosti layout_weight to není triviální. S ní však stačí přidat tomu třetímu EditTextu atribut layout_weight="1" a layout_height mu nastavit na 0dp, to z důvodů efektivity. Kdybychom nastavili jinou výšku, zbytečně by se nejprve počítalo s ní, pak by se vypočítala nová na základě layout_weight a všechno by se dělalo znovu s ní. A to je vše.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Vaše míry"
        android:inputType="text"
        android:maxLines="1" />

    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Vaše telefonní číslo"
        android:inputType="phone"
        android:maxLines="1" />

    <EditText
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:gravity="top|left"
        android:hint="Jak vypadáte po ránu?"
        android:inputType="textMultiLine" />

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="right"
        android:text="Zažádat o zaslání formuláře 'Žádost o registraci vozidla'" />

</LinearLayout>

Jen vás upozorním na inputType="textMultiLine" u třetího EditTextu, který umožní text rozdělit na více řádků a používat enter pro konec řádku (a ne přechod na následující formulářový prvek). gravity jsem mu nastavil na top|left, neboť ve výchozím nastavení byla left|center_vertical, což nevypadalo hezky.

Druhá možnost, jak použít layout_weight (a zjistíme, že ta první je vlastně jen speciální variantou druhé), je rozdělit volný prostor mezi několik view v nějakém poměru. Ve skutečnosti to totiž funguje takto:

  1. Najdi všechna view, která mají layout_weight nastavený na nulu (výchozí hodnota), a těm ponech výšku, kterou si zvolili.
  2. Spočítej zbývající dostupnou výšku.
  3. Sečti váhy všech obsažených view (samozřejmě jen přímo obsažených, neprohledávej nic rekurzivně), anebo se podívej na  android:weightSum.
  4. Vyděl zbývající dostupnou výšku zjištěnou sumou vah a toto číslo nazvi třeba „jednotková_výška”.
  5. Každému obsaženému view s nenulovou váhou nastav výšku na  weight × jednotková_výška.

Můžeme například vytvořit čtyři různobarevná view, která budou dohromady zabírat celou šířku displeje, a to v poměru 1:2:2,5:3,141592653. Zároveň dáme uživateli možnost klepnutím na některé z nich ho přesunout nahoru či dolů, podle toho, kde bylo předtím:

public class LinearLayoutActivity extends Activity {

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

    public void changeLayoutGravity(View v) {
        LinearLayout.LayoutParams params = (LinearLayout.LayoutParams)v.getLayoutParams();
        int newGravity = (params.gravity == Gravity.TOP) ? Gravity.BOTTOM : Gravity.TOP;
        params.gravity = newGravity;
        v.setLayoutParams(params);
    }
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal" >

    <View
        android:layout_width="0dp"
        android:layout_height="100dp"
        android:layout_gravity="bottom"
        android:layout_weight="1"
        android:background="@color/blue"
        android:onClick="changeLayoutGravity" />

    <View
        android:layout_width="0dp"
        android:layout_height="100dp"
        android:layout_gravity="bottom"
        android:layout_weight="2"
        android:background="@color/green"
        android:onClick="changeLayoutGravity" />

    <View
        android:layout_width="0dp"
        android:layout_height="100dp"
        android:layout_gravity="top"
        android:layout_weight="2.5"
        android:background="@color/red"
        android:onClick="changeLayoutGravity" />

    <View
        android:layout_width="0dp"
        android:layout_height="100dp"
        android:layout_gravity="bottom"
        android:layout_weight="3.141592653"
        android:background="@color/white"
        android:onClick="changeLayoutGravity" />

</LinearLayout>

Tentokrát jsme použili LinearLayout s horizontální orientací. 

Pozor si dávejte akorát na řetězení layout_weight (Eclipse vás upozorní). Pokud máte vnější LinearLayout, v něm třeba nadpis a další LinearLayout, který má nastavenou váhu na 1 a obsahuje čtyři view, každé z nich s váhou 1, musí se nejprve spočítat rozměry vnitřního LinearLayoutu a na základě nich se pak počítají rozměry jemu obsažených view. To je značně neefektivní. V tomto případě se to dá úplně jednoduše obejít odstraněním vnitřního LinearLayoutu. Ne vždy je sice řešení tak zjevné, ale snažte se toho vyvarovat.

RelativeLayout

RelativeLayout je posledním Layoutem dnešního dvojčlánku ( GridLayout považuji za nedůležitý a TableLayout za více méně zbytečný).

RelativeLayout umisťuje obsažené view ne vedle sebe, ani pod sebe či na sebe, ale dovolí vám nastavit jejich pozice v závislosti na pozících ostatních view. Můžete tedy například říci, že toto view má být nad tamtím (tzn. že horní strana tamtoho a spodní tohoto leží ne stejné přímce) nebo že tuhleto view se má vycentrovat.

Žádné speciální zajímavé metody ani atributy RelativeView  nemá.

RelativeLayout.LayoutParams

Kromě toho, že dědí od ViewGroup.MarginLayoutParams vám mohu o třídě RelativeLayout.LayoutParams říci, že se všechny jí (a že jich není málo) definované atributy kromě dvou dají rozdělit do tří skupin.

Centrovací atributy

Začneme tou nejjednodušší, a to centrovacími atributy, které jsou tři: android:layout_centerHorizontal, android:layout_centerInParent a android:layout_centerVertical. První určuje, zda má být view vycentrováno horizontálně, třetí vertikálně a centerInParent je vlastně jen zkratka pro centerHorizontal a centerVertical dohromady. Všechny tři přijímají pravdivostní hodnotu.

<?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="match_parent" >

    <TextView
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:layout_centerInParent="true"
        android:background="@color/blue"
        android:text="centerInParent" />

    <TextView
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:layout_centerVertical="true"
        android:background="@color/red"
        android:text="centerVertical" />

    <TextView
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:layout_centerHorizontal="true"
        android:background="@color/grey"
        android:text="centerHorizontal" />

</RelativeLayout>

Takhle vypadají vycentrovaná view.   

Pozicování vůči RelativeLayoutu

Tyto čtyři atributy, které všechny začínají na alignParent a liší se pouze určením místa, také přebírají pravdivostní hodnotu a určují, zda levá (horní, pravá, spodní) strana view bude ležet na stejné přímce s levou (horní, pravou, spodní) stranou RelativeLayoutu. Ty atributy se jmenují:

Než si ukážeme zase nějaký kód, ještě podotknu, že nemá-li v RelativeLayoutu view specifikovaný žádný požiční atribut (což je chyba), umístí se doleva nahoru. Pokud má specifikované umístění třeba jen vertikální, horizontálně bude úplně vlevo. Myslím si, že je docela dobrým zvykem vždy (ačkoli já to teď porušuju, abych ukázal, jak RelativeLayout funguje) nastavit jak pozici horizontální, tak vertikální, i kdyby to mělo být alignParentTopalignParentLeft.

<?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="match_parent" >

    <TextView
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:layout_alignParentTop="true"
        android:layout_centerHorizontal="true"
        android:background="@color/blue"
        android:text="alignParentTopncenterHorizontal" />

    <TextView
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:layout_alignParentLeft="true"
        android:background="@color/grey"
        android:text="alignParentLeft" />

    <TextView
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:layout_alignParentBottom="true"
        android:background="@color/violet"
        android:text="alignParentBottom" />

    <TextView
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:layout_alignParentRight="true"
        android:background="@color/red"
        android:text="alignParentRight" />


</RelativeLayout>

Schválně, koho napadla otázka, co se stane, když nastavíalignParentTop  i alignParentBottom na true ?

  

Pozicování vzhledem k ostatním view

Těchto atributů je nejvíce. Všechny jako parametr přebírají id nějakého jiného view, které se nachází ve stejném RelativeLayoutu a zarovnají se podle něho. Dají se rozdělit ještě na další dvě skupiny, a to atributy začínající na align, kdy například android:layout_alignLeft umístí levé strany obou view na stejnou přímku, a atributy nezačínající na align  – pokud má view A nastaveno android:layout_below na id view B, bude A umístěno pod B, což znamená, že na stejné přímce bude ležet spodní strana B a horní strana A. Všimněte si, že pořád říkám na stejné přímce, protože jde opravdu jen o v tomhle případě vertikální pozicování. O tom, jak vlevo nebo vpravo bude A, vůbec nic neříkáme.

Dva poslední odpadlíci

Prvním z nich je android:layout_alignBaseline. Ten srovná baseliny textů, takže nemá smysl pro netextová view. Já ho zatím nikdy nepoužil. Hezké vysvětlení je na Stack Overflow.

Druhým je android:layout_alignWithParentIfMissing, který zařídí, že není-li přítomno view s id, které jsme předali nějakému align atributu, zarovná se místo toho vzhledem k RelativeLayoutu. Poněvadž nám Eclipse nedovolí v XML souboru takhle předat nepřítomné id, týká se tento atribut jen vytváření view v programu, což nedoporučuju. Pakliže potřebujete nastavit více různých atributů a layout atributů, je určitě lepší použít LayoutInflater, s nímž začneme pracovat už brzy.

Tím pádem máme hotový i poslední Layout a zbývají nám už jen dvě poslední view.

ScrollView

ScrollView je potomkem FrameLayoutu a je jednoduché a přitom velmi užitečné. Stejně jako FrameLayoutu bude bez problému obsahovat jen jedno view, tím view však může být klidně nějaký Layout s mnoha view. A co dělá tak úžasného? ScrollView umožní svému potomkovi být jak chce vysoký ( HorizontalScrollView umožní libovolnou šířku a toto je poslední zmínka o něm – není zdaleka tak časté) a uživateli zase prohlédnout si toho potomka celého, tím, že mu umožní scrollovat.

ScrollView nemá žádné speciální vlastní layoutové parametry, jenom jeden atribut a pár zajímavých, ale jednoduchých metod (jeden atribut a jedna metoda, které jsou definované už na  View).

Nejjednodušší použití ScrollView vypadá asi takto:

<ScrollView
    android:layout_width="333dp"
    android:layout_height="666dp" >

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/some_long_text" />

</ScrollView>

Je potřeba si uvědomit, že rozměry, které chcete, aby výsledek ve skutečnosti zabral (zde 333 dp × 666 dp), musíte nastavit na ScrollView. Jeho obsah musí mít výšku určitě wrap_content. Se šířkou to záleží na vás, tam je dokonce možné na ScrollView nastavit wrap_content a fixní šířku nastavit u obsaženého view, nicméně to kvůli konzistenci nedoporučuju a obsaženému view nastavte buď wrap_content nebo match_parent, pokud nemáte úplně speciální důvod udělat to jinak.

Dejme tomu, že ale potřebujete, aby bylo ScrollView nascrollované až na polovině. K tomu slouží atribut android:scrollY, který je právě definován už na View. Ve skutečnosti totiž jakékoli view může být někam odscrollované, a to jak horizontálně, tak vertikálně, ScrollView k tomu přidává scrollbar a umožňuje uživatelskou interakci prakticky bez práce.

Pokud potřebujete někam přescrollovat ScrollView už za běhu, máte na výběr z několika metod: scrollBy(int dx, int dy), u níž v případě ScrollView za dx dosadíte 0 a která posune o  dx doprava ( dx může být záporné) a o  dy dolů. Potom existuje metoda scrollTo(int x, int y), která posune ScrollView na souřadnice x, y. Tyto metody mají ekvivalenty smoothScrollBy(int dx, int dy) resp. smoothScrollTo(int x, int y), které scrollují hezky, plynule, za předpokladu, že je to povoleno.

Jen si dovolím ukázat, jak zjistit rozměry obsahu ScrollView:

ScrollView sw = (ScrollView)findViewById(R.id.scroll_view);
int totalHeight = sw.getChildAt(0).getHeight();

Posledním dnešním atributem ScrollView je android:fillViewport. To zjednodušeně řečeno způsobí to, že je-li obsah ScrollView menší (nižší) než ono samo, ten obsah se rotzáhne (jen na výšku) tak, aby zaplnil celé ScrollView. Pro praktickou ukázku a detailnější popis vás odkážu na ScrollView’s handy trick.

Programujeme se ScrollView

public class ScrollViewActivity extends Activity {

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

    public void scroll(View v) {
        ScrollView sw3 = (ScrollView) findViewById(R.id.scroll_view_3);
        ScrollView sw1 = (ScrollView) findViewById(R.id.scroll_view_1);

        int totalHeight3 = sw3.getChildAt(0).getHeight();
        int totalHeight1 = sw1.getChildAt(0).getHeight();

        sw3.scrollTo(0, totalHeight3 / 2);
        sw1.smoothScrollTo(0, totalHeight1 / 2);
    }
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <ScrollView
        android:id="@+id/scroll_view_1"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" >

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/long_text_1" />
    </ScrollView>

    <ScrollView
        android:id="@+id/scroll_view_2"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:fillViewport="true" >

        <View
            android:layout_width="50dp"
            android:layout_height="1dp"
            android:layout_gravity="center_horizontal"
            android:background="@color/violet" />
    </ScrollView>

    <ScrollView
        android:id="@+id/scroll_view_3"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" >

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/long_text_2" />
    </ScrollView>

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:onClick="scroll"
        android:text="@string/submit" />

</LinearLayout>

Ta prostřední růžová či fialová čára je jen ukázka, jak fungujefillViewport .

AdapterView

AdapterView<T extends Adapter> je abstraktní třída, od níž dědí například ListView nebo třeba Spinner, což jsou třídy, které budete používat velmi často.

Pořádně si vysvětlit všechno důležité a zajímavé na AdapterView už dnes rozhodně nestihneme, tyto třídy budou hrát jednu z hlavních rolích v následující dvou (a možná více) článcích, ale proč si ji alespoň nepředstavit? Navíc toho využijeme k tomu, že sjednotíme všechny dnes vytvořené activity do jedné aplikace, jejíž hlavní activitou bude právě activity obsahující  ListView.

Ale postupně. Představte si, že chcete vytvořit úkolníček. To je aplikace, jejíž hlavní activitou bude seznam úkolů. Každý úkol má titulek a text. Úkoly se dají přidávat, ty, které jsou splněné, se potom odeberou tak, že na jeho titulku v seznamu uživatel podrží prst a z objevivšího se menu vybere Odstranit. Úprava proběhne zhruba stejným způsobem, po zvolení Upravit se otevře activity se stejným formulářem, jaký je pro vytvoření, ale už v něm budou vyplněna výchozí data.

Představme si, že máme nějaký proměnný počet úkolů, každý z nich má titulek a text. V seznamu chceme zobrazit jen titulky. Na každý z nich půjde klepnout a pak se zobrazí celý text, v nové activity.

Jak to uděláme? Použijeme LinearLayout. Potom v kódu projdeme seznam všech úkolů, pro každý vytvoříme TextView, jehož textem bude titulek úkolu. Každému TextView nastavíme onClick="itemClicked", povolíme, aby se na něj klepat vůbec dalo. Potom mu musíme jako tag nastavit id úkolu, abychom v metodě itemClicked věděli, na kterou položku to uživatel vlastně klepl. Každému musíme přidat i posluchače dlouhého kliknutí, v něm opět zjistíme id, ale budeme muset vytvořit nějaký dialog (s těmi se někdy seznámíme) s dalším seznamem, v němž budou možné úkony. Celý LinearLayout vložíme do ScrollView, aby mohl uživatel mít i více úkolů, než kolik se mu jich vejde na obrazovku.

A tím jsme vlastně vytvořili hodně primitivní a znovunepoužitelné  ListView.

Adapter

Co to je takový Adapter? Je to třída implementující rozhraní Adapter. A co umí? Dostane data v nějakém formátu, třeba pole objektů. Nebo seznam. Nebo Cursor. Ta data mají společné to, že je to neznámý počet stejných typů dat. Takže si můžeme představit třeba pole objektů Ukol, který má dvě položky, a to titulek a text. Adapter si ta data uchová a protože jsou stejného typu, umí každé z nich převést na view. Nějaké AdapterView si potom od Adapteru vyžádá ta view, která potřebuje (podle jejich pozice), která nějak zobrazí. To je hodně zjednodušeně  Adapter.

AdapterView kromě vyžádání, uspořádání a zobrazení jednotlivých view většinou má i metody na jednoduchou práci s událostmi – například předat callback, který se zavolá při klepnutí na některou z položek, či možnost jednoduše vytvořit popup menu při podržení některé z položek a tak podobně.

ListView zobrazuje položky ve vertikálním seznamu a nabízí mimojiné metodu setOnItemClickListener, která právě nastaví posluchače události pro kliknutí na libovolnou položku. Detaily nás teď nezajímají.

Nás teď zajímá, že existuje třída ListActivity, což je potomek Activity, který už obsahuje ListView a disponuje mnoha šikovnými metodami pro práci s ním.

Také nás zajímá, že existuje třída ArrayAdapter<T>, speciální Adapter, který umí pracovat s poli objektů, které se dají převést na řetězec (samozřejmě i řetězci). V konstruktoru přebere Context, odkaz na layout resource, který obsahuje právě jedno TextView, a pole instancí třídy  T.

Tím pádem už můžeme vytvořit takovou ListActivity, která zobrazí seznam všech dnes vytvořených activit: Vytvořte si nový projekt, překopírujte do něj dnes vytvořené třídy, layouty a další soubory, nezapomeňte je všechny zapsat do Manifestu. Jako výchozí třídu si vytvořte PohledyActivity a její kód nahraďte následujícím, který určitě pochopíte. Jen podotknu, že Android framework nabízí i některé často používané layouty, android.R.layout.simple_list_item_1 je odkaz na layout obsahující jedno TextView a nic více. Kdybyste chtěli, můžete si samozřejmě vytvořit vlastní a změnit velikost a barvu textu a třeba přidat nějaký padding a tak dále. ContentView nenastavujte! Přepsali byste to výchozí, s ListView. Samozřejmě to lze, ale jsou vyžadovány některá konkrétní view s konkrétními id.

package com.example.pohledy;

// Importy

public class PohledyActivity extends ListActivity {

    // Pole jmen dnes vytvořených tříd
    protected static String[] names = new String[]{
        "ViewActivity",
        "TextViewActivity",
        "EditTextActivity",
        "ProgressBarActivity",
        "MarginExampleActivity",
        "FrameLayoutActivity",
        "LinearLayoutFormActivity",
        "LinearLayoutActivity",
        "RelativeLayoutCenteringActivity",
        "RelativeLayoutAlignParentActivity",
        "RelativeLayoutAlignViewActivity",
        "ScrollViewActivity"
    };

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Nenastavujeme contentView!

        // Vytvoříme ArrayAdapter
        ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
                android.R.layout.simple_list_item_1, names);
        // android.R.layout.simple_list_item_1 je vestavěný layout s jedním
        // TextView

        // Řekneme ListActivity, ať svému ListView nastaví náš Adapter
        setListAdapter(adapter);
    }
}

Parádní, funguje to. Jen by se ještě hodilo, kdyby se na jednotlivé položky dalo kliknout, a to by vždy spustilo odpovídající třídu. I na to tvůrci ListActivity mysleli – stačí přepsat metodu protected void onListItemClick(ListView l, View v, int position, long id). Z jejích parametrů nás bude zajímat jen position, který odpovídá pozici názvu třídy v poli names. Vytvoříme si tedy ještě jedno statické pole, tentokráte pole tříd tak, abychom mohli vytvořit Intent se správnou třídou a pak zažádat o její spuštění:

// Pole tříd dnes vytvořených tříd - kvůli tvorbě intentu
protected static Class[] classes = new Class[]{
    ViewActivity.class,
    TextViewActivity.class,
    EditTextActivity.class,
    ProgressBarActivity.class,
    MarginExampleActivity.class,
    FrameLayoutActivity.class,
    LinearLayoutFormActivity.class,
    LinearLayoutActivity.class,
    RelativeLayoutCenteringActivity.class,
    RelativeLayoutAlignParentActivity.class,
    RelativeLayoutAlignViewActivity.class,
    ScrollViewActivity.class
};

// ...

@Override
protected void onListItemClick(ListView l, View v, int position, long id) {
    // position odpovídá pozici jméná třídy v names, a tedy i pozici třídy
    // třídy v classes
    Intent i = new Intent(this, classes[position]);
    startActivity(i);
}

Tím jsme aplikaci dokončili, všechno funguje a my si můžeme konečně jednoduše vyzkoušet všechna dnes vytvořená view.

Procvičování

Hrajte si. Vyzkoušejte, jak se chová RelativeLayout za různých situací. Co když do pozicovacího layoutového atributu nějakého view dám jeho vlastní id? Co zkombinovat tohle a támhleto?

Také doporučuji pročíst si můj článek Dej Androidu tablety!Fragmentech. Příště bychom s nimi totiž měli začít trochu pracovat, ale všechno půjde velmi rychle a budu spíše předpokládát, že máte odkázaný článek přečtený, ač třeba ne úplně stoprocentně pochopený.

Závěr

Ve dnešním dílu jsme si představili jednotlivá view, ukázali jsme si, jaké mají atributy, jaké metody, jak s nimi pracovat, na co si dát pozor a co vůbec nedělat. Snažil jsem se nekopírovat tupě dokumentaci, přidávat vždy své zkušenosti, nějakou radu nebo tip. Také jsem dokumentaci hodně osekal na ty nejčastěji používané metody a atributy. Rozhodně si nemyslím, že si musíte všechny třídy, metody a atributy, co jsme dnes zmínili, zapamatovat. Stačí tušit, že existují a vědět, že je zde najdete. Jsem ale přesvědčen, že si to postupně všechno zapamatujete, neboť tohle je opravdu denní chléb androidího vývojáře.

Zdrojové kódy

Zdrojové kódy ke dnešním ukázkám se vám, myslím, budou hodně hodit, abyste si mohli vyzkoušet jednotlivé věci, něco trochu pozměnit, poupravit anebo někam zkopírovat.

Příště (a nejen příště) si budeme povídat o různých možnostech ukládání dat, Fragmentech, pořádněji si projdeme AdapterView a ostatní věci, které s tím tematicky souvisejí.

Tip na konec

Při psaní atributů v layoutových souborech (v Eclipse) nemusíte psát namespace android. Stačí napsat samotný název atributu (např. layout_width) a z napovídače ( Ctrl + mezerník) vybrat odpovídající hodnotu včetně prefixu.

Matěj začal programovat ve třinácti v PHP, pak v JavaScriptu a Lispu. Nakonec si koupil Androida, a tak programuje hlavně pro něj.

Věděli jste, že nám můžete zasílat zprávičky? (Jen pro přihlášené.)

Komentáře: 3

Přehled komentářů

semik.m Nested class, zdrojové kódy..
Vojtěch Sejkora Korektura
Vojtěch Sejkora Re: Korektura
Zdroj: https://www.zdrojak.cz/?p=3682