Online Android szakkör (DKRMG)
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.
Az általunk tervezett program négy Activityből áll.
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.
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.
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:
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.
Az Activity indulásakor (onCreate) be kell olvasnunk a szólistát. Ezt egy az egyben ugyanúgy tehetjük meg, mint a SzavakActivityben.
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.
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, .
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.
Az activity neve legyen KvizActivity
.
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
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:
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!
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.
Hozz létre egy gombot, ami majd ellenőrzi a megoldást.
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!).
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:
Szükséged lesz egy ArrayList
változóra, ahova a szavakat be tudod olvasni
Az ArrayList
edet első használat előtt létre kell hoznod a new
szó használatával.
Nyisd meg a fájlt, majd soronként dolgozd fel a tartalmát (mint az előző leckében)
Az egész szólistát nem jeleníted meg sehol, így nem lesz szükséged Adapter
-re.
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!
Készíts egy Random
típusú változót randomGenerator
néven
(a KvizActivity
osztályodban).
Az onCreate függvényben értéket kell adnunk a változónknak.
randomGenerator = new Random();
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:
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:
használd a listád size()
függvényét, hogy megtudd hány szó közül választhatsz.
Emlékszel? ez a parancs létre hozott neked egy véletlen számot, ahol
0 ≤ szám < 100
:
int gondoltSzam = randomGenerator.nextInt(100);
Most készíts egy véletlen számot, ahol 0 ≤ szám < listaMérete
.
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!
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 KvizActivity
ben 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);
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.
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.
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)!
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.
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.
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.).
A beállításokat kezelő objektum megszerzése: SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
A szerkesztéshez kell egy szerkesztő is hozzá:
SharedPreferences.Editor editor = prefs.edit();
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ő!
Végül be kell fejezni a szerkesztést: editor.commit();
… kicsit egyszerűbb, ahhoz nem kell külön "olvasó" objektum:
A beállításokat kezelő objektum megszerzése: SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this)
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ó.
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();
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 layout
hoz 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 ListView
nak
(setAdapter
(...) belső függvény).
É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.
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.
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!
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 KvizActivity
ben! 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!
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.
jobbklikk az app -> res -> layout mappán, New -> Layout resource file
A neve legyen list_item_szo
, a Root element-et pedig írjuk át RelativeLayout
-ra.
Minden más maradhat úgy, ahogy van.
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 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.
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.
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!