Vyvíjíme pro Android: Suroviny, Intenty a jednotky

V dnešním článku, který je součástí seriálu Vyvíjíme pro Android, si povíme něco o délkových jednotkách v Androidu, naučíme se pracovat s resources, naučíme se spouštět activity a pracovat s view.

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

minulém článku jsme si vytvořili aplikaci o jedné activity a dvou view. Dnes přidáme další activity, řetězce správně umístíme do resources, o nichž si toho plno povíme, a nakoukneme pod kapotu toho, jak Android počítá délkové jednotky a jak je používat tak, aby naše aplikace fungovala na zařízeních s různým DPI.

Suroviny

Přemýšleli jste, jak by se dal do aplikace vložit obrázek? Kam dát nějakou písničku? Jak na lokalizaci textu do různých jazyků? Nebo jak nabídnout zařízením s různou velikostí displeje různé layouty? Resources jsou správná odpověď.

Jak už jsem se ptal v prvním dílu a pak se to trochu řešilo v komentářích, mám trochu problém s překladem slova resources. Nakonec jsem se rozhodl pro český značně nepřesný ekvivalent suroviny, ale to jen z toho důvodu, že nemám rád zbytečná nesklonná přeložitelná anglická slova v českých textech. Občas použiji i výraz resources, a to v případě, kdy budu mít pocit, že je potřeba termín. Překlad suroviny je tu čistě z pocitových důvodů (všechny resources vs. všechny suroviny – co zní lépe?), nesnažím se zavést nějaký nový termín.

Všechny suroviny jsou uloženy ve složce res, a to mimo jiné v následujících podsložkách:

drawable/ obsahuje bitmapové obrázky (.png, .jpg, .gif) nebo speciální XML soubory, které si Android umí převést na obrázek. Vytvořený identifikátor má formát R.drawable.nazev_souboru_bez_koncovky. Názvy souborů tedy musí být unikátní.
layout/ obsahuje layoutové soubory, s nimiž jsme se už seznámili. Vytvořený identifikátor má formát R.layout.nazev_souboru_bez_koncovky. Názvy souborů tedy musí být unikátní.
menu/ obsahuje xml soubory s definicí různých menu. S nimi se v některém z příštích dílů seznámíme. Vytvořený identifikátor má formát R.menu.nazev_souboru_bez_koncovky. Názvy souborů tedy musí být unikátní.
raw/ obsahuje, jak již název napovídá, jakékoli soubory. Ty můžeme číst například pomocí InputStream. Vytvořený identifikátor má formát R.raw.nazev_souboru_bez_koncovky. Názvy souborů tedy musí být unikátní.
values/ obsahuje určité XML soubory ve speciálním formátu s různými hodnotami používanými skrz Android. Za chvíli se tomu budeme věnovat podrobně.
xml/ obsahuje XML soubory, čitelné pomocí metody Resources.getXML(R.xml.nazev_souboru_bez_koncovky). Názvy souborů tedy musí být unikátní. Pokud chcete používat XML konfiguraci, tak se soubory šup sem.

Co je to ta záhadná složka values?

Výborná otázka. Složka values obsahuje jednoduché suroviny, jako jsou řetězce, čísla nebo třeba styly. Na rozdíl od ostatních složek zde nejsou důležité názvy souborů. Dokonce můžete mít jeden jediný soubor a v něm namixovaná všechna data. Všeobecně uznávaná konvence je však data separovat a soubory pojmenovat podle typu obsažených dat, tzn. strings.xml, styles.xml nebo  arrays.xml.

Každý obsažený soubor má kořenový element <resources>, který může mít potomky jako <string>, <color> atp., které definují typ dané hodnoty. Podle toho se také vytvoří identifikátor. Pro řetězec to bude v R.string, pro barvu v R.color. Každý potomek elementu <resources> má atribut name, jehož hodnota určuje poslední část identifikátoru. Když budu mluvit o id nebo o identifikátoru u řetězcové suroviny, mám na mysli právě hodnotu atributu name. Řetězec s atributem  name="my_string" bude tedy mít identifikátor R.string.my_string. Způsob zadání hodnoty té určité suroviny s daným identifikátorem se liší, většinou jsou to tagy párové (platí pro např. <string> nebo <color>) a hodnotou je tedy to mezi těmi dvěma tagy, jindy jde o tag nepárový nebo naopak komplikovanější s mnoha potomky. Nás budou dlouho zajímat jen řetězce.

