Egy új badás feladat bevállalása után pillanatok alatt szembesültem azzal, hogy a UI framework megoldásai nem lesznek elegendőek, egyedi vezérlőelemekre lesz szükség. Kellő fórumozás és kísérletezés után rájöttem a nyitjára, ezt osztom meg most mindenkivel.
Először viszont egy kis helyesbítés: korábbi cikkemben a nine-patch bitmapek rajzolásánál vétettem egy hibát, melynek következtében a kép alja mindig hiányosan rajzolódik ki. Akkor nem tűnt fel, mert a buborékok alján maradt üres hely, de a folytatásban bemutatott elemeknél már felütötte a fejét.
Egy kis fejtörés után rájöttem: a Canvas nem rajzolja ki a kép alsó és jobb szélét! Ennek értelmében rajzoláskor mind a négy irányban meg kell toldani egy-egy pixellel a képet, és minden a helyére kerül. A következő ábrán már egy működő címsor háttere látható, Paint.NET-ben.
SegmentedControl
Az iOS platformon létezik egy egyszerű és hasznos vezérlőelem, mely egy többállású kapcsoló viselkedését utánozza: ez a UISegmentedControl. A leggyakrabban nézetváltásra, vagy egyszerű eldöntendő kérdésre válaszolásra szolgál.
Sajnos készen nem kapunk ilyet itt, de elkészíteni se nehéz egy kis gyakorlás után. Érdekes egyébként, hogy a rendszerben ettől függetlenül megtalálható: a Samsung Apps több helyen is használja. Legalább van képanyag, amit át lehet szabni saját felhasználásra.
Anatómia
A vezérlőelem első közelítésben egy olyan konténer, amely tetszőleges — bár célszerűen korlátozott — számú gomb (ún. szegmens) tárolására képes. Biztosítani kell az új gombok hozzáadását, valamint az azok megérintésekor keletkező események megfelelő továbbítását kifelé.
Egy ilyen gombnak két állapota van — normál és megnyomott —, de rajzoláskor figyelembe kell venni azt is, hogy a sorban hol található — bal szélen, középen, jobb szélen.
A következő struktúra adja magát:
A gombokhoz - hasonlóan a beépített vezérlőelemekhez - action ID-ket rendelünk. A Segment feladata, hogy a megfelelő képet és feliratot rajzolja ki, és érintés hatására a megfelelő Action eseményt generáljon — ehhez saját érintés-eseménykezelő szükséges. A SegmentedControl megkapja ezt az eseményt, kiválasztja a megfelelő szegmenst, majd a külvilág felé továbbítja ugyanezt.
Mivel a program is váltogathatja a szegmenseket, ha arra van szükség, célszerű a szegmenseket sorszám és az actionId alapján is tárolni a gyors elérés érdekében.
Megvalósítás
Az előző ábrán is látható, hogy a két saját osztály a Panel osztályból származik, nem pedig a sokkal általánosabbnak tűnő Controlból. Ennek sajnos nagyon egyszerű oka van: a Control, mint ősosztály, nem működik.
Valószínűleg van valamilyen belső mechanizmus, amit az implementációs osztályok (*Ex) meghívnak, mi viszont nem láthatjuk. Ez lehet, hogy csak egy bug, de a félhivatalos álláspont is az a fórumok alapján, hogy használjunk Panelt. A teljesítményt nem tudom, mennyiben befolyásolja, de ez talán ennél a felhasználásnál nem is kritikus. Vannak akik a Labelre esküsznek egyébként.
Egy egyedi vezérlőelem elkészítéséhez tehát kell egy Panel-leszármazott osztály, melyben két fontos dolgot kell implementálni: a konstrukciót és a rajzolást. Vegyünk egy ilyen headert:
class SegmentedControl : public Osp::Ui::Controls::Panel, public Osp::Ui::IActionEventListener { public: static void PreloadBitmaps(); SegmentedControl(); virtual ~SegmentedControl(); result Construct(const Osp::Graphics::Rectangle &bounds); public: result AddSegment(const Osp::Base::String &caption, int actionId); // ... public: void AddActionEventListener(Osp::Ui::IActionEventListener &listener); void RemoveActionEventListener(Osp::Ui::IActionEventListener &listener); private: result OnDraw(); void OnActionPerformed (const Osp::Ui::Control &source, int actionId); // ... private: // ... (tagváltozók) };
Sokmindent kihagytam, de a forráskódot úgyis megosztom a végén. Nézzük a fontosabb lépéseket sorban. Az ősosztályt egyszerűen le lehet rendezni, ha van egy érvényes téglalapunk. A SegmentedControlt tudjuk, hova akarjuk elhelyezni, így átadható közvetlenül.
result SegmentedControl::Construct(const Rectangle &bounds) { Panel::Construct(bounds); // ősosztály 2. fázis __listeners.Construct(10); // IActionEventListenerek tömbje __segmentsById.Construct(10, 0.75); // a korábban említett actionId -> szegmens map __segmentsByIndex.Construct(10); // sorszám -> szegmens (tömb) return E_SUCCESS; }
Az érvényesség kérdése viszont fontos a szegmensek szempontjából, mivel azok helyét eleinte még nem tudjuk — azaz nem szeretném kivezetni a fejlesztő felé. Bármi, ami legalább 1x1-es méretű, már érvényesnek számít, utólag pedig meg lehet változtatni. Fontos apróság! A bounds mindig a szülő-konténer koordinátarendszerében értendő.
Az események elosztása a mi feladatunk: a szegmensek felől érkező eseményt továbbítani kell a Listenereknek. Egy for ciklus és kész is, lásd a kódban. A szegmensek pedig nem jelennek meg kívülről, így egy listener elég számukra.
A rajzolás nem hoz sok újdonságot, szereznünk kell egy Canvast, és rárajzolni a hátteret. Mivel a Panel konténer, csak önmagát kell kirajzolnia, a tartalmazott többi elem automatikusan kirajzolódik — az OnDraw() pontosan ezelőtt hívódik meg.
Célszerű a gyakran használt képeket valamilyen megoldással csak egyszer betölteni, én most egy statikus tagváltozót és lusta betöltést alkalmaztam. A gyakorlat mondjuk azt mutatja, hogy a lusta betöltés nem előnyös: célszerűbb az alkalmazás indulásakor betölteni ezeket, amíg a kezdőképernyő látszik, különben lag jelentkezhet.
A használatához végre kell hajtani a konstrukció mindkét fázisát, majd az AddControl (öröklött) metódus segítségével egy szülőhöz kell rendelni minél hamarabb. Példa:
__selector = new SegmentedControl; __selector->Construct(Rectangle(0, 0, GetWidth(), 72)); __selector->AddActionEventListener(*this); AddControl(*__selector);
Konklúzió
Ha egyszer ráérez az ember, gyorsan lehet tömegtermelni a saját vezérlőelemeket. A mellékelt zipben megtalálható a SegmentedControl teljes forrása, az Appsból kivágott képekkel, használatra készen. Ráadásként mellékeltem egy korábbi kísérletem, mely a Form címsorát másolja le, és hagyományos Buttonöket képes tárolni — egyfajta toolbarként.
Karma