Zaawansowane programowanie UI na platformie Android Karol Kuczmarski Zasoby, kontrolki, dialogi i cały ten jazz
Kim jestem? Programista w firmie Polidea głównie Android i Google App Engine projekt Apphance Programista Androida Taphoo Autor bloga xion.log (http://xion.org.pl) Moderator forum serwisu warsztat.gd Autor popularnego kursu C++ 2
Plan na dziś Wykorzystanie zasobów do modyfikacji wyglądu UI Nine patch drawables Listy stanów (state list drawables / color state lists) Animacje typu tween (przejścia) Praca z układami kontrolek (layouts) Wybór właściwego układu (podklasy ViewGroup) Ponowne używanie układów (include, merge, style/tematy) Korzystanie z powiadomień Dialogi Powiadomienia na pasku stanu Toasty 3
Wykorzystanie zasobów Drawables i animacje
Co dają nam zasoby? Określenie układu, wyglądu, a nawet zachowania interfejsu użytkownika Automatyczne uwzględnianie istotnych parametrów urządzenia i aktualnego systemu Rozmiar ekranu, orientacja, język, Łatwość tworzenia i używania zasobów Zaawansowane mechanizmy gotowe do wykorzystania Zasoby typu drawable, animacje, plurals, Niekiedy skorzystanie z zasobów jest koniecznością Najprostszy przykład: ikona aplikacji 5
Nine patch drawables Obrazy ze specjalnym rodzajem skalowania Służą do określania wyglądu obramowań i wnętrz Przyciski, pola tekstowe, okienka i inne Używane jak wartość dla android:background 6
9p drawables: jak działają? Obramowanie wyznaczane jest przez 9 łatek Cztery narożniki + cztery boki + środek Łatki odpowiednio się skalują Narożniki nie zmieniają rozmiarów Boki są powielane w jednym wymiarze Środek skaluje się w obu wymiarach 7
9p drawables: tworzenie Pojedynczy obrazek z jednopikselową ramką Białe i czarne rejony wyznaczają obszary skalowania Można też określić obszar zawartości elementu Wystarczy dowolny program graficzny Edytor (Draw 9 patch) dołączony do SDK 8
9p drawables: niekoniecznie 9 Wbrew nazwie, łatek nie musi być dokładnie 9 Bardziej skomplikowane obramowanie może mieć ich więcej Poszczególne obszary skalują się wtedy proporcjonalnie Przykład: obszar stały pośrodku boku obramowania Ramka nie musi też skalować się w obu wymiarach 9
Zasoby z listą stanów (state list) Domyślnie kontrolki zmieniają się, aby pokazać swoją interaktywność Przykład: wciskany przycisk rzeczywiście się wciska Jeśli modyfikujemy wygląd kontrolek, to powinniśmy wziąć ten fakt pod uwagę Osobne grafiki dla poszczególnych stanów kontrolki Jak zapewnić ich odpowiednie przełączanie?... Implementując OnClickListener!... Niezupełnie :-) Rozwiązanie: zasoby z listą stanów 10
State lists: definiowanie Zasobami z listami stanów definiujemy jako XML Każda pozycja to mapowanie: Zbiór stanów => odpowiedni zasób Kolejność jest istotna Ostatnia pozycja to zwykle zasób domyślny <?xml version="1.0" encoding="utf-8"?> <selector...> <item android:state_pressed="true" android:drawable="@drawable/button_pressed" /> <item android:state_focused="true" android:drawable="@drawable/button_focused" /> <item android:drawable="@drawable/button_normal" /> </selector> 11
State lists: możliwe stany android:state_enabled czy element jest aktywny Zwykle określa się zasób dla stanu nieaktywnego (false) android:state_focused czy element ma fokus Rzadko dotyczy trybu dotykowego (touch mode) android:state_pressed czy element jest wciśnięty android:state_checked czy element jest wybrany Pola wyboru (checkboxes) i przyciski radiowe Pozostałe: android:selected android:checkable android:window_focused 12
State list drawables Definiujemy jako pliki XML w res/drawable Elementy listy odwołują się do innych, już istniejących zasobów Mogą być nimi prawie dowolne drawable Gotowe zasoby pasują do wszystkich atrybutów typu drawable android:background, android:drawableleft, itd. Przykład podany wcześniej 13
Color state lists Definiujemy jako pliki XML w res/color Elementy listy bezpośrednio przechowują wartości kolorów Standardowe formaty #RGB, #RRGGBB, itd. Gotowe zasoby pasują do wszystkich atrybutów typu color android:backgroundcolor, android:textcolor, itd. <?xml version="1.0" encoding="utf-8"?> <selector...> <item android:state_pressed="true" android:color="#ffff0000"/> <item android:state_focused="true" android:color="#ff0000ff"/> <item android:color="#ff000000"/> </selector> 14
Animacje Android obsługuje dwa odmienne typy animacji dla widoków: Klatkowe (frame animations) Typu tween Animacje klatkowe działają jak zasoby typu drawable <?xml version="1.0" encoding="utf-8"?> <animation-list... android:oneshot="false"> <item android:drawable="@drawable/spinner1" android:duration="200" /> <item android:drawable="@drawable/spinner2" android:duration="200" /> <item android:drawable="@drawable/spinner3" android:duration="200" /> </animation-list> 15 spinner.setbackgroundresource(r.drawable.spinner_anim); ((AnimationDrawable)spinner.getBackground()).start();
Animacje typu tween Animacje tween pozwalają na geometryczne przekształcenia kontrolek: Zmianę położenia (translację - <translate>) Zmianę rozmiarów (skalowanie - <scale>) Obrót 2D wokół punktu - <rotate> Zmianę przezroczystości (alfy - <alpha>) Możliwe jest kontrolowanie sposobu interpolacji animowanych wartości Transformacje można składać w zbiory (<set>), aby zgrupowane animacje uruchamiały się jednocześnie 16
Przykład animacji typu tween <?xml version="1.0" encoding="utf-8"?> <set...> <alpha android:fromalpha="1.0" android:toalpha="0.0" android:duration="300" /> <scale android:interpolator="@android:anim/accelerate_interpolator" android:fromxscale="1.0" android:toxscale="0.0" android:fromyscale="1.0" android:toyscale="0.0" android:pivotx="0%" android:pivoty="100%" android:duration="300" /> </set> res/anim/example.xml 17
Stosowanie animacji typu tween Ręczne wczytywanie i kontrola animacji: Animation anim = AnimationUtils.loadAnimation(this, R.anim.example); view.startanimation(anim); Ustawianie jako przejść, np.: activity.overridependingtransition(r.anim.in, R.anim.out); // API 5+ viewanimator.setinanimation(r.anim.in); viewanimator.setoutanimation(r.anim.out); 18
Przykład animowanego przejścia <?xml version="1.0" encoding="utf-8"?> <!-- Odjazd w lewą stronę --> <translate... android:fromxdelta="0%p" android:toxdelta="-100%p" android:interpolator="@android:anim/accelerate_interpolator" android:duration="300"> </translate> res/anim/out.xml <?xml version="1.0" encoding="utf-8"?> <!-- Wjazd z prawej strony --> <translate... android:fromxdelta="100%p" android:toxdelta="0%p" android:interpolator="@android:anim/accelerate_interpolator" android:duration="300"> </translate> res/anim/in.xml 19
Praca z układami kontrolek Efektywne tworzenie layoutów
Tworzenie układów nie jest proste zwłaszcza dla programistów tradycyjnych GUI Kontrolki nie mają właściwości X i Y?! Układy wymuszają elastyczność, o którą w klasycznych PC-towych frameworkach trzeba dbać Nie można po prostu ustawić stałych pozycji/wymiarów Ceną jest większy stopień skomplikowania przy mniejszej swobodzie manewru private void MainForm_Resize(EventArgs e) { ctl.x = (Width ctl.width) / 2; ctl.y = (Height ctl.height) / 2; } Należy znać typy układów (podklasy ViewGroup na Androidzie) i ich przypadki użycia 21
Który *Layout wybrać? Prawie zawsze odpowiedzią jest RelativeLayout Pewne wyjątki: Tylko jedna kontrolka - FrameLayout Układ przypomina tabelę - TableLayout Układ jest liniowy i: Wszystkie elementy mają stały rozmiar w jednostkach bezwzględnych (dip) lub Wszystkie elementy mają rozmiar proporcjonalny do rozmiaru całego układu (android:layout_weight) - LinearLayout 30dip 40dip 30dip 45% 55% 22
Przykład: holy grail http://www.alistapart.com/articles/holygrail 23
Rozwiązanie: tylko jeden RelativeLayout <RelativeLayout...> <View android:id="@+id/header" android:layout_alignparenttop="true" android:layout_width="match_parent" android:layout_height="50dip"/> <View android:id="@+id/footer" android:layout_alignparentbottom="true" android:layout_width="match_parent" android:layout_height="50dip"/> <View android:id="@+id/left" android:layout_alignparentleft="true" android:layout_below="@id/header" android:layout_above="@id/footer" android:layout_width="70dip" android:layout_height="match_parent" /> <View android:id="@+id/right" android:layout_alignparentright="true" android:layout_width="70dip" android:layout_height="match_parent" android:layout_below="@id/header" android:layout_above="@id/footer" /> <View android:id="@+id/content" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_below="@id/header" android:layout_above="@id/footer" android:layout_torightof="@+id/left" android:layout_toleftof="@+id/right" /> </RelativeLayout> 24
z odpowiednio wyrównanymi <RelativeLayout...> <View android:id="@+id/header" android:layout_alignparenttop="true" android:layout_width="match_parent" android:layout_height="50dip"/> <View android:id="@+id/footer" android:layout_alignparentbottom="true" android:layout_width="match_parent" android:layout_height="50dip"/> <View android:id="@+id/left" android:layout_alignparentleft="true" android:layout_below="@id/header" android:layout_above="@id/footer" android:layout_width="70dip" android:layout_height="match_parent" /> <View android:id="@+id/right" android:layout_alignparentright="true" android:layout_width="70dip" android:layout_height="match_parent" android:layout_below="@id/header" android:layout_above="@id/footer" /> <View android:id="@+id/content" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_below="@id/header" android:layout_above="@id/footer" android:layout_torightof="@+id/left" android:layout_toleftof="@+id/right" /> </RelativeLayout> 25
i wymierzonymi kontrolkami <RelativeLayout...> <View android:id="@+id/header" android:layout_alignparenttop="true" android:layout_width="match_parent" android:layout_height="50dip"/> <View android:id="@+id/footer" android:layout_alignparentbottom="true" android:layout_width="match_parent" android:layout_height="50dip"/> <View android:id="@+id/left" android:layout_alignparentleft="true" android:layout_below="@id/header" android:layout_above="@id/footer" android:layout_width="70dip" android:layout_height="match_parent" /> <View android:id="@+id/right" android:layout_alignparentright="true" android:layout_width="70dip" android:layout_height="match_parent" android:layout_below="@id/header" android:layout_above="@id/footer" /> <View android:id="@+id/content" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_below="@id/header" android:layout_above="@id/footer" android:layout_torightof="@+id/left" android:layout_toleftof="@+id/right" /> </RelativeLayout> 26
Ponowne wykorzystanie elementów UI Zasada DRY (Don t Repeat Yourself) działa też przy konstruowaniu interfejsu W Androidzie jest to możliwe poprzez wyodrębnianie: Powtarzalnych wartości do postaci zasobów (res/values) Właściwości kontrolek do postaci styli/tematów Grup kontrolek do postaci dołączanych układów <?xml version="1.0" encoding="utf-8"> <resources> <color name="background_color">#c0ffee</color> <color name="text_color">#face</color> </resources> 27
Ponowne wykorzystanie: style widoków Style pozwalają na zdefiniowanie zestawu wartości dla atrybutów widoków <?xml version="1.0" encoding="utf-8"> <resources> <style name="button"> <item name="android:textcolor">@color/textcolor</item> <item name="android:gravity">right</item> <item name="android:paddingleft">15dip</item> <item name="android:layout_height">wrap_content</item> </style> </resources> <Button android:id="@+id/my_button" style="@style/button"/> 28
Style widoków: dziedziczenie Dla stylu możemy ustawić styl nadrzędny Właściwości są dziedziczone, ale możemy je nadpisywać <style name="redbutton" parent="@style/button"> <item name="android:background">#f00</item> </style> Jeżeli nadrzędnym jest nasz własny styl, możemy używać notacji kropkowej: <style name="button.red"> <item name="android:background">#f00</item> </style> <style name="button.red.big"> <item name="android:textsize">30dip</item> </style> 29
Ponowne wykorzystanie układów Układy kontrolek nie muszą być wykorzystywane bezpośrednio (np. w Activity.setContentView) W razie potrzeby układ można załadować z zasobu przy pomocy klasy LayoutInflater W ten sposób można tworzyć dynamiczne układy kontrolek LayoutInflater inflater = getlayoutinflater(); for (...) { View element = inflater.inflate(r.layout.item, null, false); //... container.addview(element); } 30
Ponowne wykorzystanie: <include> i <merge> Możliwe jest też włączanie układu do XML-a innego układu za pomocą elementu <include> <RelativeLayout...> <include layout="@layout/top_bar" android:layout_alignparentop="true" /> <LinearLayout android:id="@+id/main_content"... /> </RelativeLayout> Jeśli włączany układ ma <merge> jako główny element, wtedy wstawiana jest tylko jego zawartość <merge...> <Button android:id="@+id/button1"... /> <Button android:id="@+id/button2"... /> </merge> 31
Korzystanie z powiadomień i ich upiększanie
Android notyfikacjami stoi System oferuje kilka opcji powiadomień Zajmiemy się trzema najczęściej używanymi: Toasty Powiadomienia na pasku stanu Dialogi Różnią się one m.in. inwazyjnością i stopniem możliwej interakcji użytkownika Interaktywność Inwazyjność Toasty żadna mała Pasek stanu mała żadna Dialogi pełna duża 33
Toasty Krótko widoczne informacje wyświetlane domyślnie pośrodku dolnej części ekranu Najprostsza wersja: sam tekst Toast.makeText(getContext(), R.string.toast_text, Toast.LENGTH_SHORT).show(); Bardziej skomplikowana: własny układ kontrolek Toast toast = new Toast(getContext()); toast.setduration(toast.length_long); toast.setview(toastview); toast.show(); 34
Notyfikacje na pasku stanu Reprezentują zdarzenia oczekujące akcji użytkownika, np. przychodzące wiadomości Składają się z: Ikony na pasku stanu Tekstu powiadomienia (ticker text) na pasku stanu Widoku pokazywanego po rozwinięciu paska Powiadomieniom mogą towarzyszyć inne sygnały Dźwięki, wibracje, miganie diody LED Interakcja użytkownika z powiadomieniem powoduje wysłanie ustalonego wcześniej intenta 35
Przykład notyfikacji tekstowej Notification notification = new Notification(R.drawable.icon, "Hello world!", System.currentTimeMillis()); Intent tapintent = new Intent(this, MyActivity.class); PendingIntent contentintent = PendingIntent.getActivity(this, tapintent, 0); notification.setlatesteventinfo(this, "Hello", "Nice to meet you.", contentintent); NotificationManager nm = (NotificationManager) getsystemservice(notification_service); nm.notify(1, notification); //... nm.cancel(1); 36
Własne widoki w notyfikacjach Notyfikacje mogą zawierać bardziej skomplikowany UI niż tylko sam tekst Paski postępu ściągania plików, kontrolki odtwarzania muzyki, itp. W tym celu należy użyć zdalnych widoków (RemoteViews), podając: Identyfikator układu kontrolek (R.layout) Opcjonalne modyfikacje właściwości kontrolek, aplikowane po załadowaniu układu Nie można użyć View bezpośrednio, bo notyfikacje istnieją w procesie systemowym, a nie aplikacji 37
Przykład: pasek postępu <?xml version="1.0" encoding="utf-8"?> <FrameLayout android:layout_width="match_parent" android:layout_height="match_parent" android:padding="5dip"> <ProgressBar android:id="@+id/progress_bar" style="?android:attr/progressbarstylehorizontal" android:layout_width="match_parent" android:layout_height="match_parent" /> </FrameLayout> 38 res/layout/notification_progress.xml
Pasek postępu: użycie RemoteViews RemoteViews contentview = new RemoteView(getPackageName(), R.layout.notification_progress); contentview.setprogressbar(r.id.progress_bar, 100, 0, false); Notification notification = new Notification(...); notification.flags = Notification.FLAG_ONGOING_EVENT; notification.contentintent = PendingIntent.getActivity(...); notification.contentview = contentview; notificationmanager.notify(notification_id, notification); // update notification.contentview.setprogressbar(r.id.progress_bar, 100, percentage, false); notificationmanager.notify(notification_id, notification); 39
stworzenie notyfikacji RemoteViews contentview = new RemoteView(getPackageName(), R.layout.notification_progress); contentview.setprogressbar(r.id.progress_bar, 100, 0, false); Notification notification = new Notification(...); notification.flags = Notification.FLAG_ONGOING_EVENT; notification.contentintent = PendingIntent.getActivity(...); notification.contentview = contentview; notificationmanager.notify(notification_id, notification); // update notification.contentview.setprogressbar(r.id.progress_bar, 100, percentage, false); notificationmanager.notify(notification_id, notification); 40
i jej aktualizacja RemoteViews contentview = new RemoteView(getPackageName(), R.layout.notification_progress); contentview.setprogressbar(r.id.progress_bar, 100, 0, false); Notification notification = new Notification(...); notification.flags = Notification.FLAG_ONGOING_EVENT; notification.contentintent = PendingIntent.getActivity(...); notification.contentview = contentview; notificationmanager.notify(notification_id, notification); // update notification.contentview.setprogressbar(r.id.progress_bar, 100, percentage, false); notificationmanager.notify(notification_id, notification); 41
Dialogi Android oferuje kilka wbudowanych klas dialogów na typowe (i mniej typowe) okazje AlertDialog i jego podklasy, np. ProgressDialog Możemy też używać klasy Dialog bezpośrednio Za pomocą własnych układów kontrolek i styli, możemy dostosować dialogi do wyglądu naszej aplikacji 42
Przypadki użycia dialogów AlertDialog ze standardowym układem Proste komunikaty o błędach, potwierdzenia operacji, dialogi typu Loading, itp. AlertDialog z własnym układem kontrolek Tworzony poprzez AlertDialog.Builder metodą setview() Odpowiedni do bardziej skomplikowanych komunikatów Dialog lub własna podklasa Dialog Gdy chcemy mieć pełną kontrolę nad wyglądem dialogu Gdy chcemy mieć interaktywne kontrolki w dialogu Przykład: własne menu opcji 43
Przykład własnego dialogu public class CustomDialog extends Dialog { public CustomDialog(Context context) { super(context, android.r.style.theme_translucent_notitlebar_fullscreen); getwindow().getattributes().windowanimations = R.style.DialogTransition; } setcontentview(r.layout.dialog); ((Button)findViewById(R.id.dialog_button)).setOnClickListener(buttonListener); private OnClickListener buttonlistener = new OnClickListener(){ } }; public void onclick(view view) { dismiss(); Toast.makeText(getContext(), "Dialog closed", Toast.LENGTH_SHORT).show(); } 44
Style dla dialogów Domyślnie dialogi używają stylu android.r.style.theme_dialog Zajmują tylko część ekranu Mają tytuł i systemową ramkę z paddingiem dookoła Używając innych styli, możemy eliminować niechciane elementy (np. android.r.style.theme_notitlebar) Największe możliwości daje android.r.style.theme_translucent_notitlebar_fullscreen, ale wymaga ręcznego pozycjonowania dialogu <?xml version="1.0" encoding="utf-8"?> <RelativeLayout... android:layout_width="250dip" android:layout_height="300dip" android:layout_gravity="center">... </RelativeLayout> 45
Przykład stylizowanego dialogu 46 <RelativeLayout width=dialogwidth height=dialogheight layout_gravity=center> c <RelativeLayout width=match_parent height=match_parent layout_marginbottom=buttonsheight/2> <LinearLayout width=match_parent height=buttonsheight layout_alignparentbotom=true>
Animacje przejścia dla dialogów Modyfikując atrybuty okna (Window) dialogu, możemy ustawić dla niego animacje przejścia..chociaż teoretycznie to nie powinno działać :-) <style name="dialogtransition"> <item name="android:windowenteranimation"> @anim/in</item> <item name="android:windowexitanimation"> @anim/out</item> </style> getwindow().getattributes().windowanimations = R.style.DialogTransition; 47
Na zakończenie
Co jeszcze?... Pozostałe rodzaje zasobów Samych zasobów typu drawable jest kilkanaście! Lokalizacja Media: audio i wideo Nowości z wersji 3.0 Fragmenty Action Bar Drag & drop Schowek 49
Przydatne źródła Specyfikacje systemowych styli i tematów http://developer.android.com/reference/android/r.style.html http://developer.android.com/guide/topics/ui/themes.html#platformstyles Wskazówki odnośnie konwencji UI w Androidzie http://www.androidpatterns.com/ http://www.androiduipatterns.com/ http://developer.android.com/guide/practices/ui_guidelines/index.html Optymalizowanie aplikacji pod 3.0 http://developer.android.com/guide/practices/optimizing-for-3.0.html 50
Dziękuję za uwagę :-)