Jak vypadá takový soubor s řetězci?

<?xml version="1.0" encoding="utf-8"?>
<!-- /res/values/strings.xml -->
<resources>
    <string name="login_label">Login:</string> <!-- R.string.login_label -->
    <string name="password_label">Heslo:</string> <!-- R.string.password_label a ostatní analogicky-->
    <string name="submit_label">Přihlásit</string>
    <string name="no_menu_available">Není dostupný žádný jídelníček.</string>
    <string name="password_not_filled">Musíš zadat heslo.</string>
</resources>

Dobrá zpráva je, že ADT plugin pro Eclipse nabízí možnost zadávat všechny suroviny ve složce values pomocí různých formulářů, špatná zpráva je, že u jednoduchých hodnot typu řetězec je to nepohodlné a u složitých (styl) je to prakticky neproveditelné.

Modifikátory aneb kde začíná legrace

Slíbil jsem vám lokalizaci, různé layouty pro různá zařízení atd. a také to splním. Tohle všechno totiž dělají nenápadné a velice jednoduché modifikátory (v angličtině to nazývají qualifiers).

Takový modifikátor, to je vlastně jen pomlčkou oddělený speciální řetězec přidaný za název určité podsložky res. Modifikátorů je mnoho typů, my si zde ukážeme ty nejčastěji používané.

Typ modifikátoru Hodnoty Popis
Jazyk a region cs,
en,
fr,
en-rUS,
en-rGB
První část udává kód jazyku, druhá (nepovinná) s prefixem r označuje region.
Dostupná šířka w<N>dp
(např. w720dp,
w480dp)
Dostupná šířka se mění při změně orientace zařízení, dáte-li tedy telefon či tablet na šířku (landscape mode), dostupná šířka se oproti postavení na výšku (portrait mode) zvětší. Tento modifikátor budeme používat při tvorbě tabletových layoutů. To dp je rozměrová jednotka v Androidu, brzy se k ní dostaneme.
Orientace zařízení port,
land
port znamená portrait mode, tedy postavení na výšku, land znamená landscape mode, tedy na šířku.
Hustota pixelů ldpi,
mdpi,
hdpi,
xhdpi
Tyto modifikátory použijeme hlavně pro bitmapy, neboť aby byla reálná výsledná velikost zhruba stejná, budeme muset zařízením s jemnější hustotou pixelů dát obrázek větších rozměrů. Trochu si o tom povíme, až si dnes budeme povídat o jednotkách, více tehdy, až to budeme potřebovat.
API level v3,
v15
Chceme-li některé suroviny přidělit jen zařízením s úrovní API rovnou nebo vyšší nějakému číslu, můžeme použít tento modifikátor. Pozor na to, že API 3 resp. 4 (tzn. Android 1.5 a 1.6) přijmou za svou jen verzi API té jejich se přesně rovnající.

Modifikátorů je samozřejmě daleko více, všechny najdete hezky v tabulce v Providing Alternative Resources. Chcete-li použít více modifikátorů najednou, musíte je seřadit podle pořadí v tabulce odshora dolů (tzn. správně je values-cs-v15, ne values-v15-cs). V tabulce zde jsem pořadí zachoval, jen jsem toho plno vypustil.

Priorita modifikátorů

Priorita modifikátorů funguje poměrně intuitivně. Nejprve hledá Android požadovanou věc v nejspecifičtější složce a postupuje k těm nejméně specifickým. Například, máme-li složky /res/values, /res/values-en a /res/values/en-rGB a telefon má nastavený jazyk na britskou angličtinu, nejprve se podívá do třetí jmenované složky, pokud tam požadovanou surovinu nenajde, podívá se do prostřední, potom do první napsané, a pokud nebude surovina ani tam, vyhodí chybu.

Jednotky

