11. lecke Szótár (4)

Online Android szakkör (DKRMG)


Hol tartunk most?

Ez a szótárfüzet alkalmazás projekt negyedik – és egyben utolsó – leckéje. Gyorsan összegeznénk, hogy miként fest majd a kész program, mennyit sikerült ebből megvalósítani az előző leckékben, illetve milyen munkát tartogat a mostani segédanyag.

A program részei

Az általunk tervezett program négy Activityből áll.

Emlékeztetésként

Az előző leckékben írtunk egy névjegy (about) activityt, illetve teljesen elkészítettük a menünket és a SzavakActivityt. A szavakat egy külső fájlból olvassuk be az SD kártyáról, amit a programunk szépen kilistáz, ráadásul újabb szavakat is hozzáadhatunk / a nem kívánatos szavakat kitörölhetjük menet közben.

Tehát mi maradt a mai napra?

Létre kell hoznunk egy új Activityt a kvíz ablaknak, és meg kell írnunk azokat a program sorokat, amik a kikérdezésért felelősek.

Azt szeretnénk, ha először annyit tudna a program, hogy kiválaszt egy szót, kiírja a magyar nyelvű alakját, és bekéri az angol nyelvűt. Ez után gombnyomásra eldönti, hogy jó e a megoldás, ezt jelzi, és kérdez egy újabbat.

Nem csináltunk mi már valami hasonlót?

Ez a lecke kísértetiesen hasonlít ahhoz a munkához, amit a harmadik leckében kellett elvégeznünk. Érdemes lehet gyorsan átfutni, hogy ott mi is történt.

A következő különbségeket észben tartva javasolnánk, hogy próbáld meg először önállóan megoldani a feladatot, puskázva a harmadik lecke leírásából, és akkor nézz csak bele a lenti leírásba, ha elakadtál. Az online szakkörök szépségét is mutatja, hogy csak rajtad múlik mennyit tanulsz a lecke elvégzése közben:

  1. Ha még nincs a projektedben KvizActivity, akkor létre kell hozni. Ilyet már csináltunk sokat, például a 6. leckében.

  2. Az Activity indulásakor (onCreate) be kell olvasnunk a szólistát. Ezt egy az egyben ugyanúgy tehetjük meg, mint a SzavakActivityben.

  3. A telefon most nem egy számra gondol, hanem egy szóra. Ennek a generálásához használjuk a listánk get és size belső függvényeit és a véletlenszám-generátort.

  4. A szó helyességének ellenőrzésekor ne használjuk az egyenlőség jelet. Két String összehasonlításához a Java nyelvben sajnos más módszerhez kell folyamodnunk (equals belső függvény). Erről részletesebben itt.

A továbbiakban lépésről lépésre végigmegyünk a lecke elkészítésén, de az lenne az igazi, ha először önállóan próbálnátok megcsinálni, kitalálni a szükséges lépéseket.

Ha végeztél, vagy ha elakadtál, érdemes átfutni leírásunkat, megnézni mit csinálnánk mi másként, illetve a lecke végén találhattok sok szorgalmi feladatot illetve ötletet, .


Új Activity

A program írása közben is létre kellett már hoznunk új Activityket. Olvass kicsit vissza, hogy milyen lépéseket kell elvégezned! L06, L08.

  1. Az activity neve legyen KvizActivity.

  2. Ne felejtsd el a MenuActivity megfelelő gombjánál beállítani, hogy kattintásra meg is nyissa a KvizActivity ablakunkat. Ehhez a szükséges parancsot a korábbi leckékben, illetve a gyorssegély lapon is megtalálod

Felhasználói felület

