2. stretnutie: Hangman / Obesenec

Plán

  • základy životného cyklu aktivity a zistíme, prečo v Androide neexistuje zatváracie tlačidlo
  • obrázky realizované ako resources
  • ukážeme si, ako možno využívať v aplikáciách bežný Java kód
  • naučíme sa notifikovať používateľa pomocou toastov.

Materiály

Kostra projektu

Kostra projektu obsahuje dva elementy. Predovšetkým v nej nájdete implementáciu logiky hry ("engine", ak chcete) reprezentovanú ako bežnú Java triedu, čo nám uľahčí vývoj a umožní sústrediť sa na samotné používateľské rozhranie v Androide. Popri tom sa v archíve nachádza sedem ukážkových PNG obrázkov pre stav šibenice. (Obrázky sú kreslené v Paintbrushi, pokojne si ich na domácu úlohu premaľujte.)

Stiahnite si kostru projektu, aby ste ju o pár krokov využili pri nastavení vášho projektu.

Používateľské rozhranie a.k.a. GUI

Debata o návrhu používateľského rozhrania je veľmi dôležitá. Neraz práve použiteľnosť aplikácie a jej výzor dokážu buď prilákať, alebo okamžite odstrašiť budúcich používateľov.

Aké ovládacie prvky by sme mohli vyyužiť? Budeme potrebovať:

  • zobrazovať práve hádané slovo,
  • vykresľovať šibenicu,
  • mať spôsob pre zadávanie písmena.
  • a vidieť zoznam písmen, ktoré sme už použili,

Postupne si prejdeme jednotlivé komponenty a ukážeme ich deklaráciu v súbore.

Layout

Ako sme sa naučili minule, deklarácia komponentov sa realizuje v XML súbore, tzv. layoute. V Android Studiu otvoríme súbor res\layout\activity_main.xml prepneme sa do z návrhového režimu do režimu Text, kde môžeme priamo editovať XML.

TextView pre hádané slovo

Predovšetkým budeme potrebovať komponent, v ktorom bude vidno hádané slovo, teda písmená, ktoré sme v ňom uhádli. Keďže pôjde o text, ktorý používateľ nebude mať možnosť priamo upravovať, využijeme label... akurát, že ten sa v Androide volá TextView. Čo s neuhádnutými písmenami? Budeme ich zobrazovať cez podtržníky.

<TextView
    android:id="@+id/foundLettersTextView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:textAppearance="?android:textAppearanceLarge"
    android:typeface="monospace" 
    />

Veľký text

Estetiku aplikácie zlepšíme zväčšením veľkosti textu. Namiesto uvádzania konkrétnej veľkosti v bodoch vieme využiť zabudovaný štýl pre zväčšené písmo. (Používateľ totiž môže v systéme globálne nastaviť zväčšené písmo pre všetky aplikácie a naša aplikácia by to mala zohľadniť.) Štýly v Androide sú konceptuálne podobné CSS štýlom. Môžeme si definovať vlastné štýly (teraz to robiť nebudeme), alebo môžeme využiť niektoré z ponuky Androidu.

Odkaz na zabudovaný štýl získame predponou ?android: a názvom štýlu -- v tomto prípade textAppearanceLarge reprezentuje "veľmi veľký text".

Neproporcionálne písmo

Podobne ako v prípade štýlov, vieme meniť ak použitú rodinu písma. Typ (typeface) monospace znamená neproporcionálne písmo (niečo ako strojopisný Courier).

ImageView pre šibenicu

Čo so šibenicou? Máme veľa možností: mohli by sme ju maľovať ručne, mohli by sme ju renderovať ako 3D šibenciu cez OpenGL, ale najjednoduchšia možnosť je využiť sedem obrázkov z kostry projektu, ktoré budú zodpovedať šiestim pokusom pre hádanie (nultý obrázok bude zobrazený na začiatku.)

Kopírovanie obrázkov do projektu: adresár drawable

Keďže Android Studio zatiaľ nepodporuje priamy import obrázkov do projektu, musíme to urobiť ručne. Skopírujte všetkých sedem obrázkov do adresára projektu (nájdete ho v hlavičke okna), do podadresára src/main/res/drawable. Na mojom vývojovom stroji patria obrázky do adresára

c:\Users\rn\AndroidStudioProjects\Obesenec\app\src\main\res\drawable

Adresár drawable obsahuje obrázky, ilustrácie a iné prvky, ktoré sa dajú priamo vykresliť na obrazovku. Obrázky v tomto adresári vieme priamo využiť pri deklarácii komponentu ImageView.

Deklarácia ImageView

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

Atribút src obsahuje odkaz na obrázok v adresári drawable. Odkaz @drawable/gallows0 smeruje na súbor res/drawable/gallows0.png. Prípony v tomto prípade nie sú podstatné, Android si ich dodá automaticky.

Zadávanie písmen

