9. lecke Szótár (2)

Online Android szakkör (DKRMG)


Hol tartunk most?

Ez a szótárfüzet alkalmazás projekt második 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ében, illetve milyen munkát tartogat a mostani segédanyag.

A program részei

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

Ebből mi készült el?

Az előző leckében majdnem teljesen elkészítettük a menünket, írtunk egy névjegy (about) activityt, illetve lefektettük a SzavakActivity alapjait. Utóbbi ablak egészen pontosan ott tart, hogy az általunk beírt szavakat szépen kilistázza a Szo típusú tömbünkből.

Tehát mi maradt a mai napra?

A programunk a mostani állapotában alig pár szót használ, amit a program készítésekor kellett beírnunk a Java kódunkba. Ez nem túl kényelmes, és szélesebb körben gyakorlatilag használhatatlanná teszi az alkalmazást. Tehát ma:

A külső fájlok használata és a kvíz elkészítése a következő leckékre marad.

Szavak hozzáadása

Biztos nektek is feltűnt, hogy jelenleg ahhoz, hogy új szópárokat adhassunk hozzá az alkalmazáshoz, közvetlenül a Java forráskódot kell szerkesztgetni, és mindig újra el kell készíteni (fordítani) az alkalmazást a zöld "Futtatás" gombra kattintva. Ez egy se nem túl kellemes sem nem gyors, és hosszú távon gyakorlatilag használhatatlan megoldás.
Sokkal jobb lenne, ha az alkalmazás futása közben lehetne beírni új szavakat, amik rögtön meg is jelennének a listában illetve később már ezeket is visszakérdezné az alkalmazás. Ehhez kell egy felület, ahol be lehet írni az új szópárokat, valamint a Java kódot is meg kell írni, ami elvégzi a hozzáadást. Csináljuk is meg!

Felhasználói felület (layout)

Először a layoutot csináljuk meg, mégpedig oda, ahol a listát is megjelenítjük (activity_szavak.xml):

A feladat elvégzéséhez próbáld meg a SzavakActvity layoutját a képen ábrázolthoz hasonlóra átalakítani! (Segítség a kép alatt.)

1. feladat

Sajnos több éves pályafutásunk alatt még nem találkoztunk olyan layout szerkesztővel, ami igazán egyszerűen segíti a megálmodott layout kialakítását. Ez alól az Android Studio szerkesztője sem kivétel, bár általában elég jól használható.

A kalandvágyóbbak megpróbálkozhatnak maguk összeállítani a fenti layoutot. Ehhez az egérrel kattintgatás mellett érdemes az adott komponens layout:alignComponent tulajdonságát állítgatni (lásd kép). Itt az egyes sorok saját:másik felépítésűek, azaz például a top:bottom = hun azt jelenti, hogy az adott komponens tetejét (top) a hun azonosítójú komponens aljához (bottom) igazítja.

A kevésbé kalandvágyóbbaknak előkészítettünk két layoutot: két nyelv, három nyelv. Ezeket le kell tölteni (jobbklikk -> Link mentése másként), és felülírni velük a projekt mappában az app/src/main/res/layout/activity_szavak.xml fájlt.

Ebben a következő azonosítókat használtuk:

2. feladat

Működés (Java kód)

Most nézzük meg, hogy minek is kéne történnie a gomb megnyomásakor.

Ezeket mind a gomb onClick-jében kell megírni!

Lépésről lépésre:

  1. Szöveg kiolvasása: Ehhez hasonló már volt, szintén a 3. leckében. Olvassatok vissza, hogy kellett! Figyelem, most a kiolvasott értéket nem kell számmá alakítani!
    String magyar = hun.getText().toString();
  2. Új szó létrehozása: Erre sok példa volt az előző leckében, tessék puskázni! Javasoljuk, hogy az új változód neve legyen ujSzo!
    Szo ujSzo = new Szo(magyar, angol);
  3. Új szó hozzáadása a szavak tömbhöz: Sajnos a tömb fix méretű adatszerkezet, ha egyszer létre lett hozva, már nem lehet növelni a méretét. Amikor az onCreate-ben létrehoztuk így:
    szavak = new Szo[] {szo1, szo2, szo3};
    akkor egy 3 egység hosszú tömböt csináltunk, amibe nem fér bele egy negyedik szó!
    Bár ez a probléma áthidalható, az inkább haladóbb feladat. Az érdeklődőknek kapcsolódik ide két szorgalmi feladat, amiket most érdemes megcsinálni, mert a későbbi változtatások után már nem működnek. Dióhéjban összefoglalva azonban annyit elárulhatunk mindenkinek, hogy a tömbös megoldásnál állandóan új tömböket kell létrehoznunk, elemeket kell másolgatnunk, és össze-vissza pakolgatnunk.
  4. Létezik azonban egy sokkal egyszerűbb és szebb megoldás erre a problémára: a Listák használata. A következő részben megismerkedünk a listákkal, és befejezzük az onClick függvényt. Addig nyugodtan hagyjátok így félkészen!