Több mint tíz leckével a hátunk mögött amolyan bemelegítő gyakorlatként alakítsuk ki a kvíz ablakunk felhasználói felületet. Iránymutatásként a lépések:

  1. Helyezz el egy szöveget (TextView), ami a szó magyar formáját írja majd ki! Ne felejtsd el beállítani a szöveg id tulajdonságát, hogy menet közben le tudjuk cserélni!

  2. Alá rakj egy szövegdobozt (EditText). Ide kell majd a szavunkat beírni angolul. Ennek is legyen id azonosítója, hogy a beírt szót könnyedén ki tudjuk nyerni.

  3. Hozz létre egy gombot, ami majd ellenőrzi a megoldást.

  4. Alulra pedig tegyél egy újabb TextView-t, ahol gombnyomásra kiírjuk, hogy helyes volt-e az előző válaszunk!

Az így elkészült Activity elég primitíven néz ki. Próbáld meg egy kicsit kicsinosítani különböző színek és képek használatával. Használd például a background, textSize, hint tulajdonságokat. Ezekre példákat a korábbi leckékben is láthattál már!

Ne feledd, hogy azokhoz a komponensekhez, amikre kódból is hivatkozni szeretnél, létre kell hoznod egy-egy változót is. Ilyet is sokszor csináltunk már: az értéküket az onCreate függvényben adtuk meg a findViewById segítségével.

Próbáld meg még most önállóan végiggondolni, hogy mi mindennek kell történnie a gomb megnyomásakor (írd le papírra!).

Szavak beolvasása

Ez a rész pont ugyanúgy működik, mint az előző leckében. A kódot nem is fogjuk itt teljes egészében újra megadni. Amire figyelj:

  1. Szükséged lesz egy ArrayList változóra, ahova a szavakat be tudod olvasni

  2. Az ArrayListedet első használat előtt létre kell hoznod a new szó használatával.

  3. Nyisd meg a fájlt, majd soronként dolgozd fel a tartalmát (mint az előző leckében)

  4. Az egész szólistát nem jeleníted meg sehol, így nem lesz szükséged Adapter-re.

Szó kiválasztása

Ahhoz, hogy a program véletlenszerűen kérdezhessen tőlünk szavakat, szükségünk van egy véletlenszám generátorra. A harmadik leckében rendelkezésünkre állt egy randomGenerator nevű változó (ha nem emlékszel, lapozz vissza!). Akkor ezt a változót az előkészített projektben találtad. Nyolc lecke elteltével viszont azt reméljük, hogy te magad is létre tudod hozni!

  1. Készíts egy Random típusú változót randomGenerator néven (a KvizActivity osztályodban).

  2. Az onCreate függvényben értéket kell adnunk a változónknak. randomGenerator = new Random();

  3. Emlékszel, hogy a randomGenerator változó ezt követően hogyan adott nekünk újabb és újabb véletlen számokat?

A kvíz ablakunktól elvárnánk, hogy rögtön egy magyar szóval fogadja a felhasználót az ablak tetején. Ehhez az onCreate-ben, miután beolvastuk a fájlból a szavakat, meg kell írni, hogy:

  1. Válasszon ki egy véletlen szót a listából. Véletlen szavakat a randomGenerator nem tud létrehozni, ő csak a számokat ismeri. Ezért:

  2. használd a listád size() függvényét, hogy megtudd hány szó közül választhatsz.

  3. Emlékszel? ez a parancs létre hozott neked egy véletlen számot, ahol 0 ≤ szám < 100: int gondoltSzam = randomGenerator.nextInt(100);

  4. Most készíts egy véletlen számot, ahol 0 ≤ szám < listaMérete.

  5. Ha sikerült a megfelelő tartományban létrehoznod egy számot, akkor kérd le a lista megfelelő elemét. Ehhez használd a lista get(gondoltSzam) parancsát!

  6. Kiválasztottál egy véletlen szót! Most ezt mentsd el egy globális változóba. Ezt a változót is a KvizActivityben hozd létre, mondjuk gondoltSzo néven.

    KvizActivity elejére: Szo gondoltSzo;

    onCreate-be:

    
        int listaMeret = szolista.size();
        int gondoltSzam = randomGenerator.nextInt(listaMeret);
        gondoltSzo = szolista.get(gondoltSzam);
    
  7. Végül írd ki a gondolt szó magyar alakját a kijelzőre (készítettünk erre egy TextView-t).