Každé zařízení má nějakou velikost displeje. Většinou se udává jako velikost úhlopříčky v palcích, takže máme od třípalcových Androidů přes 5,3″ Samsung Galaxy Note až po desetipalcové tablety. Potom má každé zařízení rozlišení. To znamená, kolik pixelů (elementárních obrazových bodů schopných zobrazit jednu barvu) má. A pak existuje tzv. pixel density (já bych to přeložil jako hustota pixelů), většinou pod zkratkou PPI či DPI, která určuje jemnost zobrazení displeje. Má-li displej například hustotu 230 dpi, znamená to, že na každý palec fyzické velikosti displeje připadá 230 bodů, pixelů.

Pokud jste doteď vyvíjeli jen pro počítače, asi si říkáte, že je to celkem zbytečná teorie, že se monitory zase tolik neliší. To byla doteď celkem pravda, na některém monitoru by se vaše aplikace zobrazila trochu větší, na jiném menší, uživatelé jsou na to zvyklí a když tak mohou použít lupu nebo zoom. (Ale v posledním roce, vlastně od uvedení nového iPadu situace nabrala na obrátkách, výrobci začínají notebookům dávat jemné displeje, například nový Asus Zenbook Prime bude v 11palcové variantě mít Full HD rozlišení, což dává nějakých 200 dpi, což je dvakrát vyšší, než byl standard před dvěma lety (můj HP ProBook má DPI asi okolo 100), nové MacBooky Pro mají ultrajemné Retina displeje.) Na telefonech však uživatelé žádnou lupu nemají, nativní aplikace přiblížit nemohou a změna rozlišení také není možná. Pokud tedy nechcete, aby vaše aplikace dopadla tak, že se na jeden displej tlačítko vůbec nevejde, zatímco na jiném se na něj nedá ani tapnout, musíte hledat řešení.

DP

To řešení Android nabízí. Je jím jednotka dp, jež znamená density-independent pixel. Je to virtuální jednotka, která je definována matematickým vztahem 1dp = 160px/dpi. Z tohoto vztahu vyplývá, že koresponduje s fyzickou velikostí displeje. Tahle jednotka se potom na každém zařízení přepočítá zpět na pixely.

Pokud tedy chcete nastavit například okraje nějakého elementu, místo pixelů použijete dp (samozřejmě asi trochu jinou hodnotu). Jak jednoduché.

Fajn, tak rozměry view napíšu v dp. Ale co obrázky?

Dobrá otázka. U obrázků se to řeší modifikátory. Pro každou hustotu ldpi, mdpi, hdpi a xhdpi (případně tvdpi, ale to zatím u menších aplikací vzhledem k rozšířenosti Androidu v televizích nemusíme řešit) poskytněte obrázek v rozdílném rozlišení, a to v poměru 3:4:6:8. Má-li tedy obrázek pro ldpi rozlišení 18*18px, pro mdpi bude 24*24, pro hdpi 36*36 a pro xhdpi 48*48px. Pokud některou z hustot nezadáte, měla by se (snad) použít ta nejbližší. Není-li hodnota zadaná, jako výchozí se bere mdpi. Mezi hustotami pixelů jednotlivých zařízení jsou samozřejmě i jiné hodnoty, než které by vyhovovaly poměru 3:4:6:8. Android však obrázek zvětší nebo zmenší tak, aby dané hustotě odpovídal; prostě se o to dobře postará.

Šupito presto programovat

Dnes budeme rozšiřovat aplikaci z minulého článku. Pokud jste si ji nenaprogramovali, stáhněte si ji, odkaz na zdrojáky je v závěru odkázaného článku.

Současná aplikace se skládá z jedné activity, která obsahuje tlačítko a textové políčko. My ji vylepšíme tak, aby se po stisknutí tlačítka spustila nová activity, která zobrazí obsah EditText u. Do toho budou navíc smět být zapisována pouze čísla.

Nejprve vylepšíme existující layout.

Nejspíš si pamatujete, že jsem minule psal o „prasárně“, když jsme u  EditText u použili atribut android:text s řetězcovou hodnotou. To slovo jsem nepoužil jen tak, abych vypadal drsně, dokonce i Eclipse nám tvrdí, že je cosi v nepořádku. V Androidu se totiž všechny řetězce patřící do UI uchovávají jako suroviny, jednak pro jednodušší překlad, jednak pro jednodušší správu a obecně čistější kód.

