Prezentacja nt. programowania UI w Androidzie
W sobotę wziąłem udział w pierwszej konferencji Appspirina – wydarzeniu poświęconemu tworzeniu aplikacji mobilnych. Ta pierwsza edycja była współorganizowana przez polski GTUG i tematycznie była związana z platformą Android, więc przypadkowo w całości pokrywała się z moimi zainteresowaniami ;)
Cieszę się więc, że mogłem tam wystąpić, prezentując wykład na temat niektórych co bardziej zaawansowanych aspektów programowania interfejsu użytkownika w Androidzie. Omówiłem tam nieco rzadziej spotykane przypadki użycia takich typowych narzędzi jak zasoby (resources), układy kontrolek (layouts) czy też różne sposoby powiadamiania użytkownika o istotnych i mniej istotnych zdarzeniach.
Na facebookowej stronie inicjatywy powinno się już pojawić kilka zdjęć, a wkrótce powinien też zostać udostępniony film z każdego z (trzech) wykładów. Póki co zamieszczam dla potomności slajdy ze swojej własnej prezentacji :)
Zaawansowane programowanie UI na platformie Android [PL] (762.0 KiB, 658 ściągnięć)
Implementacja przeciąganiaPrzedstawię dzisiaj dość elementarną technikę, związaną z szeroką pojętym programowaniem grafiki – a zwłaszcza interfejsów użytkownika. Chodzi o nic innego jak przeciąganie (dragging), czyli przemieszczanie różnych elementów za pomocą myszy czy innego urządzenia wskazującego (np. palca ;]). Celowo nie dodałem tutaj drugiej połowy procesu znanego jako drag & drop – czyli upuszczania – bo nie mam w ogóle na myśli potencjalnie zaawansowanej logiki związanej z wymianą danych reprezentowanych przez przeciągany obiekt. Przeciwnie; chodzi mi wyłącznie o samo przesuwanie go po ekranie. Rezultatem (niekoniecznie jedynym, rzecz jasna) ma być po prostu zmiana jego położenia.
W zaawansowanych systemach GUI rozwiązanie sprowadza się najczęściej do instrukcji w rodzaju obj.Draggable = true;, a cała reszta odbywa się “automagicznie”. Załóżmy jednak, że nie mamy zaawansowanego systemu GUI i dysponujemy jedynie możliwością rysowania oraz odbierania zdarzeń od wskaźnika myszy. Nie jest to wcale rzadka sytuacja: żeby nie szukać daleko, wystarczy cofnąć się o tydzień do opisu elementu <canvas> z HTML5 :) W ogólności dotyczy to jakiejkolwiek “czystej” biblioteki graficznej: OpenGL, DirectX, SDL, itp.
Doprecyzujmy jeszcze to, iż “odbieranie zdarzeń od myszy” obejmuje tak naprawdę poniższe trzy zdarzenia (lub ich odpowiedniki dla innych – np. dotykowych – sposobów wskazywania):
MouseDownMouseMoveMouseUpNietrudno zauważyć, że całkowicie wystarczają one do implementacji przeciągania. Jak więc miałaby ona wyglądać?
Obrazki dziewięciołatkoweJako dzisiejszą ciekawostkę chcę przedstawić pewną technikę, która może być przydatna przy implementacji systemu GUI na przykład na potrzeby gier. Zazwyczaj bowiem elementami interfejsu są różnego rodzaju kontrolki, które – chociaż nierzadko nie przypominają klasycznych widgetów w rodzaju przycisków czy pól tekstowych – posiadają prostokątne obramowanie. Ta ramka musi mieć jedną istotną cechę: powinna dopasowywać się do rozmiaru kontrolki, dopuszczając elementy o dowolnie dużych wymiarach. A jednocześnie powinna ona być utrzymana w odpowiednim stylu graficznym, zwykle spójnym z resztą aplikacji lub gry.
Sposobem na sprostanie tym wymaganiom jest podział prostokąta kontrolki na dziewięć części, różniących się zakresem skalowania, jakiemu są poddawane, gdy kontrolka zmienia rozmiary. Wśród tych części możemy mianowicie znaleźć następujące:
Żeby więc poprawnie narysować naszą kontrolkę, potrzebujemy w ogólności aż dziewięciu różnych obrazków. Oczywiście niekiedy będzie ich mniej, jeśli sprytnie posłużymy się obrotami i odbicia obrazów. Prawie na pewno jednak będzie to więcej niż jeden sprite i każdy z nich trzeba będzie jakoś oznaczyć i zapisać razem z naszą aplikacją, wymyślając do tego mniej lub bardziej skomplikowaną konwencję.
To, co chcę przedstawić, to w gruncie rzeczy taka właśnie konwencja. Nazywa się ona nine-patch images, czyli tytułowe obrazki dziewięciołatkowe. Pochodzi ona z systemu Android i jest jedną z wielu może mało efektownych, ale pomysłowych rozwiązań, ukrytych w jego API.
Idea jest dość prosta. Jeśli mamy już obramowanie w postaci obrazka otoczonego nim prostokąta, nie musimy wycinać z niego poszczególnych części i zapisywać ich osobno. Zamiast tego zaznaczamy je bezpośrednio na obrazku, dodając wpierw jednopikselowe, czarno-białe obramowanie. Czarny fragment określa część podlegającą skalowaniu (czyli środek), zaś biały – niezmieniające rozmiaru mu rogi prostokąta. Najlepiej widać to na ilustracji po lewej stronie.
Można na niej zauważyć, że do określenia obszaru skalowania wystarczy zasadniczo jeden dodatkowy rząd i jedna dodatkowa kolumna pikseli. Pozostała część dodatkowego obramowania może nam wtedy posłużyć do zdefiniowania obszaru treści, czyli tego fragmentu kontrolki, który będzie mógł być wypełniony tekstem i innymi elementami, ustawionymi jako jej zawartość. Obszar treści nie musi pokrywać się z obszarem skalowania, co z kolei jest widoczne na obrazku po prawej stronie.
Z tak przygotowanej grafiki wciąż możemy w miarę łatwo odczytać programowo potrzebne informacje o sposobie rysowania obramowania kontrolki i wypełniania jej treści. Rozwiązanie to ma przy tym tę oczywistą zaletę, że przygotowanie takiej grafiki jest z pewnością mniej pracochłonne i kłopotliwe niż tworzenie od 3 do 9 osobnych sprite‘ów i podawanie informacji o obszarze treści w jakiegoś rodzaju pliku konfiguracyjnym.
Jeśli więc ktoś w ramach silnikologii stosowanej zajmuje się implementacją modułu GUI, może rozważyć zastosowanie podobnego systemu :)
Odchudzanie lisaJak można przeczytać tu i ówdzie (a potem sprawdzić u źródła), premiera czwartej wersji przeglądarki Firefox przesunie się z końca tego roku na początek przyszłego. Zwolennicy tzw. bardziej nowoczesnych przeglądarek zapewne skorzystają z okazji, by używać tego faktu jako argumentu na rzecz tezy, że Ognisty Lis pozostaje w tyle pod względem szybkości, zgodności z sieciowymi standardami oraz estetyki i komfortu interfejsu.
Nawet jako jego użytkownik muszę przyznać, że przynajmniej ostatni z tych zarzutów faktycznie może być zasadny – ale tylko pod jednym zastrzeżeniem, które w przypadku Firefoksa jest po prostu nie fair. O ile bowiem FF3.6 out-of-the-box jest faktycznie dość siermiężny, o tyle nadrabia to z nawiązką możliwościami konfiguracji, których nie można w uczciwym zestawieniu pominąć.