Kitérő - Két String összehasonlítása

A harmadik leckében megtanultuk, hogy a Java nyelvben milyen egyszerűen össze tudunk hasonlítani két számot (pl. a és b változókból)

if (a == b) {
    // ez történik, ha a két szám egyenlő
} else {
    //ez a rész akkor fut le, ha a két szám nem egyenlő
}

Tehát két szám egyenlőségét a == jellel tudtuk ellenőrizni.

A rossz hír az, hogy a Java nyelvben a kicsit összetettebb típusok (osztályok) egyenlőségét már nem ússzuk meg ilyen egyszerűen. Még rosszabb hír az, hogy a szövegek (String) is összetettnek számítanak ebből a szempontból. Ha meg akarjuk nézni, hogy két String egyenlő-e, akkor ehhez az egyik változónk equals belső függvényét kell használnunk. Pl.


String egyik = "kutya";
String masik = "macska";
if (egyik.equals(masik)) {
    // ez történne, ha a két szó egyenlő lenne
} else {
    // ez történik, ha a két szó nem egyenlő. Mégpedig "kutya" nem egyenlő "macska" :)
}

Érdemes megjegyezni a következő trükköt: csak olyan változóknál használhatjuk a == jelet, ahol a változók típusának neve kisbetűvel kezdődik. pl. int, long, float. Ha a típus nagybetűvel kezdődik, akkor az equals belső függvényre kell hagyatkoznunk.

Tipp ellenőrzése (gombnyomásra)

Korábban összeszedted, hogy minek is kell történnie, amikor a felhasználó megnyomja a gombot. Most próbáld meg a saját leírásod alapján (a String összehasonlításra vonatkozó részt felhasználva) megírni a gomb onClick függvényét.

Vége. Vagy mégsem?

Ha idáig eljutottál, akkor már van egy használható digitális szótárfüzeted, ami ráadásul ki is tudja kérdezni a szavakat! Gratulálunk!

Ha jobban belegondolunk, akkor azért ez még elég messze van a jól, és élvezetesen használható, alkalmazástól. Alább összeszedtünk jó pár ötletet, amik közül tetszés szerint lehet válogatni, mindegyik kicsit jobbá teszi az appot. Természetesen egyéb ötleteket is bele lehet írni, sőt, ha jó ötleted van, és megírod nekünk, akkor hozzáadjuk a listához, hogy mások is beleírhassák az alkalmazásukba.

Az elkészült műveket küldjétek el nekünk a feltöltő oldalon keresztül, akár többször is, ha közben újabb ötletek kerültek bele. Szívesen átnézzük, javítjuk ha szükséges, illetve természetesen ha bármi kérdés felmerül közben, írjatok (dkrmg.android@gmail.com)!

Ötletek - szorgalmik

A feladatok többnyire függetlenek egymástól, de van melyik épít egy másikra, de ez egyértelmű lesz. A feladatok sorrendje igyekszik a nehézségüket követni, ezért érdemes lehet sorban haladni velük, de nem kötelező.

0. Ismétlődő kérdések megszüntetése, beírt szó előfeldolgozása

Egy bemelegítő feladat, azt kéne megoldani, hogy egy "játék" alatt ne kérdezze meg kétszer ugyan azt a szót. Ehhez használd az ArrayList remove() függvényét!

Aki használ valamilyen szókiegészítés billentyűzetet, annak valószínűleg kis kellemetlenséget fog okozni, hogy azok általában egy szóközt is beírnak a kiegészített szó után, ami így már nem ugyan az, mint a fájlban tárolt szó
Hogy ne kelljen ezt mindig kézzel törölni, használjuk a String osztály trim() metódusát! Ez egy új stringet ad vissza, de nem muszáj új változót is létrehozni neki, felülírhatjuk vele a beolvasott szavat tároló változót (nálunk tipp nevű változó): tipp = tipp.trim();
Az igényesebbek megcsinálhatják ugyan ezt a SzavakActivityben, új szó hozzáadásakor is.