Otevřeme tedy soubor /res/values/strings.xml, který je již vygenerovaný. Zobrazilo se GUI se seznamem, já však doporučuji přepnout dole záložku na čistý XML soubor. Jak vidno, jsou tam už dva tagy <string>. Ten s name="hello"  je vygenerovaný řetězec, který je použit ve vygenerovaném /res/layout/main.xml. Pokud ten layout soubor smažeme, můžeme smazat i tento řetězec. Pak je tam ještě tag <string name="app_name">. Ten obsahuje jméno aplikace, které se zobrazí v menu telefonu nebo v horní liště. app_name není žádné magické slovo, podíváte-li se do manifestu, uvidíte, že tagy <application> i <activity>  ho ve svých atributech používají. V případě tagu <application> jde o popisek zobrazený v menu, při odinstalaci atd., u <activity>  jde o titulek pro horní lištu. Pokud chcete, klidně si vytvořte dva nové rozdílné řetězce v /res/values/strings.xml  a nastavte aplikaci i activity jiné popisky.

Pokud nemá Activity nastavený titulek, použije se ten specifikovaný na tagu  <application>.

My však přidáme jiné řetězce. Nejprve popisek tlačítka. Jako potomka tagu <resources> v /res/values/strings.xml  přidejte následující řádek:

<string name="button_title">Sem klikni</string>

a v /res/layout/new_layout.xml  u  EditText u změňte hodnotu atributu android:text na @string/button_title. Jeden warning hned zmizel, ale ještě je tam druhý. Ten tvrdí, že chybí atribut inputType nebo hint. Vzhledem k tomu, že chceme jen čísla, přidáme android:inputType="number". Atribut android:inputType  určuje povolené znaky a může se mu přizpůsobit softwarová klávesnice. Má hodně možných hodnot, všechny jsou vypsány a popsány jako vždy v dokumentaci.

Ještě by se hodilo mít před vstupním políčkem popisek, který by vysvětlil, co do něj napsat. Na zobrazení textu v Androidu slouží  TextView  (mimochodem, EditText je jeho potomkem). To má mnoho různých atributů, nám dnes bude stačit jen jeden, a to android:text. Jak asi tušíte, jeho hodnota určuje, jaký text TextView zobrazí. Kromě toho ještě musíme nastavit layout atributy, tedy android:layout_width resp. android:layout_height. Do řetězcových surovin přidáme řetězec Kolik Ti je let, jemuž jako identifikátor nastavíme edittext_label, a před <EditText> vložíme následující čtyři řádky:

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

Tím je úprava layoutu hotová.

PrvniKruckyActivity

Původní verze zobrazila toast a implementovala posluchače kliknutí na tlačítko. My dnes jednak přesuneme text toastu do resources a jednak hodně rozšíříme metodu buttonClicked  – ta bude muset zjistit obsah EditText u a potom spustit novou Activity, jíž zjištěné číslo předá.

Do strings.xml tedy přidáme další řetězec, s id řekněme wellcome_toast a s textem Napiš, kolik Ti je let, a pak klikni na tlačítko. Řádek s vytvořením toastu upravíme následovně:

Toast.makeText(this, R.string.wellcome_toast, Toast.LENGTH_LONG).show();

Všimněte si, že jsme tentokrát nepoužili metodu makeText se signaturou Toast makeText(Context ctx, String text, int duration), ale Toast makeText(Context ctx, int resId, int duration). Naprostá většina metod, které přijímají řetězec, který se uživateli nějakým způsobem zobrazí, má jak variantu se stringovým parametrem, tak variantu s integerovým identifikátorem.

Na chvilku bych si dovolil odbočit a zmínit se o volení správných identifikátorů. Nikde jsem nenarazil na dobře odůvodněnou radu, jaké identifikátory pro řetězce používat. Jsou víceméně dvě možnosti, každá je svým způsobem logická. Buď je jako identifikátor použit účel daného řetězce, jeho výskyt (  edittext_label, wellcome_string), anebo řetězec sám, popřípadě zkrácený popis jeho obsahu ( tap_here, please_fill_in_your_login). Já používám obě varianty, u projektů s málo řetězcovými surovinami a u řetězců, kde je jejich obsah poměrně jasný z umístění, použiju obvykle první variantu, u ostatních obvykle druhou. Rád uslyším váš názor a argumenty a rád se něčemu přiučím.