Každému by hneď napadlo, že písmená môžeme zadávať cez klávesnicu, ktorú vykreslíme používateľovi. Tým sa však vôbec nemusíme zapodievať, pretože klávesnicu dostaneme od systému automaticky. Stačí, že poskytneme používateľovi textové políčko, do ktorého bude môcť uviesť písmeno.

<EditText
    android:id="@+id/letter"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:textAppearance="?android:textAppearanceLarge"
    android:typeface="monospace" 
    android:hint="Guess a letter"
    android:maxLength="1"
    android:inputType="textCapCharacters"
    />

Textové políčko bude ukazovať veľké písmená typu monospace. Okrem toho uvedieme hint, teda text, ktorý sa zobrazí, ak políčko nebude obsahovať žiadne dáta. Aby sme používateľa nezmiatli, dáme mu možnosť uviesť len jedno písmeno (maximálna dĺžka jedna) a typ povolených písmen obmedzíme na veľké písmená (textCapCharacters).

Zároveň sa musíme zamyslieť nad zadávaním písmen: používateľ zadá písmeno a ak sa náhodou preklepne, tak ho môže opraviť. Ako však povieme aplikácii, že písmeno sme už naozaj zadali?

V našom projekte sme sa rozhodli urobiť toto potvrdenie kliknutím na šibenicu. Do deklarácie ImageView teda dodáme obsluhu pre onClick:

<ImageView
    ...
    android:onClick="gallowsImageViewClick"
/>

Do kódu aktivity potom dodáme metódu:

public void gallowsImageViewClick(View view)

Logika hry

Logika hry je reprezentovaná štandardnou Java triedou, ktorú môžeme presunúť do projektu. Na rozdiel od drawables obrázkov môžeme v tomto prípade použiť myš a drag-and-dropom presunúť triedu do adresára prislúchajúceho balíčku projektu,

Ak trieda využíva len triedy z Javy 5 a novšej, dá sa bez problémov vložiť do projektu. (Na pozadí sa skompiluje do .class súboru, ktorý sa prevedie do formátu .dex používaného virtuálnym strojom Dalvik.)

Trieda poskytuje nasledovné metódy:

  • CharSequence getGuessedCharacters() vráti reťazec s uhádnutými znakmi. Neuhádnuté znaky sú reprezentované podtržníkmi, čiže slovo uprostred hádania je _nt__t_r,
  • boolean guess(char character), ktorá vráti true, ak sa zadané písmeno nachádza v hádanom slove,
  • int getAttempsLeft(), ktorá vráti počet zostávajúcich pokusov na hádanie. Začíname so šiestimi pokusmi.
  • boolean isWon(), ktorá zistí, čí sme uhádli všetky písmená v slove,
  • String getChallengeWord(), ktorá vráti celé hádané slovo.

Nová inštancia hry znamená tiež vygenerovanie náhodného slova na hádanie. Ak chceme hádať iné slovo, musíme vytvoriť novú inštanciu hry.

Formátovanie hádaného slova

Stav hádania vieme získať z metódy getGuessedCharacters(). Ak sa háda povedzme štvorpísmenné slovo, na začiatku uvidíme štyri podtržníky vedľa seba, ktoré sa pri vykresľovaní zlejú do jednej vodorovnej čiary čo môže používateľa miasť. Preto si vytvoríme pomocnú formátovaciu metódu, ktorá vloží medzi každé dva znaky jednu medzeru.

private CharSequence formatLetters(CharSequence string) {
    StringBuilder result = new StringBuilder();
    for (int i = 0; i < string.length() - 1; i++) {
        result.append(string.charAt(i)).append(" ");
    }
    result.append(string.charAt(string.length() - 1));
    return result;
}

Programovanie hry

Programovanie aplikácie je v tejto chvíli práca, ktorá prepojí stavy hry so vzhľadom používateľského rozhrania. Väčšina sa bude odohrávať v metóde gallowsImageViewClick().

Na čo si dáme pozor:

  • ak sme vyčerpali pokusy, musíme upozorniť používateľa. Použijeme na to červený grafický filter šibenice, o ktorom si povieme nižšie.
  • musíme aktualizovať stav šibenice
  • ak používateľ nezadá žiadne písmeno, a klikne na šibenicu, upozorníme ho toastom
  • vyhratú hru zvýrazníme zelenými filtrom šibenice.

Zmena šibenice

Šibenica je reprezentovaná siedmimi obrázkami v adresári res/drawable. Čím menej pokusov ostáva, tým vyšší obrázok chceme zobraziť. Prakticky máme situáciu, keď potrebujeme pre itý zostávajúci pokus zobraziť (7-i)-ty obrázok.

K obrázkom, ktoré sú drawable, vieme pristúpiť cez klasickú pomocnú triedu R.drawable.[názovObrázkuBezPrípony].
Keďže nemáme rozumný spôsob, ako dynamicky vytvoriť názov používaného obrázku, vieme to obísť cez pole obrázkom, ktoré deklarujeme ako inštačnú premennú.