Standardowy interfejs Firefoksa
Żeby jednak nie być gołosłownym pozwolę sobie pokazać, jak w kilku krokach możemy ów niezbyt imponujący, standardowy interfejs przekształcić w coś bardziej ‘nowoczesnego’. Mój opis można będzie potem potraktować jako swego rodzaju konstruktywną ripostę w świętych wojnach przeglądarkowych :)
Własne kontrolki w Windows FormsGdy tworzymy aplikacje okienkowe w .NET z użyciem Windows Forms, możemy korzystać z mnóstwa (co najmniej kilkudziesięciu) typów kontrolek dostępnych out-of-the-box. Nie wszystkie nawet mają rację bytu jako rzeczywiste kontrolki, ale w wielu przypadkach fakt, że nimi są, ułatwia korzystanie z API, które się kryje za taki komponentami. (Przykładem jest kontrolka BackgroundWorker).
Nie oznacza to jednak, że nie istnieje czasami potrzeba stworzenia własnego rodzaju kontrolki, specyficznej dla pisanego programu. Nawet jeśli miałaby ona być użyta tylko w jednym egzemplarzu, wyodrębnienie jej logiki poza zasadniczy kod aplikacji powinno wyjść im obu na dobre. W tym sensie jest ten rodzaj dodatkowej warstwy abstrakcji, który zazwyczaj opłaca się stworzyć.
Jak to się robi? Przede wszystkim należy wiedzieć, że w .NET kontrolki definiowane przez programistę mogą być jednego z trzech rodzajów. Stosunkowo najprostszym jest user control, które to stanowi po prostu pojemnik na kilka innych kontrolek zebranych w całość. Mogą to być na przykład dwa obiekty typu NumericUpDown opatrzone etykietami “Od” i “Do”, jeśli w naszym programie wielokrotnie i w różnych miejscach zachodzi potrzeba podawania przedziału liczbowego. Właściwości takiej customowej kontrolki są najczęściej bezpośrednio mapowane na właściwości kontrolek składowych, a zdarzenia są sygnalizowane w odpowiedzi na niektóre ze zdarzeń pochodzących z tychże kontrolek. W sumie więc można powiedzieć, że trochę tak jak funkcje i klasy pozwalają na wielokrotne wykorzystywanie kodu, tak user controls dają możliwość ponownego wykorzystania pewnych fragmentów UI.
Drugim typem są kontrolki dziedziczone (inherited controls). Nazwa wskazuje, że powstają one przez odziedziczenie już istniejącego rodzaju (klasy) kontrolki i dodanie do niej lub zmianę funkcjonalności. Może to obejmować dodanie nowych właściwości, częściowego lub całkowitego obsłużenia niektórych zdarzeń i inne tego typu modyfikacje zachowania kontrolki. Ważne jest tutaj to, iż efekt, jaki chcemy osiągnąć, jest na tyle zbliżony do jednej z istniejących kontrolek, że można ją wykorzystać i nie zaczynać od zera.
No ale nie zawsze tak jest i czasem naprawdę trzeba zacząć od podstaw. Jeżeli bazujemy na zupełnie abstrakcyjnym typie z góry hierarchii interfejsu – jak Control czy Component – to mamy do czynienia z kontrolką typu owner-draw. Dla niej musimy ręcznie rysować całą zawartość przy pomocy instrukcji GDI+ (stąd nazwa), opierając się przy tym na prostych zdarzeniach w rodzaju kliknięć myszy. Nie jest to więc łatwe i szybkie do wykonania zadanie.
Bywa aczkolwiek i tak, że jest ono koniecznie. Zanim się do niego zabierzemy, lepiej jednak przejrzeć alternatywy w postaci kontrolek odziedziczonych lub user controls. Możliwe, że w ten sposób zaoszczędzimy sobie pracy.
O układaniu kontrolekNajprostszym sposobem na rozmieszczanie kontrolek w oknie programu jest oczywiście… podanie ich pozycji i rozmiarów dosłownie. Ta metoda sprawdza się dobrze jednak tylko w prostych przypadkach: gdy mamy stałą (i raczej niewielką) liczbę obiektów, gdy rozmiary okna są stałe, gdy nie dostosowujemy rozmiaru interfejsu do rozdzielczości ekranu, itd. W wielu przypadkach to jednak nie wystarcza i dlatego systemy GUI oferują też inne narzędzia pozycjonowania elementów interfejsu. Niektóre z nich są całkiem pomysłowe. Oto kilka przykładów:

Dokowanie (docking), zwane też wyrównywaniem (align). Polega to na “przyklejeniu” kontrolki do którejś z krawędzi nadrzędnego pojemnika (np. okna) w jednym wymiarze oraz do obu krawędzi w wymiarze prostopadłym. Komponentowi pozostaje wówczas jeden stopień swobody. Przykładowo, kontrolka zadokowana po lewej stronie może się jedynie rozszerzać w prawo (lub zmniejszać w lewo), zaś jej górna i dolna krawędź pokrywają się zawsze z odpowiednimi krawędziami zawierającego ją pojemnika.
Ten sposób pozycjonowania jest idealny chociażby dla pasków stanu (status bar), które zawsze umiejscowione są w dolnej krawędzi okna.

Zakotwiczanie (anchoring) polega z kolei na ustawieniu stałej odległości pewnych krawędzi kontrolki od odpowiednich krawędzi kontrolki nadrzędnej. Domyślnie na przykład każdy komponent jest zakotwiczony z lewej i z góry. Gdy zmieniamy rozmiar zawierającego go okna, dystans między lewą krawędzią tegoż okna i lewą krawędzią kontrolki pozostaje stały; podobnie jest z górnymi brzegami. Zmieniając ustawienia zakotwiczania możemy uzyskać efekt kontrolki “wyrównanej” w podobny sposób jak tekst w edytorze, np. do prawej strony okna zamiast do lewej.
Anchoring można uznać za nieco lepszą wersję dokowania, ale w istocie różni się on od niego dość znacznie (zwłaszcza jeśli dokujemy wiele kontrolek po jednej stronie). Jest on jednak przydatny wtedy, gdy kontrolka ma w określony sposób zmieniać rozmiar wraz z oknem.