Když máme hotový uvítací řetězec, musíme ještě upravit metodu, která se zavolá po kliknutí na tlačítko. Ta bude teď takto:

public void buttonClicked(View button) {
    int age = getAge();
    if(age < 0){
        informAboutInvalidAge();
    } else{
        startAgeActivity(age);
    }
}

Metody getAge, informAboutInvalidAge i startAgeActivity  musíme implementovat.

Vím, že by objektově čistší asi bylo nesprávný (nezadaný) věk implementovat pomocí výjimky, ale v ukázkovém kódu je tohle jednodušší a přímočařejší.

getAge

Metoda getAge zjistí obsah EditText u a převede ho na číslo, které vrátí.

protected int getAge(){
    // findViewById může vrátit i null (a tehdy by program vyhodil NullPointerExteption),
    // je však běžnou praxí spolehnout se, že vše bude v pořádku.
    EditText ageInput = ((EditText)findViewById(R.id.edittext));
    String ageStr = ageInput.getText().toString();
    int age = -1;
    try{
        age = Integer.parseInt(ageStr);
    }catch(NumberFormatException e){
        // age má nastavenou výchozí hodnotu
    }
    return age;
}

Použili jsme zde jednu zajímavou metodu, a to Activity.findViewById(int id). Tato metoda jako parametr přebírá identifikátor chtěného View (identifikátor ve formě vlastnosti třídy R), které se poté pokusí najít ve svém contentView (jež jsme nastavili metodou setContentView v onCreate). Pokud ho nenajde, vrátí null, pokud uspěje vrátí View foundView. To ještě musíme přetypovat, abychom mohli použít metodu getText. findViewById si určitě zapamatujte, je to jedna z nejčastěji používaných metod.

informAboutInvalidAge