private int[] gallowsLayouts = {
        R.drawable.gallows0,
        R.drawable.gallows1,
        R.drawable.gallows2,
        R.drawable.gallows3,
        R.drawable.gallows4,
        R.drawable.gallows5,
        R.drawable.gallows6
};

Jednotlivé obrázky sú reprezentované odkazmi na súbory z adresára, ktoré sú v Androide reprezentované číslami int. (Ak poznáme C, môžeme si to predstaviť ako pointre do príslušného adresára.)

Toast

Toasty slúžia na stručné informačné oznámenia, ktoré používateľ zoberie na vedomie, a ktoré príliš neberú jeho pozornosť a fokus. Toasty netreba odklikávať a predstavujú tzv. flash notifications.

Toast.makeText(this, "You must enter a letter.", Toast.LENGTH_SHORT)
.show();

Na vytvorenie toastu potrebujeme metódu makeText(), ktorá potrebuje nasledovné parametre:

  • kontext typu Context: v tomto prípade predstavuje aktivitu, z ktorej vytvárame toast. Referenciu na súčasnú aktivitu získame z premennej this.
  • text, ktorý sa zobrazí v toaste
  • dĺžku zobrazenia. V triede Toast sú k dispozícii dve konštanty pre krátke (Toast.LENGTH_SHORT) a dlhé (Toast.LENGTH_LONG) trvanie zobrazenia

Farebné filtre

Pre indikáciu výhry a prehry budeme používať farebné filtre obrázka. Ak je hra vyhratá, šibenica ozelenie, a ak je prehratá, očervenie.

Červený filter vieme vytvoriť a nastaviť na obrázku nasledovne:

ColorFilter filter = new LightingColorFilter(Color.RED, Color.BLACK); 
gallowsImageView.setColorFilter(filter);

Ak chceme zrušiť filter, použijeme nullový filter.

Životný cyklus aktivity

Každá aplikácia má možnosť reagovať na zmeny životného cyklu. Bežným príkladom je metóda onCreate(), ktorá sa zavolá vo chvíli, keď je vytváraná nová, čerstvá, inštancia aktivity. (Konštruktory aktivít sa v Androide nepoužívajú.) V tejto metóde prebehne napríklad vytvorenie všetkých ovládacích prvkov.

Naša aplikácia však nereaguje na zmenu stavov korektne. Ak rozohráme hru a otočíme displej, aktivita sa zničí a vytvorí nanovo. (V skutočnosti otočenie displeja znamená zmenu konfigurácie systému, na čo aktivita nemôže reagovať inak než zničením a znovuspustením.) Prejaví sa to vygenerovaním nového slova, úplným resetom hry vrátane šibenice. To môže byť samozrejme buď pozitívne (ak prehrávame), ale vo väčšine prípadov neželané správanie.

Našťastie máme možnosť ukladať stav aktivity tak, aby hra dokázala prežiť aj úplné zničenie a obnovenie používateľského rozhrania.

Stav aktivity

Čo je v tomto prípade stav aktivity? Tvorí ho obsah textového políčka s písmenom, aktuálny obrázok šibenice a obsah políčka s uhádnutými písmenami. Stav aktivity, teda vzhľad používateľského rozhrania, v skutočnosti odráža stav objektu HangmanGame.

Ak aktivita odchádza na pozadie, máme možnosť uložiť stav aktivity, a to pomocou metódy onSaveInstanceState() a jej parametra typu Bundle.

Bundle

Bundle je mapa, do ktorej môžeme uložiť objekty, ktoré majú prežiť reštart aktivity. Predstaviť si ju môžeme ako zaváraninové poháre, do ktorých si odložíme požadované veci na "zimu", teda na moment, keď bude aktivita zničená.

Objekty uložené v bundle si môžeme potom vyzdvihnúť v metóde onCreate(), ktorá má identický parameter. Ak je aktivita zabitá a znovuvytvorená, v Bundle príde mapa s predošlými uloženými hodnotami. V opačnom prípade je parameter bundle rovný null.

Implementácia onSaveInstanceState()

Pri implementácii onSaveInstanceState() nezabudnime zavolať rodičovskú implementáciu cez super.onSaveInstanceState()!

Implementácia v šibenici

Ak celú triedu HangmanGame vyhlásime za Serializable, vieme ju ľahko pchať do Bundle cez metódu putSerializable() a podobne vytiahnuť z bundle cez getSerializable().

Ukladanie stavu komponentov

Stav komponentu, ktorý má priradený identifikátor (teda nastavené android:id), je automaticky ukladaný a obnovovaný bez toho, aby sme museli čokoľvek programovať

Platnosť uloženia stavu

Ukladanie stavu do bundle sa týka prechodného stavu, teda prípadov, keď potrebujeme uložiť a obnoviť stav aktivity medzi chvíľami, keď je aktívna a chvíľami, keď je na pozadí.

Ak chceme ukladať perzistentný stav, ktorý má pretrvať aj reštart telefónu, musíme využiť SQL databázu, súbory a pod. o čom si povieme v budúcnosti

Záver

Zdrojové kódy aplikácie sa nachádzajú na Githube.