Második megközelítés: List (Lista)

Remélhetőleg ezen a ponton mindenkiben felmerül a kérdés: ha a tömbökbe ilyen nehéz új elemeket beilleszteni, törölni pedig elegánsan és gyorsan szinte lehetetlen, akkor milyen más megoldás van helyette?

Az ArrayList

A jó hír, hogy létezik Java-ban egy a tömbökhöz hasonló, ArrayList nevű adatszerkezet, amit pont az ilyen problémákra találtak ki. Az ArrayList jó tulajdonságait átvette a tömböktől, a rosszakat pedig igyekezett a legjobban áthidalni. Nézzük meg, mit tud:

Néhány példa a használatára:

Mi változik?

Láthatjuk, hogy a bonyolultabb műveleteknél sokkal kényelmesebb listákat használni. Írjuk is át az alkalmazásunkat listásra!

  1. Ismét módosítani kell a szavak változó típusát, ezúttal Szo[] helyett ArrayList<Szo> legyen!
  2. Az onCreate-ben most pirossal alá van húzva a szavak létrehozása. Ez azért van, mert már nem tömb típusú a változó, de még úgy szeretnénk inicializálni, mintha tömb lenne. Írjuk át ezt is:
    szavak = new ArrayList<Szo>();
  3. Talán az egyetlen apróbb hátránya a listáknak, hogy nem annyira egyszerű a kezdeti elemeket belepakolni, mint a tömbbe. Ezt a részt a következő leckében, korábbi ígéretünknek megfelelően, átalakítjuk majd, hogy egy csinos kis fájlból olvassa be a szavakat. Addig viszont írd át a kódot, hogy az alábbi mintát kövesse:
    szavak.add(new Szo("kutya", "dog"));
    szavak.add(new Szo("macska", "cat"));
    // stb…
  4. Az adapter létrehozását nem kell módosítani. Hurrá!
  5. Most már be tudjuk fejezni a félbehagyott onClick függvényünket:
    Az első két lépés stimmel, tehát továbbra is ki kell olvasni a magyar és az angol alakját a szónak, és gyártani kell belőlük egy új szót. Ez lesz az ujSzo változóban.
    A 3. pontot most már mindenki meg tudja oldani, ugye?
    szavak.add(ujSzo);
  6. Ha most kipróbálod a programot, azt láthatod, hogy sajnos nem igazán úgy működik, ahogy szeretnénk: gombnyomásra nem jelenik meg az új szó a listában! Ez azért van, mert mi a szavak listát módosítottuk ugyan, de arról nem szóltunk senkinek, hogy módosult! Az adapter pedig nem figyeli folyamatosan, külön szólni kell neki, ha változás történt. Elméletileg megtehetnénk, hogy a lista minden egyes módosításakor létrehozunk egy új adaptert, de ennél sokkal csinosabb megoldás, ha inkább elmentjük az adapterünket egy változóban, majd az notifyDataSetChanged() belső függvényt meghívjuk a változtatások után. Tehát: ahhoz, hogy ezt használhassuk, először az adaptert el kell tárolni egy külön változóba, ami mindenhonnan elérhető:
    1. Az Activity elején a szavak változó deklarációja alá hozzunk létre egy új változót, aminek típusa ArrayAdapter<Szo>, neve adapter legyen.
    2. Az onCreate függvényben keressük meg ezt a sort:
      ArrayAdapter<Szo> adapter = new ArrayAdapter<Szo>(this, android.R.layout.simple_list_item_1, szavak);
      és töröljük ki az elejéről az ArrayAdapter<Szo> részt. Figyeld meg, hogy megváltozott az "adapter" változó színe! Ez azért van, mert eddig úgynevezett lokális változó volt, azaz csak a függvényen belül létezett, mostantól pedig az Activity globális változóját jelenti (amit az előbb fent deklaráltunk). Az adapterünk mostantól az osztály összes függvényében elérhető.
  7. Menjünk vissza az onClick függvénybe, és a végén hívjuk meg a globális adapter notifyDataSetChanged() belső függvényét:
    adapter.notifyDataSetChanged();
  8. A még jobb felhasználói élmény eléréséhez érdemes lenne szólni a felhasználónak, hogy sikeresen hozzá lett adva az új szó:
    A függvény végén (de még a kapcsos zárójelen belül!) jeleníts meg egy Toast-ot pl. ezzel a felirattal:
    "Hozzáadva: <az új szó szöveges formája, mint a listában>"
    Figyelj, hogy itt most neked kell meghívnod az új szó toString függvényét! Ha nem emlékszel a Toastokra, akkor puskázz a gyorssegély lapról.
    Még hasznosabb lenne, ha rezegne is a telefon egy rövidet (pl.: 100 ms) ilyenkor! Írd meg ezt is (A 7. lecke 3. szorgalmijánál, vagy a gyorssegély lapon találsz hozzá segítséget)!

Ezzel készen is vagyunk! Próbáld ki, adj hozzá pár új szót a listához!