V této metodě upozorníme uživatele, že zadal nevalidní data (v praxi to znamená, že nechal políčko prázdné, neboť jsme nastavili parametr inputType), odstraníme obsah EditText u (ve skutečnosti by nemělo být potřeba, ale procvičíme si tím findViewById a metodou View.requestFocus nastavíme EditText u focus. Do strings.xml přidejte řetězec s id invalid_age a textem typu  Musíš zadat platný věk.

protected void informAboutInvalidAge(){
    Toast.makeText(this, R.string.invalid_age, Toast.LENGTH_SHORT).show();
    EditText ageInput = ((EditText)findViewById(R.id.edittext));
    ageInput.setText("");
    ageInput.requestFocus();
}

startAgeActivity

startAgeActivity spustí AgeDisplayerActivity a předá jí zjištěný věk. Nejprve si ukážeme, jak vypadá, a pak si k ní něco řekneme. AgeDisplayerActivity je Activity, jíž si vytvoříme za chvilku.

protected void startAgeActivity(int age){
    Intent intent = new Intent(this, AgeDisplayerActivity.class);
    intent.putExtra(AgeDisplayerActivity.AGE, age);
    startActivity(intent);
}

Základním komunikačním kamenem mezi jednotlivými prvky aplikací v Androidu jsou takzvané intenty. Intent je třída, která obsahuje popis nějakého záměru a případné argumenty. Intent pak předáme frameworku a ten se postará o to, aby byl splněn. Jako záměr jsme v naší aplikaci použili konkrétní třídu ( AgeDisplayerActivity.class), ale stejně tak může být záměr obecný (například Intent.ACTION_VIEW). Tehdy se pak framework podle dalších dat rozhodne, co a jakým způsobem náš intent splní. (V konkrétním případě Intent.ACTION_VIEW záleží na předaných datech k zobrazení – je-li to url adresa, spustí se prohlížeč, pokud však pro danou konkrétní url adresu naní nastaven jiný splňovatel (třeba v případě Play Store se místo prohlížeče spustí Play Store aplikace). Pokud je více možných splnitelů, nabídne se uživateli notoricky známá obrazovka s výběrem toho, kterého si uživatel přeje. Tohle všechno je ale už trochu intentová vyšší dívčí, takže to můžete zapomenout a dostaneme se k tomu později.)

Metoda putExtra přebírá dva argumenty: String name, což je unikátní identifikátor dané věci a samotnou věc, což může být jakákoli elementární hodnota anebo cokoliv, co umí Android nějakým způsobem serializovat. Druhé jmenované nás teď také nemusí zajímat, my předáváme číslo.

Activity.startActivity je jedna z metod, která přebírá Intent, a dovolil bych si tvrdit, že ta nejjednodušší. Dělá jen to, že předá androidímu frameworku daný Intent a řekne mu, že má spustit nějakou Activity, ale že ji už nezajímá, co se dál bude dít.

Tím máme dokončenou celou PrvniKruckyActivity a jdeme se vrhnout na  AgeDisplayerActivity.

AgeDisplayerActivity

Klikněte pravým tlačítkem na namespace, které je podsložkou src v projektovém stromu vlevo, vyberte New → Class. Zobrazí se průvodce vytvořením nové třídy. Do Name  napište AgeDisplayerActivity, jako Superclass  vyberte android.app.Activity a klikněte na Finish. Tím se vytvořil nový soubor, který se zároveň otevřel. Ale moc toho v něm není.

Nejprve musíme vytvořit konstantu AGE, kterou používá PrvniKruckyActivity. Hned za deklaraci třídy tedy přidejte tento řádek:

public static final String AGE = "age";

Hned po spuštění activity budeme chtít zjistit, jaký věk jí byl předán, a ten zobrazit v nějakém TextView. Potřebujeme tedy implementovat metodu  onCreate.

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

Protože nám stačí jedno jediné View, a také proto, abychom si to zkusili, tentokrát metodě setContentView nepředáme identifikátor layoutu, ale přímo TextView objekt:

    TextView tv = new TextView(this);
    setContentView(tv);

Teď musíme zjistit předaný věk. Pomocí getIntent získáme objekt Intent, který vytvořila PrvniKruckyActivity a z něj díky getIntExtra získáme věk. Předáme jí ještě pro jistotu druhý parametr, kterým je výchozí hodnota pro případ, že v  Intent u žádná vlastnost s daným klíčem není.

    Intent i = getIntent();
    int age = i.getIntExtra(AGE, -1);

Zjištěný věk a vytvořené TextView potom předáme metodě  displayAge.

    displayAge(age, tv);
}

Tím máme metodu onCreate vyřešenou. Ale ještě než se vrhneme na displayAge, přidáme si jeden řetězec do surovin.

Otevřete si tedy zase soubor /res/values/strings.xml a vytvořte řetězec s id displayed_age a textem Je Ti … moment. Tady něco nehraje. Vždyť přece musíme do řetězce vložit věk. Tak tedy vytvoříme dva řetězce, jeden s id displayed_age_1 a textem Je Ti (a nezapomeňte na mezeru na konci!) a druhý s id displayed_age_1 a textem let. To je ten nejlepší věk! (nezapomeňte na mezeru na začátku). Tyhle řetězce potom spojíme a mezi ně ještě přidáme věk.

„Ale jak spojíme? Vždyť máme jen jejich identifikátory! A vůbec, je to fujfuj. A když budeme chtít přeložit aplikaci, jak mám vysvětlit překladateli, co to znamená? A co když zapomene na ty mezery?“ Dobře, uděláme to jinak. A jak? Podíváme se do manuálu. Ten tvrdí, že máme napsat řetězec jako formátovací řetězec pro metodu String String.format(String formatter, Object... args). Smažte tedy displayed_age_1 a displayed_age_2 a místo toho přidejte jeden, s id displayed_age a textem Je Ti %1$d let. To je ten nejlepší věk!, kde %1$d je speciální formátovací sekvence a znamená to, že první argument se tam vloží jako číslo.

