Hirdetés

Alkalmazásfejlesztés badára: Még több UI mágia

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!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

Előzmények