1. Számláló, statisztika

Sokkal informatívabb lenne a program, ha kikérdezés közben folyamatosan visszajelzést adna a teljesítményről. Például számolhatnánk a helyes válaszok számát, majd kiírhatnánk a helyes/összes szó arányt, %-os eredmény, esetleg ez alapján az érdemjegyet.

Ehhez be kell vezetni pár új (globális) változót, ami tárolja a megkérdezett és a helyesen megválaszolt szavakat számát.

Szükség lesz még egy (vagy több) TextView-ra, ami megjeleníti a statisztikát, valamint arra, hogy tippeléskor újra számoljuk és meg is jelenítsük a statisztikát.

Amire vigyáznod kell: ha a helyes és megkérdezett számokat int típusú változóban tárolod, akkor az osztás művelet közöttük a Java maradékos osztásként értelmezi. Tehát például 8/10=0. Az ebből származó hibák elkerüléséhez előbb megszorozhatod a helyes válaszok számát százzal, így a százalékos eredményt kapva végül. Pl. 100 * 8 / 10 = 80 (%). Elegánsabb megoldás ennél, ha valamelyik számot float típusúvá alakítod.

2. Highscores

Jó motiváció lenne, ha elmentenénk a felhasználó rekordját (hány kérdésből mennyit tudott, százalékosan), és miközben válaszolgat a szavakra, folyamatosan látná, hogy hogy áll a rekordhoz képest.

Erre használhatnánk egy külön fájlt a már ismert módon, de rövidebb adatok tárolására az Android rendszer nyújt egy sokkal jobb lehetőséget is: SharedPreferences-t (így tárolódik pl. az is, hogy mi a háttérképed, rezegjen vagy ne a telefon, stb.).

Szám mentése

  1. A beállításokat kezelő objektum megszerzése: SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);

  2. A szerkesztéshez kell egy szerkesztő is hozzá: SharedPreferences.Editor editor = prefs.edit();

  3. Bele kell írni az adatot (pl. az eltalált szavak számát): editor.putInt("kulcs, pl.: maxTalalatokSzama", eltalaltSzavakSzama); Természetesen több ehhez hasonló utasítással több mindent is bele lehet írni, csak a kulcs legyen különböző!

  4. Végül be kell fejezni a szerkesztést: editor.commit();

Az olvasás…

… kicsit egyszerűbb, ahhoz nem kell külön "olvasó" objektum:

  1. A beállításokat kezelő objektum megszerzése: SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this)

  2. Az adott kulcsú érték kiolvasása egy változóba: int legtobbEltalaltSzo = prefs.getInt("maxTalalatokSzama", 0); A végén a 0 egy alapértelmezett érték, azért kell, hogy ha esetleg nincs az adott kulcshoz tartozó érték (pl első induláskor), akkor se legyen baj.

Törtszámot és szöveget is lehet benne tárolni, akkor a getInt és putInt függvények helyett a getFloat/putFloat, Stringek esetén pedig a getString/putString használható.

Törlés

Ha minden statisztikát törölni szeretnél, akkor (pl. menünyomás hatására) azt így tudod:

SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
SharedPreferences.Editor editor = prefs.edit();
editor.clear();
editor.commit();

3. Rontott szavak kiírása a végén

Nagyban elősegítené a tanulást, ha a kikérdezés végén megjelennének az elrontott szavak.

Ehhez kelleni fog egy újabb lista, amihez minden rontáskor hozzáadjuk az aktuális szót. A megjelenítéshez adjunk hozzá a layouthoz egy ListView-t, de állítsuk be a visibility tulajdonságát (jobboldalt, a lista legalján) "gone" értékre, hogy amíg a kikérdezés zajlik, ne legyen látható.

Valahogy jelezni is kéne a programnak, hogy nem kérünk több szót, viszont szeretnénk látni a statisztikánkat és a hibáinkat. Erre például tökéletes lenne egy menü. Menü hozzáadására és használatára már láthattál példát a korábbi leckékben (pl L08).