Dobře, ale co když je někomu 1 rok? Anebo tři roky? Nejsme přece amatéři, abychom zobrazili 1 let nebo 3 let. Dobrá, dobrá, vyřešíme i tohle. V už odkazované kapitole manuálu se o jednu sekci výše píše i o tom, jak vyřešit různé plurály. Nás bude speciálně zajímat jednička a potom malá čísla (2, 3, 4). Zbytek je stejný (včetně nuly).

Jak se píše v manuálu, místo elementu <string> musíme použít element <plurals> s několika elementy <item> jako potomky. Finální verze bude vypadat takto:

<plurals name="displayed_age">
    <item quantity="one">Je Ti, %1$d rok. To je ten nejlepší věk!</item>
    <item quantity="few">Jsou Ti, %1$d roky. To je ten nejlepší věk!</item>
    <item quantity="other">Je Ti, %1$d let. To je ten nejlepší věk!</item>
</plurals>

 

Jako identifikátor bude sloužit R.plurals.displayed_age a zformátovaný řetězec získáme metodou  Resources.getQuantityString(int resId, int quantity, Object... args).

Je tu ale jeden velký, ošklivý, nepěkná věc. Každý jazyk má totiž speciální varianty pro různá množství. Angličtina například používá jen one a other. Polština používá few pro každé číslo končící na 2, 3 nebo 4, ale ne na 12, 13 a 14. A nastavení jazyka aplikace přebírá ze systému.

Máme-li náš strings.xml uložený ve složce values bez nějakého modifikátoru, bere se, že je v ní ten jazyk, který má uživatel nastavený. Já kupříkladu používám angličtinu, takže by mi fungoval pouze plurál one, few by se úplně ignorovalo a místo toho by se použilo other. Pokud z values  uděláme values-cs, budeme muset poskytnout ještě values, protože jinak aplikace na nečeských telefonech nepůjde spustit.

Abych to shrnul, předtím, než použijete plurálovou funkcionalitu, dvakrát si rozmyslete, jestli to nepůjde jinak ( Měsíce: 7). Pro dnešek se smíříme s tím, že řekneme  Je Ti 2 let.

Tip na konec bloku

Je možné programově změnit locale, jak na to si přečtěte například v Set-up the application Language in Android Preferences. Chcete-li to dělat pro celou aplikaci, nebylo by od věci vytvořit si nějakou BaseActivity, v jejímž onCreate by se tohle provedlo. Stejně by však bylo potřeba mít i values  bez modifikátoru – kvůli názvu aplikace. A mimochodem, když už jsme u toho, vlastní BaseActivity je poměrně běžná věc, řeší se tím například sdílení menu mezi activitami.

Tím tedy máme hotové suroviny a ještě musíme implementovat metodu displayAge. Ta získá objekt Resources, od něj obdrží zformátovaný řetězec a ten zobrazí v  TextView.

protected void displayAge(int age, TextView tv){
    Resources res = getResources();
    String text = res.getString(R.string.displayed_age, age);
    tv.setText(text);
}

Hotovo!

Úspěšně jsme dokončili dnešní aplikaci. Zkusme ji tedy spustit a použít. Buť si připojte telefon, anebo spusťte emulátor, klikněte na zelenou šipku, zvolte cílové zařízení, chvíli počkejte a pak se kochejte krásou… moment! To nefunguje.

Manifest

Každý, kdo si během článku ťukal na čelo, že jsem zapomněl zapsat activity do manifestu, má jedničku s hvězdičkou. Tohle je má nejčastější chyba, není activity, již bych naprogramoval, spustil a hned na první pokus běžela – nikdy není zapsaná v manifestu. A říkal jsem si, že když ze sebe udělám hlupáka, třeba si to lépe zapamatujete :).

Otevřete tedy AndroidManifest.xml a jako potomka <application> přidejte upozornění na existující  AgeDisplayerActivity.

<activity android:name="AgeDisplayerActivity"/>

Teď už aplikace funguje.

Časté problémy

Nechce se mi zkompilovat soubor strings.xml, přestože mám všechny tagy určitě dobře uzavřené.

Nemáte někde v textu apostrof nebo uvozovky? Pokud ano, jsou dvě možnosti. Buď před každý apostrof a uvozovky v textu dáte zpětné lomítko, anebo celý text ohraničíte tím z množiny {apostrof, uvozovky}, co v textu není použito.