Układy (layouts) kontrolek. Są to bardziej wyspecjalizowane narzędzia, których jedynym zadaniem jest układanie elementów interfejsu w określony sposób. Do najbardziej przydatnych należy układ typu flow oraz układ tabelkowy. Ten pierwszy polega na rozmieszczaniu kontrolek po kolei (w ustalonym porządku) tak, jak wypisuje się tekst w kolejnych wierszach. (Jak widać, analogie tekstowe w projektowaniu interfejsu pojawiają się całkiem często ;]). Z kolei układ tabelkowy, jak nazwa wskazuje, dzieli zadany obszar na wiersze i kolumny tak, że w każdej z powstałych w ten sposób komórek mieści się dokładnie jedna kontrolką i wypełnia ją całkowicie.
Układy mają spore zastosowanie zwłaszcza wtedy, gdy rozmieszczane kontrolki są tworzone dynamicznie w trakcie działania programu. Potrafią wówczas zaoszczędzić dużo pracy z ręcznym przeliczaniem ich pozycji i/lub rozmiarów.
To stwierdzenie odnosi się zresztą do każdego z tych trzech sposobów na określanie wymiarów elementu interfejsu. Obecnie bardzo często okazuje się, że nawet stosunkowo zaawansowany układ kontrolek, który musi dostosowywać się do zmian rozmiaru okna, da się stworzyć bez obsługiwania zdarzeń typu OnResize i ręcznego rozmieszczania komponentów. A to może oszczędzić kilku kłopotów i pozwala łatwiej skoncentrować się na tych częściach programu, których wprawdzie nie widać, ale które są o wiele ważniejsze :)
System GUI #7 – Pole tekstoweWśród standardowych kontrolek spotykanych w każdym systemie graficznego interfejsu jest jedna, która zdecydowanie wyróżnia się stopniem skomplikowania. Z wierzchu to tylko mały, podłużny prostokąt z wpisanym ciągiem znaków, który można modyfikować. Jednak mechanizmy sprawiające, że jest to możliwe, nie są wcale takie proste.
Mówię tu oczywiście polu tekstowym, znanym też jako textbox, editbox lub po prostu edit.
Powodów, dla których właśnie ta kontrolka wyróżnia się złożonością, jest przynajmniej kilka. Są to chociażby:



Tak naprawdę choćby w systemie Windows pola tekstowe umożliwiają nieco więcej, jak na przykład możliwość cofnięcia ostatniej operacji, obsługa Schowka czy nawet menu kontekstowe. Takie cuda nie są chyba jednak potrzebne do szczęścia :)
Zauważmy też, że kontrolka umożliwiająca edycję dłuższego tekstu dzielonego wiersza to coś zupełnie innego niż zwykły textbox i jej możliwości są nieporównywalnie większe. Dochodzą tam bowiem paski przewijania, zaznaczanie rozciągnięte na kilka wierszy, i tak dalej.
Implementując proste pole tekstowe zauważyłem, że bardzo pomaga przy tym odpowiednia konstrukcja pewnych podstawowych mechanizmów GUI – jak mouse capture czy fokus klawiatury, modelu propagacji zdarzeń oraz modułu odpowiedzialnego za wypisywanie tekstu. Łącznie jednak wychodzi z tego i tak nadspodziewanie duża ilość kodu ze sporą liczbą zagmatwanych ifów :]