Ha a felhasználó megnyomja a menüt, a következőket kell csinálni: El kell rejteni a kikérdezéshez használt komponenseket (EditText, Button, TextView), amire mindegyiknek a setVisibility(View.GONE) belső függvénye használható, majd meg kell jeleníteni az elrejtett ListView-t a setVisibility(View.VISIBLE) belső függvényével.

Léte kell hozni egy adaptert is (pl. L08) a rontott szavakat gyűjtő listánkkal és be kell állítani a ListViewnak (setAdapter(...) belső függvény).

4. Szó találati arányának mentése fájlba

Érdekes dolgok derülhetnek ki, ha az egyes kikérdezések között meg tudjuk őrizni, hogy melyik szó hányszor lett megkérdezve, és ebből hányszor sikerült eltalálnia a felhasználónak.

Ezt az információt talán a legkézenfekvőbb a szavakkal együtt a fájlban tárolni, de ez azzal jár, hogy mind a Szo osztályt, mind a beolvasás és (fájlba) kiírás kódját módosítani kell.

Szo.java:

Fel kell venni két új, public int típusú belső változót, egyet az összes megkérdezés nyilvántartására, egy másikat pedig a helyes válaszok számolására (mi osszes és helyes néven fogokunk hivatkozni rájuk). Mást nem kell módosítani itt.

Kiírás kódja:

Minden marad a régi, kivéve a writer.println(...) sor. Ide bele kell még venni, hogy a szó osszes és helyes belső változóinak értékét is kiírja, vesszővel elválasztva. Felmerülhet a kérdés, hogy melyiket írjuk ki előbb. Igazából mindegy, csak az a fontos, hogy következetesek legyünk, azaz beolvasásnál is azt keressük először, amit kiírtunk (fura lenne, ha ezt írnánk ki …,25,20 vagyis 25 kérdezésből 20szor találtuk el, míg beolvasásnál a találatokat vennénk előre, és azt találnánk, hogy 20 kérdezésből 25ször talált…). Egyezzünk meg abban, hogy mindenki az összes kérdezést írja előre, és utána a helyes válaszok számát!

Beolvasás kódja:

A beolvasás egy icipicit bonyolultabb lesz. Eddig így nézett ki:

while (reader.ready()) {
    String sor = reader.readLine(); // a kiolvasott sor most a sor változóban lakik.
    String[] nyelvek = sor.split(",");
    Szo ujSzo = new Szo(nyelvek[0], nyelvek[1]);
    szolista.add(ujSzo);
}

Ezt módosítani kell erre:

while (reader.ready()) {
    String sor = reader.readLine(); // a kiolvasott sor most a sor változóban lakik.
    String[] nyelvek = sor.split(",");
    Szo ujSzo = new Szo(nyelvek[0], nyelvek[1]);
    ujSzo.osszes = Integer.parseInt(nyelvek[2]);
    ujSzo.helyes = Integer.parseInt(nyelvek[3]);
    szolista.add(ujSzo);
}

Erre azért van szükség, mert a fájlból Sztring-ként érkezik az adat, de a két belső változónk int típusú. Vagyis a stringet át kell alakítani int-té.

Vigyázz! Ha a fájl még régi, és nincs a szavakhoz statisztika, akkor bizony a beolvasás hibát fog jelezni ("Az alkalmazás váratlanul leállt"). Ezt a legegyszerűbb úgy megkerülni, hogy törlöd a fájl tartalmát, vagyis egy sort sem olvasol be. Miután egyszer az új verzióval kiírtad a szavakat, minden működni fog.

Ennél elegánsabb, ha berakod az új részletet egy elágazásba, ami csak akkor fut, ha a nyelvek tömbben van 2. és 3. elem (feltételezzük, hogy csak 2 nyelvet tárolsz).

Végezetül ne felejtsd el a számokat frissíteni a KvizActivityben! Most már tényleg csak rajtad múlik, hogy milyen messze mész el a programoddal. Rendezheted például a szavakat nehézségi sorrendbe, vagy ráveheted a kvíz ablakot, hogy csak az új, könnyű, vagy épp a nehéz szavak közül kérdezzen!