Aplikace se nechce spustit, hlásí ActivityNotFoundException.

Přidejte danou Activity do manifestu a zkontrolujte překlepy. Případně ji tam zkuste přidat včetně namespace.

Procvičování

Naprogramujte si aplikaci skládající se ze dvou activit. V té první se uživatele zeptáte na jméno, věk, zda je svobodný/á a poprosíte ho, aby vám napsal pár vět o sobě, a po stisku tlačítka se zobrazí druhá activity, která mu vypíše vizitku. Aplikaci lokalizujte do dvou jazyků (výchozí – values angličtina a pak čeština nebo slovenština ( values-cs či values-sk)). Odesílacímu tlačítku nastavte rozměry 200×70 dp.

Asi se vám bude hodit vědět, že View pro checkbox se jmenuje CheckBox. Pokud budete mít nějaké problémy, neváhejte se zeptat.

Dnes procvičování opravdu doporučuji, naučili jsme se toho docela hodně a příště už budu počítat s tím, že to znáte.

Závěr

Ve dnešním poměrně dlouhém článku jsme si pořádně vysvětlili suroviny, pochopili jsme, co to je a k čemu je dobrá jednotka dp, naučili jsme se vytvářet nové activity, prohodili jsme první nesmělá slova s intenty, zjišťovali a nastavovali jsme různé vlastnosti View za běhu, naučili jsme se dávat řetězce do surovin a využívat schopnosti String.format na surovinových řetězcích. Ke stažení vám jako vždy nabízím ukázkové zdrojové kódy dnešní aplikace. Máte-li jakoukoli otázku, připomínku nebo s něčím nesouhlasíte, něco byste udělali lépe, napište do komentářů.

Příště si představíme jednotlivá View, s nimiž se budete ve své praxi setkávat.

Tip na konec

diskusi pod prvním dílem se řešila poměrně zoufalá rychlost emulátorů a objevila se tam zajímavá rada, a to při vytváření AVD povolit snapshot. Doporučuji.

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: 43

Přehled komentářů

Ronon Dex Resources jako suroviny?
Ronon Dex Re: Resources jako suroviny?
Matěj Konečný Re: Resources jako suroviny?
2X4B-523P Re: Resources jako suroviny?
langpa Re: Resources jako suroviny?
phoose plurals
Matěj Konečný Re: plurals
Matěj Konečný Re: plurals
phoose Re: plurals
razor Fajn, tak u textu napíšu velikost v dp. Ale co obrázky?
Matěj Konečný Re: Fajn, tak u textu napíšu velikost v dp. Ale co obrázky?
pravdokop Když už překlad, tak ne polovičatý
ded kenedy Re: Když už překlad, tak ne polovičatý
RDPanek Perfektní článek
Martin Hassman Re: Perfektní článek
Matěj Konečný Re: Perfektní článek
phoose Instalace aplikace
Matěj Konečný Re: Instalace aplikace
phoose Re: Instalace aplikace
Radek Chybička ve článku?
rogisoft Spouštění aplikace v AVD
Matěj Konečný Re: Spouštění aplikace v AVD
rogisoft Re: Spouštění aplikace v AVD
Jan Ptacek Nesrovnalosti v kódu
Matěj Konečný Re: Nesrovnalosti v kódu
msx eclipse - poriadok v projektoch
xOB Re: eclipse - poriadok v projektoch
msx Re: eclipse - poriadok v projektoch
xXx Chybicka?
dioda Re: Chybicka?
Kubíík Předávání argumentů
Matěj Konečný Re: Předávání argumentů
trr unfortunately .. stop :(
Matěj Konečný Re: unfortunately .. stop :(
trr Re: unfortunately .. stop :(
n00ber Kódy
Pyro Re: Kódy
Pyro Re: Kódy
Pyro Pada pri kliknuti na tlacitko
Pyro Re: Pada pri kliknuti na tlacitko
karel.cervicek.7 Nechápu proč tak složitě
Robo Precvicenie
Libor Nezadaný věk
Zdroj: https://www.zdrojak.cz/?p=3675