Szavak törlése

Beírtál egy szót, de már megtanultad, és csak útban van? Vagy esetleg elírtál valamit? Sajnos jelenleg az egytelen lehetőségünk a törlésre, hogy kilépünk a programból, újraindítjuk azt, majd újra bevisszük az összes többi szót. Ez így nem túl használható, bár kétségkívül ha sokszor írjuk be a szópárokat, egy idő után megtanulja az ember őket!

Sokkal jobb lenne, ha egy-egy szót tudnánk törölni a listából anélkül, hogy az egészet előről kéne kezdenünk.

Természetesen erre is van megoldás, nézzük is meg!

Melyik szót töröljük?

Ahhoz, hogy megtudjuk melyik szót kell törölni a listából, valahogy lehetőséget kéne biztosítani a felhasználónak arra, hogy kiválassza a törlendő szót. Android eszközökön általában "hosszú érintésre" (long tap) felugrik egy kis helyi menü, hasonlóan ahhoz, amikor a számítógépen jobb egérgombbal kattintasz valamire. Mi most a helyi menüt (aki utánanézne: context menu a kulcsszó) kihagyjuk, helyette hosszú érintésre rögtön törölni fogunk.

A gombok onClick-jéhez hasonlóan írhatunk egy olyan függvényt, ami az ilyen hosszú érintésekre figyel. Itt is kell valami a Java kódban, ahol megírhatjuk, hogy mi történjen ha a felhasználó rajtatartja az ujját a lista egy során. A szimpla kattintással/érintéssel szemben azonban nem elég egyetlen függvényt megírni, és a layout szerkesztőben hozzárendelni, hanem egy egész picit bonyolultabban kell megoldani.

A SzavakActivity onCreate függvényébe másold be az alábbi kódot:

lista.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
    @Override
    public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
        // a szavak lista position-adik elemére kattintott a felhasználó

        // itt kell megírni, hogy mi történjen hosszú érintésre

        return true; // ez fontos, ezzel jelezzük a rendszernek, hogy tudomásul vettük a hosszú érintést.
    }
});

A fenti sorokból ijesztőnek tűnhetnek, de most elég annyit megérteni belőlük, hogy: A fenti kód megmondja, hogy a lista változó OnItemLongClick eseményénél mi történjen. Tehát nem elég, hogy hivatkozunk a lista változónkra, de rögtön azt a kódot is ide írjuk be, amit egyébként külön függvénybe helyeznénk.

És most nézzük meg, hogy mit is kell csinálni, a törléshez:

  1. Ki kell olvasni a szavak listából a törlendő szót.
  2. Ki kell törölni a listából a törlendő szót.
  3. Érdemes lehet egy rövid rezgést is elindítani, hogy a még jobb felhasználói élmény kedvéért.

Részletesen:

  1. Ha megnézed fent létrehozott onItemLongClick függvényt, akkor láthatod, hogy a harmadik paramétere (int position) egy szám, mégpedig pont a törlendő szó indexe a szavak listában.
    Csináljunk itt helyben, a függvényen belül egy Szo típusú változót, és rakjuk bele a törlendő szót! (Olvass vissza, hogy hogyan kell a listából kiolvasni egy adott elemet!).
    Szo torlendo = szavak.get(position);
  2. A szó törlését puskázd ki fentebbről (az ArrayList leírásánál szerepelt már!)
    Ne felejtsd el, hogy itt is a listát módosítod, amit az Adapter/ListView nem figyel folyamatosan, így a törlés után külön szólni kell az adapternek, hogy változás történt (notifyDataSetChanged()).
  3. A rezgés elindításának viszont már fejből kéne mennie! :)
    Vagy puskázhatsz a hozzáadás részből is.

Az lenne az igazi, ha a fenti 3-4 lépést már magadtól (esetleg a korábbi kódjaidat felhasználva) sikerülne megírni, de ha esetleg mégis elakadtál volna, vagy csak leellenőriznéd a munkádat, itt egy lehetséges megoldás:

lista.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
    @Override
    public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
        // keressük ki a törlendő szót
        Szo torlendo = szavak.get(position);

        // töröljük a szót a listából
        szavak.remove(position);

        //szóljuk az adapternek, hogy módosult a lista
        adapter.notifyDataSetChanged();

        rezgoMotor.vibrate(100);

        return true;
    }
});

Ne felejtsd el, hogy ezt az onCreate-be kell beírnod!

Vége!

Gratulálunk! Ha idáig eljutottál, az azt jelenti hogy sikerült mindent pontosan követned ebben a nem túl egyszerű, ám annál több helyen elrontható leckében!

Ha minden működik, csinálj pár screenshotot, amin látszik, hogy hozzáadtál pár saját szót!
Az elkészült projektet csomagold be, és küldd el nekünk a feltöltő oldalon keresztül!

Hamarosan jelentkezünk a következő leckével, amiben végre megoldjuk, hogy megmaradjanak a szavaink két programfuttatás között is!