5. Statisztika megjelenítése a SzavakActivity listában

Szuper, van statisztikánk, ami meg is marad az újraindítások között, de jó lenne, ha nem csak számítógépen, a fájlt megnyitva látnánk azt. Készítsünk egy saját Adapter-t és egy új layout-ot, amiben leírjuk, hogy hogyan nézzen ki a ListView egy-egy sora.

Kezdjük a layouttal:

  1. jobbklikk az app -> res -> layout mappán, New -> Layout resource file

  2. A neve legyen list_item_szo, a Root element-et pedig írjuk át RelativeLayout-ra. Minden más maradhat úgy, ahogy van.

  3. A layout szerkesztőben rakjunk össze egy egyszerű, kétsoros layoutot, valami hasonlót, mint a képen látható. A TextViewknak adjunk id-t is, pl.: nyelvek és statisztika.

A saját adapter:

  1. A SzavakActivity legaljára, de még a legutolsó záró kapcsos zárójelen belülre másold be a következő kódot. Alatta találsz egy kis magyarázatot az egyes részeihez, érdemes elolvasni, hogy mi mire jó / miért kell.

    class SajatAdapter extends ArrayAdapter<Szo> { // (1)
    
            SajatAdapter(Context context, int resource, List<Szo> objects) { // (2)
                super(context, resource, objects);
            }
    
            @Override
            public View getView(int position, View convertView, ViewGroup parent) { // (3)
                View v = LayoutInflater.from(getContext()).inflate(R.layout.list_item_szo, parent, false); // (4)
    
                TextView nyelvek = (TextView) v.findViewById(R.id.nyelvek); // (5)
                TextView statiszitka = (TextView) v.findViewById(R.id.statisztika);
    
                Szo aktualisSzo = getItem(position);
    
                nyelvek.setText(aktualisSzo.toString()); // (6)
                double szazalek = aktualisSzo.helyes / aktualisSzo.osszes * 100;
                statiszitka.setText(aktualisSzo.helyes + "/" + aktualisSzo.osszes + " - " + szazalek + "%");
    
                return v; // (7)
            }
        }

    Kattints rá alul a zöld keretes részekre, hogy megtudd, mire valók!

    1: extends ArrayAdapter<Szo>, azaz mindent megkap, amit az ArrayAdapter<Szo> tud, de kibővíthetjük illetve bizonyos tulajdonságait módosíthatjuk.

    2: Konstruktor. Semmi extrát nem csinál, csak továbbadja a paramétereit az ArrayAdapter<Szo>-nak.

    3: Az adapterek getView belső függvénye felelős az egyes listaelemek tartalmának meghatározásáért. Az alap ArrayList<Szo> csak egy sornyi szöveget tud megjeleníteni, azt is csak úgy, hogy az adott sorhoz tartozó objektumnak meghívja a toString belső függvényét. Ez nekünk kevés, ezért lecseréljük egy saját megoldásra.

    4: Először csinálunk egy új View-t (minden komponens egy View, így a list_item_szo layoutben Root element-ként megadott RelativeLayout is) a layoutunk alapján.

    5: Aztán megkeressük benne a két TextView-t.

    6: Kiírjuk rá a szöveget, amit szeretnénk

    7: végül visszaadjuk az így elkészített View-t, hogy a rendszer hozzáadja a ListView-hoz.

    
                                
  2. Az onCreate-ben, ahol az adaptert létrehozod, ne new ArrayAdapter<Szo>(...)-t hozz létre, hanem new SajatAdapter(...)-t! Valamint ugyanitt az android.R.layout.simple_list_item_1-et cseréld le R.layout.list_item_szo-ra! Minden más maradhat a régiben.

  3. A fenti kódrészletben a (6) részt nyugodtan írd át ahogy Te szeretnéd, hogy megjelenjen a listában a szó, ez csak egy példa megoldás!