Podziękowania Jestem wdzięczny redaktor naczelnej Laurze Lewin za to, że uwierzyła we mnie i umożliwiła napisanie tej książki. Moja wdzięczność należy się Songlin Qiu, redaktor prowadzącej, która przekazała mi wiele opinii, a one pomogły ukształtować wszystkie rozdziały. To właśnie ona odegrała kluczową rolę w poprawie struktury książki oraz jakości informacji w niej zawartych. Muszę też podziękować korektorom, Jamesowi Becwarowi, Rominowi Iraniemu i Prashantowi Thakkarowi (Pandhi), za ich doskonały i szczegółowy przegląd książki oraz wiele przydatnych komentarzy i sugestii. Specjalne podziękowania należą się Karen Gill za jej najwyższej klasy redakcję językową i strukturalną tekstu. Doceniam jej starania w zakresie poszerzenia zawartości książki i jej wygładzenia. Wielkie i nieustające podziękowania dla Olivii Basegio, asystentki redakcji, za świetną robotę. Wyrazy wdzięczności dla całego zespołu za wydanie książki na czas. Specjalne podziękowania dla redakcji i całego zespołu w Pearson Technology Group, który pracował bez wytchnienia, aby wydać tę książkę. Bardzo podobała mi się współpraca z każdym z was. Jestem również wdzięczny rodzinie, która jest moim małym światem: mojej żonie Anuszce i moim wspaniałym dzieciom Chiragowi i Namanowi, za inspirację i motywowanie mnie do pracy. Proszę ich o wybaczenie za to, że spędzałem tyle czasu przy komputerze. Muszę wspomnieć też o moich studentach, którzy byli dla mnie dobrymi nauczycielami. Ich interesujące pytania pomogły zrozumieć oczekiwania czytelnika i zainspirowały mnie do napisania książki, w której skupiłem się na praktycznym podejściu.
Spis treści
O autorze . .........................................................................11 Wstęp . .............................................................................13
Część I
Techniki interfejsu użytkownika ..........................21
Rozdział 1
Przegląd aplikacji na tablety z systemem Android . .............23 Receptura: wprowadzenie do tabletów z systemem Android ..............23 Receptura: różnice pomiędzy telefonami Android a tabletami Android .....................................................................25 Receptura: zapewnianie kompatybilności aplikacji z telefonami i tabletami z systemem Android ...................................................26 Receptura: tworzenie urządzeń AVD ................................................27 Receptura: struktura katalogów projektu Android .............................32 Receptura: konwersja aplikacji z telefonu Android w aplikację na tablet Android ........................................................................37 Receptura: wymuszanie, aby aplikacja działała jedynie na tabletach ...48 Receptura: aktywności ...................................................................49 Receptura: cykl życia aktywności w systemie Android .......................49 Receptura: rozpoczynanie korzystania z intencji ...............................53 Receptura: przekazywanie danych z jednej aktywności do drugiej ......58 Podsumowanie .............................................................................64
Rozdział 2
Fragmenty . .......................................................................67
Receptura: wprowadzenie do fragmentów .............................................................68 Receptura: cykl życia fragmentu ..........................................................................68 Receptura: tworzenie fragmentów pierwszego planu oraz różnice pomiędzy fragmentami pierwszego planu a fragmentami w tle ...............................70 Receptura: dodawanie i usuwanie fragmentów w przypadku zmiany orientacji urządzenia .............................................................78 Receptura: rola klas FragmentManager i FragmentTransaction w obsłudze fragmentów ......................................................................................83 Receptura: tworzenie fragmentów dynamicznie w trakcie wykonywania aplikacji ....................................................86
6
Android na tablecie. Receptury Receptura: implementowanie komunikacji pomiędzy fragmentami .....92 Receptura: wyświetlanie opcji za pomocą klasy ListFragment ............98 Receptura: wyświetlanie okien dialogowych za pomocą klasy DialogFragment ...............................................102 Receptura: konfigurowanie preferencji użytkownika za pomocą klasy PreferenceFragment .........................................109 Podsumowanie ...........................................................................117
Rozdział 3
Paski akcji w działaniu . ...................................................119 Receptura: różnice pomiędzy menu i paskiem akcji ........................119 Receptura: przełączanie widoczności paska akcji ...........................120 Receptura: komponenty paska akcji .............................................121 Receptura: wyświetlanie elementów akcji w pasku akcji .................121 Receptura: nawigowanie do strony głównej po wybraniu ikony aplikacji .........................................................126 Receptura: wyświetlanie widoków akcji w pasku akcji .....................127 Receptura: wyświetlanie podmenu w pasku akcji ...........................132 Receptura: tworzenie paska zadań z zakładkami ............................139 Receptura: tworzenie paska akcji z rozwijaną listą .........................145 Podsumowanie ...........................................................................149
Rozdział 4
Nowe widżety . ................................................................151
Receptura: wyświetlanie kalendarza w aplikacji Android .......................................151 Receptura: wyświetlanie i wybieranie liczb za pomocą widżetu NumberPicker .....................................................................154 Receptura: tworzenie stosu obrazów za pomocą widżetu StackView ...........................................................................160 Receptura: wyświetlanie listy opcji za pomocą widżetu ListPopupWindow .........................................165 Receptura: sugerowanie opcji za pomocą widżetu PopupMenu ........170 Podsumowanie ...........................................................................172
Część II Rozdział 5
Zarządzanie zawartością ..................................175 Schowek systemowy oraz operacja przeciągnij i upuść ......177 Receptura: operacja przeciągnij i upuść ........................................177 Receptura: przeciąganie i upuszczanie tekstu ................................179 Receptura: przeciąganie i upuszczanie obrazów .............................188 Receptura: wycinanie, kopiowanie i wklejanie tekstu przy wykorzystaniu schowka systemowego ..................................198 Podsumowanie ...........................................................................202
Rozdział 6
Powiadomienia oraz intencje oczekujące . ........................205 Receptura: intencje oczekujące ....................................................205 Receptura: rozgłaszanie intencji ...................................................207 Receptura: system powiadomień systemu Android .........................214 Receptura: tworzenie powiadomień ...............................................215
Spis treści Receptura: wykorzystanie klasy Notification.Builder .......................216 Receptura: pozyskiwanie obiektu klasy NotificationManager ...........218 Receptura: tworzenie powiadomienia i wykorzystywanie intencji oczekującej w celu rozpoczęcia aktywności .................................218 Podsumowanie ...........................................................................222
Rozdział 7
Ładowarki . ......................................................................225
Receptura: ładowarki ........................................................................................225 Receptura: dostawca treści ...............................................................................226 Receptura: zastosowanie klasy CursorLoader w celu uzyskania dostępu do informacji przechowywanych przez dostawcę treści Contacts .........................................................................228 Receptura: Tworzenie niestandardowego dostawcy treści ....................................233 Receptura: wyświetlanie informacji z niestandardowego dostawcy treści ..................................................................243 Receptura: aktualizowanie i usuwanie informacji przechowywanych w niestandardowym dostawcy treści Podsumowanie ...........................................................................252
Część III Techniki multimedialne ....................................255 Rozdział 8
Animacje . .......................................................................257 Receptura: typy animacji ..............................................................257 Receptura: korzystanie z klasy ValueAnimator ...............................259 Receptura: wykorzystanie klasy ObjectAnimator do animowania widoków ............................................................267 Receptura: uruchamianie wielu animacji za pomocą klasy AnimatorSet ....................................................273 Receptura: animacja poklatkowa ..................................................279 Receptura: animacja generująca klatki pośrednie ..........................283 Korzystanie z klasy AlphaAnimation .........................................287 Korzystanie z klasy TranslateAnimation ...................................287 Korzystanie z klasy RotateAnimation .......................................288 Korzystanie z klasy ScaleAnimation .........................................289 Receptura: zastosowanie animacji układu .....................................293 Receptura: gromadzenie i wyświetlanie sekwencji animacji za pomocą klasy AnimationSet ...................................................301 Podsumowanie ...........................................................................306
Rozdział 9
Sprzętowa akceleracja grafiki 2D . ...................................309 Receptura: akceleracja sprzętowa ................................................309 Receptura: korzystanie z warstw widoku ........................................313 Receptura: poprawa wydajności aplikacji opartych na grafice przy wykorzystaniu klasy SurfaceView .........................................317 Receptura: zastosowanie transformacji z wykorzystaniem klasy TextureView ...........................................323 Podsumowanie ...........................................................................326
7
8
Android na tablecie. Receptury
Rozdział 10 Tworzenie i renderowanie grafiki . .....................................327 Receptura: interfejsy API wymagane dla grafiki ..............................327 Receptura: tworzenie i renderowanie prostokąta przy użyciu OpenGL ...................................................................328 Receptura: zastosowanie kolorów wieloodcieniowych .....................334 Receptura: rotacja grafiki .............................................................337 Receptura: skalowanie grafiki ......................................................342 Receptura: przesuwanie grafiki .....................................................346 Podsumowanie ...........................................................................349
Rozdział 11 Przechwytywanie audio, wideo i obrazów . ........................351 Receptura: przechwytywanie obrazu z wykorzystaniem wbudowanej intencji ........................................351 Receptura: przechwytywanie obrazu za pomocą kodu Java . .................................................356 Receptura: nagrywanie audio z wykorzystaniem wbudowanej intencji ........................................362 Receptura: klasa CamcorderProfile ...............................................365 Receptura: klasa MediaRecorder i jej metody ................................372 Receptura: nagrywanie audio z wykorzystaniem kodu Java ..............373 Receptura: nagrywanie wideo za pomocą wbudowanej intencji ........379 Receptura: nagrywanie wideo z użyciem kodu Java .........................382 Podsumowanie ...........................................................................389
Część IV Interfejs sieciowy i sprzętowy ...........................391 Rozdział 12 Łączność bezprzewodowa . ...............................................393 Receptura: wiązanie ze sobą dwóch urządzeń Bluetooth .................393 Receptura: ręczne przesyłanie plików z jednego urządzenia na drugie z wykorzystaniem technologii Bluetooth ........................397 Receptura: łączenie w parę urządzenia Bluetooth z komputerem z systemem Windows ..........................................399 Receptura: włączanie lokalnego urządzenia Bluetooth ....................400 Receptura: wyświetlanie listy powiązanych urządzeń ......................405 Receptura: przesyłanie plików za pomocą technologii Bluetooth ......410 Receptura: standard Wi-Fi ............................................................412 Receptura: włączanie i wyłączanie Wi-Fi .........................................414 Receptura: Wi-Fi Direct ................................................................418 Podsumowanie ...........................................................................423
Rozdział 13 Rdzenie i wątki . ..............................................................425 Receptura: użyteczność architektury procesorów wielordzeniowych ...425 Receptura: użyteczność procesów odzyskiwania pamięci ................426 Receptura: wątki .........................................................................429 Receptura: używanie wielu wątków ...............................................432 Receptura: korzystanie z klasy AsyncTask .....................................437 Podsumowanie ...........................................................................442
Spis treści
Rozdział 14 Klawiatury i sensory . .......................................................443 Receptura: zmiana klawiatury i metody wprowadzania danych w systemie Android ...................................................................443 Receptura: sensory .....................................................................445 Receptura: lista sensorów obsługiwanych przez urządzenie ............448 Receptura: korzystanie z akcelerometru ........................................450 Receptura: korzystanie z czujnika zbliżeniowego ............................455 Receptura: korzystanie z żyroskopu ..............................................458 Podsumowanie ...........................................................................461
Część V
Eksploracja sieci WWW ....................................463
Rozdział 15 JSON . .............................................................................465 Receptura: JSON .........................................................................465 Receptura: wykorzystywanie obiektu JSONObject do przechowywania informacji ....................................................468 Receptura: zagnieżdżanie obiektów JSONObject ............................471 Receptura: korzystanie z tablicy JSONArray ...................................473 Receptura: korzystanie z klas JsonReader oraz JsonWriter .............478 Receptura: wykorzystywanie usług sieciowych JSON w aplikacjach Android ................................................................483 Podsumowanie ...........................................................................488
Rozdział 16 Klasa WebView . ..............................................................489 Receptura: klasa WebView i jej metody ..............................................................489 Receptura: wyświetlanie stron WWW za pomocą kontrolki WebView ....................................................491 Receptura: korzystanie z klasy WebViewClient ...............................496 Receptura: korzystanie z klasy WebViewFragment ..........................499 Podsumowanie ...........................................................................507
Część VI Zaawansowane techniki systemu Android .........509 Rozdział 17 Obsługa małych ekranów . ................................................511 Receptura: czynniki decydujące o obsłudze różnych ekranów i gęstości .......................................511 Receptura: zapewnianie obsługi dla różnych wersji platformy ...............................513 Receptura: wykorzystanie pakietu Android Support Library do zapewnienia obsługi starszych wersji systemu ...............................................518 Receptura: dostosowywanie aplikacji do orientacji ekranu za pomocą kotwiczenia kontrolek ......................................................................524 Receptura: obsługa orientacji ekranu przy użyciu alternatywnych układów ............................................528 Podsumowanie ...........................................................................532
9
10
Android na tablecie. Receptury
Rozdział 18 Widżety ekranu głównego . ...............................................535 Receptura: widżety aplikacji oraz widżety ekranu głównego .............535 Receptura: metody cyklu życia widżetu aplikacji .............................538 Receptura: tworzenie widżetów ekranu głównego ...........................539 Receptura: aktualizowanie widżetu ekranu głównego za pomocą kontrolki Button .......................................................547 Receptura: zastosowanie klasy AlarmManager do częstej aktualizacji widżetu ekranu głównego ..........................551 Podsumowanie ...........................................................................554
Rozdział 19 Android Beam . ................................................................555 Receptura: standard NFC .............................................................555 Receptura: znaczniki NFC .............................................................556 Receptura: struktura wykorzystywana do wymiany informacji za pomocą znaczników NFC ........................557 Receptura: odczytywanie danych ze znaczników NFC ......................560 Receptura: zapisywanie danych do znacznika NFC ............................566 Receptura: korzystanie z funkcji Android Beam ..............................570 Receptura: przesyłanie danych za pomocą funkcji Android Beam .....571 Podsumowanie ...........................................................................575
Rozdział 20 Analityka i śledzenie aplikacji . .........................................577 Receptura: analizowanie i śledzenie aplikacji .................................577 Receptura: wykorzystanie biblioteki EasyTracker do śledzenia aplikacji Android ....................................................578 Receptura: wykorzystanie narzędzia GoogleAnalytics do śledzenia aplikacji Android ....................................................587 Podsumowanie ...........................................................................589
Skorowidz. .......................................................................591
O autorze B.
M. Harwani jest założycielem i właścicielem uczelni Microchip Computer Education (MCE), której siedziba mieści się w mieście Ajmer, w Indiach. Uczelnia prowadzi szkolenia komputerowe z zakresu programowania oraz projektowania stron WWW. Ukończył Uniwersytet w Pune z tytułem magistra inżyniera i uzyskał dyplom magistra technologii komputerowych na wydziale Elektroniki i Akredytacji Kursów Komputerowych (DOEACC), katedrze podległej indyjskiemu ministerstwu ds. komunikacji i technologii. Autor działa na polu edukacji od ponad dziewiętnastu lat, dzięki czemu mógł rozwinąć sztukę tłumaczenia nawet najbardziej skomplikowanych tematów w prosty i zrozumiały sposób. Napisał kilka książek, wśród nich Android Programming Unleashed (Sams Publishing), i od siedemnastu lat prowadzi kursy programowania. Aby dowiedzieć się więcej o autorze, odwiedź jego blog: http://bmharwani.com/blog.
12
Android na tablecie. Receptury
Wstęp A
ndroid jest dostarczoną przez firmę Google bezpłatną platformą na licencji open source, opartą na języku Java, która przeznaczona jest dla urządzeń mobilnych. Tablety z dnia na dzień zyskują coraz większą popularność. Gadżety te plasują się pomiędzy smartfonami i komputerami osobistymi. Mają procesory szybsze niż smartfony, ale mniejszą moc obliczeniową niż komputery. Są lekkie i poręczne. Na tablecie możesz oglądać filmy, słuchać muzyki, surfować w Internecie, wykonywać połączenia telefoniczne przez sieć Wi-Fi, czytać dokumenty elektroniczne, grać i uruchamiać różne aplikacje. Tablet Android jest posiadającym ekran dotykowy urządzeniem mobilnym, które jest obsługiwane przez system operacyjny Android. Google przyczyniła się do rozwoju tabletów z systemem Android, wydając pakiet Android 3.0 SDK. W rzeczywistości pakiet Android 3.0 SDK został zaprojektowany ze specjalnym uwzględnieniem tabletów (czyli urządzeń o większych ekranach). Wydanie pakietu Android 3.0 SDK zmusiło producentów na całym świecie do produkcji tabletów Android. W miarę zapełniania rynku tabletami Android, programiści coraz chętniej zajmują się przygotowywaniem aplikacji przeznaczonych na te urządzenia. Duży nacisk na rozwój aplikacji na tablety z systemem Android zainspirował mnie do napisania tej książki.
Kto powinien przeczytać tę książkę? Jak każda dobra książka, również ta pozycja dostarcza łatwych do ponownego wykorzystania kodów przeznaczonych do rozwiązywania rzeczywistych problemów i skierowanych do średnio zaawansowanych i zaawansowanych użytkowników. Zakres tematyczny tej książki obejmuje podstawowe i złożone problemy, które zazwyczaj napotykają na swojej drodze programiści. Książka ta będzie przydatna zarówno dla programistów, jak i instruktorów, którzy chcą nauczyć się programowania aplikacji na tablety Android lub przekazać tę wiedzę innym. Mówiąc w skrócie, jest to podręcznik przydatny dla każdego, kto chce zrozumieć wszystkie kluczowe aspekty programowania aplikacji na tablety Android i znaleźć szybkie rozwiązania różnych krytycznych problemów, które pojawiają się podczas przygotowywania aplikacji Android.
14
Android na tablecie. Receptury
Kluczowe tematy omówione w tej książce Książka jest kompleksowym opracowaniem, w którym każdy temat został omówiony bardzo szczegółowo. Oto tematy kluczowe. Aktywności i cykl życia aktywności w systemie Android. Uruchamianie aktywności za pomocą intencji oraz przekazywanie danych
z jednej aktywności do drugiej. Wyświetlanie i używanie fragmentów ListFragment, DialogFragment
oraz PreferenceFragment. Tworzenie paska akcji z zakładkami i rozwijanymi listami opcji. Tworzenie niestandardowych dostawców treści. Korzystanie z animacji poklatkowej oraz animacji generującej klatki pośrednie. Korzystanie z sensorów, takich jak akcelerometr, czujnik zbliżeniowy i żyroskop. Zastosowanie formatu JSON do zarządzania formatami wymiany danych. Włączanie i wyłączanie modułu Wi-Fi oraz korzystanie z Wi-Fi Direct. Korzystanie z klasy RemoteViews oraz tworzenie i aktualizacja widżetów ekranu
głównego. Tworzenie i renderowanie grafiki za pomocą OpenGL. Wycinanie, kopiowanie i wklejanie tekstu z wykorzystaniem schowka
systemowego. Czytanie i zapisywanie znaczników NFC. Przesyłanie danych za pomocą funkcji Android Beam. Stosowanie do grafiki kolorów wieloodcieniowych oraz rotacji, skalowania
i przesunięcia. Przechwytywanie obrazu, audio i wideo za pomocą wbudowanej intencji
oraz kodu Java. Wykorzystywanie narzędzi EasyTracker Library oraz Google Analytics
Singleton do śledzenia aplikacji Android. Książka ta jest w całości zaktualizowana pod kątem najnowszego systemu Jelly Bean 4.2.
Główne korzyści oferowane przez tę książkę Po lekturze tej książki będziesz dobrze znał wymienione niżej pojęcia. Dodawanie i usuwanie fragmentów podczas zmiany orientacji urządzenia. Implementowanie komunikacji pomiędzy fragmentami. Wyświetlanie w pasku akcji elementów akcji, widoków akcji oraz podmenu. Tworzenie stosu obrazów za pomocą klasy StackView.
Jak podzielona jest książka? Wyświetlanie listy opcji za pomocą klas ListPopupWindow i PopupMenu. Przeciąganie i upuszczanie tekstu oraz obrazów. Tworzenie i używanie powiadomień. Tworzenie intencji oczekującej i używanie jej do uruchamiania aktywności. Wykorzystywanie ładowarek w celu uzyskiwania dostępu do informacji. Używanie klas ValueAnimator i ObjectAnimator do animowania widoków. Implementowanie wielu animacji za pomocą klasy AnimatorSet. Stosowanie animacji układu. Stosowanie przyspieszenia sprzętowego w celu zwiększenia wydajności
graficznej systemu. Wykorzystywanie warstw widoku oraz klasy SurfaceView w celu zwiększenia
wydajności graficznej aplikacji. Stosowanie transformacji za pomocą klasy TextureView. Przesyłanie plików za pomocą technologii Bluetooth. Używanie wątków i wielu wątków jednocześnie. Wykorzystywanie klas WebView, WebViewClient oraz WebViewFragment
do wyświetlania stron WWW. Obsługa różnych wersji platformy. Obsługa starszych wersji systemu Android. Dostosowywanie aplikacji do różnych orientacji ekranu. Różnice pomiędzy aplikacjami przeznaczonymi na tablety Android i telefony
Android. Przenoszenie aplikacji pomiędzy telefonami z niewielkimi ekranami a tabletami.
Jak podzielona jest książka? Książka jest podzielona na sześć części. Część I: Techniki interfejsu użytkownika
W rozdziale 1., „Przegląd aplikacji na tablety z systemem Android”, przeczytasz o rozmiarach i funkcjach różnych tabletów Android. Nauczysz się także tworzyć wirtualne urządzenia Android i poznasz strukturę katalogową projektów Android. Ponadto dowiesz się, czym są aktywności i cykl życia aktywności w systemie Android. Nauczysz się uruchamiać aktywność za pomocą intencji oraz przekazywać dane z jednej aktywności do drugiej. Rozdział 2., „Fragmenty”, koncentruje się na koncepcji fragmentów. Podczas jego lektury zapoznasz się z cyklem życia fragmentu oraz procedurą tworzenia
15
16
Android na tablecie. Receptury
fragmentów pierwszego planu. Poznasz również różnicę pomiędzy fragmentami pierwszego planu oraz fragmentami w tle oraz dowiesz się, jak dodawać i usuwać fragmenty przy zmianie orientacji urządzenia. Zobaczysz, w jaki sposób fragmenty są obsługiwane przez klasy FragmentManager i FragmentTransaction. Ponadto nauczysz się tworzyć fragmenty dynamicznie w trakcie działania aplikacji oraz implementować komunikację pomiędzy fragmentami. Na koniec prześledzisz procedury wyświetlania opcji za pomocą fragmentu ListFragment, wyświetlania okien dialogowych przy użyciu fragmentu DialogFragment oraz ustawiania preferencji użytkownika z wykorzystaniem fragmentu PreferenceFragment. Rozdział 3., „Paski akcji w działaniu”, poświęcony jest zastosowaniu pasków akcji w aplikacjach Android. Czytając go, dowiesz się, jaka jest różnica pomiędzy menu a paskiem akcji. Nauczysz się przełączać widoczność paska akcji, poznasz jego różne komponenty oraz prześledzisz procedurę wyświetlania elementów akcji w pasku akcji. Nauczysz się wyświetlać w pasku akcji widoki akcji oraz podmenu. Na koniec zobaczysz, jak w pasku akcji tworzy się zakładki oraz rozwijaną listę opcji. Rozdział 4., „Nowe widżety”, to omówienie działania nowych widżetów. Nauczysz się, jak wyświetlić kalendarz w aplikacji Android, wyświetlać i wybierać liczby za pomocą widżetu NumberPicker i tworzyć stos obrazów przy użyciu widżetu StackView. Ponadto dowiesz się, jak wyświetlać listę opcji, wykorzystując widżety ListPopupWindow i PopupMenu. Część II: Zarządzanie zawartością
W rozdziale 5., „Schowek systemowy oraz operacja przeciągnij i upuść”, nauczysz się, jak przeciągać i upuszczać tekst oraz obrazy. Ponadto dowiesz się, jak wycinać, kopiować i wklejać tekst, wykorzystując schowek systemowy. Czytając rozdział 6., „Powiadomienia oraz intencje oczekujące”, dowiesz się, czym są intencje oczekujące oraz jak intencje są rozgłaszane. Poznasz także system powiadomień Androida. Nauczysz się tworzyć powiadomienia, korzystać z klasy Notification.Builder oraz uzyskiwać obiekt klasy NotificationManager. Na koniec będziesz tworzył powiadomienia i wykorzystywał intencję oczekującą w celu uruchomienia aktywności. Rozdział 7., „Ładowarki”, poświęcony jest koncepcji ładowarek. Tu dowiesz się również, czym są dostawcy treści i jak korzystać z ładowarki CursorLoader w celu uzyskania dostępu do informacji zawartych w dostawcy treści kontaktów. Na koniec nauczysz się tworzyć własnych niestandardowych dostawców treści. Część III: Techniki multimedialne
W rozdziale 8., „Animacje”, poznasz różne typy animacji. Nauczysz się korzystać z klas ValueAnimator i ObjectAnimator przy animowaniu widoków. Dowiesz się, jak implementować wiele animacji za pomocą klasy AnimatorSet. Ponadto zapoznasz się z animacją poklatkową i animacją generującą klatki
Jak podzielona jest książka?
pośrednie. Nauczysz się stosować animację układu oraz poznasz procedurę gromadzenia i sekwencyjnego uruchamiania aplikacji za pomocą klasy AnimationSet. W rozdziale 9., „Sprzętowa akceleracja grafiki 2D”, nauczysz się korzystać z przyspieszenia sprzętowego. Dowiesz się, jak używać warstw widoku i zwiększać wydajność aplikacji opartych na grafice, wykorzystując klasę SurfaceView. Na koniec nauczysz się stosować transformacje za pomocą klasy TextureView. Rozdział 10., „Tworzenie i renderowanie grafiki”, to objaśnienie różnych interfejsów API, które są wymagane do wyświetlania grafiki. Nauczysz się tworzyć i renderować prostokąt, wykorzystując OpenGL. Poznasz także różnicę pomiędzy kolorowaniem wierzchołka i oświetleniem. Ponadto nauczysz się stosować w grafice kolory wieloodcieniowe oraz rotację, skalowanie i przesunięcie. Rozdział 11., „Przechwytywanie audio, wideo i obrazów”, poświęcony jest technice przechwytywania obrazu za pomocą wbudowanej intencji oraz kodu Java. Podczas jego lektury nauczysz się nagrywać dźwięk i wideo, wykorzystując intencję i kod Java, oraz poznasz klasy CamcorderProfile i MediaRecorder wraz z ich metodami. Część IV: Interfejs sieciowy i sprzętowy
Rozdział 12., „Łączność bezprzewodowa”, koncentruje się na ustanawianiu bezprzewodowych połączeń pomiędzy urządzeniami. Nauczysz się, w jaki sposób powiązać dwa urządzenia z obsługą Bluetooth, ręcznie przesyłać pliki z jednego urządzenia na drugie z wykorzystaniem technologii Bluetooth oraz powiązać urządzenie z obsługą Bluetooth z komputerem na platformie Windows. Dowiesz się także, jak włączyć lokalne urządzenie Bluetooth, wyświetlić listę powiązanych urządzeń oraz przesyłać pliki za pomocą technologii Bluetooth. Na koniec zapoznasz się z koncepcją Wi-Fi, dowiesz się, jak włączyć i wyłączyć moduł Wi-Fi oraz poznasz zastosowanie Wi-Fi Direct. Z rozdziału 13., „Rdzenie i wątki”, dowiesz się, jaka jest użyteczność architektury procesorów wielordzeniowych. Poznasz zastosowanie mechanizmu odzyskiwania pamięci. Przeczytasz także o wątkach oraz wielu równoczesnych wątkach. Na koniec nauczysz się stosować klasę AsyncTask. W rozdziale 14., „Klawiatury i sensory”, dowiesz się, jak zmienić w systemie Android klawiatury i metody wprowadzania danych. Nauczysz się, czym są sensory i jak wyświetlać listę sensorów obsługiwanych przez dane urządzenie. Na koniec poznasz sposób korzystania z sensorów, takich jak akcelerometr, czujnik zbliżeniowy oraz żyroskop. Część V: Eksploracja sieci WWW
W rozdziale 15., „JSON”, poznasz koncepcję formatu JSON. Nauczysz się korzystać z obiektu JSONObject, zagnieżdżać obiekty JSONObject oraz stosować tablicę JSONArray do przechowywania informacji. Ponadto nauczysz się używać klas JsonReader i JsonWriter.
17
18
Android na tablecie. Receptury
W rozdziale 16., „Klasa WebView”, nauczysz się, jak wyświetlać strony WWW za pomocą klasy WebView. Ponadto skorzystasz z klasy WebViewClient oraz fragmentu WebViewFragment. Część VI: Zaawansowane techniki systemu Android
W rozdziale 17., „Obsługa małych ekranów”, przeczytasz o czynnikach związanych z obsługą różnych ekranów i gęstości. Dowiesz się, w jaki sposób w aplikacjach Android obsługiwane są różne wersje platformy oraz jak pakiet Support Library jest wykorzystywany do obsługi starszych wersji systemu Android. Nauczysz się adaptować aplikacje do orientacji ekranu za pomocą kotwiczenia kontrolek. Ponadto nauczysz się definiować alternatywne układy dla obsługi orientacji ekranu. W rozdziale 18., „Widżety ekranu głównego”, poznasz klasę RemoteViews, widżety aplikacji oraz widżety ekranu głównego. Przeczytasz o metodach cyklu życia widżetów aplikacji. Ponadto nauczysz się tworzyć widżety ekranu głównego i aktualizować je za pomocą kontrolki Button. Na koniec dowiesz się, jak aktualizować widżety ekranu głównego za pomocą klasy AlarmManager. W rozdziale 19., „Android Beam”, przeczytasz o standardzie NFC (ang. Near Field Communication) i roli znaczników NFC. Poznasz także struktury wykorzystywane przy wymianie danych za pomocą znaczników NFC oraz dowiesz się, jak odczytywać i pisać znaczniki NFC. Na koniec zapoznasz się z funkcją Android beam i nauczysz się przesyłać pliki z jej wykorzystaniem. W rozdziale 20., „Analityka i śledzenie aplikacji”, zapoznasz się z koncepcją analizowania i śledzenia aplikacji. Nauczysz się wykorzystywać narzędzia EasyTracker Library i Google Analytics Singleton do śledzenia aplikacji Android.
Przykłady kodu dla tej książki Wszystkie receptury Android omówione w tej książce możesz pobrać ze strony wydawnictwa Helion, pod adresem www.helion.pl/ksiazki/antare.htm. Po pobraniu rozpakuj paczkę z kodami i wykonaj wymienione poniżej czynności, aby wykorzystać dostarczone kody. Uruchom środowisko Eclipse. Wybierz z menu opcję File/Import (plik/importuj). W wyświetlonym oknie
dialogowym Import wybierz Existing Android Code Into Workspace (istniejący kod Android do przestrzeni roboczej) i kliknij przycisk Next (dalej). W kolejnym oknie dialogowym kliknij przycisk Browse (przeglądaj), aby
zlokalizować i wybrać folder, do którego rozpakowałeś paczkę z kodami.
Założenia
Po wybraniu paczki z kodami wszystkie zawarte w niej projekty Android
pojawią się na liście w oknie Import Projects (importuj projekty). Domyślnie wszystkie projekty będą zaznaczone. Możesz usunąć zaznaczenie projektu lub projektów, których nie chcesz importować. Następnie kliknij przycisk Finish (zakończ) i gotowe. Projekty zostaną zaimportowane do środowiska Eclipse i będą gotowe do uruchomienia.
Założenia We wszystkich recepturach przedstawionych w tej książce przyjęto trzy następujące założenia. Jeśli nie określono inaczej, atrybuty android:minSdkVersion
i android:targetSdkVersion wszystkich aplikacji z tej książki mają odpowiednio wartości 11 i 17. Interfejsy API poziomu 11. i 17. odnoszą się odpowiednio do systemów Android 3.0 (Honeycomb) i Android 4.2 (Jelly Bean). Oznacza to również, że aplikacje z tej książki wymagają do uruchomienia co najmniej API poziomu 11. Ponadto aplikacje te zostały skompilowane i przeznaczone do uruchomienia na API poziomu 17. Założono, że w folderze res/values utworzony został plik XML dimens.xml
zawierający następujący kod:
14sp
Założono, że w folderze res utworzone zostały dwa foldery: values-sw600dp
i values-sw720dp. Ponadto do tych dwóch folderów powinien zostać skopiowany plik dimens.xml z folderu res/values. W celu dopasowania aplikacji do ekranów tabletów 7-calowych i 10-calowych zasób wymiarów text_size w pliku dimens.xml z folderów values-sw600dp i values-sw720dp powinien zostać zmodyfikowany odpowiednio wartościami 24sp i 32sp. Więcej informacji na ten temat znajdziesz w podrozdziale „Receptura: zapewnianie kompatybilności aplikacji z telefonami i tabletami z systemem Android”, w rozdziale 1.
19
20
Android na tablecie. Receptury
Część I Techniki interfejsu użytkownika
1 Przegląd aplikacji na tablety z systemem Android P
akiet Android 3.0 SDK został zaprojektowany i opublikowany z myślą o tabletach. To SDK wprowadza kilka nowych funkcji oferujących obsługę urządzeń z panoramicznym ekranem. Do tych funkcji należą zoptymalizowany pod kątem tabletów interfejs użytkownika, obsługa procesorów wielordzeniowych, usprawniona wielozadaniowość oraz zaawansowane opcje przeglądarki internetowej. Ten rozdział poświęcony jest funkcjom tabletów z systemem Android, omówione w nim zostaną różnice pomiędzy telefonami Android i tabletami Android oraz procedury implementowania w aplikacjach telefonicznych kompatybilności z tabletami Android. W trakcie lektury tego rozdziału zapoznasz się ze strukturą projektu Android. Ponadto przygotujesz aktywności i zapoznasz się z cyklem życia aktywności w systemie Android. Na koniec dowiesz się, jak rozpocząć aktywność, wykorzystując intencję, oraz jak przekazywać dane z jednej aktywności do drugiej.
Receptura: wprowadzenie do tabletów z systemem Android Rozmiary tabletów zazwyczaj wahają się od 7 do 12 cali przekątnej ekranu. Na rynku dostępnych jest wiele różnych tabletów, takich jak Google Nexus 7, Google Nexus 10, Samsung Galaxy Note 10.1, Toshiba AT300, Amazon Kindle Fire HD, Asus Transformer Pad 300 oraz Samsung Galaxy Tab 8.9. W naszej dyskusji skupimy się na tabletach produkowanych przez Google, czyli Google Nexus 7 oraz Google Nexus 10. Google Nexus 7 (patrz rysunek 1.1) to tablet firmy Google. Wyprodukowany został we współpracy z firmą Asus, dostawcą sprzętu komputerowego klasy ekonomicznej. Jest to tablet o przekątnej 7 cali oraz interfejsie użytkownika zorganizowanym na wzór telefonu. Tablet ten wykorzystuje nowy układ zaprojektowany dla 7-calowych urządzeń tego typu. Oto kilka jego cech.
24
Rozdział 1. Przegląd aplikacji na tablety z systemem Android
Rysunek 1.1. Google Nexus 7 (po lewej) oraz Google Nexus 10 (po prawej) Wymiary 198,5×120×10,5 mm. Rozdzielczość 800×1200 pikseli. Wielodotykowy wyświetlacz chroniony taflą Corning Glass. Wydajny i niezawodny system operacyjny Android 4.1 Jelly Bean. Cena waha się od 800 do 900 zł (w zależności od wybranego modelu),
co znaczy, że tablet ten to niedrogie urządzenie. Ekran ma gęstość tvdpi, zatem otrzymujemy oczekiwany rozmiar interfejsu
użytkownika (ang. user interface — UI) w odpowiedniej odległości. Ponadto bitmapy mogą być z łatwością skalowane bez zwracania uwagi użytkownika. Oznaczenie tvdpi reprezentuje charakterystyczną dla telewizorów gęstość pikseli i umożliwia podgląd aplikacji androidowych na telewizorze bez efektu rozmazania obrazu. Aplikacje prezentują się znacznie lepiej na ekranach z gęstością tvdpi niż na ekranach innych 7-calowych tabletów z gęstością mdpi. Potężny, czterordzeniowy procesor Tegra, który zapewnia doskonałe efekty
wizualne w grach. Obsługa sieci 2G i 3G oraz zapewniona łączność w standardach Wi-Fi 802.11 b/g/n,
Bluetooth i NFC. Wersje o pojemności pamięci trwałej 8 GB i 16 GB.
Google Nexus 10 został wyprodukowany przez firmę Google we współpracy z Samsungiem (patrz rysunek 1.1). Jak sugeruje nazwa, tablet ten wyposażony został w 10-calowy wyświetlacz. Oto kilka cech tabletu Google Nexus 10. Czterordzeniowy procesor zapewniający wysoką wydajność. Wysoka rozdzielczość ekranu oraz format 16:9. Najnowsza wersja systemu operacyjnego Android 4.2 Jelly Bean.
Receptura: różnice pomiędzy telefonami Android a tabletami Android
Obsługa Wi-Fi oraz do wyboru 16 GB lub 32 GB pamięci trwałej (w zależności
od modelu). Cena około 1800 zł dla modelu 16 GB lub około 2000 zł dla modelu 32 GB.
Więcej informacji na temat tabletów z serii Google Nexus znajdziesz na stronie www.google.com/nexus/#/features.
Receptura: różnice pomiędzy telefonami Android a tabletami Android Pierwszą różnicą pomiędzy telefonami a tabletami z systemem Android jest rozmiar ekranu. Większość ekranów telefonicznych mierzy od 3 do 5 cali, podczas gdy w przypadku tabletów rozpiętość ta sięga od 7 do 12 cali. Drugą różnicę stanowi rozdzielczość ekranu wyrażona w pikselach. Rozdzielczość pikselowa (ang. pixel resolution) to całkowita liczba pikseli wyświetlana na ekranie. Oto rozdzielczości pikselowe telefonów i tabletów. Telefony mają zazwyczaj rozdzielczość 800×400. Tablety 7-calowe mają zazwyczaj rozdzielczość 1024×600. Tablety 10-calowe mają zazwyczaj rozdzielczość 1280×800.
Gęstość (ang. density), czyli inaczej liczba pikseli w określonym obszarze ekranu, stanowi trzecią różnicę pomiędzy tabletami a telefonami. Rozmiar pozostaje ten sam, kiedy liczba pikseli oraz gęstość rosną jednocześnie. Załóżmy, że ekran 1. ma rozmiar 100×100 pikseli, a ekran 2. ma rozmiar 150×150 pikseli. Rozmiary te będą równe, jeśli gęstość ekranu 2. będzie stanowiła 1,5 gęstości ekranu 1. Ponadto grafika wyświetlana na urządzeniu o podwójnej gęstości będzie sprawiała wrażenie o połowę mniejszej niż na urządzeniu, którego współczynnik gęstości wynosi 1. Urządzenia mogą oferować niską, średnią, wysoką lub ekstrawysoką gęstość. W urządzeniu o średniej gęstości (ang. medium-density — mdpi) mamy 160 pikseli na cal. W urządzeniach o wysokiej gęstości (ang. high-density — hdpi) oraz ekstrawysokiej gęstości (ang. extra-high-density — xhdpi) mamy odpowiednio 240 oraz 320 pikseli na cal. Aby znormalizować rozmiary ekranu, w Androidzie potraktowano mdpi jako gęstość bazową. Oznacza to, że dla urządzeń mdpi 1 dp = 1 piksel, gdzie dp oznacza piksel niezależny od urządzenia (ang. device-independent pixel). Gęstość wszystkich innych urządzeń konwertowana jest na mdpi dla ujednolicenia. Objaśnimy to na następujących przykładach. Rozmiarowi ekranu telefonu Android (mdpi) wynoszącemu 320×480 pikseli
odpowiada 320 dp×480 dp (ponieważ w przypadku urządzeń mdpi 1 piksel = 1 dp). Rozmiarowi ekranu telefonu Android (hdpi) wynoszącemu 480×800 pikseli
odpowiada (480×160/240)×(800×160/240), czyli 320 dp×533 dp.
25
26
Rozdział 1. Przegląd aplikacji na tablety z systemem Android
Rozmiarowi ekranu tabletu Android (xdpi) wynoszącemu 1600×2560 pikseli
odpowiada (1600×160/320)×(2560×160/320), czyli 800 dp×1280 dp. Poniżej zamieszczono formułę przeliczania liczby rzeczywistych pikseli fizycznych (px) na jednostki niezależne od urządzenia (dp): dp = px×(gęstość/160) Android kategoryzuje ekrany według następujących rozmiarów: mały, normalny, duży i ekstraduży. Rozmiarom tym odpowiadają następujące wielkości: ekstraduże ekrany mają co najmniej 960 dp×720 dp, duże ekrany mają co najmniej 640 dp×480 dp, normalne ekrany mają co najmniej 470 dp×320 dp, małe ekrany mają co najmniej 426 dp×320 dp.
Rozmiary ekranu telefonu, 7-calowego tabletu i 10-calowego tabletu są określane odpowiednio jako normalny, duży i ekstraduży. Podczas tworzenia aplikacji brana jest pod uwagę szerokość ekranu w celu odpowiedniego rozmieszczenia przycisków kontrolnych interfejsu użytkownika. Oto standardowe szerokości dla telefonów i tabletów: szerokość ekranu telefonu wynosi 320 dp, szerokość ekranu 7-calowego tabletu wynosi 600 dp, szerokość ekranu 10-calowego tabletu wynosi 720 dp.
Receptura: zapewnianie kompatybilności aplikacji z telefonami i tabletami z systemem Android System Android ułatwia tworzenie aplikacji, które działają równie dobrze na szeregu urządzeń o różnych rozmiarach ekranów. Pozwala to przygotować pojedynczą aplikację, która może być szeroko dystrybuowana na wszystkie wymagane urządzenia docelowe. Trzeba jedynie zoptymalizować układy i inne komponenty interfejsu użytkownika dla każdej z docelowych konfiguracji ekranu. Tworząc aplikacje, które mają działać zarówno na telefonach Android, jak i na tabletach Android, zadbaj o następujące rzeczy. Kontrolki interfejsu użytkownika (ang. UI controls) nie powinny być
niepotrzebnie rozciągnięte w celu wypełnienia pustej przestrzeni. Kontrolki układaj w bloki, aby zredukować ich szerokość. Korzystaj z wieloekranowych układów, aby lepiej wykorzystać szersze ekrany.
Używaj fragmentów, tylko wtedy, kiedy jest to konieczne. Definiuj osobne układy dla normalnych, dużych i ekstradużych ekranów.
Receptura: tworzenie urządzeń AVD
Modyfikuj rozmiar czcionki i inne wymiary kontrolek interfejsu użytkownika
według konkretnych układów. Elementy graficzne, ikony oraz inne bitmapy wykorzystywane w danej aplikacji
muszą odpowiadać gęstościom ekranów telefonów i tabletów. Definiując wymiary w plikach XML układu, używaj atrybutów wrap_content,
match_parent lub jednostek dp. Nie definiuj stałych rozmiarów dla kontrolek interfejsu użytkownika. Jeśli trzeba, w pliku AndroidManifest.xml deklaruj element
z odpowiednimi atrybutami.
Receptura: tworzenie urządzeń AVD AVD (ang. Android Virtual Device, czyli urządzenie wirtualne Android) reprezentuje konfigurację urządzenia. Różne urządzenia z systemem Android mają odmienne konfiguracje. Aby sprawdzić, czy dana aplikacja Android jest kompatybilna z pewnym zestawem urządzeń Android, należy utworzyć reprezentujące ich konfiguracje urządzenia AVD, na których można przetestować aplikację. Utworzysz teraz trzy urządzenia AVD: dla telefonu Android, dla 7-calowego tabletu oraz dla 10-calowego tabletu. Aby w środowisku Eclipse utworzyć urządzenia AVD dla telefonu i tabletów, wybierz opcję Window/Android Virtual Device Manager. Otworzy się okno dialogowe Android Virtual Device Manager pokazane na rysunku 1.2. W oknie wyświetlona zostanie lista istniejących urządzeń AVD; w oknie tym można nimi zarządzać oraz tworzyć nowe urządzenia AVD. Jeśli nie zostały zdefiniowane żadne urządzenia AVD, wyświetlona lista będzie pusta.
Rysunek 1 2. Okno dialogowe menedżera urządzeń AVD
Kliknij przycisk New, aby zdefiniować nowe urządzenie AVD. Zostanie wyświetlone okno dialogowe Create new Android Virtual Device (AVD) umożliwiające utworzenie nowego urządzenia wirtualnego (patrz rysunek 1 3). W oknie znajdziesz następujące pola.
27
28
Rozdział 1. Przegląd aplikacji na tablety z systemem Android
Rysunek 1.3. Specyfikacja nowego urządzenia AVD o nazwie TelefonAVD AVD Name (nazwa urządzenia AVD). W tym polu określasz nazwę
wirtualnego urządzenia Android. Wpisz nazwę TelefonAVD. Utworzysz najpierw AVD dla telefonu Android, a następnie AVD dla tabletu Android. Device (urządzenie). W tym polu definiujesz urządzenie, dla którego ma być
testowana dana aplikacja. Z rozwijanej listy wybierz pozycję Nexus S (4.0", 480 × 800:hdpi). Target (cel). W tym polu określasz docelowy poziom API (ang. API level).
Twoja aplikacja będzie testowana pod kątem wskazanego poziomu API. Z rozwijanej listy wybierz co najmniej poziom Android 4.2.2 - API Level 17. CPU/ABI. W tym polu wskazujesz procesor, który ma być emulowany na
Twoim urządzeniu. Wybierz opcję ARM (armeabi-v7a). Keybord (klawiatura). Zaznacz pole wyboru Hardware keyboard present
(klawiatura sprzętowa), aby oprócz klawiatury wyświetlonej na ekranie emulatora korzystać również z fizycznej klawiatury podłączonej do komputera. Skin (skórka). Jeśli zaznaczysz pole wyboru Display a skin with hardware
controls (wyświetlaj skórkę z kontrolkami sprzętowymi), w emulatorze po prawej stronie zostaną wyświetlone fizyczne przyciski urządzenia. Obejmują
Receptura: tworzenie urządzeń AVD
one podstawowe kontrolki (głośnik, przyciski on/off) oraz przyciski Home, Menu, Back i Search. Front Camera/Back Camera (przednia/tylna kamera). Jeśli posiadasz podłączoną
do swojego komputera kamerkę internetową i chcesz wykorzystać ją w swojej aplikacji, wybierz z rozwijanego menu opcję Webcam0. Jeżeli nie masz kamerki, wybierz opcję Emulated (emulowana). Gdy aplikacja nie wymaga użycia kamerki internetowej, zostaw ustawienie domyślne None (brak). Memory Options (opcje pamięci). W tym polu możesz dla danego urządzenia
zdefiniować ilość pamięci RAM oraz rozmiar sterty maszyny wirtualnej (ang. VM Heap). Pozostaw ustawienia domyślne. Internal Storage (pamięć wewnętrzna). To pole pozwala zdefiniować rozmiar
zewnętrznej pamięci trwałej urządzenia. Ponownie pozostaw domyślną wartość 200MiB. SD Card (karta SD). Ta opcja rozszerza pamięć trwałą urządzenia. Większe
pliki, takie jak pliki audio i wideo, dla których niewystarczająca jest wbudowana pamięć flash, przechowywane są na karcie SD. Ustaw rozmiar karty SD na 128MiB. Im większa będzie alokowana przestrzeń karty SD, tym dłuższy proces tworzenia urządzenia AVD. Postaraj się utrzymać możliwie niewielką przestrzeń dla karty SD, o ile nie ma innych wyraźnych wymagań w tej kwestii. Snapshot (zrzut urządzenia). Włącz tę opcję, aby uniknąć każdorazowego
bootowania emulatora i rozpoczynać od ostatniego zapisanego zrzutu stanu urządzenia. Opcja ta jest wykorzystywana do szybkiego rozpoczęcia pracy z emulatorem Android. Use Host GPU (użyj procesora graficznego hosta). Zaznacz to pole wyboru,
aby włączyć emulację procesora graficznego (GPU). Emulacja GPU zwiększa wydajność emulatora. Zostanie utworzone nowe wirtualne urządzenia Android o nazwie TelefonAVD. W podobny sposób, wykorzystując nazwy Tablet7AVD oraz Tablet10AVD, utwórz urządzenia AVD dla tabletów 7-calowego i 10-calowego. Definiując urządzenie Tablet10AVD dla określenia urządzenia docelowego, wybierz opcję 10.1" WXGA (Tablet) (1280 × 800:mdpi), ponieważ odpowiada ona rozmiarowi ekranów większości 10-calowych tabletów (patrz rysunek 1.4, górny obrazek). Aby dla urządzenia Tablet7AVD określić urządzenie docelowe, wybierz opcję 7.0" WSVGA (Tablet) (1024 × 600:mdpi). Wszystkie trzy utworzone urządzenia zostaną wyświetlone na liście urządzeń AVD, co możesz zobaczyć na rysunku 1.4 (dolny obrazek). Jeśli nowo utworzone urządzenie AVD nie pojawi się na liście, wciśnij przycisk Refresh (odśwież).
29
30
Rozdział 1. Przegląd aplikacji na tablety z systemem Android
Rysunek 1.4. Na górnym obrazku widać rozwiniętą listę opcji dla pozycji Device. Na dolnym obrazku przedstawiono okno menedżera AVD, w którym wyświetlona jest lista nowo utworzonych urządzeń AVD
Aby włączyć nowo utworzone urządzenie AVD, zaznacz je na liście w oknie menedżera urządzeń AVD i kliknij przycisk Start. Uruchomiony zostanie emulator AVD pokazany na rysunku 1.5. Na górnym obrazku z rysunku 1.5 przedstawiono emulator telefonu Android w orientacji pionowej i poziomej. Środkowy i dolny obrazek z rysunku 1.5 pozwalają porównać rozmiary emulatora 7-calowego tabletu z rozmiarami emulatora 10-calowego tabletu.
Receptura: tworzenie urządzeń AVD
Rysunek 1.5. Emulator telefonu w orientacji pionowej i poziomej (górny obrazek), emulator 7-calowego tabletu (po środku) oraz emulator 10-calowego tabletu (dolny obrazek)
31
32
Rozdział 1. Przegląd aplikacji na tablety z systemem Android
Receptura: struktura katalogów projektu Android Teraz zbudujesz nowy projekt Android, aby zapoznać się z różnymi plikami i katalogami, które tworzone są automatycznie przez moduł dodatkowy ADT (ang. Android Developer Tools), czyli wtyczkę narzędzi deweloperskich Android. Uruchom Eclispe i wybierz z menu opcję File/New/Android Application Project (plik/nowy/projekt aplikacji Android) lub kliknij ikonę Android Project Creator (kreator projektu Android) na pasku zadań Eclipse. Zobaczysz okno dialogowe pokazane na rysunku 1.6 (górny obrazek). W polu Application Name (nazwa aplikacji) wpisz FirstAndroidTabletApp jako nazwę projektu Android Tablet. Pole Project Name (nazwa projektu) jest wypełniane automatycznie i domyślnie jest to ta sama nazwa, co nazwa aplikacji. Ponieważ pole Package Name (nazwa paczki) określa unikatowy identyfikator, wpisz jako nazwę paczki com.androidtablet.firstandroidtabletapp. Z rozwijanej listy pozycji Minimum Required SDK (minimalne wymagane SDK) wybierz opcję API 11: Android 3.0 (Honeycomb), aby wskazać, że dana aplikacja do uruchomienia będzie wymagała co najmniej API poziomu 11. Dla pozycji Target SDK (docelowe SDK) wybierz opcję Android 4.2 (API 17), ponieważ ma to być wersja, której będą używać Twoi docelowi odbiorcy. Ponadto wybierz opcję Android 4.2 (API 17) dla pozycji Compile With (kompiluj za pomocą), aby skompilować daną aplikację przy użyciu wskazanego SDK. Pożądany motyw dla aplikacji wybierz z rozwijanej listy w pozycji Theme (motyw). Domyślnym ustawieniem dla nowej aplikacji jest motyw Holo Light with Dark Action Bar. Ponieważ jest to nowy projekt, w następnym oknie zaznacz pole wyboru przy opcji Create Project in Workspace (utwórz projekt w obszarze roboczym), aby pliki danego projektu zostały utworzone w obszarze roboczym, który został wskazany przy pierwszym uruchomieniu Eclipse. Pole wyboru opcji Create Custom Launcher Icon (utwórz niestandardową ikonę uruchamiania) jest zaznaczone domyślnie i pozwala Ci zdefiniować własną ikonę dla aplikacji. Podobnie pole wyboru opcji Create Activity (utwórz aktywność) jest domyślnie zaznaczone, aby plik aktywności dla Twojej aplikacji został utworzony automatycznie. Kliknij przycisk Next (dalej), aby przejść do kolejnego okna dialogowego o nazwie Configure Launcher Icon (konfiguracja ikony uruchamiania), które pozwala skonfigurować własną ikonę dla aplikacji. Ponieważ chcesz, aby aplikacja posiadała domyślną ikonę, w tym oknie dialogowym pozostaw wszystkie ustawienia domyślne i kliknij przycisk Next, aby przejść dalej. W kolejnym oknie dialogowym będziesz musiał zdecydować, czy chcesz utworzyć aktywność, oraz wybrać rodzaj aktywności. Ponieważ chcesz utworzyć pustą aktywność, zaznacz pole wyboru opcji Create Activity (utwórz aktywność) i wybierz pozycję Blank Activity (pusta aktywność). Następnie kliknij Next.
Receptura: struktura katalogów projektu Android
Rysunek 1.6. Okno dialogowe wprowadzania informacji na temat nowej aplikacji (obrazek górny) oraz okno dialogowe wprowadzania informacji o domyślnej aktywności aplikacji (obrazek dolny)
33
34
Rozdział 1. Przegląd aplikacji na tablety z systemem Android
W kolejnym oknie dialogowym (patrz rysunek 1.6, dolny obrazek) zostaniesz poproszony o wprowadzenie informacji dla nowo utworzonej aktywności. Nazwij tę aktywność FirstAndroidTabletAppActivity. Nazwa układu (pole Layout Name) oraz nazwa tytułowa zmienią się automatycznie, aby odzwierciedlić nowo przydzieloną nazwę aktywności. Nazwą układu będzie activity_first_android_tablet_app, a tytuł aplikacji zmieni się na FirstAndroidTabletAppActivity. Zawsze masz możliwość zmiany przydzielonych automatycznie nazw układu i tytułu aplikacji. Ponieważ w tej aplikacji nie będziesz potrzebował opcji nawigacyjnych (takich jak zakładki lub rozwijana lista opcji) dla pozycji Navigation Type (rodzaj nawigacji), pozostaw domyślną wartość None (brak). Nie zmieniaj też automatycznie przydzielonych nazwy układu i tytułu aplikacji. Kliknij przycisk Finish (zakończ) po wprowadzeniu wymaganych informacji. Wtyczka ADT utworzy daną aplikację i wszystkie niezbędne pliki. Na rysunku 1.7 przedstawiono strukturę plików i katalogów, które są automatycznie tworzone dla Twojego nowego projektu Android. Oto przegląd plików i katalogów przedstawionych na rysunku 1.7. Folder /src. Zawiera cały plik źródłowy Javy dla danej aplikacji. Folder
ten posiada strukturę katalogów odpowiadającą nazwie paczki wskazanej w aplikacji. Zawiera domyślną paczkę tego projektu, czyli com.androidtablet. firstandroidtabletapp. W paczce po rozwinięciu znajdziesz aktywność danej aplikacji w postaci pliku FirstAndroidTabletAppActivity.java. /src/com.androidtablet.firstandroidtabletapp. Odwołuje się do nazwy paczki
aplikacji. Aby uniknąć konfliktów pomiędzy nazwami klas, nazwami zmiennych i innymi nazwami pozostałych aplikacji Android, każda aplikacja musi być spakowana w unikatowym kontenerze. /src/com.androidtablet.firstandroidtabletapp/FirstAndroidTabletAppActivity
.java. Plik domyślnej aktywności danej aplikacji. Każda aplikacja posiada co najmniej jedną aktywność, która stanowi główny punkt wejścia do danej aplikacji. Plik aktywności jest automatycznie definiowany jako domyślna aktywność uruchomieniowa w pliku Android Manifest. Folder /gen. Zawiera pliki Javy wygenerowane przez ADT po skompilowaniu
aplikacji. Oznacza to, że folder gen jest tworzony po pierwszym skompilowaniu aplikacji. Folder ten zawiera plik R.java, w którym przechowywane są referencje do wszystkich zasobów zdefiniowanych w katalogu /res. Znajduje się tu również plik BuildConfig.java, który uruchamia kod tylko w trybie debugowania. I wreszcie zawarta jest tu również stała DEBUG, która pomaga uruchamiać wyłącznie funkcje debugowania.
Receptura: struktura katalogów projektu Android
Rysunek 1.7. Struktura katalogów projektu FirstAndroidTabletApp /gen/com.androidtablet.firstandroidtabletapp/R.java. Wszystkie układy i inne
informacje o zasobach zakodowane w plikach XML są konwertowane w kod źródłowy Javy i umieszczane w pliku R.java. Z tego powodu plik ten zawiera identyfikatory wszystkich zasobów aplikacji. Plik R.java jest kompilowany do postaci plików kodu bajtowego Javy, a następnie konwertowany do formatu .dex. Nigdy nie powinieneś ręcznie edytować tego pliku, ponieważ jest automatycznie nadpisywany, kiedy dodajesz lub edytujesz zasoby. Android4.2.2/android.jar. Plik .jar pakietu Android SDK dla platformy
docelowej.
35
36
Rozdział 1. Przegląd aplikacji na tablety z systemem Android
Folder /assets. Domyślnie pusty. Przechowywane są w nim surowe pliki
aktywów, które mogą być wymagane w danej aplikacji. Może zawierać czcionki, zewnętrzne pliki .jar itp., które mają być użyte w danej aplikacji. Folder assets jest podobny do folderu zasobów, w którym przechowywane są nieskompilowane zasoby projektu. Dla przechowywanych tutaj zasobów nie są generowane żadne identyfikatory. Folder /bin. W nim przechowuje się skompilowaną wersję aplikacji. Folder /res. Przechowywane są tutaj wszystkie zasoby aplikacji (obrazki, pliki
układów oraz pliki ciągów znaków). Zamiast na stałe umieszczać jakiś obrazek lub ciąg znaków w aplikacji, lepszym podejściem jest utworzenie odpowiedniego zasobu w folderze /res i dodanie referencji do tego zasobu w aplikacji. W ten sposób możesz zmieniać obrazek, ciąg znaków lub każdy inny zasób w dowolnym momencie bez potrzeby zakłócania działania kodu. Każdy zasób jest przydzielany do unikatowego identyfikatora, który automatycznie pojawia się w pliku R.java, a zatem również w klasie R po kompilacji, co umożliwia odwoływanie się do tego zasobu w kodzie. W celu skategoryzowania i zorganizowania zasobów domyślnie tworzone są cztery podkatalogi: drawable, layout, menu oraz values. /res/drawable-xxhdpi, /res/drawable-xhdpi, /res/drawable-hdpi,
/res/drawable-mdpi, /res/drawable-ldpi. W tych folderach przechowywane są ikony i graficzne zasoby aplikacji. Ponieważ urządzenia posiadają ekrany o różnych gęstościach, znajdziesz w tych folderach grafiki o różnych rozdzielczościach. W aplikacjach Android wykorzystywane są zazwyczaj grafiki o parametrach 480 dpi, 320 dpi, 240 dpi, 160 dpi oraz 120 dpi. Są one przechowywane odpowiednio w folderach res/drawable-xxhdpi, res/drawable-xhdpi, res/drawable-hdpi/, res/drawable-mdpi oraz res/drawableldpi. Aplikacja pobierze grafikę z właściwego folderu po określeniu gęstości bieżącego urządzenia. /res/layout. Przechowuje plik układu (lub pliki układów) w formacie XML. /res/layout/activity_first_android_tablet_app.xml. Pliki układu wykorzystywane
przez aktywność FirstAndroidTabletAppActivity do rysowania podglądów na ekranie. Podglądy i kontrolki są zorganizowane w określonym układzie. /res/menu. Folder menu, który przechowuje plik XML menu. /res/values. Przechowuje wszystkie zasoby wartości. Zasoby wartości obejmują
wiele rodzajów, w tym zasoby ciągów znaków, zasoby wymiarów i zasoby kolorów. /res/values/strings.xml. Zawiera zasoby ciągów znaków. Ciągi znaków stanowią
treści w postaci tekstów, które mają być przypisane różnym kontrolkom aplikacji. Plik ten definiuje również tablice ciągów.
Receptura: konwersja aplikacji z telefonu Android w aplikację na tablet Android
/res/values/dimens.xml. Zawiera zasoby wymiarów. Definiowanie zasobów
wymiarów w pliku dimens.xml ma na celu dostosowywanie projektu Android do ekranów telefonów i tabletów. /res/values-sw600dp, /res/values-sw720dp-land. Zawierają odpowiednio
zasoby wymiarów dla 7-calowych i 10-calowych tabletów, np. dla dużych i ekstradużych ekranów. /res/values-v11, /res/values-v14. Zawierają odpowiednio motywy dla API
poziomu 11. i 14. AndroidManifest.xml. Centralny plik konfiguracyjny aplikacji. Proguard-project.txt. Definiuje, w jaki sposób ProGuard optymalizuje kod
aplikacji. ProGuard jest narzędziem, które usuwa z aplikacji Android nieużywany kod i optymalizuje kod, zwiększając tym samym wydajność. Narzędzie to pomaga również zaciemniać kod w celu ochrony przed dekompilacją. project.properties. Plik build wykorzystywany przez wtyczki Eclipse i Android
ADT. Zawiera ustawienia projektu, takie jak lista build target. Możesz wykorzystać ten plik do zmiany różnych właściwości projektu. Jeśli musisz go wyedytować, nie powinieneś robić tego bezpośrednio, lecz skorzystać z edytorów dostępnych w Eclipse.
Receptura: konwersja aplikacji z telefonu Android w aplikację na tablet Android Ponieważ występują różnice w rozmiarach ekranów telefonów i tabletów z systemem Android, dla tabletów należy zdefiniować osobne układy. Podczas definiowania układów dla tabletów powinieneś wziąć pod uwagę następujące istotne kwestie. Zwiększenie rozmiaru czcionki kontrolek interfejsu użytkownika. Wypełnienie pustej przestrzeni, która pojawia się po prawej stronie,
ponieważ ekran jest szerszy. Przed opublikowaniem systemu Android 3.2 układy dla tabletów były umieszczane w katalogach z dużymi i ekstradużymi kwalifikatorami konfiguracji. Przykładowo do przechowywania zasobów układów dużych i ekstradużych ekranów wykorzystywane były katalogi res/layout-large/ i res/layout-xlarge/. Pamiętaj, że 7-calowe i 10-calowe tablety są uznawane za urządzenia z dużymi i ekstradużymi ekranami. Począwszy od wersji 3.2 systemu Android, kwalifikatory dla katalogów układów są oparte na wielkości obszaru wymaganego przez dany układ. Jeśli przykładowo układ wymaga minimalnego obszaru ekranu 600 dp, zasób układu będzie przechowywany w katalogu res/layout-sw600dp/. Jeśli szerokość ekranu urządzenia wynosi co najmniej 600 dp, wykorzystywane będą tylko zasoby z katalogu res/layout-sw600dp/.
37
38
Rozdział 1. Przegląd aplikacji na tablety z systemem Android
Uwaga Określenie sw wykorzystywane w nazwach katalogów zasobów odnosi się do najmniejszej szerokości (ang. smallest width). Kwalifikator najmniejszej szerokości, swdp, reprezentuje krótszy z boków ekranu, bez względu na aktualną orientację urządzenia.
Analogicznie jeśli 720 dp to najmniejsza dostępna szerokość obsługiwana przez układ Twojego tabletu, zasoby tego układu możesz zdefiniować w katalogu res/layout-sw720dp/. Aby zapewnić obsługę ekranów telefonów Android oraz 7- i 10-calowych tabletów, musisz zapewnić odpowiednie układy w następujących trzech katalogach: res/layout/activity_layout.xml dla telefonów, res/layout-sw600dp/activity_layout.xml dla 7-calowych tabletów, res/layout-sw720dp/activity_layout.xml dla 10-calowych tabletów.
Zwyczajową orientacją dla tabletu jest orientacja pozioma. Dlatego pliki układów o orientacji poziomej (ang. landscape-oriented) dla 7- i 10-calowych tabletów umieszczasz odpowiednio w katalogach res/layout-sw600dp i res/layout-sw720dp. Ponadto, jeśli trzeba, tworzysz dla tych tabletów katalogi res/layout-sw600dp-port i res/layout-sw720dp-port do przechowywania plików układów pionowych (ang. portrait-oriented). A teraz praktyka. Aby zrozumieć procedurę konwersji aplikacji z telefonów Android na aplikację dla tabletów, otwórz projekt Android FirstAndroidTabletApp, który utworzyłeś według wskazówek z poprzedniej receptury. Domyślny kod z pliku układu aktywności — activity_first_android_tablet_app.xml — został przedstawiony w listingu 1.1. Listing 1.1. Domyślny kod w pliku układu aktywności activity_first_android_tablet_app.xml
Łatwo zauważyć, że domyślny kod z pliku układu spowoduje wyświetlenie tekstu Witaj, świecie! pośrodku ekranu, co zostało pokazane na rysunku 1.8. Tekst ten pojawi
się w wyznaczonych przez standardowe marginesy odległościach do brzegów ekranu.
Receptura: konwersja aplikacji z telefonu Android w aplikację na tablet Android
Rysunek 1.8. Efekt uruchomienia aplikacji FirstAndroidTabletApp na emulatorze telefonu
Rozmiar tekstu Witaj, świecie! jest odpowiedni dla ekranu telefonu, ale na ekranie 10-calowego tabletu tekst jest tak mały, że prawie niewidoczny (patrz rysunek 1.9).
Rysunek 1.9. Efekt uruchomienia aplikacji FirstAndroidTabletApp na ekranie 10-calowego tabletu
39
40
Rozdział 1. Przegląd aplikacji na tablety z systemem Android
Pierwszym krokiem do sprawienia, by aplikacja stała się kompatybilna zarówno z telefonami, jak i tabletami, jest dostosowanie rozmiaru kontrolek interfejsu użytkownika. Ponieważ bieżąca aplikacja obejmuje kontrolkę TextView, dodaj do pliku układu tej aplikacji atrybut android:textSize, aby dostosować rozmiar czcionki wyświetlanego tekstu. Ponadto usuń domyślne atrybuty marginesów z kontenera RelativeLayout. Po dodaniu wskazanego atrybutu plik układu aktywności przyjmie postać pokazaną w listingu 1.2. Dodany został tylko ten wiersz kodu, który zaznaczony jest pogrubioną czcionką. Listing 1.2. Kod w pliku układu aktywności activity_first_android_tablet_app.xml
Możesz zauważyć, że zasób wymiarów text_size przydziela rozmiar czcionki do tekstu wyświetlanego za pomocą kontrolki TextView. Aby zdefiniować zasób wymiarów, dodaj do folderu res/values Twojej aplikacji plik XML o nazwie dimens.xml. Zdefiniuj zasób wymiarów text_size, wpisując w pliku dimens.xml następujący kod: 14sp
Uwaga Android 4.2.2 automatycznie tworzy plik dimens.xml w folderze res/values.
W powyższym kodzie przypisujesz rozmiar 14 sp do tekstu, który jest wyświetlany w telefonach i tabletach z systemem Android. Rozmiar 14 sp jest dobry dla telefonów Android, ale dość mały dla tabletów Android. Aby zdefiniować osobny zasób wymiarów dla 7-calowych i 10-calowych tabletów, utwórz w folderze res swojej aplikacji dwa foldery o nazwach values-sw600dp i values-sw720dp.
Uwaga Android 4.2.2 automatycznie tworzy dwa foldery, values-sw600dp i values-sw720dp-land, z plikiem dimens.xml.
Receptura: konwersja aplikacji z telefonu Android w aplikację na tablet Android
Skopiuj plik dimens.xml z folderu res/values do nowo utworzonych folderów values-sw600dp i values-sw720dp. Aby zdefiniować rozmiar tekstu dla 7-calowego tabletu, otwórz plik dimens.xml znajdujący się w folderze values-sw600dp i zwiększ wartość zasobu text_size do 24 sp, tak jak zostało to pokazane w kolejnym fragmencie kodu. 24sp
Podobnie dla 10-calowego tabletu otwórz plik dimens.xml znajdujący się w folderze values-sw720dp i ustaw wartość zasobu text_size na 32 sp, co zostało pokazane w poniższym fragmencie kodu. 32sp
Zasób wymiarów text_size jest teraz zdefiniowany tak, aby wyświetlać w telefonie, 7-calowym tablecie i 10-calowym tablecie tekst o rozmiarze czcionki odpowiednio 14 sp, 24 sp i 32 sp. Po dodaniu pliku dimens.xml do folderu values i utworzeniu w folderze res folderów values-sw600dp i values-sw720dp aplikacja będzie wyświetlana w eksploratorze paczek Package Explorer w sposób przedstawiony na rysunku 1.10 (obrazek po lewej). Po zastosowaniu rozmiarów czcionek 24 sp i 32 sp do tekstu wyświetlanego na ekranach 7-calowych i 10-calowych tabletów tekst będzie bardziej czytelny i jaśniejszy niż w przypadku użycia czcionki o rozmiarze 14 sp. Na rysunku 1.11 przedstawiono efekt uruchomienia danej aplikacji na emulatorze 7-calowego tabletu (obrazek górny) oraz na emulatorze 10-calowego tabletu (obrazek dolny). Dotychczas używałeś tego samego pliku układu dla telefonu i tabletów. Ponieważ normalna orientacja dla tabletu to orientacja pozioma oraz dlatego, że tablet ma szerszy ekran, zawsze pozostaje sporo wolnej przestrzeni po prawej stronie ekranu. Wymagany może być więc osobny układ dla tabletów, aby właściwie rozmieścić kontrolki interfejsu użytkownika i wykorzystać dodatkową przestrzeń. Aby zdefiniować osobny układ dla tabletów, utwórz w folderze res dwa foldery o nazwach layout-sw600dp i layout-sw720dp, gdzie folder layout-sw600dp będzie wykorzystywany do definiowania zasobów układu dla 7-calowych tabletów, a folder layout-sw720dp będzie przechowywał zasoby układu dla 10-calowych tabletów. Skopiuj plik układu aktywności activity_first_android_tablet_app.xml z folderu /res/layout do nowo utworzonych folderów layout-sw600dp i layout-sw720dp. Folder res/layout będzie teraz wykorzystywany do przechowywania zasobów układu jedynie dla telefonów. Zdefiniuj układ dla 7-calowego tabletu, modyfikując plik układu activity_first_android_tablet_app.xml (w folderze res/layout-sw600dp) w sposób przedstawiony w listingu 1.3.
41
42
Rozdział 1. Przegląd aplikacji na tablety z systemem Android
Rysunek 1.10. Package Explorer wyświetlający pliki i foldery zasobów wymiarów (obrazek po lewej) oraz Package Explorer wyświetlający foldery przechowujące zasoby układów dla 7-calowych i 10-calowych tabletów (obrazek po prawej) Listing 1.3. Kod w pliku układu aktywności activity_first_android_tablet_app.xml dla 7-calowego tabletu
Receptura: konwersja aplikacji z telefonu Android w aplikację na tablet Android
Rysunek 1.11. Wiadomość tekstowa „Witaj, świecie!” wyświetlona na emulatorze 7-calowego tabletu (obrazek górny) oraz na emulatorze 10-calowego tabletu (obrazek dolny)
43
44
Rozdział 1. Przegląd aplikacji na tablety z systemem Android
Możesz zauważyć, że kontrolka TextView zawarta w kontenerze LinearLayout została ustawiona tak, aby wyświetlać wiadomość tekstową 7-calowy tablet w orientacji poziomej. Tekst ten będzie prezentowany pogrubioną czcionką o rozmiarze zdefiniowanym w zasobie wymiarów text_size. Analogicznie dla 10-calowego tabletu zmodyfikuj plik układu activity_first_android_tablet_app.xml znajdujący się w folderze layout-sw720dp. W tym pliku układu wstaw kod przedstawiony w listingu 1.3 ale z drobną modyfikacją. Zmień treść wyświetlanej wiadomości w następujący sposób: android:text="10-calowy tablet w orientacji poziomej"
Po uruchomieniu tej aplikacji emulator telefonu nadal będzie wyświetlał tę samą starą wiadomość Witaj, świecie!, ale emulatory tabletów 7- i 10-calowego będą wyświetlać odpowiednio komunikaty 7-calowy tablet w orientacji poziomej oraz 10-calowy tablet w orientacji poziomej. Na rysunku 1.12 widać wspomnianą wiadomość tekstową wyświetloną na emulatorze 10-calowego tabletu w orientacji poziomej.
Rysunek 1.12. Wiadomość tekstowa wyświetlona na emulatorze 10-calowego tabletu w orientacji poziomej
Wiadomość tekstowa wyświetlana przez emulatory 7- i 10-calowych tabletów będzie taka sama, bez względu na to, czy będą one w orientacji poziomej, czy pionowej. Co trzeba zrobić, jeśli chcesz zmienić układ dla tabletów, kiedy użytkownicy zmieniają orientację na pionową? Aby zdefiniować osobny układ dla tabletów w orientacji pionowej, musisz w folderze res Twojej aplikacji utworzyć dwa foldery o nazwach layout-sw600dp-port i layout-sw720dp-port. Tak jak poprzednio, skopiuj plik układu aktywności
Receptura: konwersja aplikacji z telefonu Android w aplikację na tablet Android
activity_first_android_tablet_app.xml z folderu /res/layout do nowo utworzonych folderów layout-sw600dp-port i layout-sw720dp-port. Aby zdefiniować układ dla 7-calowego tabletu w orientacji pionowej, zmodyfikuj znajdujący się w folderze layout-sw600dp-port plik activity_first_android_tablet_app.xml w sposób pokazany w listingu 1.4. Listing 1.4. Kod w pliku układu aktywności activity_first_android_tablet_app.xml dla 7-calowego tabletu w orientacji pionowej
Możesz zauważyć, że kontrolka TextView została ustawiona tak, aby wyświetlać wiadomość tekstową 7-calowy tablet w orientacji pionowej. Tekst ten będzie prezentowany pogrubioną czcionką o rozmiarze zdefiniowanym w zasobie wymiarów text_size. Dla 10-calowego tabletu zmodyfikuj znajdujący się w folderze layout-sw720dp plik układu activity_first_android_tablet_app.xml w sposób przedstawiony w listingu 1.4 z niewielką różnicą w wiadomości tekstowej. Zmień treść wyświetlanej wiadomości w następujący sposób: android:text="10-calowy tablet w orientacji pionowej"
Teraz po uruchomieniu aplikacji emulatory tabletów 7-calowego i 10-calowego w orientacji pionowej wyświetlą odpowiednio komunikaty 7-calowy tablet w orientacji pionowej oraz 10-calowy tablet w orientacji pionowej. Na rysunku 1.13 widać wspomnianą wiadomość tekstową wyświetloną na emulatorze 10-calowego tabletu w orientacji pionowej. Dowiedziałeś się już wystarczająco dużo na temat układów tabletów w orientacji pionowej i poziomej. Należy jeszcze zadbać o to, aby dana aplikacja była całkowicie kompatybilna z telefonami i tabletami Android, czyli trzeba zdefiniować układ dla telefonu w orientacji poziomej. W tej chwili ten sam układ, który jest zdefiniowany w folderze res/layout, będzie wykorzystywany zarówno dla orientacji pionowej, jak i poziomej. Zatem, tak jak było w przypadku orientacji pionowej, telefon w orientacji poziomej wyświetli tę samą wiadomość Witaj, świecie! (patrz rysunek 1.14, obrazek po lewej). Aby zdefiniować osobny układ telefonu dla orientacji poziomej, musisz utworzyć nowy folder o nazwie layout-land. Utwórz go w folderze res swojej aplikacji i skopiuj do niego plik układu activity_first_ android_tablet_app.xml z folderu res/layout. Aby zdefiniować układ dla telefonu w orientacji poziomej, zmodyfikuj plik układu activity_first_android_tablet_app.xml w sposób pokazany w listingu 1.5.
45
46
Rozdział 1. Przegląd aplikacji na tablety z systemem Android
Rysunek 1.13. Wiadomość tekstowa wyświetlona na emulatorze 10-calowego tabletu w orientacji pionowej Listing 1.5. Kod w pliku układu aktywności activity_first_android_tablet_app.xml dla telefonu Android w orientacji poziomej
Receptura: konwersja aplikacji z telefonu Android w aplikację na tablet Android
Rysunek 1.14. Wiadomość tekstowa „Witaj, świecie!” wyświetlona w telefonie w orientacji pionowej (obrazek po lewej) oraz zmodyfikowana wiadomość „Telefon w orientacji poziomej” wyświetlana w telefonie w orientacji poziomej (obrazek po prawej)
Kontrolka TextView została ustawiona w taki sposób, aby wyświetlany był tekst Telefon w orientacji poziomej. Tekst ten będzie prezentowany pośrodku ekranu i z zastosowaniem pogrubionej czcionki o rozmiarze zdefiniowanym w zasobie wymiarów text_size. Po utworzeniu wszystkich plików i folderów omówionych w tej recepturze nasza aplikacja będzie wyglądała tak, jak zostało to przedstawione na rysunku 1.10 (obrazek po prawej) w oknie eksploratora Package Explorer. Teraz po uruchomieniu aplikacji zamiast wiadomości Witaj, świecie! wyświetlanej w orientacji pionowej telefon po przejściu do orientacji poziomej będzie wyświetlał pośrodku ekranu tekst Telefon w orientacji poziomej (patrz rysunek 1.14, obrazek po prawej). W ramach podsumowania poniżej wymienione zostały czynności, które musisz wykonać, aby aplikacja Android była kompatybilna zarówno z telefonami, jak i tabletami Android. 1. W folderze res/values utwórz plik XML o nazwie dimens.xml, który będzie
zawierał następujący kod: 14sp
2. W folderze res utwórz dwa foldery o nazwach values-sw600dp i values-sw720dp.
Skopiuj do tych dwóch folderów plik dimens.xml z folderu res/values. Następnie w obu tych folderach otwórz plik dimens.xml i zmień rozmiar czcionki w zasobach wymiarów text_size odpowiednio na 24 sp i 32 sp.
47
48
Rozdział 1. Przegląd aplikacji na tablety z systemem Android 3. Zmodyfikuj plik układu aktywności i przydziel do atrybutu size wszystkich
kontrolek interfejsu użytkownika zasoby wymiarów w następujący sposób: android:textSize="@dimen/text_size"
4. Jeśli układ kontrolek interfejsu użytkownika w tabletach różni się od układu
w telefonie, utwórz w folderze res dwa foldery o nazwach layout-sw600dp i layout-sw720dp. Następnie skopiuj do tych dwóch folderów plik układu aktywności z folderu res/layout. Zorganizuj kontrolki interfejsu użytkownika w skopiowanym pliku układu w tych folderach w taki sposób, aby wykorzystać dodatkową wolną przestrzeń po prawej stronie. 5. Jeśli potrzebujesz osobnego układu dla tabletów w orientacji pionowej, utwórz
dwa foldery o nazwach layout-sw600dp-port i layout-sw720dp-port. Skopiuj do tych dwóch folderów plik układu aktywności z folderu res/layout. Zorganizuj kontrolki interfejsu użytkownika w skopiowanym pliku układu w taki sposób, aby pasowały do pionowej orientacji tabletów. 6. Jeśli dla telefonu układy dla orientacji pionowej i poziomej się różnią, utwórz
w folderze res folder layout-land i skopiuj do niego plik układu aktywności z folderu res/layout. W folderze layout-land w pliku układu zorganizuj kontrolki interfejsu użytkownika w taki sposób, aby pasowały do poziomej orientacji telefonu.
Receptura: wymuszanie, aby aplikacja działała jedynie na tabletach Aby aplikacja była przeznaczona jedynie dla tabletów, musisz określić dwa elementy. Upewnij się, że urządzenie posiada system operacyjny Android 3.0
(Honeycomb) lub nowszy. Upewnij się, że urządzenie posiada duży lub ekstraduży ekran
(czyli co najmniej 7-calowy). Aby aplikacja działała jedynie na urządzeniach z dużym lub ekstradużym ekranem, niezależnie od wersji systemu Android zainstalowanego na danym urządzeniu, zmodyfikuj plik Android Manifest.xml, aby wyglądał następująco:
Możesz również użyć atrybutu requiresSmallestWidthDp elementu , aby określić minimalną szerokość urządzenia, na którym uruchamiana ma być aplikacja. Za pomocą atrybutu android:requiresSmallestWidthDp możesz definiować minimalne
Receptura: aktywności
wymiary obszaru ekranu (w jednostkach dp), które są wymagane przez kontrolki interfejsu użytkownika do uruchomienia Twojej aplikacji. Urządzenie, które posiada szerokość równą lub większą wartości określonej w atrybucie requiresSmallestWidthDp, będzie w stanie uruchomić daną aplikację. Poniższa instrukcja powoduje, że aplikacja może być uruchamiana tylko na urządzeniach o szerokości co najmniej 600 dp. ...
Receptura: aktywności Ekran aplikacji składający się z kontrolek, z którym użytkownik wchodzi w interakcję, wyświetlany jest za pomocą aktywności (ang. activity). Każdy ekran reprezentowany jest przez pewną aktywność. Prosta aplikacja może składać się z tylko jednej aktywności, podczas gdy większe aplikacje zawierają kilka aktywności. W trakcie działania aplikacji utrzymywany jest stos aktywności, a aktywność znajdująca się na wierzchu stosu jest tą, która jest aktualnie wyświetlana. Kiedy naciskasz przycisk Back (wstecz), dana aktywność jest zdejmowana ze stosu, a aktualną aktywnością staje się poprzednia aktywność, która wyświetla poprzedni ekran. Przechodzenie od jednej aktywności do drugiej odbywa się za pomocą asynchronicznych komunikatów zwanych intencjami (ang. intents). Intencje mogą przekazywać dane z jednej aktywności do drugiej. Wszystkie aktywności w danej aplikacji muszą być zdefiniowane w pliku manifestu tej aplikacji. Każda aktywność w aplikacji Android jest bezpośrednią podklasą podstawowej klasy aktywności lub podklasą podklasy aktywności.
Receptura: cykl życia aktywności w systemie Android Cykl życia aktywności w systemie Android definiuje stany lub zdarzenia, przez które przechodzi aktywność od momentu jej utworzenia do chwili zakończenia. Aktywność monitoruje te zdarzenia i reaguje na nie, wykonując metody, które zastępują metody klasy aktywności dla każdego zdarzenia. Lista metod wykonywanych podczas różnych zdarzeń cyklu życia aktywności w systemie Android została przedstawiona w tabeli 1.1.
49
50
Rozdział 1. Przegląd aplikacji na tablety z systemem Android Tabela 1.1. Lista metod instancjonowanych podczas różnych zdarzeń cyklu życia aktywności w systemie Android
Metoda
Opis
onCreate()
Metoda wywoływana, kiedy tworzona jest aktywność. Inicjuje aktywność i jest wykorzystywana do tworzenia widoków aplikacji, otwierania trwałych plików danych wymaganych przez aktywność itd.
onStart()
Metoda wywoływana tuż przed tym, zanim aktywność staje się widoczna na ekranie. Aktywność może być na pierwszym planie lub w tle. Kiedy aktywność przełącza się do stanu w tle, wykonywana jest metoda onStop(), a kiedy przełącza się do stanu na pierwszym planie, wywoływana jest metoda onResume().
onResume()
Metoda wywoływana za każdym razem, kiedy dana aktywność staje się aktywnością pierwszego planu, zarówno wtedy, kiedy następuje to zaraz po wykonaniu metody onStart(), jak i wtedy, gdy istnieje jakaś inna aktywność pierwszego planu, a dana aktywność pojawia się na wierzchu stosu i sama staje się aktywnością pierwszego planu. Aktywność pierwszego planu wchodzi w interakcje z użytkownikiem, otrzymuje dane wejściowe z klawiatury i ekranu dotykowego oraz generuje właściwą odpowiedź.
onPause()
Metoda wykonywana, kiedy aktywność jest zatrzymana i nie jest dłużej widoczna na pierwszym planie, a jakaś inna aktywność zostaje przełączona na pierwszy plan. Ponieważ dana aktywność nie jest widoczna, metoda ta zawiera polecenia mające na celu minimalizację wykorzystania zasobów i przechowanie stanu aktywności, który zostanie użyty, gdy aktywność wróci na pierwszy plan. Możesz użyć tej metody do zawieszenia dowolnej akcji, która wykorzystuje cykle procesora lub zużywa baterię.
onStop()
Metoda wywoływana, kiedy aktywność nie jest już widoczna, ponieważ została zniszczona lub inna aktywność została przełączona na pierwszy plan.
onDestroy()
Metoda wykorzystywana, kiedy aktywność jest zakończona i ma zostać zniszczona. Ta metoda może, ale nie musi być wywoływana (czyli system może po prostu przerwać proces). Możesz użyć tej metody do uwolnienia zasobów wykorzystywanych przez daną metodę.
Poniżej przedstawiono podsumowanie dotyczące cyklu życia aktywności (patrz rysunek 1.15). Kiedy aktywność jest rozpoczynana, wywoływane są metody onCreate(),
onStart() i onResume(), które prowadzą aktywność do stanu działania. Kiedy przyciśnięty zostaje przycisk Back, wywoływane są metody onPause(),
onStop() i onDestroy(), a aktywność zostaje zakończona. Kiedy przyciśnięty zostaje przycisk Home, aktywność jest pauzowana
lub przerywana, co powoduje wywołanie metod onPause() i onStop(). Kiedy aktywność jest w stania zapauzowania, może być przywrócona do stanu
działania poprzez wywołanie metod onRestart(), onStart() i onResume().
Receptura: cykl życia aktywności w systemie Android
Rysunek 1.15. Cykl życia aktywności
Aby zrozumieć sekwencję metod, które są wywoływane podczas cyklu życia aktywności, utwórz projekt Android o nazwie ActivityLifecycleApp. W pliku aktywności Java ActivityLifecycleAppActivity.java wpisz kod przedstawiony w listingu 1.6. Listing 1.6. Kod wpisany w pliku aktywności Java ActivityLifecycleAppActivity.java package com.androidtablet.activitylifecycleapp; import android.os.Bundle; import android.app.Activity; import android.util.Log; public class ActivityLifecycleAppActivity extends Activity { private static final String tag = "Stan: ";
51
52
Rozdział 1. Przegląd aplikacji na tablety z systemem Android @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_activity_lifecycle_app); Log.d(tag,"onCreate"); } @Override protected void onStart() { super.onStart(); Log.d(tag,"onStart"); } @Override protected void onRestart() { super.onRestart(); Log.d(tag,"onRestart"); } @Override protected void onResume() { super.onResume(); Log.d(tag,"onResume"); }; @Override protected void onPause() { super.onPause(); Log.d(tag,"onPause"); } @Override protected void onStop() { super.onStop(); Log.d(tag,"onStop"); }; @Override protected void onDestroy() { super.onDestroy(); Log.d(tag,"onDestroy"); }; }
Po uruchomieniu aplikacji będziesz mógł zobaczyć komunikaty dziennika zdarzeń wyświetlone w oknie LogCat (patrz rysunek 1.16). Jeśli nie znajdziesz na ekranie okna LogCat, wybierz z menu Window/Show View/Other, a następnie w wyświetlonym oknie dialogowym Show View wybierz opcję LogCat i kliknij przycisk OK. Okno LogCat pojawi się na dole ekranu. Aplikacja spowoduje wyświetlenie trzech komunikatów: onCreate, onStart oraz onResume. To potwierdzenie, że aktywność jest teraz w stanie działania. Jeśli wciśniesz przycisk Home, aktywność zostanie wstrzymana, ale nie zakończona. W oknie LogCat pojawią się komunikaty onPause i onStop, ale nie pojawi się komunikat onDestroy. Potwierdza to, że aktywność jest w stanie zapauzowania (patrz rysunek 17, dolny obrazek). Jeśli aplikacja jest uruchomiona, kiedy aktywność jest w stanie zapauzowania, nie zostanie ona przeładowana, a po prostu przywrócona do swojego stanu działania. Przywrócenie aktywności do stanu działania jest potwierdzane komunikatami onRestart, onStart i onResume, które są wyświetlane w oknie LogCat.
Receptura: rozpoczynanie korzystania z intencji
Rysunek 1.16. Okno LogCat wyświetlające komunikaty dziennika, które reprezentują metody cyklu życia aplikacji
W oknie LogCat wyświetlane są wszystkie komunikaty, które pojawiają się w trakcie wykonywania aplikacji. Aby widzieć jedynie komunikaty dziennika związane z metodami wywoływanymi w Twojej aplikacji, dodaj filtr LogCat. W tym celu kliknij najpierw zielony symbol plus znajdujący się w lewym górnym rogu okna LogCat. W wyświetlonym oknie Logcat Message Filter Settings (ustawienia filtrów komunikatów LogCat) wpisz w polu Filter Name nazwę filtra jako Cykl życia (patrz rysunek 1.17, górny obrazek). W polu by Log Tag (według znacznika logu) wpisz Stan:, tak aby przez filtr były przepuszczane jedynie komunikaty dziennika zawierające podany znacznik. Po kliknięciu przycisku OK wyświetlane będą tylko komunikaty dziennika związane ze znacznikiem Stan, tak jak zostało to pokazane na rysunku 1.17 (dolny obrazek).
Receptura: rozpoczynanie korzystania z intencji Struktura wykorzystywana do rozpoczęcia i zatrzymania aktywności oraz do przechodzenia pomiędzy aktywnościami w ramach aplikacji zwana jest intencją (ang. intent). Intencja jest strukturą danych opisującą operacje, które mają być przeprowadzone w aplikacji Android. Składa się z akcji, którą musi wykonać aplikacja, danych, na których przeprowadzana jest operacja, oraz innych informacji pomocnych podczas wykonywania konkretnej operacji. Intencje mogą być jawne i niejawne. Intencja jawna (ang. explicit intent) — w niej określasz aktywność wymaganą
do udzielenia odpowiedzi na daną intencję, czyli w sposób wyraźny wyznaczasz komponent docelowy. Ponieważ deweloperzy innych aplikacji nie znają nazw komponentów w Twojej aplikacji, intencja jawna jest ograniczona do zastosowania w ramach jednej aplikacji, np. do przesyłania komunikatów wewnętrznych.
53
54
Rozdział 1. Przegląd aplikacji na tablety z systemem Android
Rysunek 1.17. Okno dialogowe dla tworzenia filtra LogCat (górny obrazek) oraz komunikaty dziennika LogCat reprezentujące cykl życia aktywności (dolny obrazek) Intencja niejawna (ang. implicit intent) — w niej po prostu deklarujesz intencję
i platformie pozostawiasz znalezienie aktywności, która może odpowiedzieć na daną intencję. Oznacza to, że nie określasz docelowego komponentu, który powinien odpowiedzieć na intencję. Ten rodzaj intencji jest wykorzystywany do aktywacji komponentów innych aplikacji. To zadaniem platformy Android jest wyszukanie najbardziej odpowiedniego komponentu do obsługi danej intencji niejawnej. Metoda wykorzystywana do rozpoczynania aktywności nosi nazwę startActivity(). Najpierw tworzysz obiekt intencji niejawnej lub jawnej i przekazujesz go do metody startActivity() w następującym formacie: startActivity(moja_intencja);
Tutaj element moja_intencja odnosi się do intencji, która jest przekazywana do metody jako parametr. Metoda startActivity() odnajduje i rozpoczyna tę pojedynczą aktywność, która najlepiej odpowiada danej intencji. Aby w sposób wyraźny ustalić aktywność, którą chcesz rozpocząć za pomocą intencji, utwórz nową intencję, określając kontekst bieżącej aplikacji oraz nazwę klasy aktywności, jaką chcesz uruchomić, i przekaż tę intencję do metody startActivity() w następujący sposób: startActivity(new Intent(this, WelcomeActivity.class));
Receptura: rozpoczynanie korzystania z intencji
W powyższej instrukcji utworzyłeś intencję jawną i zainicjowałeś ją, przekazując kontekst aktywności this oraz instancję klasy WelcomeActivity, będącą aktywnością, którą chcesz uruchomić. Obiekt intencji jest przekazywany do metody startActivity(), która uruchamia aktywność opisaną przez WelcomeActivity.class. Jeśli metoda startActivity() nie jest w stanie odnaleźć określonej aktywności, wyrzucany jest wyjątek android.content.ActivityNotFoundException. Teraz zapoznasz się z koncepcją tworzenia i rozpoczynania własnej aktywności na konkretnym przykładzie. Utworzysz aplikację wyświetlającą kontrolkę Button, która po kliknięciu rozpoczyna nową aktywność. Uruchomiona aktywność będzie wyświetlać użytkownikowi wiadomość powitalną. Utwórz nowy projekt Android o nazwie IntentApp. Aby wyświetlić kontrolkę Button, w pliku układu aktywności activity_intent_app.xml wpisz kod przedstawiony w listingu 1.7. Listing 1.7. Kod wpisany w pliku układu aktywności activity_intent_app.xml
Możesz zauważyć, że kontrolka Button o identyfikatorze start_button jest definiowana z opisem Rozpocznij aktywność. Aktywność ma być uruchomiona, kiedy kliknięty zostanie przycisk Rozpocznij aktywność. Wtedy wywoływana jest nowa aktywność WelcomeActivity. Ponieważ aktywność WelcomeActivity musi wyświetlić użytkownikowi wiadomość powitalną, utwórz nowy plik układu dla swojej aktywności WelcomeActivity. W tym celu w oknie Package Explorer kliknij prawym przyciskiem myszy folder res/layout i wybierz opcję New/Android XML File (nowy/plik XML Android). Wyświetlone zostanie okno dialogowe, w którym należy podać informacje na temat nowego pliku XML Android. W polu tekstowym File (plik) wpisz nazwę pliku jako welcome (nie musisz dopisywać rozszerzenia .xml). Z listy Root Element (element główny) wybierz opcję LinearLayout, co oznacza, że chcesz utworzyć plik układu liniowego. Na koniec kliknij przycisk Finish (zakończ). W folderze res/layout utworzony zostanie plik welcome.xml. Aby wyświetlić wiadomość powitalną, musisz w pliku układu zdefiniować kontrolkę TextView. Po zdefiniowaniu tej kontrolki plik układu welcome.xml będzie wyglądał tak, jak pokazano w listingu 1.8. Listing 1.8. Kod wpisany w pliku welcome.xml
55
56
Rozdział 1. Przegląd aplikacji na tablety z systemem Android android:layout_width="match_parent" android:layout_height="match_parent">
Po przygotowaniu pliku układu czas na utworzenie Twojej nowej aktywności WelcomeActivity w paczce com.androidtablet.intentapp, która znajduje się w folderze
src aplikacji. W tym celu kliknij prawym przyciskiem myszy pozycję package com.androidtablet.intentapp i wybierz opcję New/Class (nowa/klasa). Wyświetlone zostanie okno New Java Class (nowa klasa Java), w którym należy wprowadzić nazwę pliku Java. W polu tekstowym Name (nazwa) wpisz nazwę pliku aktywności: WelcomeActivity (nie musisz dodawać rozszerzenia .java). Pozostawiając dla pozostałych opcji ustawienia domyślne, kliknij przycisk Finish, aby utworzyć plik Java. Do paczki dodana zostanie aktywność WelcomeActivity.java. Aktywność WelcomeActivity ma za zadanie wyświetlanie użytkownikowi wiadomości powitalnej za pomocą kontrolki TextView zdefiniowanej w pliku układu welcome.xml. Aby załadować interfejs użytkownika zdefiniowany w pliku układu welcome.xml, wpisz w pliku WelcomeActivity.java kod pokazany w listingu 1.9. Listing 1.9. Kod wpisany w pliku WelcomeActivity.java package com.androidtablet.intentapp; import android.app.Activity; import android.os.Bundle; public class WelcomeActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.welcome); } }
Tylko komponenty zadeklarowane w pliku manifestu aplikacji AndroidManifest.xml są rozpoznawane przez system Android i dlatego mogą być użyte do uruchomienia danej aplikacji. Oznacza to, że nowa aktywność WelcomeActivity.java musi być zadeklarowana w pliku AndroidManifest.xml, aby była widoczna dla systemu Android i mogła być uruchomiona. Plik AndroidManifest.xml został pokazany w listingu 1.10. Instrukcja zaznaczona pogrubioną czcionką została dodana w celu zarejestrowania nowo utworzonej aktywności WelcomeActivity.java.
Receptura: rozpoczynanie korzystania z intencji Listing 1.10. Kod w pliku AndroidManifest.xml
Po zarejestrowaniu aktywności WelcomeActivity.java w pliku AndroidManifest.xml możesz teraz uruchomić tę aktywność, wykorzystując metodę startActivity() omówioną na początku tej receptury. Ponieważ nowa aktywność zostanie uruchomiona z domyślnego pliku aktywności aplikacji IntentAppActivity.java, metoda startActivity() również musi zostać dodana do tego samego pliku. Kod w domyślnym pliku aktywności, intentAppActivity.java, po dodaniu metody startActivity() będzie wyglądał, tak jak przedstawiony w listingu 1.11. Listing 1.11. Kod wpisany w pliku IntentAppActivity.java package com.androidtablet.intentapp; import import import import import
android.os.Bundle; android.app.Activity; android.widget.Button; android.view.View; android.content.Intent;
public class IntentAppActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_intent_app); Button startButton = (Button)this.findViewById(R.id.start_button); startButton.setOnClickListener(new Button.OnClickListener(){
57
58
Rozdział 1. Przegląd aplikacji na tablety z systemem Android public void onClick(View v) { startActivity(new Intent(getBaseContext(),WelcomeActivity.class)); } }); } }
Dostęp do kontrolki Button jest uzyskiwany z poziomu pliku układu za pomocą jej identyfikatora o nazwie start_button, a kontrolka jest mapowana na swój obiekt startButton. Kiedy obiekt startButton zostanie kliknięty, wywoływana jest metoda startActivity(). Metoda startActivity() tworzy intencję jawną, która w wyraźny sposób określa, że plik aktywności, WelcomeActivity.java, ma być uruchomiony, kiedy przycisk zostanie kliknięty. Metoda getBaseContext() wykorzystana w powyższym kodzie zwraca instancję klasy Context. Instancja klasy Context odwołuje się do aplikacji. Po uruchomieniu aplikacji na ekranie wyświetlony zostaje przycisk Rozpocznij aktywność, co pokazano na rysunku 1.18 (górny obrazek). Po kliknięciu tego przycisku nowa aktywność, WelcomeActivity, wyświetli na ekranie wiadomość powitalną (patrz rysunek 1.18, dolny obrazek).
Receptura: przekazywanie danych z jednej aktywności do drugiej W tej recepturze nauczysz się przekazywać dane z jednej aktywności do drugiej. W tym celu musisz wykorzystać obiekt Bundle. Oznacza to, że tworzysz obiekt Bundle i stosując odpowiedni klucz, umieszczasz w nim wartość typu String, Short, Float itp. Klucz służy do późniejszego wydobywania danych z obiektu Bundle. Poniższy kod powoduje wstawienie ciągu znaków do obiektu Bundle przy wykorzystaniu klucza username (nazwa użytkownika). Bundle dataBundle = new Bundle(); dataBundle.putString("username", "Kelly"); Intent welcomeIntent = new Intent(getBaseContext(), WelcomeActivity.class); welcomeIntent.putExtras(dataBundle); startActivityForResult(welcomeIntent, WELCOME_REQUEST_CODE);
Jak widzisz w tym kodzie tworzony jest obiekt Bundle o nazwie dataBundle. W obiekcie Bundle przechowywane jest imię Kelly z wykorzystaniem klucza username. Tworzona jest intencja welcomeIntent określająca bieżący kontekst aplikacji oraz nazwę klasy podaktywności WelcomeActivity — aktywności, którą chcesz uruchomić. Za pomocą metody putExtras() do welcomeIntent dodawany jest obiekt dataBundle zawierający dane, które mają być przekazane do podaktywności. Na koniec intencja jest przekazywana do metody startActivityForResult(). Metoda ta uruchomi wskazaną podaktywność i po zakończeniu jej działania dostarczy wynik. Oznacza to, że kiedy uruchomiona podaktywność istnieje, metoda onActivityResult() będzie wywoływana za pomocą danego kodu żądania (ang. request code). Możesz przeanalizować wartość kodu żądania, aby określić, czy podaktywność została wykonana z powodzeniem.
Receptura: przekazywanie danych z jednej aktywności do drugiej
Rysunek 1.18. Przycisk „Rozpocznij aktywność” wyświetlony po uruchomieniu aplikacji (górny obrazek) oraz nowa aktywność, WelcomeActivity, wyświetlająca wiadomość powitalną (dolny obrazek)
W tej podaktywności możesz pobrać dane przekazywane przez obiekt Bundle. W przedstawionym poniżej kodzie pokazano, w jaki sposób się to odbywa. Bundle extras = getIntent().getExtras(); if(extras !=null) { String userName=extras.getString("username"); }
59
60
Rozdział 1. Przegląd aplikacji na tablety z systemem Android
Możesz zauważyć, że tworzony jest obiekt Bundle o nazwie extras. Metoda getExtras() jest wywoływana w celu pobrania rozszerzonych danych z intencji. Obiekt dataBundle przesłany przez welcomeIntent jest pobierany i przypisywany do obiektu Bundle o nazwie extras. Za pomocą metody getString() uzyskiwany jest dostęp do ciągu przechowywanego w obiekcie Bundle pod kluczem username. Ciąg ten jest przydzielany do userName w celu dalszego przetwarzania. Łatwiej będzie zrozumieć omówioną właśnie koncepcję za pomocą działającego przykładu. Utwórz nowy projekt Android o nazwie CommunicateDataApp. W tej aplikacji wprowadzone przez użytkownika imię będziesz przesyłał do podaktywności, w której wyświetlana jest wiadomość powitalna wraz z przesłanym imieniem. Po kliknięciu kontrolki Button użytkownik będzie kierowany z powrotem z podaktywności do aktywności głównej. Ponieważ chcesz, aby użytkownik wpisał imię i kliknął kontrolkę Button w celu przesłania tego imienia do podaktywności, musisz w pliku układu aktywności activity_communicate_data_app.xml zdefiniować kontrolki EditText, Button oraz TextView. Po zdefiniowaniu tych trzech kontrolek plik układu aktywności będzie wyglądał tak, jak pokazano w listingu 1.12. Listing 1.12. Kod wpisany w pliku układu aktywności activity_communicate_data_app.xml
Możesz zauważyć, że kontrolki EditText, Button i TextView zostały zdefiniowane z wykorzystaniem identyfikatorów user_name, start_button oraz response. W kontrolce EditText będzie wprowadzane imię, a kontrolka Button zostanie wykorzystana do uruchomienia podaktywności — tej, która ma wysyłać wpisane imię i wykorzystywać kontrolkę TextView do wyświetlenia wiadomości tekstowej, kiedy nawigujesz z powrotem do głównej aktywności z podaktywności.
Receptura: przekazywanie danych z jednej aktywności do drugiej
Następnie musisz w głównym pliku aktywności Java napisać kod, który będzie wykonywał następujące zadania. Pobierał imię wprowadzone w kontrolce EditText. Tworzył obiekt Bundle. Umieszczał dane imię w obiekcie Bundle. Tworzył intencję w celu określenia podaktywności, którą chcesz uruchomić. Dodawał obiekt Bundle do tej intencji. Uruchamiał daną podaktywność za pomocą kodu żądania. Analizował kod żądania, aby uzyskać informacje na temat wykonania
podaktywności. Aby wykonać te wszystkie zadania, w głównym pliku aktywności Java CommunicateDataAppActivity.java napisany został kod przedstawiony w listingu 1.13. Listing 1.13. Kod napisany w pliku aktywności Java CommunicateDataAppActivity.java package com.androidtablet.communicatedataapp; import import import import import import import import
android.os.Bundle; android.app.Activity; android.widget.EditText; android.widget.Button; android.widget.TextView; android.view.View; android.content.Intent; android.widget.Toast;
public class CommunicateDataAppActivity extends Activity { private static final int WELCOME_REQUEST_CODE = 0; TextView response; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_communicate_data_app); Button startButton = (Button)this.findViewById(R.id.start_button); final EditText userName=(EditText)findViewById(R.id.user_name); response=(TextView)findViewById(R.id.response); startButton.setOnClickListener(new Button.OnClickListener(){ public void onClick(View v) { if(userName.getText().length() >0) { Bundle dataBundle = new Bundle(); dataBundle.putString("username", userName. getText().toString()); Intent welcomeIntent = new Intent( getBaseContext(), WelcomeActivity.class); welcomeIntent.putExtras(dataBundle); startActivityForResult(welcomeIntent, WELCOME_REQUEST_CODE); } else Toast.makeText(CommunicateDataAppActivity.this,
61
62
Rozdział 1. Przegląd aplikacji na tablety z systemem Android "Wpisz imię", Toast.LENGTH_SHORT). show(); } }); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode ==WELCOME_REQUEST_CODE) { if (resultCode == RESULT_OK) { response.setText("Powrót z aktywności powitania"); } } } }
Podaktywnością, którą chcesz uruchomić, jest WelcomeActivity. W tej podaktywności chcesz wyświetlić przesłane przez aktywność główną imię wraz z wiadomością powitalną. Ponadto chcesz wyświetlić kontrolkę Button, która po kliknięciu przekieruje z powrotem do aktywności głównej. Z tego powodu musisz zdefiniować układ dla danej podaktywności. W tym celu kliknij prawym przyciskiem myszy folder res/layout i dodaj plik XML o nazwie welcome.xml. W pliku układu welcome.xml zdefiniuj kontrolki TextView i Button w sposób pokazany w listingu 1.14. Listing 1.14. Kod wpisany w pliku układu welcome.xml
Możesz zauważyć, że kontrolki TextView i Button zostały zdefiniowane z wykorzystaniem odpowiednio identyfikatorów welcomemsg i goback_button. Kontrolka TextView jest inicjowana do wyświetlenia tekstu To jest aktywność powitania, a kontrolka Button ma ustawić nagłówek przycisku jako Powróć.
Receptura: przekazywanie danych z jednej aktywności do drugiej
Teraz nadszedł czas, aby do aplikacji dodać plik klasy Java dla Twojej podaktywności. W tym celu kliknij prawym przyciskiem myszy paczkę com.androidtablet.communicatedataapp w oknie Package Explorer i dodaj plik klasy Java o nazwie WelcomeActivity.java. Podaktywność WelcomeActivity.java powinna wykonywać następujące zadania. Uzyskiwanie dostępu do obiektu Bundle zawierającego dane przesłane przez
aktywność główną. Uzyskiwanie dostępu do imienia przechowywanego w obiekcie Bundle
i wyświetlanie za pomocą kontrolki TextView tego imienia wraz z wiadomością powitalną. Przerywanie działania podaktywności, ustawianie wartości kodu żądania
i powrót do głównej aktywności po kliknięciu kontrolki Button. Aby wykonać te zadania, w pliku podaktywności WelcomeActivity.java wpisany został kod przedstawiony w listingu 1.15. Listing 1.15. Kod wpisany w pliku podaktywności WelcomeActivity.java package com.androidtablet.communicatedataapp; import import import import import
android.app.Activity; android.os.Bundle; android.widget.TextView; android.widget.Button; android.view.View;
public class WelcomeActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.welcome); final TextView welcomeMsg=(TextView)findViewById( R.id.welcomemsg); Bundle extras = getIntent().getExtras(); if(extras !=null) { String userName=extras.getString("username"); welcomeMsg.setText("Witaj, "+userName+" "); } Button gobackButton = (Button)this.findViewById( R.id.goback_button); gobackButton.setOnClickListener(new Button. OnClickListener(){ public void onClick(View v) { setResult(RESULT_OK, null); finish(); } }); } }
63
64
Rozdział 1. Przegląd aplikacji na tablety z systemem Android
Aby w pliku AndroidManifest.xml poinformować projekt Android o swojej podaktywności WelcomeActivity, dodaj w elemencie następującą instrukcję:
Teraz Twoja aplikacja jest gotowa do uruchomienia. Po jej uruchomieniu główna aktywność poprosi użytkownika o wpisanie imienia w kontrolce EditText (patrz rysunek 1.19, górny obrazek). Kiedy użytkownik wpisze imię i kliknie przycisk Rozpocznij aktywność, uruchomiona zostanie podaktywność WelcomeActivity, która wyświetli wiadomość powitalną oraz imię wprowadzone w głównej aktywności (patrz rysunek 1.19, środkowy obrazek). Po wybraniu w podaktywności przycisku Powróć użytkownik zostanie z powrotem skierowany do aktywności głównej. Kontrolka TextView poprzez wyświetlenie komunikatu Powrót z aktywności powitania potwierdza powrót z podaktywności do aktywności głównej (patrz rysunek 1.19, dolny obrazek).
Podsumowanie W tym rozdziale poznałeś funkcje tabletów Google Nexus 7 i 10, różnice pomiędzy telefonami i tabletami z systemem Android oraz procedury umożliwiające dostosowanie aplikacji z telefonów Android w taki sposób, aby mogły być uruchamiane na tabletach Android. Dowiedziałeś się jak uruchamiać urządzenia AVD dla telefonów i tabletów z systemem Android. Odkryłeś zastosowanie różnych plików i katalogów w projekcie Android. Dowiedziałeś się, czym są aktywności oraz cykl życia aktywności w systemie Android. Wreszcie zapoznałeś się z czynnościami, które są niezbędne do uruchomienia aktywności z wykorzystaniem intencji oraz przekazania danych z jednej aktywności do drugiej. W kolejnym rozdziale przeczytasz o fragmentach, o tym, jaki jest ich cykl życia oraz jakie mają praktyczne zastosowanie w aplikacjach Android. Zobaczysz, w jaki sposób można dynamicznie dodawać fragmenty do aplikacji za pomocą kodu Java. Ponadto dowiesz się, jak dane przekazywane są pomiędzy dwoma fragmentami. Na koniec nauczysz się tworzyć wyspecjalizowane fragmenty ListFragment, DialogFragment oraz PreferenceFragment.
Podsumowanie
Rysunek 1.19. Nazwa użytkownika wpisana w pierwszej aktywności (obrazek górny), wiadomość powitalna wyświetlona w drugiej aktywności wraz z nazwą z pierwszej aktywności (środkowy obrazek) oraz powrót do pierwszej aktywności (dolny obrazek)
65
66
Rozdział 1. Przegląd aplikacji na tablety z systemem Android
2 Fragmenty R
ozdział ten poświęcony został fragmentom. Aplikacje Android są często podzielone na fragmenty, żeby ułatwić zarządzanie nimi. Fragmenty posiadają niezależny interfejs użytkownika i mogą być łatwo dodawane do aplikacji lub z niej usuwane w celu dopasowania do różnych rozmiarów ekranu. W tym rozdziale poznasz zastosowanie fragmentów, dowiesz się, jaki jest ich cykl życia oraz jakie jest ich praktyczne zastosowanie w aplikacjach Android. Zobaczysz również, w jaki sposób za pomocą kodu Java można dynamicznie dodawać fragmenty do aplikacji. Ponadto dowiesz się, jak dane są przekazywane pomiędzy dwoma fragmentami. W tym rozdziale objaśnione zostanie także zastosowanie wyspecjalizowanych fragmentów ListFragment, DialogFragment oraz PreferenceFragment. Nauczysz się wyświetlać opcje za pomocą fragmentu ListFragment, pokazywać okna dialogowe przy użyciu fragmentu DialogFragment oraz konfigurować preferencje użytkownika z wykorzystaniem fragmentu PreferenceFragment. Aby zapewnić obsługę telefonów, 7-calowych tabletów oraz 10-calowych tabletów, przyjęto założenie, że dla wszystkich aplikacji w tej książce będziesz wykonywał trzy poniższe czynności. 1. W folderze res/values utwórz plik XML o nazwie dimens.xml, który będzie
zawierał następujący kod: 14sp
2. W folderze res utwórz dwa foldery o nazwach values-sw600dp oraz values-
sw720dp i skopiuj do tych folderów plik dimens.xml z folderu res/values. 1. W plikach dimens.xml z folderów values-sw600dp i values-sw720dp ustaw
wartość zasobu wymiarów text_size odpowiednio na 24 sp i 32 sp. Więcej informacji na ten temat możesz znaleźć w rozdziale 1., „Przegląd aplikacji na tablety z systemem Android”, w podrozdziale „Receptura: konwersja aplikacji z telefonu Android w aplikację na tablet Android”.
68
Rozdział 2. Fragmenty
Receptura: wprowadzenie do fragmentów Fragmenty pozwalają fragmentować lub dzielić aktywności na zhermetyzowane moduły wielokrotnego użytku, z których każdy posiada własny interfejs użytkownika, dzięki czemu możesz dostosować swoją aplikację do różnych rozmiarów ekranu. Oznacza to, że w zależności od dostępnego rozmiaru ekranu możesz dodawać fragmenty do swoich aktywności lub też je z nich usuwać. Szerokość i wysokość ekranu zmieniają się wraz ze zmianą orientacji urządzenia z pionowej na poziomą. W trybie poziomym ekran staje się szerszy i pojawia się wolna przestrzeń po prawej stronie. Przy tym mniejsza jest wysokość, co powoduje ukrycie kontrolek znajdujących się na dole obszaru wyświetlania. Ponadto mamy również do czynienia z różnymi rozmiarami ekranów samych tabletów z systemem Android. Przygotowując aplikację, musisz zaaranżować widoki w taki sposób, aby użytkownik miał pełny podgląd zarówno przy orientacji poziomej, jak i pionowej. Jeśli nie weźmiesz tego pod uwagę, problemy będą narastać, kiedy użytkownik będzie zmieniał orientację urządzenia przy uruchomionej aplikacji. Fragmenty umożliwiają aranżację widoków, które są odpowiednie dla obu orientacji urządzenia. Fragment jest kombinacją aktywności oraz układu i zawiera zestaw widoków, które tworzą niezależny i niepodzielny interfejs użytkownika. Przykładowo jeden fragment lub kilka z nich można osadzić w aktywności w celu wypełnienia wolnej przestrzeni, która pojawia się z prawej strony ekranu przy zmianie orientacji urządzenia z pionowej na poziomą. Analogicznie fragment lub fragmenty można usuwać dynamicznie, jeśli rozmiar ekranu nie pozwala na pomieszczenie wszystkich widoków. Oznacza to, że fragmenty umożliwiają zarządzanie widokami w zależności od urządzenia i konfiguracji, które są docelowe. Załóżmy np., że posiadasz dwa fragmenty, Fragment1 i Fragment2, a każdy z nich dysponuje własnym zestawem widoków. Jeśli urządzenie znajduje się w trybie pionowym, możesz utworzyć dwie aktywności, z których każda posiada jeden fragment, i wyświetlać jedną aktywność w danym momencie. Jeśli urządzenie jest w stanie pomieścić widoki obu fragmentów, mogą one zostać osadzone w jednej aktywności, aby wypełnić cały ekran. Fragment jest jak podaktywność z własnym cyklem życia i hierarchią widoków. Możesz dodawać lub usuwać fragmenty, kiedy aktywność jest uruchomiona. Fragmenty istnieją w kontekście aktywności i nie mogą być wykorzystywane bez niej.
Receptura: cykl życia fragmentu Aby utworzyć fragment, musisz rozszerzyć klasę Fragment i zaimplementować kilka metod wywołań zwrotnych cyklu życia. Na cykl życia fragmentu wpływ ma cykl życia aktywności, w którym fragment jest osadzony. Oznacza to, że kiedy aktywność jest wstrzymywana, wstrzymywane są również wszystkie znajdujące się w niej fragmenty. Kiedy aktywność jest niszczona, niszczone są także wszystkie jej fragmenty. Cykl życia fragmentu obejmuje kilka metod wywołań zwrotnych (patrz rysunek 2.1), takich jak wymienione poniżej.
Receptura: cykl życia fragmentu
Rysunek 2.1. Cykl życia fragmentu
69
70
Rozdział 2. Fragmenty onAttach() — metoda wywoływana, kiedy fragment jest dołączany do danej
aktywności. onCreate() — metoda wywoływana podczas tworzenia fragmentu.
Jest stosowana do inicjowania elementów fragmentu, które chcesz zachować, kiedy fragment jest wznawiany po wstrzymaniu lub zatrzymaniu. onCreateView() — metoda wywoływana do tworzenia widoku dla fragmentu. onActivityCreated() — metoda wywoływana, kiedy zwracana jest metoda
onCreate() danej aktywności. onStart() — metoda wywoływana, kiedy fragment jest widoczny
dla użytkownika. Jest powiązana z metodą aktywności onStart(). onResume() — metoda wywoływana, kiedy fragment jest widoczny
i uruchomiony. Jest powiązana z metodą aktywności onResume(). onPause() — metoda wywoływana, kiedy fragment jest widoczny,
ale nie jest aktywny. Jest dołączana do metody aktywności onPause(). onStop() — metoda wywoływana, kiedy fragment nie jest widoczny.
Jest powiązana z metodą aktywności onStop(). onDestroyView() — metoda wywoływana, kiedy fragment ma być zapisany
lub zniszczony. Hierarchia widoków jest usuwana z fragmentu. onDestroy() — metoda wywoływana, kiedy fragment nie jest już używany.
Żadna hierarchia widoków nie jest powiązana z tym fragmentem, ale fragment jest wciąż dołączony do aktywności. onDetach() — metoda wywoływana, kiedy fragment jest odłączany
od aktywności, a zasoby alokowane do fragmentu są uwalniane.
Receptura: tworzenie fragmentów pierwszego planu oraz różnice pomiędzy fragmentami pierwszego planu a fragmentami w tle Fragmenty pierwszego planu (ang. foreground fragments) to takie, które wchodzą w interakcje z użytkownikiem, pobierają dane, przetwarzają je i wyświetlają żądany wynik. Fragmenty pierwszego planu zawierają zestaw widoków, dzięki którym są widoczne na ekranie. Z kolei fragmenty w tle (ang. background fragments) to takie, które nie współpracują z użytkownikiem, ale wykonują przydzielone zadania w ramach aktywności w tle. Ponieważ te fragmenty nie zawierają żadnych kontrolek interfejsu użytkownika (UI), przy definiowaniu fragmentów w tle nie jest wywoływana metoda onCreateView(). Aby zrozumieć koncepcję fragmentów pierwszego planu, utwórz projekt Android o nazwie ForegroundFragmentApp. W tej aplikacji utworzysz dwa fragmenty: Fragment1 i Fragment2. Fragment o nazwie Fragment1 będzie zawierał widżet wyboru ListView,
Receptura: tworzenie fragmentów pierwszego planu
który wyświetla kilka produktów, z których można wybierać. Fragment2 będzie zawierał kontrolkę TextView, która wyświetli produkt wybrany z widżetu ListView fragmentu Fragment1. Fragmenty wykorzystują indywidualne pliki XML układu do zdefiniowania własnych widoków. Dodaj więc dla nich w folderze res/layout dwa pliki XML o nazwach fragment1.xml i fragment2.xml. Aby zdefiniować kontrolkę ListView w pierwszym fragmencie, wpisz w pliku XML fragment1.xml kod przedstawiony w listingu 2.1. Listing 2.1. Kod wpisany w pliku XML fragment1.xml
Jak widać, widżet wyboru ListView został zdefiniowany z identyfikatorem products_list. By rozróżnić oba fragmenty, tło tego zostało ustawione w kolorze niebieskim. Aby zdefiniować kontrolkę TextView dla drugiego fragmentu, należy w pliku XML fragment2.xml wpisać kod przedstawiony w listingu 2.2. Listing 2.2. Kod wpisany w pliku XML fragment2.xml
Jak widać, kontrolka selectedopt TextView została zdefiniowana i ustawiona w taki sposób, aby wyświetlać tekst Wybierz produkt. Tekst wyświetlany za pomocą kontrolki TextView będzie prezentowany pogrubioną czcionką o rozmiarze zdefiniowanym w zasobie wymiarów text_size.
71
72
Rozdział 2. Fragmenty
Uwaga Zasób wymiarów text_size powinien być już zdefiniowany w pliku dimens.xml w folderach values-sw600dp i values-sw720dp Twojej aplikacji.
Domyślny rozmiar elementów listy wyświetlanych w ListView nadaje się do telefonów, ale jest za mały dla tabletów. Aby zmienić rozmiar elementów listy dla ListView według rozmiaru ekranu określonego urządzenia, dodaj w folderze res/layout jeszcze jeden plik XML o nazwie list_item.xml. Wpisz w tym pliku następujący kod:
Zgodnie z powyższym kodem elementy listy ListView zostaną rozdzielone spacjami o szerokości 6 dp i będą prezentowane pogrubioną czcionką o rozmiarze zdefiniowanym w zasobie wymiarów text_size. Każdy fragment zawiera klasę Java, która ładuje jego interfejs użytkownika z pliku XML, więc dla tych dwóch fragmentów musisz dodać do swojej aplikacji dwie klasy Java. Dodaj więc do paczki com.androidtablet.foregroundfragmentsapp swojej aplikacji pliki Fragment1Activity.java i Fragment2Activity.java. Kod pokazany w listingu 2.3 powinien być wpisany w pliku klasy Java pierwszego fragmentu, czyli w pliku Fragment1Activity.java. Listing 2.3. Kod wpisany w pliku klasy Java Fragment1Activity.java package com.androidtablet.foregroundfragmentapp; import import import import import import import import import import import import
android.app.Fragment; android.os.Bundle; android.view.ViewGroup; android.view.View; android.view.LayoutInflater; android.widget.ListView; android.widget.ArrayAdapter; android.content.Context; android.widget.AdapterView; android.widget.AdapterView.OnItemClickListener; android.widget.TextView; android.app.Activity;
public class Fragment1Activity extends Fragment { OnOptionSelectedListener myListener; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { Context c = getActivity().getApplicationContext();
Receptura: tworzenie fragmentów pierwszego planu View vw = inflater.inflate(R.layout.fragment1, container, false); String[] products={"Aparat", "Laptop", "Zegarek", "Smartfon", "Telewizor"}; ListView productsList = (ListView) vw.findViewById( R.id.products_list); ArrayAdapter arrayAdpt= new ArrayAdapter (c, R.layout.list_item, products); productsList.setAdapter(arrayAdpt); productsList.setOnItemClickListener(new OnItemClickListener(){ @Override public void onItemClick(AdapterView parent, View v, int position, long id){ myListener.onOptionSelected(((TextView) v).getText().toString()); } }); return vw; } public interface OnOptionSelectedListener { public void onOptionSelected(String message); } @Override public void onAttach(Activity activity) { super.onAttach(activity); try { myListener = (OnOptionSelectedListener) activity; } catch (ClassCastException e) { throw new ClassCastException(activity.toString() + " musisz zaimplementować interfejs OnItemClickListener"); } } }
Jak widać, klasa Java dla tego fragmentu rozszerza klasę bazową Fragment. Aby dla tego fragmentu uzyskać dostęp do interfejsu użytkownika i ten interfejs utworzyć, nadpisywana jest metoda onCreateView(). W metodzie onCreateView() wykorzystywany jest obiekt LayoutInflater w celu wypełnienia interfejsu użytkownika — kontrolki ListView zdefiniowanej w pliku fragment1.xml. Z poziomu plików układu uzyskiwany jest dostęp do kontrolek ListView i TextView, które są mapowane odpowiednio na obiekty productsList i selectedOpt. Adapter arrayAdpt zawierający elementy tablicy, czyli obiekt products w formularzu TextView, jest przydzielany do kontrolki ListView w celu wyświetlenia użytkownikowi możliwych wyborów. Interfejs OnItemClickListener jest implementowany za pomocą anonimowej klasy, która implementuje metodę wywołania zwrotnego onItemClick(). Referencja do tej anonimowej klasy jest przekazywana do obiektu productsList należącego do kontrolki ListView w celu wywołania metody wywołania zwrotnego onItemClick(), kiedy użytkownik kliknie którąś z pozycji w ListView. Jak widzisz, zdefiniowany został interfejs OnOptionSelectedListener, który zawiera pojedynczą metodę onOptionSelected(). Oznacza to, że aktywność powiązana z tym fragmentem musi zaimplementować interfejs OnOptionSelectedListener i zdefiniować treść główną metody onOptionSelected(). Kiedy wybrana zostanie któraś pozycja
73
74
Rozdział 2. Fragmenty
z ListView, w aktywności wykonawczej wywoływana jest metoda onOptionSelected(), przekazująca jej jako parametr nazwę wybranej pozycji. Aby z pliku XML fragment2.xml załadować interfejs użytkownika dla drugiego fragmentu, wpisz w jego pliku klasy Java Fragment2Activity.java kod przedstawiony w listingu 2.4. Listing 2.4. Kod wpisany w pliku klasy Java Fragment2Activity.java package com.androidtablet.foregroundfragmentapp; import import import import import import
android.app.Fragment; android.os.Bundle; android.view.ViewGroup; android.view.View; android.view.LayoutInflater; android.widget.TextView;
public class Fragment2Activity extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment2, container, false); } public void dispOption(String msg){ TextView selectedOpt = (TextView) getActivity(). findViewById(R.id.selectedopt); selectedOpt.setText("Wybrałeś "+msg); } }
Podobnie jak w przypadku klasy Java pierwszego fragmentu, ta klasa rozszerza klasę bazową Fragment. Metoda onCreateView() jest nadpisywana, kiedy obiekt LayoutInflater jest wykorzystywany do wypełnienia kontrolki TextView zdefiniowanej w pliku fragment2.xml. Definiowana jest metoda dispOption(), w której uzyskiwany jest dostęp do kontrolki TextView z identyfikatorem selectedopt, ustawionej tak, aby wyświetlić nazwę produktu przekazaną do niej w postaci parametru.
Uwaga Metoda getActivity() wykorzystana w powyższym kodzie zwraca aktywność, z którą powiązany jest bieżący fragment. Metoda ta umożliwia interakcję pomiędzy aktywnością i fragmentem.
Aby pomieścić oba fragmenty w aplikacji, kod przedstawiony w listingu 2.5 został wpisany w pliku układu activity_foreground_fragment_app.xml.
Receptura: tworzenie fragmentów pierwszego planu Listing 2.5. Plik układu activity_foreground_fragment_app.xml po dodaniu obu fragmentów
Łatwo tu zauważyć, że oba fragmenty zostały dodane do aktywności za pomocą elementów . Fragmentom tym przypisane zostały odpowiednio identyfikatory fragment1 i fragment2. Fragmenty skonfigurowano tak, by odwoływały się do swoich właściwych klas Java za pomocą atrybutu android:name. Pierwszy fragment odwołuje się do swojego pliku klasy Java Fragment1Activity, który został umieszczony w paczce com.androidtablet.foregroundfragmentapp. Orientacja kontenera LinearLayout została ustawiona horyzontalnie (horizontal), więc fragmenty będą wyświetlane obok siebie. Plik aktywności Java musi implementować interfejs OnOptionSelectedListener, dlatego zdefiniuj metodę onOptionSelected(). Przypominamy, że zdefiniowałeś interfejs OnOptionSelectedListener w pliku Fragment1Activity.java. Kod wpisany w pliku aktywności Java ForegroundFragmentAppActivity.java został przedstawiony w listingu 2.6. Listing 2.6. Kod wpisany w pliku aktywności Java ForegroundFragmentAppActivity.java package com.androidtablet.foregroundfragmentapp; import android.app.Activity; import android.os.Bundle; import com.androidtablet.foregroundfragmentapp. Fragment1Activity.OnOptionSelectedListener; public class ForegroundFragmentAppActivity extends Activity implements OnOptionSelectedListener { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_foreground_fragment_app); }
75
76
Rozdział 2. Fragmenty public void onOptionSelected(String msg) { Fragment2Activity frag2 = (Fragment2Activity) getFragmentManager().findFragmentById( R.id.fragment2); frag2.dispOption(msg); } }
Metoda onOptionSelected() będzie wywoływana, kiedy wybrany zostanie dowolny element z ListView. Nazwa elementu wybranego z ListView jest przekazywana do tej metody jako parametr. Nazwa ta jest wyświetlana na ekranie poprzez wywołanie metody dispOption() z fragment2. Oznacza to, że za pomocą FragmentManager uzyskiwany jest dostęp do fragment2 i wywoływana jest metoda dispOption() zdefiniowana w jego aktywności Fragment2Activity. Po uruchomieniu danej aplikacji na tabletach dwa interfejsy użytkownika zdefiniowane we fragmentach Fragment1 i Fragment2 będą wyświetlane obok siebie. Kontrolka ListView dla Fragment1 wyświetla listę elementów, a Fragment2 wyświetla kontrolkę TextView i prosi użytkownika o wybranie produktu, tak jak to przedstawiono na rysunku 2.2 (górny obrazek). Po wybraniu produktu z ListView jego nazwa będzie wyświetlana za pomocą TextView, tak jak widać na rysunku 2.2 (dolny obrazek). Kiedy aplikacja zostanie uruchomiona na telefonie Android w orientacji poziomej, zawartości obu fragmentów, Fragment1 i Fragment2, pojawią się obok siebie, tak jak można było się spodziewać. Kontrolka ListView dla Fragment1 będzie wyświetlać listę elementów, a Fragment2 będzie wyświetlał kontrolkę TextView, proszącą użytkownika o wybranie produktu, tak jak pokazano na rysunku 2.3 (lewy obrazek). Nazwa wybranego produktu z ListView jest wyświetlana za pomocą kontrolki TextView, co widać na rysunku 2.3 (prawy obrazek). Ponieważ orientacja pozioma telefonu Android zapewnia wystarczającą szerokość, nie ma problemów z miejscem przy wyświetlaniu zawartości fragmentów obok siebie. Prawdziwy problem z tą aplikacją jest taki, że zawartość obu tych fragmentów będzie pojawiać się obok siebie nawet wtedy, kiedy telefon jest w orientacji pionowej (patrz rysunek 2.4). W orientacji pionowej szerokość ekranu telefonu nie jest wystarczająca do wyświetlenia jednocześnie zawartości obu fragmentów. W rezultacie zawartości te zostaną zwężone. Oczekuje się, że kiedy telefon znajduje się w orientacji pionowej, tylko zawartość jednego fragmentu będzie wyświetlana na ekranie. Oznacza to, że pojawić się powinna tylko kontrolka ListView wyświetlająca listę produktów. Kiedy jakiś produkt zostanie wybrany z ListView, jego nazwa powinna wyświetlić się na kolejnym ekranie. Jak to osiągnąć, dowiesz się w trakcie lektury następnej receptury.
Receptura: tworzenie fragmentów pierwszego planu
Rysunek 2 2. Kontrolki ListView i TextView wyświetlone w tablecie za pomocą dwóch fragmentów (obrazek górny) oraz kontrolka TextView drugiego fragmentu pokazująca element wybrany z ListView pierwszego fragmentu (obrazek dolny)
77
78
Rozdział 2. Fragmenty
Rysunek 2.3. Kontrolki ListView i TextView wyświetlone w telefonie w orientacji poziomej (lewy obrazek) oraz kontrolka TextView drugiego fragmentu pokazująca element wybrany z ListView (prawy obrazek)
Rysunek 2.4. Kontrolki ListView i TextView wyświetlone obok siebie w telefonie w orientacji pionowej (lewy obrazek) oraz kontrolka TextView pokazująca nazwę elementu wybranego z ListView (prawy obrazek)
Receptura: dodawanie i usuwanie fragmentów w przypadku zmiany orientacji urządzenia Główną korzyścią płynącą ze stosowania fragmentów jest swoboda ich dodawania do aktywności, kiedy urządzenie zostanie przełączone w tryb poziomy lub kiedy pojawia się wolna przestrzeń po prawej stronie. Łatwiej też usuwać fragmenty, kiedy urządzenie przechodzi w tryb pionowy. Zmodyfikuj swoją aplikację ForegroundFragmentApp w taki sposób, że kiedy urządzeniem będzie telefon Android w trybie pionowym, tylko jeden
Receptura: dodawanie i usuwanie fragmentów w przypadku zmiany orientacji urządzenia 79
fragment będzie widoczny. Kiedy natomiast urządzeniem będzie dowolny tablet lub telefon w trybie poziomym, oba fragmenty będą widoczne obok siebie, żeby wypełnić pustą przestrzeń po prawej stronie. Innymi słowy, chcesz, aby pojawiła się tylko kontrolka ListView, kiedy telefon będzie w orientacji pionowej. Po wybraniu elementu z ListView kontrolka TextView powinna pojawić się na następnym ekranie w kolejnej aktywności. Ponadto, kiedy telefon jest w trybie poziomym lub kiedy urządzeniem jest dowolny tablet, obie kontrolki, ListView i TextView, powinny pojawić się obok siebie, ponieważ będzie wystarczająco dużo miejsca po prawej stronie. Plik układu activity_foreground_fragment_app.xml jest skonfigurowany w taki sposób, aby wyświetlać dane widoki, kiedy urządzenie jest w trybie pionowym. Ponieważ chcesz, aby w tym trybie dla telefonu widoczny był tylko Fragment1, plik układu activity_foreground_fragment_app.xml został zmodyfikowany w sposób przedstawiony w listingu 2.7. Listing 2.7. Plik układu activity_foreground_fragment_app.xml z pojedynczym fragmentem
Jak widzisz, do tego pliku układu dodany został Fragment1 z identyfikatorem fragment1 . Kiedy zatem telefon będzie w trybie pionowym, zostanie wyświetlona tylko kontrolka ListView z Fragment1. Ponieważ chcesz, aby interfejs użytkownika fragmentów Fragment1 i Fragment2 pojawiał się, kiedy telefon jest w trybie poziomym, w folderze res utwórz folder o nazwie layout-land i skopiuj do niego plik XML activity_foreground_fragment_app.xml z folderu res/layout. Pamiętaj: kiedy telefon przełącza się do trybu poziomego, plik układu z folderu res/layout-land jest wykorzystywany do wyświetlenia widoków na ekranie. Kiedy telefon przełącza się do trybu pionowego, do wyświetlenia widoków na ekranie wykorzystywany jest plik układu z folderu res/layout. Do pliku activity_foreground_fragment_app.xml w folderze res/layout-land dodaj oba fragmenty Fragment1 i Fragment2. Po dodaniu tych fragmentów plik powinien wyglądać w sposób przedstawiony w listingu 2.8.
80
Rozdział 2. Fragmenty Listing 2.8. Plik układu activity_foreground_fragment_app.xml z dwoma fragmentami
Jak widać, fragmenty Fragment1 i Fragment2 zostały dodane do pliku układu. Następnie musisz we fragmencie Fragment1 zmodyfikować klasę Java Fragment1Activity w taki sposób, aby wyglądała tak, jak w listingu 2.9. Dodane zostały jedynie fragmenty kodu zaznaczone pogrubioną czcionką. Reszta pozostaje bez zmian, jak w listingu 2.3. Listing 2.9. Kod wpisany w pliku klasy Java Fragment1Activity.java package com.androidtablet.foregroundfragmentapp; import import import import import import import import import import import import import import
android.app.Fragment; android.os.Bundle; android.view.ViewGroup; android.view.View; android.view.LayoutInflater; android.widget.ListView; android.widget.ArrayAdapter; android.content.Context; android.widget.AdapterView; android.widget.AdapterView.OnItemClickListener; android.widget.TextView; android.app.Activity; android.content.res.Configuration; android.content.Intent;
public class Fragment1Activity extends Fragment { OnOptionSelectedListener myListener; boolean large, xlarge; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
Receptura: dodawanie i usuwanie fragmentów w przypadku zmiany orientacji urządzenia 81 Context c = getActivity().getApplicationContext(); View vw = inflater.inflate(R.layout.fragment1, container, false); String[] products={"Aparat", "Laptop", "Zegarek", "Smartfon", "Telewizor"}; large = ((getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) == Configuration.SCREENLAYOUT_SIZE_LARGE); xlarge =((getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) == Configuration.SCREENLAYOUT_SIZE_XLARGE); ListView productsList = (ListView) vw.findViewById( R.id.products_list); ArrayAdapter arrayAdpt= new ArrayAdapter (c, R.layout.list_item, products); productsList.setAdapter(arrayAdpt); productsList.setOnItemClickListener(new OnItemClickListener(){ @Override public void onItemClick(AdapterView parent, View v, int position, long id){ if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE || large || xlarge){ myListener.onOptionSelected(((TextView) v).getText().toString()); } else { Intent intent = new Intent(getActivity(). getApplicationContext(), DisplayItemActivity.class); intent.putExtra("item", ((TextView) v).getText().toString()); startActivity(intent); } } }); return vw; } public interface OnOptionSelectedListener { public void onOptionSelected(String message); } @Override public void onAttach(Activity activity) { super.onAttach(activity); try { myListener = (OnOptionSelectedListener) activity; } catch (ClassCastException e) { throw new ClassCastException(activity.toString() + " musisz zaimplementować interfejs OnItemClickListener"); } } }
Przyjrzyj się metodzie onItemClick(), która jest wywoływana, kiedy wybierany jest dowolny element w kontrolce ListView wyświetlanej za pomocą Fragment1.
82
Rozdział 2. Fragmenty
W tej metodzie najpierw sprawdzane jest, czy urządzenie posiada duży albo ekstraduży ekran (tablet) lub jest w trybie poziomym. Wiesz już, że Fragment2 będzie dostępny, jeśli urządzenie jest w trybie poziomym lub posiada duży albo ekstraduży ekran. Jeśli więc urządzenie wyposażone jest w duży ekran lub jest w trybie poziomym, w aktywności wykonawczej wywołana zostanie metoda onOptionSelected(), która przekazuje do tej aktywności w postaci parametru nazwę wybranego elementu, aby nazwa ta mogła być wyświetlona na ekranie za pomocą kontrolki TextView fragmentu Fragment2. Jeśli urządzenie posiada normalny ekran (telefon) i jest w trybie pionowym, Fragment2 nie będzie dostępny, więc nazwa elementu wybranego z ListView zostanie wyświetlona w innym ekranie. Wyświetlenie następnego ekranu wymaga kolejnej aktywności. Aby rozpocząć aktywność, musisz najpierw utworzyć nową intencję, określając bieżący kontekst aplikacji oraz nazwę klasy aktywności, którą chcesz uruchomić. Następnie musisz przekazać tę intencję do metody startActivity(). Tworząc nową intencję, określ nazwę klasy nowej aktywności jako DisplayItemActivity. Ponieważ chcesz wyświetlić w nowej aktywności wybrany z ListView element, musisz ten element umieścić w danej intencji pod kluczem item. W aktywności DisplayItemActivity będziesz pobierał wybrany element za pomocą tego klucza. Dodaj do swojego projektu klasę Java o nazwie DisplayItemActivity.java i wpisz jej zawartość w sposób przedstawiony w listingu 2.10. Listing 2.10. Kod wpisany w pliku aktywności dla drugiego fragmentu package com.androidtablet.foregroundfragmentapp; import import import import
android.app.Activity; android.content.res.Configuration; android.os.Bundle; android.widget.TextView;
public class DisplayItemActivity extends Activity{ @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) { finish(); return; } setContentView(R.layout.fragment2); Bundle extras = getIntent().getExtras(); if (extras != null) { String selectedItem = extras.getString("item"); TextView selectedOpt = (TextView) findViewById(R.id.selectedopt); selectedOpt.setText("Wybrałeś "+selectedItem); } } }
Receptura: rola klas FragmentManager i FragmentTransaction
Sprawdzasz tutaj, czy urządzenie znajduje sie w trybie poziomym. Jeśli tak jest, kończysz aktywność (zamykasz ekran). Nie jest to wymagane, ponieważ widoki obu fragmentów mogą pomieścić się na pojedynczym ekranie. Jeśli urządzeniem jest telefon Android w trybie pionowym, wywoływana jest metoda getExtras(), aby sprawdzić, czy cokolwiek zostało do niej przekazane. Jeżeli obiekt Bundle zostanie przekazany do danej intencji, uzyskiwany jest dostęp do jego wartości przechowywanej pod kluczem item. Uzyskiwany jest też dostęp do kontrolki TextView z pliku fragment2.xml, która jest mapowana na obiekt TextView o nazwie selectedOpt, a wartość przekazana do danej intencji (nazwa elementu wybranego z ListView) jest wyświetlana za pomocą kontrolki TextView. Przypomnijmy, że tylko te elementy, które zostały zadeklarowane w pliku manifestu aplikacji AndroidManifest.xml, są widoczne dla systemu Android. Aby zatem nowo zdefiniowana aktywność DisplayItemActivity.java była dla systemu Android widoczna, musi być zadeklarowana w pliku AndroidManifest.xml za pomocą następującej instrukcji:
W tym przypadku chcesz, aby aplikacja zachowywała się w ten sam sposób, kiedy urządzeniem jest telefon Android w orientacji poziomej albo tablet w dowolnej orientacji. Musisz więc do folderów zasobów tabletów skopiować zasoby układów telefonu w orientacji poziomej (czyli zawartość folderu layout-land). Nie masz jeszcze folderów zasobów dla tabletów. Utwórz więc w folderze res dwa foldery o nazwach layout-sw600dp oraz layout-sw720dp i skopiuj do nich plik układu aktywności activity_foreground_fragment_app.xml z folderu res/layout-land. Jeśli urządzeniem jest telefon Android (o normalnym ekranie) w orientacji pionowej, widoczny będzie tylko interfejs użytkownika dla fragmentu Fragment1, tak jak pokazano na rysunku 2.5 (obrazek po lewej). Kiedy wybrany zostaje element ListView, jego nazwa jest wyświetlana za pomocą kontrolki TextView w nowym ekranie lub aktywności, co widać na rysunku 2.5 (obrazek po prawej). Kiedy urządzenie zostaje przełączone do trybu poziomego albo posiada duży lub ekstraduży ekran (tablet), widoczne będą obok siebie interfejsy użytkownika obu fragmentów, czyli ListView i TextView, tak jak zostało to przedstawione na rysunku 2.6 (lewy obrazek). Element wybrany z ListView jest wyświetlany na tym samym ekranie za pomocą kontrolki TextView, jak widać na rysunku 2.6 (prawy obrazek).
Receptura: rola klas FragmentManager i FragmentTransaction w obsłudze fragmentów Jak sugeruje nazwa, klasa FragmentManager (menedżer fragmentów) jest wykorzystywana do zarządzania fragmentami w aktywności. Zapewnia ona metody uzyskiwania dostępu do fragmentów obecnych w aktywności i pozwala wykonywać klasę FragmentTransaction wymaganą w celu dodawania, usuwania i zastępowania fragmentów. Aby uzyskać dostęp do klasy FragmentManager, użyj metody getFragmentManager() w sposób przedstawiony poniżej: FragmentManager fragmentManager = getFragmentManager();
83
84
Rozdział 2. Fragmenty
Rysunek 2.5. W trybie pionowym wyświetlany jest tylko interfejs użytkownika pierwszego fragmentu, czyli ListView (obrazek po lewej), a element wybrany z ListView jest wyświetlany za pomocą kolejnej aktywności (obrazek po prawej)
Rysunek 2.6. Telefon w orientacji poziomej lub tablet w dowolnej orientacji wyświetlą obok siebie interfejsy użytkownika obu fragmentów, czyli kontrolki ListView i TextView (lewy obrazek), a element wybrany z ListView będzie wyświetlany na tym samym ekranie za pomocą kontrolki TextView drugiego fragmentu (prawy obrazek)
Receptura: rola klas FragmentManager i FragmentTransaction w obsłudze fragmentów
Podczas przeprowadzania transakcji fragmentu instancja klasy FragmentTransaction wykorzystywana jest w następujący sposób: FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
Nowa transakcja FragmentTransaction jest tworzona przy użyciu metody beginTransaction() klasy FragmentManager. W poniższym kodzie pokazano, jak dodać fragment. private static final String TAG1 = "1"; FragmentManager fragmentManager = getFragmentManager() FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); Fragment1Activity fragment = new Fragment1Activity(); fragmentTransaction.add(R.id.fragment_container, fragment, TAG1); fragmentTransaction.commit()
Tutaj Fragment1Activity jest klasą Java danego fragmentu, która jest również wykorzystywana do załadowania interfejsu użytkownika tego fragmentu z jego pliku XML. Możesz założyć, że fragment_container to identyfikator kontenera, który istnieje w pliku układu, gdzie chcesz umieścić swój fragment. Zazwyczaj jako fragment_container wykorzystywane są układy LinearLayout lub FrameLayout. Znacznik TAG1 odwołuje się do unikatowego identyfikatora w celu zidentyfikowania fragmentu i uzyskania do niego dostępu. Do zatwierdzenia zmian wykorzystywana jest metoda commit(). Zatwierdzenie zmian nie odbywa się natychmiastowo i może mieć miejsce później, kiedy gotowy będzie wątek. Ponieważ w metodzie commit() może mieć miejsce opóźnienie, nie próbuj uzyskać referencji do fragmentu, który właśnie utworzyłeś.
Uwaga Aby dodawać fragmenty dynamicznie, kontener widoku musi istnieć w układzie, w którym wyświetlane będą widoki fragmentu.
Przed dodaniem fragmentu warto sprawdzić, czy taki fragment już nie istnieje. Można to zrobić, modyfikując kod w następujący sposób. private static final String TAG1 = "1"; FragmentManager fragmentManager = getFragmentManager(); FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); if(null==fragmentManager.findFragmentByTag(TAG1)){ Fragment1Activity fragment = new Fragment1Activity(); fragmentTransaction.add(R.id.fragment_container, fragment, TAG1); } fragmentTransaction.commit();
Jak widzisz, za pomocą metody findFragmentByTag() klasy FragmentManager odbywa się sprawdzenie, czy jakikolwiek fragment istnieje w danym znaczniku. Kolejną metodą, która może być wykorzystana do identyfikacji fragmentu, jest findFragmentById(). Identyfikuje ona fragment, który został dodany do układu aktywności. W innych
85
86
Rozdział 2. Fragmenty
przypadkach preferowana jest metoda findFragmentByTag(). W powyższym kodzie Fragment1Activity jest klasą Java przeznaczoną do ładowania widoków zdefiniowanych w pliku układu danego fragmentu. Aby fragment lub zawartość wyświetlaną w kontenerze fragment_container zastąpić widokiem (lub widokami) z innego fragmentu, użyj metody replace() klasy FragmentTransaction w następujący sposób: private static final String TAG2 = "2"; fragmentTransact.replace(R.id.fragment_container, fragment2, TAG2);
W tej instrukcji widok (lub widoki) fragmentu Fragment2 zostaje zastąpiony zawartością wyświetlaną w kontenerze fragment_container układu aktywności. Aby usunąć fragment, identyfikujesz go za pomocą metod findFragmentById() lub findFragmentByTag(), a następnie używasz metody remove() klasy FragmentTransaction. W zamieszczonym poniżej kodzie fragment jest identyfikowany za pomocą metody findFragmentById(), a następnie usuwany. FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); Fragment fragment = fragmentManager.findFragmentById( R.id.fragment); fragmentTransaction.remove(fragment); fragmentTransaction.commit();
Tutaj zakładamy, że fragment o identyfikatorze fragment istnieje w danej aktywności. Aby zidentyfikować ten fragment za pomocą metody findFragmentByTag(), poprzednio zastosowaną instrukcję możesz zastąpić następującą: Fragment fragment = fragmentManager.findFragmentByTag(TAG1);
Receptura: tworzenie fragmentów dynamicznie w trakcie wykonywania aplikacji Fragmenty można definiować w sposób statyczny przy wykorzystaniu elementów w pliku układu aplikacji. Można też definiować je za pomocą kodu w trakcie wykonywania aplikacji. Do tworzenia, dodawania i zastępowania fragmentów dynamicznie wykorzystywana jest klasa FragmentManager. Koncepcję definiowania fragmentów dynamicznie łatwiej można zrozumieć na działającym przykładzie. Utwórz projekt Android o nazwie FragmentsByCodeApp. W tej aplikacji zbudujesz dynamicznie dwa fragmenty: Fragment1 i Fragment2. Fragment o nazwie Fragment1 będzie zawierał widżet wyboru ListView wyświetlający kilka produktów, spośród których należy wybrać. Fragment2 będzie zawierał kontrolkę TextView wyświetlającą produkt wybrany z ListView fragmentu Fragment1. Aby pomieścić oba fragmenty w danej aplikacji, musisz zdefiniować ich odpowiednie kontenery. Zdefiniuj dwa układy FrameLayout jako kontenery obu fragmentów w pliku układu activity_fragments_by_code_app.xml w sposób przedstawiony w listingu 2.11.
Receptura: tworzenie fragmentów dynamicznie w trakcie wykonywania aplikacji Listing 2.11. Kod wpisany w pliku układu activity_fragments_by_code_app.xml
Łatwo zauważyć, że do aktywności zostały dodane dwa kontenery LinearLayout z identyfikatorami odpowiednio frag1_container i frag2_container . Za pomocą kodu Java trzeba zdefiniować oba te fragmenty dynamicznie i określić, że będą przechowywane we wskazanych kontenerach. Orientacja najbardziej zewnętrznego kontenera LinearLayout została zdefiniowana horyzontalnie, więc fragmenty pojawią się obok siebie. Pierwszy z dwóch fragmentów, które musisz zdefiniować, będzie wyświetlał widżet ListView, aby pokazać kilka produktów. Drugi fragment będzie wyświetlał kontrolkę TextView, która ma prezentować produkt wybrany z ListView. Aby zdefiniować widoki dla tych dwóch fragmentów, musisz do folderu res/layout projektu dodać dwa pliki XML o nazwach fragment1.xml i fragment2.xml. By zdefiniować kontrolkę ListView dla pierwszego fragmentu, dodaj do pliku XML fragment1.xml kod przedstawiony w listingu 2.12. Listing 2.12. Kod wpisany w pliku XML fragment1.xml
Jak widzisz, widżet wyboru ListView został zdefiniowany z identyfikatorem products_list. W celu rozróżnienia obu fragmentów tło tego fragmentu zostało ustawione w kolorze niebieskim. Aby zdefiniować kontrolkę TextView dla drugiego fragmentu, w pliku XML fragment2.xml wpisz kod przedstawiony w listingu 2.13.
87
88
Rozdział 2. Fragmenty Listing 2.13. Kod wpisany w pliku XML fragment2.xml
Jak widzisz, kontrolka selectedopt TextView została skonfigurowana w taki sposób, aby wyświetlać komunikat Wybierz produkt. Tekst ten będzie prezentowany pogrubioną czcionką o rozmiarze zdefiniowanym w zasobie wymiarów text_size. Aby zmienić rozmiar elementów listy kontrolki ListView według rozmiarów ekranu urządzenia, dodaj do folderu res/layout plik XML o nazwie list_item.xml.
Powyższy kod spowoduje rozdzielenie elementów listy kontrolki ListView spacjami o szerokości 6 dp i wyświetlenie ich pogrubioną czcionką o rozmiarze zdefiniowanym w zasobie wymiarów text_size. Aby załadować interfejs użytkownika z odpowiednich plików fragment1.xml i fragment2.xml, musisz dodać klasę Java dla każdego z dwóch fragmentów. Dwie klasy Java o nazwach Fragment1Activity.java i Fragment2Activity.java zostały dodane do paczki aplikacji com.androidtablet.fragmentsbycodeapp. Kod przedstawiony w listingu 2.14 został wpisany do pliku klasy Java pierwszego fragmentu Fragment1Activity.java. Listing 2.14. Kod wpisany w pliku klasy Java Fragment1Activity.java package com.androidtablet.fragmentsbycodeapp; import import import import import import import import import import
android.app.Fragment; android.os.Bundle; android.view.ViewGroup; android.view.View; android.view.LayoutInflater; android.widget.ListView; android.widget.ArrayAdapter; android.content.Context; android.widget.AdapterView; android.widget.AdapterView.OnItemClickListener;
Receptura: tworzenie fragmentów dynamicznie w trakcie wykonywania aplikacji import android.widget.TextView; import android.app.Activity; public class Fragment1Activity extends Fragment { OnOptionSelectedListener myListener; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { Context c = getActivity().getApplicationContext(); View vw = inflater.inflate(R.layout.fragment1, container, false); String[] products={"Aparat", "Laptop", "Zegarek", "Smartfon", "Telewizor"}; ListView productsList = (ListView) vw. findViewById(R.id.products_list); ArrayAdapter arrayAdpt= new ArrayAdapter (c, R.layout.list_item, products); productsList.setAdapter(arrayAdpt); productsList.setOnItemClickListener(new OnItemClickListener(){ @Override public void onItemClick(AdapterView parent, View v, int position, long id){ myListener.onOptionSelected(((TextView) v).getText().toString()); } }); return vw; } public interface OnOptionSelectedListener { public void onOptionSelected(String message); } @Override public void onAttach(Activity activity) { super.onAttach(activity); try { myListener = (OnOptionSelectedListener) activity; } catch (ClassCastException e) { throw new ClassCastException(activity.toString() + " musisz zaimplementować interfejs OnItemClickListener"); } } }
Jak możesz zauważyć, klasa Java Fragment1Activity rozszerza klasę bazową Fragment. W metodzie onCreateView() obiekt LayoutInflater jest wykorzystywany do wypełnienia kontrolki ListView zdefiniowanej w pliku fragment1.xml. Dostęp do kontrolki ListView jest uzyskiwany z poziomu pliku układu, a sama kontrolka jest mapowana na obiekt productsList. Adapter arrayAdpt zawierający elementy tablicy products jest przydzielany do kontrolki ListView w celu wyświetlenia na ekranie różnych produktów. Interfejs OnItemClickListener jest implementowany za pomocą anonimowej klasy, która implementuje metodę wywołania zwrotnego onItemClick(). Referencja do tej anonimowej klasy jest
89
90
Rozdział 2. Fragmenty
przekazywana do obiektu productsList należącego do kontrolki ListView w celu wywołania metody wywołania zwrotnego onItemClick(), kiedy użytkownik kliknie którąś z pozycji w ListView. Zdefiniowany został interfejs OnOptionSelectedListener, który zawiera pojedynczą metodę onOptionSelected(). Aktywność powiązana z tym fragmentem zaimplementuje interfejs OnOptionSelectedListener i zdefiniuje treść główną metody onOptionSelected(). W metodzie onItemClick() uzyskiwany jest dostęp do metody onOptionSelected() i przekazywana jest do niej nazwa elementu wybranego z ListView. Następnie musisz w pliku klasy Java Fragment2Activity.java napisać kod, który wykona następujące zadania. Załaduje interfejs użytkownika drugiego fragmentu z pliku XML
fragment2.xml. Zdefiniuje metodę dispOption(), która będzie wyświetlać nazwę produktu
wybranego przez użytkownika. Aby te zadania zostały wykonane, należy w pliku klasy Java Fragment2Activity.java wpisać kod przedstawiony w listingu 2.15. Listing 2.15. Kod wpisany w pliku klasy Java Fragment2.Activity.java package com.androidtablet.fragmentsbycodeapp; import import import import import import
android.app.Fragment; android.os.Bundle; android.view.ViewGroup; android.view.View; android.view.LayoutInflater; android.widget.TextView;
public class Fragment2Activity extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment2, container, false); } public void dispOption(String msg){ TextView selectedOpt = (TextView) getActivity(). findViewById(R.id.selectedopt); selectedOpt.setText("Wybrałeś "+msg); } }
Ponownie, aby utworzyć fragment, klasa Java Fragment2Activity rozszerza klasę bazową Fragment. Nadpisywana jest metoda onCreateView(), za pomocą której obiekt LayoutInflater jest wykorzystywany do wypełnienia kontrolki TextView zdefiniowanej w pliku fragment2.xml. Nazwa produktu wybrana z ListView jest przekazywana do metody dispOption(), która wyświetla ją za pomocą kontrolki TextView.
Receptura: tworzenie fragmentów dynamicznie w trakcie wykonywania aplikacji
Aby utworzyć dwa fragmenty i dodać je do właściwych kontenerów frag1_container i frag2_container zdefiniowanych w pliku układu aktywności, należy w głównym pliku aktywności Java FragmentsByCodeAppActivity.java wpisać kod przedstawiony w listingu 2.16. Listing 2.16. Kod wpisany w pliku aktywności Java FragmentsByCodeAppActivity.java package com.androidtablet.fragmentsbycodeapp; import import import import import
android.os.Bundle; android.app.Activity; android.app.FragmentTransaction; android.app.FragmentManager; com.androidtablet.fragmentsbycodeapp.Fragment1Activity.OnOptionSelectedListener;
public class FragmentsByCodeAppActivity extends Activity implements OnOptionSelectedListener { private static final String TAG1 = "1"; private static final String TAG2 = "2"; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_fragments_by_code_app); FragmentManager fragmentManager = getFragmentManager(); FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); if(null==fragmentManager.findFragmentByTag(TAG1)){ Fragment1Activity fragment1 = new Fragment1Activity(); fragmentTransaction.add(R.id.frag1_container, fragment1, TAG1); } if(null==fragmentManager.findFragmentByTag(TAG2)){ Fragment2Activity fragment2 = new Fragment2Activity(); fragmentTransaction.add(R.id.frag2_container, fragment2, TAG2); } fragmentTransaction.commit(); } public void onOptionSelected(String msg){ Fragment2Activity frag2 = (Fragment2Activity) getFragmentManager().findFragmentById(R.id.frag2_container); frag2.dispOption(msg); } }
Jak możesz zauważyć w powyższym kodzie, obiekt FragmentManager jest definiowany, aby uzyskać dostęp do fragmentów. Obiekt klasy FragmentTransaction o nazwie fragmentTransaction jest definiowany w celu przeprowadzenia związanej z fragmentami transakcji, takiej jak dodanie, usunięcie lub zastąpienie fragmentu. Tworzona jest nowa transakcja z wykorzystaniem metody beginTransaction() klasy FragmentManager.
91
92
Rozdział 2. Fragmenty
Do identyfikatora kontenera pierwszego fragmentu frag1_container, który istnieje w pliku układu, dodajesz swój pierwszy fragment w celu jego wyświetlenia. Przed dodaniem tego fragmentu musisz sprawdzić, czy on już nie istnieje. Wykorzystując metodę findFragmentByTag() klasy FragmentManager, sprawdzasz, czy istnieje jakikolwiek fragment ze znacznikiem TAG1. Jeśli taki fragment nie istnieje, definiujesz obiekt klasy Fragment1Activity o nazwie fragment1 i dodajesz go do kontenera frag1_container. Obiekt Fragment1Activity to powiązana z obiektem fragment1 klasa Java, której zadaniem jest ładowanie widoków zdefiniowanych w pliku układu fragment1.xml danego fragmentu. Podobnie dodajesz drugi fragment fragment2 do jego kontenera frag2_container zdefiniowanego w pliku układu. Na koniec proces transakcji (dodanie fragmentów do ich właściwych kontenerów) jest zatwierdzany poprzez wywołanie metody commit() klasy FragmentTransaction. Metoda onOptionSelected() będzie wywołana, kiedy wybrany zostanie któryś z elementów z ListView. Nazwa wybranego elementu zostanie przekazana do tej metody jako parametr. Nazwa ta jest wyświetlana na ekranie poprzez wywołanie metody dispOption() fragmentu fragment2. Po uruchomieniu aplikacji pierwszy fragment wyświetla kontrolkę ListView pokazującą kilka produktów. Ponadto drugi fragment wyświetla kontrolkę TextView zainicjowaną w celu wyświetlenia wiadomości tekstowej dyrektywy Wybierz produkt (patrz rysunek 2.7, górny obrazek). Po wybraniu produktu z listy ListView jego nazwa zostanie wyświetlona za pomocą zdefiniowanej w drugim fragmencie kontrolki TextView, tak jak pokazano na rysunku 2.7 (dolny obrazek).
Receptura: implementowanie komunikacji pomiędzy fragmentami Klasa Fragment oferuje dwie metody służące przekazywaniu danych pomiędzy fragmentami; są to setArguments() i getArguments(). Metoda setArguments() przechowuje we fragmencie obiekt Bundle, podczas gdy metoda getArguments() pobiera obiekt Bundle, aby wydobyć przekazane informacje. Zamieszczony poniżej kod powoduje przekazywanie informacji z fragmentu fragment1 do fragmentu fragment2. Zakładamy, że frag2_container jest identyfikatorem istniejącego w pliku układu kontenera fragmentu, w którym chcesz wyświetlać fragment2. Fragment2Activity fragment2 = new Fragment2Activity(); Bundle args = new Bundle(); String message=" Wiadomość z fragmentu 1"; if(null==fragmentManager.findFragmentByTag(TAG2)){ args.putString("msg", message); fragment2.setArguments(args); fragmentTransaction.replace(R.id.frag2_container, fragment2); String tag = null; fragmentTransaction.addToBackStack(tag); fragmentTransaction.commit(); }
#1 #2 #3 #4 #5 #6 #7 #8 #9
Receptura: implementowanie komunikacji pomiędzy fragmentami
Rysunek 2.7. Kontrolki ListView i TextView wyświetlone za pomocą dwóch fragmentów (górny obrazek) oraz kontrolka TextView drugiego fragmentu pokazująca element wybrany z kontrolki ListView pierwszego fragmentu (dolny obrazek)
Instrukcja #1, Fragment2Activity, reprezentuje klasę Java fragmentu fragment2. Budowana jest instancja klasy Java o nazwie fragment2. Instrukcja #2 tworzy obiekt Bundle o nazwie args. W instrukcji #3 definiowany jest ciąg znaków message, który chcesz przekazać do fragmentu fragment2. Instrukcja #4 sprawdza, czy fragment fragment2 istnieje już w układzie. W instrukcji #5 zmienna message jest zapisywana w obiekcie Bundle o nazwie args pod kluczem msg. Obiekt Bundle o nazwie args jest umieszczany do przechowania we fragmencie fragment2 w instrukcji #6. W instrukcji #7 fragment2 zastępuje widok View w kontenerze fragmentu pliku układu. Zadaniem instrukcji #8 i #9 jest nawigowanie do poprzedniego fragmentu.
93
94
Rozdział 2. Fragmenty
Stos aktywności śledzi poprzednie aktywności. Kiedy wciskasz przycisk Back (wstecz), pojawiają się aktywności ze stosu aktywności, przez co ich widoki są wyświetlane. Innymi słowy, stos aktywności pozwala nawigować z powrotem do poprzednich ekranów za pomocą przycisku Back. Ta sama idea ma zastosowanie do fragmentów. Aby dodać FragmentTransaction do stosu back stack, musisz wywołać metodę addToBackStack() klasy FragmentTransaction przed wywołaniem metody commit(). W przedstawionym wcześniej fragmencie kodu fragment2 zastępuje fragment, który był wyświetlany w kontenerze fragmentu z pliku układu. Poprzedni fragment zostanie dodany do stosu back stack, dzięki czemu jego widoki będą wyświetlane. Wciśnięcie przycisku Back odwróci poprzednią transakcję, klasa FragmentTransaction zwróci widok poprzedniego fragmentu. Być może zastanawiasz się, w jaki sposób zawartość przekazywana za pomocą Bundle jest pobierana we fragmencie, który ją otrzymuje. Odpowiedzią na to pytanie jest zastosowanie metody getArguments() w celu uzyskania dostępu do zawartości przekazanej do fragmentu za pomocą obiektu Bundle, który został zapisany przy wykorzystaniu metody setArguments(). Za pomocą zamieszczonego poniżej kodu uzyskuje się dostęp do obiektu Bundle przekazanego do danego fragmentu. Uzyskuje się również dostęp do zawartości przekazanej pod kluczem msg i przydziela ją do ciągu messageReceived. String messageReceived =""; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { Bundle bundle=getArguments(); if(bundle !=null) { messageReceived = bundle.getString("msg"); } }
Aby zrozumieć komunikację pomiędzy fragmentami, spróbuj wysłać wiadomość tekstową o treści Wiadomość z fragmentu 1 z fragmentu fragment1 do fragmentu fragment2 w aplikacji FragmentsByCodeApp, którą właśnie utworzyłeś w trakcie omawiania poprzedniego problemu. Kod w głównym pliku aktywności Java FragmentsByCodeAppActivity.java zmień, aby wyglądał tak, jak zostało to przedstawione w listingu 2.17. Zmodyfikowane zostały tylko fragmenty kodu zaznaczone pogrubioną czcionką. Reszta tak jak w listingu 2.16. Listing 2.17. Kod wpisany w pliku aktywności Java FragmentsByCodeAppActivity.java package com.androidtablet.fragmentsbycodeapp; import android.os.Bundle; import android.app.Activity; import android.app.FragmentTransaction; import android.app.FragmentManager; import com.androidtablet.fragmentsbycodeapp. Fragment1Activity.OnOptionSelectedListener;
Receptura: implementowanie komunikacji pomiędzy fragmentami public class FragmentsByCodeAppActivity extends Activity implements OnOptionSelectedListener { private static final String TAG1 = "1"; private static final String TAG2 = "2"; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_fragments_by_code_app); FragmentManager fragmentManager = getFragmentManager(); FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); if(null==fragmentManager.findFragmentByTag(TAG1)){ Fragment1Activity fragment1 = new Fragment1Activity(); fragmentTransaction.add(R.id.frag1_container, fragment1, TAG1); } Bundle args = new Bundle(); String message="Wiadomość z fragmentu 1"; if(null==fragmentManager.findFragmentByTag(TAG2)){ Fragment2Activity fragment2 = new Fragment2Activity(); args.putString("msg", message); fragment2.setArguments(args); fragmentTransaction.add(R.id.frag2_container, fragment2, TAG2); } fragmentTransaction.commit(); } public void onOptionSelected(String msg){ Fragment2Activity frag2 = (Fragment2Activity) getFragmentManager().findFragmentById( R.id.frag2_container); frag2.dispOption(msg); } }
Jak możesz zauważyć utworzona została instancja args obiektu Bundle. Przy wykorzystaniu klucza msg dodawana jest do niej wiadomość o treści Wiadomość z fragmentu 1, którą chcesz wysłać do fragmentu fragment2. Za pomocą tego klucza wiadomość ta zostanie pobrana w otrzymującym ją fragmencie (czyli we fragmencie fragment2). Instancja args Bundle jest dołączana do fragmentu fragment2 za pomocą metody setArguments(). Po jej dołączeniu fragment2 jest dodawany do swojego kontenera frag2_container, zdefiniowanego w pliku układu aktywności. Aby uzyskać dostęp do obiektu Bundle wysłanego przez pierwszy fragment, uzyskać dostęp do danych z tego obiektu oraz wyświetlić je za pomocą drugiego fragmentu, powinieneś zmodyfikować plik klasy Java Fragment2Activity.java drugiego fragmentu, aby wyglądał tak jak przedstawiono w listingu 2.18. Zmodyfikowane zostały tylko fragmenty kodu zaznaczone pogrubioną czcionką. Reszta pozostaje tak samo jak w listingu 2.15.
95
96
Rozdział 2. Fragmenty Listing 2.18. Kod wpisany w pliku klasy Java Fragment2Activity.java package com.androidtablet.fragmentsbycodeapp; import import import import import import
android.app.Fragment; android.os.Bundle; android.view.ViewGroup; android.view.View; android.view.LayoutInflater; android.widget.TextView;
public class Fragment2Activity extends Fragment { TextView selectedOpt; String messageReceived=""; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View vw= inflater.inflate(R.layout.fragment2, container, false); Bundle bundle=getArguments(); if(bundle !=null) { messageReceived = bundle.getString("msg"); selectedOpt = (TextView) vw.findViewById( R.id.selectedopt); selectedOpt.setText(messageReceived); } return vw; } public void dispOption(String msg){ selectedOpt = (TextView) getActivity(). findViewById(R.id.selectedopt); selectedOpt.setText("Wybrałeś "+msg); } }
Jak możesz zauważyć, w powyższym kodzie obiekt Bundle wysłany przez fragment1 jest pobierany w danym fragmencie za pomocą metody getArguments(). Wysłana wiadomość tekstowa jest wydobywana z tego obiektu za pomocą klucza msg. Pobrana z obiektu Bundle wiadomość tekstowa jest wyświetlana w drugim fragmencie za pomocą kontrolki TextView. Po uruchomieniu aplikacji wyświetlane są oba fragmenty, czyli fragment1 wyświetlający listę produktów oraz fragment2 pokazujący wiadomość tekstową Wiadomość z fragmentu 1, wysłaną z fragmentu fragment1 (patrz rysunek 2.8, górny obrazek). Po wybraniu produktu jego nazwa zostanie wyświetlona za pomocą kontrolki TextView we fragmencie fragment2 (patrz rysunek 2.8, dolny obrazek).
Receptura: implementowanie komunikacji pomiędzy fragmentami
Rysunek 2.8. Wysłana przez pierwszy fragment wiadomość tekstowa wyświetlona za pomocą kontrolki TextView w drugim fragmencie (górny obrazek) oraz kontrolka TextView drugiego fragmentu pokazująca element wybrany z kontrolki ListView należącej do pierwszego fragmentu (dolny obrazek)
Fragment, poza klasą bazową Fragment, może jeszcze rozszerzać kilka innych podklas klasy Fragment, takich jak DialogFragment, ListFragment oraz PreferenceFragment.
97
98
Rozdział 2. Fragmenty
Receptura: wyświetlanie opcji za pomocą klasy ListFragment ListFragment to fragment zawierający wbudowaną kontrolkę ListView, która może być skonfigurowana w celu wyświetlania elementów z określonego źródła danych. Źródłem danych mogą być tablica lub kursor. Aby zrozumieć, jak działają fragmenty typu ListFragment, utwórz aplikację składającą się z kontrolek ListView i TextView. Kontrolka ListView będzie wyświetlać pewne elementy do wyboru. Element wybrany z ListView będzie wyświetlany za pomocą kontrolki TextView. W tej aplikacji kontrolka ListView będzie wyświetlana przez fragment ListFragment, a kontrolka TextView będzie wyświetlana przez prosty fragment. Element wybrany z ListView we fragmencie ListFragment będzie wyświetlany przy użyciu kontrolki TextView w prostym fragmencie. Ten nowy projekt Android nazwij ListFragmentApp. Najpierw musisz utworzyć fragment przechowujący kontrolkę TextView. W tym celu do folderu res/layout swojego projektu dodaj plik XML o nazwie fragment2.xml. W listingu 2.19 pokazano, w jaki sposób należy zdefiniować kontrolkę TextView w pliku fragment2.xml. Listing 2.19. Kod wpisany w pliku XML fragment2.xml
Jak możesz zauważyć, kontrolka TextView o identyfikatorze selectedopt została zdefiniowana w kontenerze LinearLayout. Początkowy tekst przydzielony do tej kontrolki to Wybierz produkt. Tekst ten wyświetlany za pomocą kontrolki TextView będzie prezentowany pogrubioną czcionką o rozmiarze zdefiniowanym w zasobie wymiarów text_size. Do tej kontrolki TextView za pomocą kodu Java będzie przypisywany tekst, który ma wskazać element wybrany z ListView. Aby rozmiar elementów kontrolki ListView dostosować do rozmiarów ekranu urządzenia, dodaj do folderu res/layout plik XML o nazwie list_item.xml. W tym pliku wpisz przedstawiony poniżej kod.
Receptura: wyświetlanie opcji za pomocą klasy ListFragment
Zgodnie z powyższym kodem elementy listy kontrolki ListView zostaną rozdzielone spacjami o szerokości 6 dp oraz będą prezentowane pogrubioną czcionką o rozmiarze zdefiniowanym w zasobie wymiarów text_size. Aby załadować interfejs użytkownika danego fragmentu z pliku fragment2.xml, musisz utworzyć odpowiedni plik klasy Java. Dodaj więc do paczki com.androidtablet.listfragmentapp plik klasy Java o nazwie Fragment2Activity.java. Wpisz w tym pliku kod przedstawiony w listingu 2.20. Listing 2.20. Kod wpisany w pliku klasy Java Fragment2Activity drugiego fragmentu package com.androidtablet.listfragmentapp; import import import import import import
android.app.Fragment; android.os.Bundle; android.view.ViewGroup; android.view.View; android.view.LayoutInflater; android.widget.TextView;
public class Fragment2Activity extends Fragment { public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment2, container, false); } public void dispOption(String msg){ TextView selectedOpt = (TextView) getActivity(). findViewById(R.id.selectedopt); selectedOpt.setText("Wybrałeś "+msg); } }
Jak możesz zauważyć, dany plik klasy Java rozszerza klasę bazową Fragment. Metoda onCreateView() jest nadpisywana, kiedy obiekt LayoutInflater jest wykorzystywany do wypełnienia interfejsu użytkownika kontrolki TextView, który został zdefiniowany w pliku fragment2.xml. Fragmentu ListFragment będziesz używał do wyświetlenia kontrolki ListView. Jak wspomniano wcześniej, ListFragment zawiera już kontrolkę ListView, więc nie musisz definiować interfejsu użytkownika dla tego fragmentu. Możesz bezpośrednio dodać plik klasy Java, który rozszerza klasę ListFragment. W tym pliku klasy Java musisz wpisać kod w celu zdefiniowania elementów, które mają być wyświetlane za pomocą kontrolki ListView fragmentu ListFragment oraz w celu wyświetlania elementu wybranego z ListView przy użyciu kontrolki TextView fragmentu Fragment2. Dodaj więc do swojego projektu plik klasy Java o nazwie Fragment1Activity.java i wpisz w nim kod przedstawiony w listingu 2.21.
99
100
Rozdział 2. Fragmenty Listing 2.21. Kod wpisany w pliku klasy Java Fragment1Activity.java dla pierwszego fragmentu package com.androidtablet.listfragmentapp; import import import import import import
android.app.ListFragment; android.os.Bundle; android.widget.ArrayAdapter; android.view.View; android.widget.ListView; android.widget.TextView;
public class Fragment1Activity extends ListFragment { String[] products={"Aparat", "Laptop", "Zegarek", "Smartfon", "Telewizor"}; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ArrayAdapter arrayAdpt = new ArrayAdapter (getActivity(),R.layout.list_item, products); setListAdapter(arrayAdpt); } @Override public void onListItemClick(ListView l, View v, int position, long id) { Fragment2Activity frag = (Fragment2Activity) getFragmentManager().findFragmentById( R.id.fragment2); frag.dispOption(((TextView) v).getText().toString()); } }
Jak można było oczekiwać, dana klasa Java rozszerza klasę bazową ListFragment, aby utworzyć Fragment z kontrolką ListView. By wyświetlić zawartość za pomocą kontrolki ListView fragmentu ListFragment, definiowana jest tablica o nazwie products, do której przydzielane są nazwy produktów. W metodzie onCreate() definiowany jest obiekt klasy ArrayAdapter o nazwie arrayAdpt w celu wyświetlenia elementów tablicy products w trybie simple_list_item_1. Za pomocą metody setListAdapter() zawartość obiektu arrayAdpt klasy ArrayAdapter jest przydzielana do kontrolki ListView w celu wyświetlenia. Jak można oczekiwać, metoda onListItemClick() będzie wywoływana, kiedy wybrany zostanie któryś z produktów wyświetlanych za pomocą kontrolki ListView. W tej metodzie wyświetlana jest nazwa produktu wybranego z wykorzystaniem kontrolki TextView zdefiniowanej w pliku fragment2.xml. Aby w danej aplikacji pomieścić oba fragmenty, w pliku activity_list_fragment_app.xml należy wpisać kod przedstawiony w listingu 2.22. Listing 2.22. Plik układu activity_list_fragment_app.xml po dodaniu obu fragmentów
Receptura: wyświetlanie opcji za pomocą klasy ListFragment android:orientation="horizontal" >
Możesz zauważyć, że fragmenty fragment1 i fragment2 zostały dodane do danej aktywności za pomocą elementów . Fragmenty zostały ustawione tak, aby odwoływać się do swoich właściwych klas Java za pomocą atrybutu android:name. W pliku aktywności Java ListFragmentAppActivity.java danej aplikacji nie musisz wpisywać żadnego kodu. W tym pliku aktywności pozostawiasz bez zmian domyślny kod, co zostało przedstawione w listingu 2.23. Listing 2.23. Domyślny kod w pliku aktywności Java ListFragmentAppActivity.java package com.androidtablet.listfragmentapp; import android.app.Activity; import android.os.Bundle; public class ListFragmentAppActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_list_fragment_app); } }
Po uruchomieniu tej aplikacji zobaczysz oba fragmenty wyświetlone obok siebie, tak jak pokazano na rysunku 2.9 (górny obrazek). Kontrolka ListView po lewej stronie wyświetlana jest przez fragment ListFragment. Zawartość kontrolki ListView wyświetlana jest za pomocą pliku klasy Java Fragment1Activity.java fragmentu ListFragment. Element wybrany z ListView jest wyświetlany za pomocą kontrolki TextView zdefiniowanej we fragmencie Fragment2, co zostało pokazane na rysunku 2.9 (dolny obrazek).
101
102
Rozdział 2. Fragmenty
Rysunek 2.9. Kontrolka ListView wyświetlona przez fragment ListFragment (górny obrazek) oraz element wybrany z ListView fragmentu ListFragment wyświetlony za pomocą kontrolki TextView drugiego fragmentu (dolny obrazek)
Receptura: wyświetlanie okien dialogowych za pomocą klasy DialogFragment W systemie Android dialogi są asynchroniczne. Synchroniczne dialogi to takie, w których aktywność zawiesza swoje wykonywanie do czasu, kiedy dialog zostanie zwolniony. Gdy użytkownik jest w interakcji z oknem dialogowym, nie odbywa się żadne dalsze wykonywanie. Asynchroniczne dialogi to takie, w których aktywność kontynuuje swoje normalne wykonywanie, podczas gdy użytkownik jest w interakcji z oknem dialogowym. Aktywność uzyskuje dostęp do interakcji użytkownika z oknem dialogowym
Receptura: wyświetlanie okien dialogowych za pomocą klasy DialogFragment
po zaimplementowaniu metody wywołania zwrotnego. Dialogi w systemie Android są z natury modalne. Kiedy okno dialogowe jest otwarte, użytkownicy nie mogą uzyskać dostępu do żadnej innej części danej aplikacji. Korzyścią płynącą z wywoływania dialogów asynchronicznie jest nie tylko zwiększenie wydajności kodu, ale również możliwość zwalniania dialogu za pomocą kodu. Możesz wyświetlić okno dialogowe DialogFragment, rozszerzając klasę bazową DialogFragment, która z kolei wywodzi się z klasy Fragment. Aby sprawdzić, jak działa DialogFragment, utwórz projekt Android o nazwie DialogFragmentApp. W tym projekcie wykorzystasz dwa fragmenty. Jeden będzie pokazywał dialog DialogFragment, a drugi będzie wyświetlał kontrolkę TextView. Interakcja użytkownika z oknem dialogowym DialogFragment jest przekazywana za pomocą kontrolki TextView w drugim fragmencie. Wybrany przycisk w oknie DialogFragment jest wyświetlany przez kontrolkę TextView w drugim fragmencie. Przed utworzeniem okna dialogowego DialogFragment zdefiniuj interfejs użytkownika prostego fragmentu, składający się z kontrolki TextView. W tym celu do folderu res/layout dodaj plik XML o nazwie fragment2.xml. W tym pliku wpisz kod przedstawiony w listingu 2.24. Listing 2.24. Kod wpisany w pliku XML fragment2.xml
Jak możesz zauważyć, kontrolka TextView została zdefiniowana wewnątrz kontenera LinearLayout. Ma ona przypisany identyfikator selectedopt i jest inicjowana w celu wyświetlenia tekstu Wybierz przycisk Otwórz okno dialogowe. Tekst ten będzie prezentowany pogrubioną czcionką o rozmiarze zdefiniowanym przez zasób wymiarów text_size. Ta kontrolka TextView będzie wyświetlała opcję wybraną przez użytkownika w oknie dialogowym DialogFragment. Aby załadować interfejs użytkownika danego fragmentu z pliku fragment2.xml, do projektu został dodany plik klasy Java o nazwie Fragment2Activity.java. Wpisz w tym pliku kod przedstawiony w listingu 2.25.
103
104
Rozdział 2. Fragmenty Listing 2.25. Kod wpisany w pliku klasy Java Fragment2Activity.java drugiego fragmentu package com.androidtablet.dialogfragmentapp; import import import import import
android.app.Fragment; android.os.Bundle; android.view.ViewGroup; android.view.View; android.view.LayoutInflater;
public class Fragment2Activity extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment2, container, false); } }
Ta klasa Java rozszerza klasę bazową Fragment. Metoda onCreateView() jest nadpisywana, kiedy obiekt LayoutInflater jest wykorzystywany do wypełnienia interfejsu użytkownika kontrolki TextView, zdefiniowanego w pliku fragment2.xml. Aby pomieścić fragment zdefiniowany w pliku fragment2.xml, musisz w pliku układu activity_dialog_fragment_app.xml wpisać kod przedstawiony w listingu 2.26. Listing 2.26. Plik układu activity_dialog_fragment_app.xml po dodaniu fragmentu oraz przycisku
Została zdefiniowana kontrolka Button, ponieważ chcesz, aby okno dialogowe DialogFragment pojawiało się tylko wtedy, kiedy w aplikacji zostanie wybrany odpowiedni przycisk. Obie kontrolki, Fragment i Button, są zagnieżdżone w kontenerze LinearLayout. Kontrolka Fragment ma przydzielony identyfikator fragment2 i jest ustawiona w taki sposób, aby odwoływać się do swojej klasy Java za pomocą atrybutu android:name. Kontrolka Button ma przypisany identyfikator dialog_button oraz nagłówek Otwórz okno dialogowe. Tekst nagłówka kontrolki Button będzie wyświetlany czcionką
Receptura: wyświetlanie okien dialogowych za pomocą klasy DialogFragment
o rozmiarze zdefiniowanym w zasobie wymiarów text_size. Fragment o nazwie fragment2 będzie wyświetlać kontrolkę TextView, która ma pokazać opcję wybraną przez użytkownika w oknie dialogowym DialogFragment. Teraz nadszedł czas, aby przygotować kod wyświetlający okno dialogowe DialogFragment. Jak wspomniano wcześniej, aby pokazać okno dialogowe DialogFragment, klasa Java musi rozszerzyć klasę DialogFragment. W tym celu do paczki com.androidtablet.dialogfragmentapp dodaj plik klasy Java o nazwie Fragment1Activity.java. Aby wyświetlić okno dialogowe DialogFragment w pliku Fragment1Activity.java, wpisz kod przedstawiony w listingu 2.27. Listing 2.27. Kod wpisany w pliku klasy Java Fragment1Activity.java pierwszego fragmentu package com.androidtablet.dialogfragmentapp; import import import import import
android.app.DialogFragment; android.os.Bundle; android.app.Dialog; android.app.AlertDialog; android.content.DialogInterface;
public class Fragment1Activity extends DialogFragment{ static Fragment1Activity newInstance(String title) { Fragment1Activity fragment = new Fragment1Activity(); Bundle args = new Bundle(); args.putString("title", title); fragment.setArguments(args); return fragment; } @Override public Dialog onCreateDialog(Bundle savedInstanceState) { String title = getArguments().getString("title"); Dialog diag = new AlertDialog.Builder(getActivity()) .setIcon(R.drawable.ic_launcher) .setTitle(title) .setPositiveButton("OK", new DialogInterface. OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { ((DialogFragmentAppActivity) getActivity()). PositiveButton(); } }) .setNegativeButton("Anuluj", new DialogInterface. OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { ((DialogFragmentAppActivity) getActivity()). NegativeButton(); } }).create(); return diag; } }
105
106
Rozdział 2. Fragmenty
Aby utworzyć okno dialogowe DialogFragment, dana klasa Java rozszerza klasę DialogFragment. Metoda newInstance() tworzy nową instancję fragmentu. Tytuł okna
dialogowego DialogFragment jest przekazywany do tej metody jako argument, który z kolei jest przechowywany w obiekcie Bundle i powiązany z fragmentem zwracanym przez tę metodę. Aby utworzyć hierarchę widoków okna dialogowego DialogFragment, nadpisywana jest metoda onCreateDialog() klasy DialogFragment, a obiekt Bundle przenoszący tytuł fragmentu oraz inne informacje (jeśli takie istnieją) jest do niej przekazywany. W metodzie onCreateDialog() do utworzenia obiektu okna dialogowego wykorzystywany jest konstruktor okna dialogowego ostrzeżenia (AlertDialog). W metodzie onCreateDialog() tworzony jest AlertDialog z dwoma przyciskami OK i Anuluj, a tytuł, który ma być wyświetlony w danym fragmencie, jest pozyskiwany z argumentu title zapisanego w obiekcie Bundle. Metoda onClickListener() jest powiązywana z przyciskami OK i Anuluj, co skutkuje wywołaniem odpowiedniej metody onClick(), kiedy konkretny przycisk zostaje kliknięty. Kiedy wybrany zostanie przycisk OK, z aktywności wywołana zostanie metoda PositiveButton(). Analogicznie, kiedy wybrany zostanie przycisk Anuluj, wywołana zostanie z aktywności metoda NegativeButton(). Metoda ta zwraca utworzony AlertDialog. W pliku aktywności Java musisz wpisać kod wywołujący okno dialogowe DialogFragment. Kod ten jest wymagany w celu podjęcia niezbędnych działań, kiedy z okna dialogowego DialogFragment wybrany zostanie przycisk OK lub Anuluj. Kod wpisany do pliku aktywności Java DialogFragmentAppActivity.java został przedstawiony w listingu 2.28. Listing 2.28. Kod wpisany w pliku aktywności Java DialogFragmentAppActivity.java package com.androidtablet.dialogfragmentapp; import import import import import
android.app.Activity; android.os.Bundle; android.widget.Button; android.view.View; android.widget.TextView;
public class DialogFragmentAppActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_dialog_fragment_app); Button dialogButton = (Button)findViewById( R.id.dialog_button); dialogButton.setOnClickListener(new Button. OnClickListener(){ @Override public void onClick(View arg0) { Fragment1Activity dialogFragment = Fragment1Activity.newInstance( "Czy chcesz kontynuować przetwarzanie?"); dialogFragment.show(getFragmentManager(), "Przykład zastosowania klasy DialogFragment"); }
Receptura: wyświetlanie okien dialogowych za pomocą klasy DialogFragment }); } public void PositiveButton() { TextView selectedOpt = (TextView)findViewById( R.id.selectedopt); selectedOpt.setText("Wybrałeś przycisk OK"); } public void NegativeButton() { TextView selectedOpt = (TextView) findViewById( R.id.selectedopt); selectedOpt.setText("Wybrałeś przycisk Anuluj"); } }
Okno dialogowe DialogFragment powinno pojawiać się, kiedy w aplikacjo zostanie wybrany odpowiedni przycisk. Jak możesz zauważyć, kontrolka dialogButton Button jest przechwytywana z pliku układu i mapowana na obiekt Button o nazwie dialogButton. Obiekt OnClickListener został powiązany z kontrolką Button, a metoda wywołania zwrotnego onClick() jest wywoływana, kiedy w aplikacji zostanie wybrana kontrolka Button. W metodzie onClick() okno dialogowe DialogFragment jest formowane poprzez utworzenie instancji dialogFragment pliku aktywności Java Fragment1Activity. Tytuł tego okna dialogowego DialogFragment jest do niej przekazywany w postaci tekstu Czy chcesz kontynuować przetwarzanie?
Okno dialogowe DialogFragment staje się widoczne po wywołaniu jego metody show(). Metoda show() dodaje ten fragment do danego obiektu FragmentManager. W tym kodzie zdefiniowane są również dwie metody, PositiveButton() i NegativeButton(),
które będą wywołane, kiedy w oknie dialogowym DialogFragment wybrane zostaną przyciski OK i Anuluj. W obu tych metodach uzyskiwany jest dostęp do kontrolki selectedOpt TextView zdefiniowanej w pliku fragment2.xml, a sama kontrolka jest mapowana na obiekt TextView o nazwie selectedOpt. Kiedy wybrany zostaje przycisk OK, wiadomość Wybrałeś przycisk OK wyświetlana jest w kontrolce TextView za pomocą instancji selectedOpt. Analogicznie, kiedy z okna dialogowego DialogFragment wybrany zostanie przycisk Anuluj, w kontrolce TextView drugiego fragmentu pojawi się komunikat Wybrałeś przycisk Anuluj. Kiedy uruchomisz aplikację, wyświetlone zostaną kontrolki TextView i Button (patrz rysunek 2.10, pierwszy obrazek). Kontrolka TextView wyświetlana jest za pomocą pliku fragment2.xml. Pokazuje ona początkowy tekst Wybierz przycisk Otwórz okno dialogowe, wskazując użytkownikowi, że powinien kliknąć przycisk Otwórz okno dialogowe. Po kliknięciu tego przycisku otwarte zostaje okno dialogowe fragmentu o tytule Czy chcesz kontynuować przetwarzanie?. Wyświetla ono dwa przyciski, OK i Anuluj, tak jak zostało to pokazane na rysunku 2.10 (drugi obrazek). Po kliknięciu przycisku OK w oknie dialogowym DialogFragment pokazywana jest za pomocą kontrolki TextView wiadomość Wybrałeś przycisk OK, co widać na rysunku 2.10 (trzeci obrazek). Jeśli ponownie klikniesz przycisk Otwórz okno dialogowe, DialogFragment zostanie otwarty raz jeszcze.
107
108
Rozdział 2. Fragmenty
Rysunek 2.10. Kontrolki TextView i Button są wyświetlane po uruchomieniu aplikacji (pierwszy obrazek). Okno dialogowe DialogFragment pojawia się po kliknięciu odpowiedniego przycisku (drugi obrazek). Trzeci obrazek przedstawia kontrolkę TextView pokazującą, że kliknięty został przycisk OK okna dialogowego DialogFragment. Na czwartym obrazku widać kontrolkę TextView pokazującą, że kliknięty został przycisk Anuluj okna dialogowego DialogFragment
Receptura: konfigurowanie preferencji użytkownika za PreferenceFragment
Tym razem, jeśli w oknie dialogowym wybierzesz przycisk Anuluj, kontrolka TextView wyświetli wiadomość Wybrałeś przycisk Anuluj, tak jak pokazano na rysunku 2.10 (czwarty obrazek). Fragment okna dialogowego jest wyświetlany modalnie. Z tego względu należy go używać w aplikacjach, w których chcesz ostrzec użytkownika, wyświetlając istotny komunikat, lub w których chcesz, aby użytkownik dostarczył pewne informacje, zanim uruchomione zostanie dalsze przetwarzanie.
Receptura: konfigurowanie preferencji użytkownika za pomocą klasy PreferenceFragment Fragment PreferenceFragment umożliwia użytkownikom konfigurowanie i personalizowanie aplikacji. PreferenceFragment może zawierać kilka widoków preferencji (ang. preference views), które pomagają w sposób jednolity ustawić preferencje aplikacji przy minimalnym wysiłku. Lista widoków preferencji, które mogą być wyświetlane za pomocą fragmentu PreferenceFragment, została przedstawiona w tabeli 2.1. Widoki preferencji są modyfikowane przez klasę SharedPreferences. Klasa ta umożliwia zapisywanie i pobieranie trwałych par klucz-wartość, które reprezentują preferencje użytkownika. Tabela 2.1. Widoki preferencji, które mogą być wyświetlane za pomocą fragmentu PreferenceFragment
Widok preferencji
Opis
PreferenceScreen
Element główny pliku XML wykorzystywanego do zdefiniowania ekranu preferencji.
CheckBoxPreference
Wyświetla proste pole wyboru, które przy zaznaczeniu zwraca wartość true (prawda). W innym przypadku zwraca false (fałsz).
ListPreference
Wyświetla listę przycisków opcji umożliwiających użytkownikowi dokonanie wyboru.
EditTextPreference
Wyświetla okno dialogowe z kontrolką EditText, które umożliwia użytkownikowi wprowadzanie tekstu.
RingtonePreference
Wyświetla przyciski opcji wskazujących dostępne do wyboru dźwięki dzwonka.
PreferenceCategory
Stosowany przy grupowaniu w kategorie powiązanych preferencji.
Preference
Niestandardowa preferencja, która działa jak kontrolka Button.
Aby zrozumieć, w jaki sposób konfigurowane są preferencje aplikacji, utwórz nowy projekt Android o nazwie PrefFragmentApp. Istnieją dwa sposoby wyświetlania widoków preferencji we fragmencie PreferenceFragment: za pomocą pliku XML lub przy użyciu
109
110
Rozdział 2. Fragmenty
kodu. Preferowane jest podejście XML, więc najpierw dodasz do folderu res folder o nazwie xml. W folderze res/xml dodasz plik XML o nazwie preferences.xml. Plik ten będzie zawierał widoki preferencji, które chcesz wyświetlić użytkownikowi w celu skonfigurowania aplikacji. Opcje wybrane przez użytkownika w widokach preferencji będą trwałe w aplikacji. Kod wpisany w pliku preferences.xml został przedstawiony w listingu 2.29. Listing 2.29. Kod wpisany w pliku XML preferences.xml
Jak widać, widoki preferencji zostały przedstawione w dwóch kategoriach, Kategoria 1 i Kategoria 2. Kategoria o nazwie Kategoria 1 zawiera dwa widoki preferencji, CheckBoxPreference i EditTextPreference. Kategoria nazwana Kategoria 2 zawiera RingtonePreference i ListPreference. Każdy widok preferencji musi posiadać wartość android:key, która identyfikuje wartość widoku i pozwala uzyskać do niej dostęp. Atrybut android:title jest wykorzystywany do przydzielenia widokowi preferencji początkowego tekstu, a atrybut android:defaultValue służy do przydzielania domyślnej wartości do widoku preferencji. Widok preferencji CheckBoxPreference wyświetla pole wyboru jako element interfejsu użytkownika i przechowuje jego wartość jako zmienną logiczną — true (prawda) lub false (fałsz). Wartość true jest zapisywana, kiedy pole wyboru CheckBoxPreference jest zaznaczone, a wartość false, kiedy pole wyboru pozostaje
Receptura: konfigurowanie preferencji użytkownika za PreferenceFragment
niezaznaczone. Domyślna wartość false jest przypisywana do CheckBoxPreference za pomocą atrybutu android:defaultValue. Do widoku preferencji EditTextPreference przypisany został klucz Namekey, a tytuł Wprowadź swoje imię: będzie się wyświetlał w postaci tekstu widoku preferencji. Po wybraniu preferencji EditTextPreference wyświetlone zostanie okno dialogowe zatytułowane Wprowadź swoje imię, czyli prośba do użytkownika o wpisanie odpowiedniej informacji. Kiedy użytkownik kliknie OK, wprowadzona informacja jest zapisywana w magazynie preferencji. Widok preferencji RingtonePreference będzie otwierał okno dialogowe pokazujące listę dźwięków dzwonka i pozwalające użytkownikowi na wybranie domyślnego dźwięku dzwonka lub trybu cichego. Klucz przypisany do RingtonePreference to Audio, a tytuł okna dialogowego to Wybierz dźwięk. Atrybut android:ringtoneType pozwala określić listę dźwięków dzwonka, które mają być wyświetlane. Poprawne wartości dla atrybutu android:ringtoneType to ringtone, notification, alarm oraz all. Widok preferencji ListPreference pokazuje okno dialogowe z zestawem preferencji w formie przycisków opcji, które umożliwiają użytkownikowi wykonanie odpowiedniego wyboru. Okno dialogowe będzie zatytułowane Lista produktów; przydzielony do niego zostanie klucz products_list. Atrybut android:entries przydziela do ListPreference tablicę ciągów znaków products, aby wyświetlić listę preferencji. Oznacza to, że elementy w tablicy products będą wyświetlać tekst dla przycisków opcji pokazywanych za pomocą widoku preferencji ListPreference. Atrybut android:entryValues definiuje kolejną tablicę o nazwie prodselected, która przechowuje wartości elementów zdefiniowanych w tablicy products. Atrybut android:entryValues reprezentuje tablicę, która przechowuje wartości odpowiadające przyciskom opcji wybranych przez użytkownika. Elementy wyświetlają w PreferenceFragment przycisk Wyślij, który użytkownik klika po wybraniu żądanej preferencji z preferencji widoku, w celu zapisania preferencji lub wykonania innej czynności. Przycisk Wyślij ma przydzielony klucz submitPref, który będzie go identyfikował w kodzie Java. Następnym krokiem jest zdefiniowanie w pliku zasobów strings.xml dwóch tablic, jednej dla wyświetlania tekstu dla przycisków opcji w ListPreference, a drugiej dla przechowywania wartości odpowiednich elementów pierwszej tablicy. Wygląd pliku strings.xml po zdefiniowaniu tych dwóch tablic przedstawiono w listingu 2.30. Listing 2.30. Plik zasobu ciągów strings.xml po zdefiniowaniu dwóch tablic Aplikacja z użyciem klasy PreferenceFragment Ustawienia - Aparat
- Laptop
- Zegarek
- Smartfon
- Telewizor
111
112
Rozdział 2. Fragmenty - Wybrałeś Aparat
- Wybrałeś Laptop
- Wybrałeś Zegarek
- Wybrałeś Smartfon
- Wybrałeś Telewizor
Elementy tablicy products są wykorzystywane do wyświetlenia tekstu dla przycisków opcji pokazywanych w widoku preferencji ListPreference, a elementy w tablicy prodselected pokazują wartości, które będą zwracane, jeśli wybrany zostanie odpowiedni element z tablicy products. Aby załadować widoki preferencji zdefiniowane w pliku preferences.xml, do projektu dodany został plik klasy Java o nazwie PrefFragActivity.java. Wpisz w tym pliku kod przedstawiony w listingu 2.31. Listing 2.31. Kod wpisany w pliku klasy Java PrefFragActivity.java package com.androidtablet.preffragmentapp; import import import import import
android.os.Bundle; android.app.Activity; android.preference.Preference; android.preference.Preference.OnPreferenceClickListener; android.preference.PreferenceFragment;
public class PrefFragActivity extends Activity { public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); getFragmentManager().beginTransaction().replace( android.R.id.content, new PrefsFragment()).commit(); } public static class PrefsFragment extends PreferenceFragment { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); addPreferencesFromResource(R.xml.preferences); Preference submitPref = (Preference) findPreference( "submitPref"); submitPref.setOnPreferenceClickListener(new OnPreferenceClickListener() { public boolean onPreferenceClick(Preference preference) { getActivity().finish(); return true; } }); } } }
Receptura: konfigurowanie preferencji użytkownika za PreferenceFragment
Aby utworzyć PreferenceFragment, zdefiniowana została klasa Java PrefsFragment, która rozszerza klasę PreferenceFragment. Metoda addPreferencesFromResource() jest wywoływana w celu załadowania we fragmencie PreferenceFragment widoków preferencji z pliku XML preferences.xml. Za pomocą elementu uzyskiwany jest dostęp do przycisku Wyślij zdefiniowanego w pliku preferences.xml, a sam przycisk jest mapowany na obiekt Preference o nazwie submitPref. Procedura obsługi zdarzeń OnPreferenceClickListener jest dodawana do obiektu submitPref. Implementowana jest jego metoda wywołania zwrotnego onPreferenceClick(), która jest wykonywana, kiedy kliknięty zostanie obiekt submitPref. Na koniec w metodzie onPreferenceClick() zamykasz obiekt PreferenceFragment i powracasz do pliku PreferenceFragActivity.java, aby wykonać odpowiednie działania na wybranych preferencjach. Za pomocą pliku aktywności PrefFragmentAppActivity.java będziesz wyświetlał preferencje wybrane przez użytkownika, korzystając z kontrolki TextView. Aby wyświetlić opcje wybrane z widoków preferencji pokazanych we fragmencie PreferenceFragment, musisz zdefiniować w pliku układu activity_pref_fragment.xml cztery kontrolki TextView. Po zdefiniowaniu tych kontrolek plik activity_pref_fragment_app.xml będzie wyglądał tak, jak przedstawiono w listingu 2.32. Listing 2.32. Plik układu activity_pref_fragment_app.xml po dodaniu czterech kontrolek TextView
113
114
Rozdział 2. Fragmenty
Jak możesz zauważyć, czterem kontrolkom TextView przypisane zostały identyfikatory newsletter, name, ringtone i product. Kontrolki te są ułożone pionowo wewnątrz kontenera LinearLayout. Kontrolka newsletter TextView będzie wykorzystywana do wskazania, czy użytkownik zaznaczył pole wyboru w widoku preferencji CheckBoxPreference. Kontrolka name TextView posłuży do wyświetlenia nazwy wprowadzonej przez użytkownika w widoku preferencji EditTextPreference. Kontrolka ringtone TextView będzie wykorzystywana do wyświetlenia rodzaju dźwięku dzwonka wybranego przez użytkownika w widoku preferencji RingtonePreference. Kontrolka product TextView posłuży do wyświetlenia produktu wybranego przez użytkownika w widoku preferencji ListPreference. Tekst wyświetlany przy użyciu kontrolki TextView będzie prezentowany pogrubioną czcionką o rozmiarze zdefiniowanym z zasobie wymiarów text_size. Aby wyświetlić fragment PreferenceFragment i pokazać preferencje wybrane przez użytkownika, musisz do głównego pliku aktywności PrefFragmentAppActivity.java wpisać kod przedstawiony w listingu 2.33. Listing 2.33. Kod wpisany w głównym pliku aktywności PrefFragmentAppActivity.java package com.androidtablet.preffragmentapp; import import import import import import
android.app.Activity; android.os.Bundle; android.content.Intent; android.preference.PreferenceManager; android.content.SharedPreferences; android.widget.TextView;
public class PrefFragmentAppActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_pref_fragment_app); startActivity(new Intent(this, PrefFragActivity.class)); } @Override public void onResume() { super.onResume(); SharedPreferences prefs=PreferenceManager. getDefaultSharedPreferences(this); TextView newsletter=(TextView)findViewById( R.id.newsletter); TextView name=(TextView)findViewById(R.id.name); TextView ringtone=(TextView)findViewById(R.id.ringtone); TextView product=(TextView)findViewById(R.id.product); if(Boolean.valueOf(prefs.getBoolean("Newskey", false))) newsletter.setText("Wybrałeś Biuletyn"); else newsletter.setText(""); ringtone.setText("Wybrałeś dzwonek telefonu "+prefs. getString("Audio", "Silent")); name.setText("Wprowadziłeś imię "+prefs.getString(
Receptura: konfigurowanie preferencji użytkownika za PreferenceFragment "Namekey","")); String selectedProduct = prefs.getString( "products_list", "Aparat"); product.setText(selectedProduct); } }
W celu wyświetlenia fragmentu PreferenceFragment inicjowana jest jego klasa aktywności PrefFragActivity.class. Aby wyświetlić preferencje wybrane przez użytkownika w PreferenceFragment, trzeba uzyskać dostęp do kontrolek TextView zdefiniowanych w pliku układu activity_pref_fragment_app.xml, a same kontrolki należy zmapować na obiekty TextView. Kontrolki newsletter TextView, name TextView, ringtone TextView oraz product TextView są mapowane odpowiednio na obiekty TextView o nazwach newsletter, name, ringtone oraz product. Aby znaleźć opcje wybrane przez użytkownika w widokach preferencji, tworzony jest obiekt SharedPreferences o nazwie prefs. Pamiętaj, że aby pobrać wartości z obiektu SharedPreferences za pomocą metod getInt(), getString() lub getBoolean(), musisz dostarczyć klucz, którego wartość jest potrzebna, oraz opcjonalnie domyślną wartość zwracaną, jeśli klucz nie jest obecny. Aby odczytać wartość widoku preferencji CheckBoxPreference, musisz uzyskać dostęp do udostępnianych preferencji i wywołać metodę getBoolean(), przekazując jej klucz tego widoku preferencji. Kiedy klucz Newskey widoku preferencji CheckBoxPreference zostanie przekazany do metody getBoolean() instancji SharedPreference, zwróci wartość true lub false, wskazując, czy zostało zaznaczone pole wyboru w CheckBoxPreference. Następnie uzyskiwany jest dostęp do widoku preferencji EditTextPreference poprzez przekazanie jej klucza Namekey do metody getString() instancji SharedPreference. Pusty ciąg znaków jest uznawany za domyślny, jeśli użytkownik nie wprowadzi żadnej nazwy. Podobnie, dostęp do widoków preferencji RingtonePreference oraz ListPreference uzyskiwany jest poprzez przekazanie ich kluczy, Audio i products_list, do metody getString() instancji SharedPreference. Preferencje wybrane przez użytkownika w widokach preferencji są wyświetlane przy użyciu kontrolki TextView. Możesz zauważyć, że jeśli użytkownik nie wybierze niczego, domyślny dźwięk dzwonka jest ustawiany jako Silent, a domyślnym produktem jest Aparat. Aby nowo dodana aktywność PrefFragActivity.java była widoczna dla systemu Android, jest deklarowana w pliku AndroidManifest.xml poprzez dodanie następującej instrukcji:
Po uruchomieniu tej aplikacji zobaczysz widoki preferencji zdefiniowane w kategoriach Kategoria 1 i Kategoria 2. Pole wyboru widoku preferencji CheckBoxPreference
domyślnie pozostaje niezaznaczone. Kiedy to pole wyboru będzie wybrane, zostanie zaznaczone, tak jak pokazano na rysunku 2.11 (pierwszy obrazek). Kiedy wybrany zostanie widok preferencji EditTextPreference z tekstem Wprowadź swoje imię:, wyskoczy okno dialogowe zatytułowane Wprowadź swoje imię. Możesz wpisać imię
115
116
Rozdział 2. Fragmenty
Rysunek 2.11. Fragment PreferenceFragment pokazujący różne widoki preferencji (pierwszy obrazek). Widok preferencji EditTextPreference, w którym użytkownik zostaje poproszony o wpisanie imienia (drugi obrazek). Widok preferencji RingtonePreference, w którym użytkownik zostaje poproszony o wybranie dźwięku dzwonka (trzeci obrazek). Widok preferencji ListPreference pokazujący dostępne do wyboru produkty w formie przycisków opcji (czwarty obrazek)
Podsumowanie
lub odwołać operację, wciskając Anuluj. Wpisz imię Kelly, tak jak pokazano na rysunku 2.11 (drugi obrazek), a następnie kliknij OK, aby powrócić do fragmentu PreferenceFragment. Po kliknięciu przycisku Wybierz dźwięk reprezentującego widok preferencji RingtonePreference otwarte zostaje okno dialogowe z prośbą do użytkownika o wybranie dźwięku dzwonka, tak jak pokazano na rysunku 2.11 (trzeci obrazek). Wybierz domyślny dzwonek, klikając Dzwonek domyślny, a następnie kliknij OK, aby powrócić do PreferenceFragment. Po wybraniu widoku preferencji ListPreference reprezentowanej przez przycisk Lista produktów wyświetlone zostanie okno dialogowe zatytułowane Wybierz produkt i wyświetlające kilka produktów w formie przycisków opcji, co widać na rysunku 2.11 (czwarty obrazek). Wybierz Zegarek. Po wybraniu produktu automatycznie powracasz do fragmentu PreferenceFragment. Na koniec klikasz widoczny na dole fragmentu PreferenceFragment przycisk Wyślij, aby zamknąć ten fragment i wyświetlić wybrane preferencje. Wszystkie wybrane preferencje są wyświetlane przez kontrolki TextView, co pokazano na rysunku 2.12.
Rysunek 2.12. Wszystkie wybrane preferencje wyświetlone przez kontrolki TextView
Podsumowanie W tym rozdziale poznałeś zastosowanie fragmentów i ich praktyczną implementację w przygotowywaniu aplikacji dostosowanej do różnych rozmiarów ekranu. Nauczyłeś się, czym jest cykl życia fragmentu i w jaki sposób fragmenty są dynamicznie dodawane do aplikacji Android w trakcie jej działania. Poznałeś sposoby przekazywania danych
117
118
Rozdział 2. Fragmenty
z jednego fragmentu do drugiego, procedury użycia wyspecjalizowanych fragmentów (czyli wyświetlania opcji przy użyciu ListFragment), wyświetlania okien dialogowych za pomocą DialogFragment oraz konfigurowania preferencji użytkownika z wykorzystaniem PreferenceFragment. W następnym rozdziale dowiesz się, jak używać paska akcji (ang. ActionBar) i odkryjesz jego przewagę nad menu. Zrozumiesz, w jaki sposób włącza się pasek akcji w aplikacji Android i poznasz jego różne komponenty. Nauczysz się wyświetlać elementy akcji w pasku akcji, przechodzić do ekranu głównego aplikacji po wybraniu ikony aplikacji oraz wyświetlać widoki akcji w pasku akcji. Dowiesz się również, jak wyświetlać podmenu w pasku akcji oraz tworzyć pasek akcji z zakładkami i pasek akcji z rozwijaną listą wyboru.
3 Paski akcji w działaniu N
ajczęściej używane akcje kluczowe dla aplikacji są wyświetlane za pomocą paska akcji (ang. ActionBar). Pasek akcji wyświetla logo aplikacji, elementy akcji oraz widoki akcji. Może mieć formę zakładek lub rozwijanej listy wyboru. W tym rozdziale nauczysz się używać paska akcji w aplikacji Android i zobaczysz, czym różni się to od korzystania z menu. Dowiesz się, jak włączyć pasek akcji i przełączyć jego widoczność oraz poznasz różne komponenty paska akcji. Nauczysz się także wyświetlać element akcji w pasku akcji i zobaczysz, w jaki sposób ikona aplikacji jest wykorzystywana do nawigowania do strony głównej aplikacji. Ponadto nauczysz się wyświetlać w pasku akcji widoki akcji i podmenu. Na koniec poznasz sposób tworzenia pasków akcji w formie zakładek oraz rozwijanej listy wyboru.
Receptura: różnice pomiędzy menu i paskiem akcji Pasek akcji jest funkcją okienkową zastępującą pasek tytułu w górnej części aktywności i wyświetlającą elementy nawigacyjne oraz istotne funkcjonalności aplikacji. Zapewnia aplikacji spójny interfejs użytkownika i pomaga wyświetlać typowe akcje kluczowe, które mają być widoczne na ekranie w trakcie działania aplikacji. Menu również pomaga wywoływać różne funkcje aplikacji, ale ma pewną wadę: jest wyświetlane lub wywoływane po wciśnięciu przycisku Menu na urządzeniu lub AVD. Wiele urządzeń Android nie posiada już dedykowanego przycisku Menu, więc paski akcji są preferowanym zamiennikiem. Elementy akcji, które pojawiają się w pasku akcji, są dostępne natychmiast, bez konieczności wciskania przycisku Menu. Domyślnie po lewej stronie paska akcji znajduje się logo aplikacji, a po prawej stronie tytuł aktywności i opcjonalnie elementy akcji. Elementy akcji są ekwiwalentem pozycji, które możesz znaleźć w menu. Logo aplikacji może być powiązane ze stroną główną aplikacji — oznacza to, że w dowolnym miejscu aplikacji możesz kliknąć logo i zostaniesz przekierowany do strony głównej.
120
Rozdział 3. Paski akcji w działaniu
Pasek akcji jest włączony, jeśli w aplikacji użyto domyślnego motywu Theme.Holo oraz wyposażono ją w docelowe (lub minimalne) SDK w wersji 11. lub wyższej. Przykład:
Uwaga Abyś mógł użyć paska akcji, atrybut minSdkVersion aplikacji musi wynosić co najmniej 11. W innym przypadku w trakcie kompilacji zgłoszony zostanie błąd.
Receptura: przełączanie widoczności paska akcji Aby przełączyć widoczność paska akcji w trakcie działania aplikacji, możesz skorzystać z jego metod show (pokaż) i hide (ukryj), tak jak pokazano poniżej. ActionBar actionBar = getActionBar(); actionBar.hide(); // Ukrywa pasek akcji actionBar.show(); // Sprawia, że pasek akcji staje się widoczny
Tutaj metoda getActionBar() jest wywoływana w celu uzyskania obiektu ActionBar. Jego metody hide() i show() służą odpowiednio ukrywaniu i pokazywaniu paska akcji. Aby ukryć pasek akcji w aktywności, możesz również zastosować motyw, który paska akcji nie obsługuje. W pliku AndroidManifest.xml możesz ustawić dla danej aktywności motyw Theme.Holo.NoActionBar w następujący sposób.
Ukrywanie i pokazywanie paska akcji prowadzi do ponownego narysowania układu. Możesz tego uniknąć, stosując atrybut android:windowActionBarOverlay. Ponadto jeśli używasz niestandardowego motywu, ustaw atrybut android:windowActionBar na false (fałsz), aby usunąć pasek akcji. Widoczność ikony lub logo w pasku akcji jest kontrolowana za pomocą przekazywania wartości logicznej do metody setDisplayShowHomeEnabled(). Przekazanie do tej metody wartości false (fałsz) ukryje logo lub ikonę w pasku akcji. Analogicznie przekazanie do tej metody wartości true (prawda) spowoduje wyświetlenie logo lub ikony w pasku zadań. Wygląda to w następujący sposób: actionBar.setDisplayShowHomeEnabled(true);
Widoczność tytułu w pasku akcji możesz kontrolować, przekazując wartość logiczną do metody setDisplayShowTitleEnabled(). Poniższa instrukcja stanowi przykład ukrycia tytułu w pasku akcji: actionBar.setDisplayShowTitleEnabled(false);
Receptura: komponenty paska akcji
Receptura: komponenty paska akcji Oto komponenty wchodzące w skład paska akcji. Ikona lub logo aplikacji — komponent wyświetlany w lewym górnym rogu
paska akcji. Tytuł aktywności — wyświetla tytuł paska akcji. Zakładki — wyświetla zakładki paska akcji, jeśli tryb nawigacji jest ustawiony
na tabs (patrz rysunek 3.1, środkowy obrazek). Rozwijana lista wyboru — wyświetla elementy akcji w formie rozwijanej listy,
jeśli tryb nawigacyjny jest ustawiony na list navigation (patrz rysunek 3.1, dolny obrazek). Elementy akcji — wyświetlają różne funkcje aplikacji. Aby wywołać moduł lub
wykonać określone zadanie, z paska akcji wybierany jest odpowiedni element akcji. Grupy lub powiązane elementy akcji mogą być łączone w podmenu. Widoki akcji — wyświetla w pasku zadań niestandardowe widoki. Na rysunku 3.1
(środkowy obrazek) pokazano widok akcji w postaci widżetu wyszukiwania. Menu przepełnienia — wyświetla elementy akcji, które nie zmieściły się
w pasku akcji. Przycisk menu przepełnienia — wyświetlany w postaci trzech ułożonych
pionowo kropek (patrz rysunek 3.1, górny obrazek). Po jego wybraniu ukryte elementy akcji są wyświetlane w menu dodatkowym. Podmenu — reprezentuje zbiór elementów menu powiązanych z konkretnym
elementem menu. Po kliknięciu wybranego elementu menu pojawia się podmenu (jeśli takie istnieje).
Receptura: wyświetlanie elementów akcji w pasku akcji Aby wyświetlić elementy akcji w pasku akcji, musisz dodać atrybut android:showAsAction do elementów menu podczas ich definiowania w pliku menu. Atrybut showAsAction określa sposób wyświetlania elementu akcji. Atrybut ten może przyjmować jedną z poniższych wartości. always — powoduje, że element akcji pojawia się w pasku akcji. ifRoom — powoduje, że element akcji pojawia się w pasku akcji, ale tylko wtedy,
kiedy jest dla niego miejsce. Jeśli nie ma wystarczająco dużo miejsca, dany element pojawi się w menu przepełnienia.
121
122
Rozdział 3. Paski akcji w działaniu
Rysunek 3.1. Ekran z komponentami paska akcji (górny obrazek). Ekran, na którym znajdują się zakładki akcji oraz widok akcji (środkowy obrazek). Ekran z rozwijaną listą (dolny obrazek) never — powoduje, że element menu zawsze pojawia się w menu przepełnienia.
Aby wyświetlić menu przepełnienia, wciśnij przycisk Menu na urządzeniu AVD lub przycisk menu przepełnienia na urządzeniu fizycznym. withText — wyświetla tytuł elementu akcji wraz z ikoną (jeśli taka jest).
Ta wartość atrybutu może być ustawiona wraz z innymi wartościami poprzez rozdzielenie ich pionową kreską (|). collapseActionView — powoduje, że widok akcji powiązany z elementem akcji
może być zwijany. Oznacza to, że widok akcji zwija się do standardowego elementu akcji. Teraz na przykładzie działającej aplikacji zapoznasz się z koncepcją paska akcji. W tym celu utwórz nowy projekt Android o nazwie ActionItemsApp. Ustaw wartości atrybutów minSdkVersion i targetSdkVersion odpowiednio na 14 i 17. W tej aplikacji
Receptura: wyświetlanie elementów akcji w pasku akcji
będziesz wyświetlał dwa elementy akcji o nazwach Utwórz (ang. create) i Zaktualizuj (ang. update). Te elementy akcji to elementy do klikania, które pojawiają się w pasku akcji. Elementy akcji możesz zdefiniować w pliku menu activity_ action_items_app.xml (nazwa tego pliku zależy od nazwy projektu Android), który jest domyślnie tworzony w folderze res/menu Twojej aplikacji. Po zdefiniowaniu tych dwóch elementów akcji o nazwach Utwórz i Zaktualizuj plik menu activity_action_items_app.xml będzie wyglądał tak, jak przedstawiono w listingu 3.1. Listing 3.1. Kod wpisany w pliku menu activity_action_items_app.xml
Jak możesz zauważyć, element akcji create został zdefiniowany w taki sposób, aby pojawiał się w pasku akcji tylko wtedy, kiedy będzie wystarczająco dużo miejsca. Ponadto ten element akcji będzie pojawiał się z tekstem tytułowym Utwórz. Element akcji update został zdefiniowany tak, aby zawsze pojawiał się w pasku akcji. Ponieważ chcesz, aby elementy akcji Utwórz i Zaktualizuj były reprezentowane przez ikony, musisz skopiować do folderu res/drawable swojej aplikacji pliki obrazów create.png i update.png. Teraz Twoja aplikacja jest gotowa do uruchomienia. Nie trzeba wpisywać żadnego kodu w pliku układu activity_action_items_app.xml ani w pliku aktywności Java ActionItemsAppActivity.java. Po uruchomieniu aplikacji zobaczysz dwa elementy akcji przedstawione na rysunku 3.2. Jeśli element akcji zostanie wyświetlony jedynie za pomocą ikony, możesz kliknąć i przytrzymać ikonę, aby pojawił się również opis elementu przydzielony do niego w atrybucie android:title. Kiedy zwiększasz liczbę elementów akcji w pasku akcji, te, które mogą się zmieścić, pojawią się w nim, a reszta zostanie ukryta. Za każdym razem, kiedy będą niewidoczne elementy akcji, po prawej stronie w pasku akcji pojawi się przycisk menu przepełnienia. Po wybraniu tego przycisku w tym miejscu wyświetlone zostaną ukryte elementy akcji. Aby zobaczyć, jak to się dzieje, zwiększ liczbę elementów akcji w pliku menu activity_action_items_app.xml zgodnie z listingiem 3.2.
123
124
Rozdział 3. Paski akcji w działaniu
Rysunek 3.2. Dwa elementy akcji, Utwórz i Zaktualizuj, wyświetlone w pasku akcji Listing 3.2. Kod wpisany w pliku menu activity_action_items_app.xml
Jak możesz zauważyć, do paska akcji dodane zostały cztery elementy akcji: Wylistuj wiersze, Szukaj, Usuń i Wstaw. Elementy Wylistuj wiersze i Szukaj mają się pojawić w pasku akcji, jeśli będzie wystarczająco dużo miejsca. W przeciwnym razie pojawią się w menu przepełnienia. Element akcji Usuń nigdy nie będzie widoczny w pasku akcji,
Receptura: wyświetlanie elementów akcji w pasku akcji
nawet jeśli będzie miejsce, ponieważ zawsze pojawiać się będzie w menu przepełnienia. Element akcji Wstaw został zdefiniowany tak, aby zawsze pojawiał się w pasku akcji. Po uruchomieniu tej aplikacji zauważysz, że cztery z sześciu elementów akcji pojawią się w pasku zadań, a po prawej stronie pojawi się przycisk menu przepełnienia. Elementy akcji widoczne w pasku akcji to: Utwórz, Zaktualizuj, Wylistuj wiersze i Wstaw (patrz rysunek 3.3, górny obrazek). Po wybraniu przycisku menu przepełnienia pojawią się ukryte elementy akcji: Szukaj i Usuń (patrz rysunek 3.3, dolny obrazek).
Rysunek 3.3. Widoczne elementy akcji i przycisk menu przepełnienia (górny obrazek) oraz ukryte elementy akcji w menu przepełnienia (dolny obrazek)
125
126
Rozdział 3. Paski akcji w działaniu
Wartość collapseActionView atrybutu showAsAction została objaśniona w podrozdziale „Receptura: wyświetlanie widoków akcji w pasku zadań”.
Receptura: nawigowanie do strony głównej po wybraniu ikony aplikacji Kiedy użytkownik kliknie logo lub ikonę paska akcji, zostanie przekierowany do strony głównej aplikacji. W tym przypadku strona główna aplikacji oznacza jej główną aktywność, czyli element główny Twojego stosu aktywności. Domyślnie logo lub ikona wyświetlana w pasku akcji nie są klikalne. Aby takie się stały, musisz wywołać metodę setHomeButtonEnabled() paska akcji i przekazać do niej wartość logiczną true, tak jak pokazano poniżej: actionBar.setHomeButtonEnabled(true);
Klikanie logo lub ikony aplikacji jest traktowane jak klikanie elementu menu i obsługiwane przez procedurę onOptionsItemSelected Twojej aktywności. Efekt kliknięcia logo lub ikony aplikacji jest równoważny z kliknięciem elementu menu z identyfikatorem android.R.id.home. Innymi słowy, po kliknięciu logo lub ikony wywoływana jest metoda onOptionItemSelected(), do której przekazywany jest jako parametr identyfikator android.R.id.home. Jeśli założymy, że bieżącą aktywnością jest CreateActivity, a aktywnością główną, do której chcesz przejść po kliknięciu ikony aplikacji, jest ActionItemsAppActivity, kod powinien wyglądać tak, jak zostało to przedstawione w listingu 3.3. Listing 3.3. Kod wpisany w pliku aktywności CreateActivity.java package com.androidtablet.actionitemsapp; import import import import import
android.app.ActionBar; android.app.Activity; android.content.Intent; android.os.Bundle; android.view.MenuItem;
public class CreateActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState){ super.onCreate(savedInstanceState); setContentView(R.layout.create); ActionBar actionBar = getActionBar(); actionBar.setHomeButtonEnabled(true); }
Receptura: wyświetlanie widoków akcji w pasku akcji @Override public boolean onOptionsItemSelected(MenuItem item) { #1 switch (item.getItemId()) { case (android.R.id.home) : Intent intent = new Intent(this, ActionItemsAppActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); startActivity(intent); break; default: return super.onOptionsItemSelected(item); } return true; } }
W powyższym kodzie założono, że plik układu o nazwie create.xml znajduje się w folderze res/layout. W celu przejścia do aktywności głównej używana jest flaga intencji o nazwie FLAG_ACTIVITY_CLEAR_TOP, która zamyka wszystkie aktywności rozpoczęte od momentu uruchomienia aktywności głównej. Analizując powyższy kod, możesz zauważyć, że dostęp do obiektu paska zadań actionBar jest uzyskiwany za pomocą wywołania metody getActionBar(), a do metody setHomeButtonEnabled() przekazywana jest wartość logiczna true, co sprawia, że logo aplikacji staje się klikalne. Kliknięcie logo aplikacji generuje zdarzenie kliknięcia elementu menu o identyfikatorze android.R.id.home. W metodzie procedury obsługi onOptionsItemSelected() sprawdzasz, czy kliknięty został element menu o identyfikatorze android.R.id.home, czyli czy kliknięte zostało logo aplikacji. Jeśli logo aplikacji zostało kliknięte, przechodzisz z powrotem do głównej aktywności tej aplikacji, czyszcząc wszystkie aktywności (jeśli takie istnieją) na szczycie stosu. W systemie Android 4.1 i w wersjach późniejszych cała metoda onOptionsItemSelected reprezentowana w powyższym kodzie przez instrukcję #1 może zostać usunięta. Oznacza to, że możesz przejść do głównej aktywności, dodając do pliku AndroidManifest.xml poniższą instrukcję:
Receptura: wyświetlanie widoków akcji w pasku akcji Widoki akcji zapewniają osadzoną kontrolę dla bardziej natychmiastowych akcji. Zasadniczo widoki akcji pozwalają umieścić w pasku akcji niestandardowy widok. Najbardziej typowym widokiem używanym w aplikacjach Android jest SearchView. Widok SearchView zapewnia interfejs użytkownika pozwalający na wprowadzenie zapytania i wysłanie żądania do dostawcy wyszukiwania. Interfejs wyświetla również listę sugestii lub wyników (jeśli takie są) zapytań, co daje użytkownikowi możliwość dokonania
127
128
Rozdział 3. Paski akcji w działaniu
spośród nich wyboru. Aby sprawdzić wystąpienie zdarzenia w widoku SearchView, ustawiany jest nasłuchiwacz setOnQueryTextListener, który reaguje na tekst wprowadzany lub zmieniany przez użytkownika. Nasłuchiwacz OnQueryTextListener wymaga dwóch metod, onQueryTextChange i onQueryTextSubmit. Metoda onQueryTextChange jest wywoływana za każdym razem, kiedy użytkownik zmienia wyszukiwany tekst, a metoda onQueryTextSubmit jest wywoływana, kiedy użytkownik wciska Enter lub Search. Aby zrozumieć, w jaki sposób widok akcji jest używany w aplikacji Android, utwórz projekt Android o nazwie ActionViewApp. W tej aplikacji będziesz wyświetlał SearchView jako widok akcji, w którym użytkownik może wpisać tekst do wyszukania. Aby wyświetlić widok SearchView, wpisz w pliku activity_action_view_app.xml znajdującym się w folderze res/menu kod przedstawiony w listingu 3.4. Listing 3.4. Kod wpisany w pliku menu activity_ action_view_app.xml
Po zastosowaniu tego kodu widok SearchView zostanie wyświetlony w formie ikony, co zostało pokazane na rysunku 3.4 (górny obrazek). Możesz również sprawić, aby widok SearchView pojawiał w formie elementu akcji. W tym celu ustaw atrybut android:showAsAction w następujący sposób: android:showAsAction="ifRoom|collapseActionView"
Wiesz już, że collapseActionView sprawia, iż widok akcji powiązany z danym elementem akcji może być zwijany. Oznacza to, że widok akcji zwija się do postaci standardowego elementu akcji. Teraz widok SearchView będzie pojawiał się jako element akcji, tak jak widać na rysunku 3.4 (środkowy obrazek). Po wybraniu widoku SearchView w jednym z dwóch formatów (ikony lub elementu akcji) otwarte zostanie pole wyszukiwania, umożliwiające użytkownikowi wpisanie tekstu do wyszukania (patrz rysunek 3.4, dolny obrazek). Aby zrozumieć, w jaki sposób działa nasłuchiwacz zdarzeń setOnQueryTextListener, wyświetlisz komunikaty dziennika pokazujące tekst, który użytkownik wprowadził lub zmienił w polu wyszukiwania. Wyświetlisz również ostateczny tekst wpisany w polu wyszukiwania. W tym celu w pliku aktywności Java ActionViewAppActivity.java wpisz kod przedstawiony w listingu 3.5.
Receptura: wyświetlanie widoków akcji w pasku akcji
Rysunek 3.4. Widok SearchView wyświetlony w formie ikony (górny obrazek), widok SearchView wyświetlony w formie elementu akcji (środkowy obrazek) oraz pole wyszukiwania ukazujące się po wybraniu SearchView (dolny obrazek)
129
130
Rozdział 3. Paski akcji w działaniu Listing 3.5. Kod wpisany w pliku aktywności Java ActionViewAppActivity.java package com.androidtablet.actionviewapp; import import import import import import import
android.os.Bundle; android.app.Activity; android.view.Menu; android.view.MenuInflater; android.widget.SearchView; android.widget.SearchView.OnQueryTextListener; android.util.Log;
public class ActionViewAppActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_action_view_app); }
}
@Override public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.activity_action_view_app, menu); SearchView searchView = (SearchView) menu.findItem( R.id.action_search).getActionView(); searchView.setOnQueryTextListener(new OnQueryTextListener(){ @Override public boolean onQueryTextChange(String newText) { Log.d("Tekst nowy:", newText); return false; } @Override public boolean onQueryTextSubmit(String query) { Log.d("Tekst końcowy:", query); return false; } }); return true; }
Jak możesz zauważyć, uzyskiwany jest tutaj z pliku menu dostęp do widżetu SearchView o identyfikatorze action_search, a widżet ten jest przypisywany do obiektu SearchView o nazwie searchView. Nasłuchiwacz setOnQueryTextListener jest powiązywany z obiektem SearchView o nazwie searchView, aby konieczne akcje mogły być podejmowane,
kiedy użytkownik wprowadzi lub zatwierdzi tekst w polu wyszukiwania. Jeśli użytkownik wpisze lub zmieni jakikolwiek tekst w polu wyszukiwania, wywołana zostanie metoda onQueryTextChange(). W tej metodzie przy użyciu komunikatów dziennika wyświetlany jest zmodyfikowany tekst z pola wyszukiwania. Kiedy użytkownik zakończy wpisywanie tekstu w polu wyszukiwania poprzez wciśnięcie przycisku Enter lub Search, wywołana zostanie metoda onQueryTextSubmit() i za pomocą komunikatów dziennika wyświetlony cały tekst wprowadzony w polu wyszukiwania.
Receptura: wyświetlanie widoków akcji w pasku akcji
Po uruchomieniu tej aplikacji zobaczysz widok SearchView w formie ikony w pasku akcji (patrz rysunek 3.5, górny obrazek). Kiedy użytkownik wybierze widok akcji SearchView w pasku zadań, wyświetlone zostanie pole wyszukiwania, w którym należy wpisać tekst do wyszukania (patrz rysunek 3.5, środkowy obrazek). Tekst wpisany w polu wyszukiwania pojawi się w komunikatach dziennika, tak jak widać na rysunku 3.5 (dolny obrazek).
Rysunek 3.5. Widok akcji SearchView wyświetlony po uruchomieniu aplikacji (górny obrazek), pole wyszukiwania wyświetlone po wybraniu SearchView, umożliwiające wpisanie tekstu (środkowy obrazek), oraz komunikaty dziennika pokazujące tekst, który został wpisany lub zmodyfikowany w polu wyszukiwania (dolny obrazek)
131
132
Rozdział 3. Paski akcji w działaniu
Receptura: wyświetlanie podmenu w pasku akcji Aby wyświetlić podmenu w pasku akcji, musisz zgrupować elementy menu danego podmenu w obrębie znacznika i zagnieździć go wewnątrz elementu akcji, który chcesz powiązać z tym podmenu. Rozważmy sytuację, w której chcesz utworzyć dwa elementy akcji, Utwórz i Zaktualizuj. Kiedy użytkownik kliknie element akcji Utwórz, rozwinięte zostanie podmenu wyświetlające trzy elementy menu: Utwórz fakturę, Utwórz klienta oraz Utwórz produkt. Aby zrozumieć, w jaki sposób definiuje się podmenu dla elementu akcji, utwórz projekt Android o nazwie ActionBarSubmenu. W celu zdefiniowania elementów akcji Utwórz i Zaktualizuj oraz podmenu dla elementu Utwórz w pliku activity_action_bar_submenu.xml znajdującym się w folderze /res/menu wpisz kod przedstawiony w listingu 3.6. Listing 3.6. Kod wpisany w pliku menu activity_action_bar_submenu.xml -
Jak możesz zauważyć, ten kod zawiera atrybut android:showAsAction, który ma wyświetlać elementy menu w pasku zadań, jeśli jest wystarczająco dużo miejsca. Element zdefiniowany wewnątrz elementu akcji Utwórz definiuje podmenu składające się z trzech elementów menu: Utwórz fakturę, Utwórz klienta oraz Utwórz produkt. Aby po wybraniu któregoś elementu akcji lub menu wyświetlić odpowiedź, musisz zdefiniować kontrolkę TextView w pliku układu o nazwie activity_action_bar_submenu.xml. Kontrolka TextView będzie wyświetlać tekst wskazujący, które menu akcji lub jaki element akcji zostały wybrane. Po zdefiniowaniu tej kontrolki plik układu activity_action_bar_submenu.xml będzie wyglądał tak, jak przedstawiono w listingu 3.7.
Receptura: wyświetlanie podmenu w pasku akcji Listing 3.7. Kod wpisany w pliku układu activity_action_bar_submenu.xml
W celu identyfikacji kontrolki TextView w kodzie Java został jej przypisany identyfikator selectedopt. Tekst wyświetlany za pomocą kontrolki TextView będzie prezentowany pogrubioną czcionką o rozmiarze 18 dp. Aby wyświetlić odpowiedź, kiedy wybrany zostanie któryś z elementów menu lub akcji, zmodyfikuj plik ActionBarSubmenuActivity.java w sposób przedstawiony w listingu 3.8. Kod ten służy do wyświetlenia tekstu wskazującego, który element menu lub akcji został wybrany. Listing 3.8. Kod wpisany w pliku aktywności Java ActionBarSubmenuActivity.java package com.androidtablet.actionbarsubmenu; import import import import import import
android.os.Bundle; android.app.Activity; android.view.Menu; android.view.MenuInflater; android.view.MenuItem; android.widget.TextView;
public class ActionBarSubmenuActivity extends Activity { private TextView selectedOpt; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_action_bar_submenu); selectedOpt=(TextView)findViewById(R.id.selectedopt); } @Override public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.activity_action_bar_submenu, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) {
133
134
Rozdział 3. Paski akcji w działaniu case R.id.create: selectedOpt.setText("Wybrałeś break; case R.id.update: selectedOpt.setText("Wybrałeś break; case R.id.create_invoice: selectedOpt.setText("Wybrałeś break; case R.id.create_customer: selectedOpt.setText("Wybrałeś break; case R.id.create_product: selectedOpt.setText("Wybrałeś break;
opcję Utwórz");
opcję Zaktualizuj");
opcję Utwórz fakturę"); opcję Utwórz klienta");
opcję Utwórz produkt");
} return true; } }
Aby wyświetlić pasek akcji, trzeba wypełnić lub scalić menu zdefiniowane w pliku activity_action_bar_submenu.xml. W ten sposób uzyskujesz MenuInflater z klasy aktywności. Obiekt inflater jest tworzony z klasy MenuInflater, a jego metoda inflate jest wywoływana w celu wypełnienia lub scalenia menu zdefiniowanego w pliku activity_action_bar_submenu. Metoda onCreateOptionsMenu() została ustawiona tak, aby zwracać wartość logiczną true, co umożliwia systemowi Android wyświetlenie menu. Wszystkie wybierane elementy menu lub akcji są obsługiwane za pomocą metody onOptionsItemSelected(). Wybrany element menu lub akcji jest przekazywany do tej metody jako parametr MenuItem. Nadpisujesz tę metodę w aktywności, wpisując kod, który chcesz wykonać, kiedy wybrany zostaje któryś element menu lub akcji. W metodzie tej wydobywasz identyfikator wybranego elementu menu lub akcji w celu identyfikacji tego elementu i podjęcia odpowiedniego działania. Metoda getItemId() pomaga odnaleźć identyfikator wybranego elementu menu lub akcji. Następnie za pomocą instrukcji switch wyświetlasz wiadomość tekstową wskazującą, który element menu lub akcji został wybrany, wykorzystując w tym celu obiekt selectedOpt należący do kontrolki TextView. Po uruchomieniu danej aplikacji zauważysz w pasku akcji elementy akcji Utwórz i Zaktualizuj (patrz rysunek 3.6, górny obrazek). Po wybraniu elementu akcji Utwórz kontrolka TextView wyświetla komunikat Wybrałeś opcję Utwórz. Wyświetlone zostaje również podmenu pokazane na rysunku 3.6 (środkowy obrazek). Po wybraniu dowolnego elementu menu z tego podmenu odpowiednia wiadomość tekstowa zostanie wyświetlona za pomocą kontrolki TextView. Po wybraniu z tego podmenu na przykład elementu menu Utwórz klienta pojawi się wiadomość Wybrałeś opcję Utwórz klienta, co widać na rysunku 3.6 (dolny obrazek).
Receptura: wyświetlanie podmenu w pasku akcji
Rysunek 3.6. Dwa elementy akcji wyświetlone po uruchomieniu aplikacji (górny obrazek). Podmenu wyświetlone po wybraniu elementu akcji Utwórz (środkowy obrazek). Kontrolka TextView, za pomocą której można zobaczyć, jaki element menu został wybrany z podmenu (dolny obrazek)
135
136
Rozdział 3. Paski akcji w działaniu
Czy element menu może być wyposażony w pole zaznaczania? Oczywiście. Aby wybrany element menu posiadał pole zaznaczania, przydziel do jego atrybutu android:checkable wartość logiczną true. Aby przykładowo wyświetlany w podmenu element menu Utwórz produkt posiadał pole wyboru, wpisz następujący kod:
Ten kod spowoduje, że element menu Utwórz produkt będzie wyświetlany z polem zaznaczania (patrz rysunek 3.7, górny obrazek).
Rysunek 3.7. Element menu Utwórz produkt wyświetlony wraz z polem zaznaczania (górny obrazek) oraz elementy menu w formie przycisków opcji (dolny obrazek)
Receptura: wyświetlanie podmenu w pasku akcji
Elementy menu mogą również być wyświetlane w postaci przycisków opcji. Oznacza to, że użytkownik będzie mógł wybrać pojedynczy element menu w grupie. Jeśli wybrany zostanie pojedynczy element menu w grupie, z wszystkich poprzednio wybranych elementów menu zostanie automatycznie usunięte zaznaczenie. Aby zrozumieć koncepcję przycisków opcji, dodaj do elementu akcji Zaktualizuj grupę wzajemnie wykluczających się elementów menu. Grupa ta będzie wyświetlać elementy menu: Zaktualizuj kod, Zaktualizuj imię oraz Zaktualizuj cenę. Te elementy menu będą wyświetlane jako przyciski opcji, więc tylko jeden z elementów będzie mógł być zaznaczony w danej grupie. Kiedy wybrany zostanie któryś element menu, z każdego poprzednio wybranego elementu zostanie usunięte zaznaczenie. Aby dodać grupę wzajemnie wykluczających się elementów menu do elementu akcji Zaktualizuj, zmodyfikuj plik activity_action_bar_submenu.xml w sposób przedstawiony w listingu 3.9. Listing 3.9. Kod wpisany w pliku menu activity_action_bar_submenu.xml -
-
Jak możesz zauważyć, elementy menu są zagnieżdżone w elemencie , a atrybut android:checkableBehavior=single jest powiązany z daną grupą, aby zapewnić, że wybrany będzie mógł być tylko jeden element menu w grupie. Z tego względu elementy menu
137
138
Rozdział 3. Paski akcji w działaniu
Zaktualizuj kod, Zaktualizuj imię oraz Zaktualizuj cenę będą wyświetlane w formie przycisków opcji. Aby wiedzieć, który z przycisków opcji został wybrany, dodaj w pliku ActionBarSubmenuActivity.java kod przedstawiony w listingu 3.10. Dodane zostały tylko fragmenty kodu zaznaczone pogrubioną czcionką. Reszta pozostaje taka sama jak w listingu 3.8. Listing 3.10. Kod w pliku aktywności Java ActionBarSubmenuActivity.java package com.androidtablet.actionbarsubmenu; import import import import import import
android.os.Bundle; android.app.Activity; android.view.Menu; android.view.MenuInflater; android.view.MenuItem; android.widget.TextView;
public class ActionBarSubmenuActivity extends Activity { private TextView selectedOpt; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_action_bar_submenu); selectedOpt=(TextView)findViewById(R.id.selectedopt); } @Override public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.activity_action_bar_submenu, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.create: selectedOpt.setText("Wybrałeś opcję Utwórz"); break; case R.id.update: selectedOpt.setText("Wybrałeś opcję Zaktualizuj"); break; case R.id.create_invoice: selectedOpt.setText("Wybrałeś opcję Utwórz fakturę"); break; case R.id.create_customer: selectedOpt.setText("Wybrałeś opcję utwórz klienta"); break; case R.id.create_product: selectedOpt.setText("Wybrałeś opcję Utwórz produkt"); break; case R.id.update_code: selectedOpt.setText("Wybrałeś opcję Zaktualizuj kod"); break; case R.id.update_name:
Receptura: tworzenie paska zadań z zakładkami selectedOpt.setText("Wybrałeś opcję Zaktualizuj imię"); break; case R.id.update_price: selectedOpt.setText("Wybrałeś opcję Zaktualizuj cenę"); break; } return true; } }
Po uruchomieniu danej aplikacji i wybraniu elementu akcji Zaktualizuj pojawi się podmenu wyświetlające w formie przycisków opcji elementy menu Zaktualizuj kod, Zaktualizuj imię oraz Zaktualizuj cenę. Kiedy wybrany zostanie któryś z elementów menu, z każdego uprzednio wybranego elementu menu zostanie usunięte zaznaczenie, a odpowiednia wiadomość tekstowa będzie wyświetlona za pomocą kontrolki TextView. Przykładowo po wybraniu z podmenu elementu menu Zaktualizuj kod pojawi się komunikat Wybrałeś opcję Zaktualizuj kod, co pokazano na rysunku 3.7 (dolny obrazek).
Receptura: tworzenie paska zadań z zakładkami Paski zadań z zakładkami są jak przyciski z niestandardową procedurą obsługi zaprojektowane do stosowania z menedżerem fragmentów. W celu wyświetlenia zakładek nawigacyjnych w pasku zadań wywoływana jest metoda setNavigationMode() poprzez przekazanie w postaci parametru wartości ActionBar.NAVIGATION_MODE_TABS w sposób pokazany poniżej: actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
Po określeniu trybu nawigacji do paska zadań za pomocą wywołania metody addTab() dodawane są zakładki. Oto sposób wykonania tego zadania. actionBar.addTab(actionBar.newTab().setText("Utwórz"). setTabListener(this));
W kodzie tworzona jest nowa zakładka, ustawiany dla niej tekst Utwórz i dołączany nasłuchiwacz TabListener. Na koniec dodaje się nowo utworzoną zakładkę do paska akcji. Przykładowo użyta tutaj metoda setText() ustawia tytuł zakładki. Możesz także wywołać metodę setIcon(), aby dla danej zakładki zdefiniować obrazek. Ponadto możesz wywołać metodę setContentDescription(), aby dostarczyć bardziej szczegółowe informacje o zakładce. Przykład: Tab tab1 = actionBar.newTab(); tabOne.setText("Utwórz") .setIcon(R.drawable.ic_launcher) .setContentDescription("Tworzenie faktury")
139
140
Rozdział 3. Paski akcji w działaniu .setTabListener(this)); actionBar.addTab(tab1);
W tym kodzie do paska zadań dodana zostaje zakładka z tekstem Utwórz. Ikona przypisana do zakładki to domyślna ikona ic_launcher, a przypisany opis szczegółowy to Tworzenie faktury. Kiedy klikniesz tę zakładkę, dane zdarzenie zostanie obsłużone przez TabListener, który wykona żądane zadanie. Teraz na rzeczywistym przykładzie zapoznasz się z koncepcją paska zadań z zakładkami. W tym celu utwórz projekt Android o nazwie ActionBarTabApp. W aplikacji będziesz tworzył dwie zakładki, Utwórz i Zaktualizuj. Po wybraniu którejkolwiek z nich odpowiedni fragment zostanie wywołany i wyświetli wiadomość tekstową informującą, że ten fragment jest aktywny. Widoki obu fragmentów będą wyświetlane za pomocą indywidualnych plików układu XML. Dodaj więc w folderze res/layout dwa pliki XML o nazwach odpowiednio createfragment.xml i updatefragment.xml. W pliku createfragment.xml wpisz kod przedstawiony w listingu 3.11. Listing 3.11. Kod wpisany w pliku createfragment.xml
Jak możesz zauważyć, w powyższym pliku układu definiowana jest kontrolka TextView, inicjowana w celu wyświetlenia tekstu To jest fragment tworzenia.
Wyświetlenie tej wiadomości będzie oznaczać wywołanie pierwszego fragmentu. Teraz wpisz w pliku updatefragment.xml kod przedstawiony w listingu 3.12. Listing 3.12. Kod wpisany w pliku updatefragment.xml
Receptura: tworzenie paska zadań z zakładkami
Ponownie kontrolka TextView jest definiowana w pliku układu dla drugiego fragmentu. Kontrolka ta jest inicjowana w celu wyświetlenia tekstu To jest fragment aktualizacji, co wskazuje wywołanie drugiego fragmentu. Aby wyświetlić zawartość wymaganego fragmentu, kiedy wybrana zostaje zakładka akcji, musisz zdefiniować kontener fragmentu w pliku układu aktywności. Wpisz więc w pliku układu aktywności activity_action_bar_tab_app.xml kod przedstawiony w listingu 3.13. Listing 3.13. Kod wpisany w pliku układu aktywności activity_action_bar_tab_app.xml
Aby załadować widoki tych dwóch fragmentów, dodaj do paczki com.androidtablet.actionbartabapp Twojego projektu Android dwa pliki klas Java o nazwach CreateActivity.java i UpdateActivity.java. W celu załadowaniu widoku zdefiniowanego w pliku układu createfragment.xml wpisz w pliku CreateActivity.java kod przedstawiony w listingu 3.14. Listing 3.14. Kod wpisany w pliku CreateActivity.java package com.androidtablet.actionbartabapp; import import import import import
android.app.Fragment; android.view.View; android.view.ViewGroup; android.view.LayoutInflater; android.os.Bundle;
public class CreateActivity extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.createfragment, container, false); } }
Ponownie, aby załadować widok zdefiniowany w pliku układu updatefragment.xml, wpisz w pliku UpdateActivity.java kod przedstawiony w listingu 3.15.
141
142
Rozdział 3. Paski akcji w działaniu Listing 3.15. Kod wpisany w pliku UpdateActivity.java package com.androidtablet.actionbartabapp; import import import import import
android.app.Fragment; android.view.View; android.view.ViewGroup; android.view.LayoutInflater; android.os.Bundle;
public class UpdateActivity extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.updatefragment, container, false); } }
Następnie w głównym pliku aktywności ActionBarTabAppActivity.java musisz napisać kod Java, który wykona następujące zadania. Zdefiniuje w Twojej aplikacji dwie zakładki, czyli Utwórz i Zaktualizuj. Zdefiniuje nasłuchiwacze zakładek i powiąże je z tymi zakładkami. Aktywuje fragment Utwórz i Zaktualizuj, kiedy kliknięta zostanie odpowiednia
zakładka. W celu wykonania powyższych zadań wpisz w pliku aktywności Java ActionBarTabAppActivity.java kod przedstawiony w listingu 3.16. Listing 3.16. Kod wpisany w pliku aktywności Java ActionBarTabAppActivity.java package com.androidtablet.actionbartabapp; import import import import import import import import
android.os.Bundle; android.app.Activity; android.app.ActionBar; android.app.ActionBar.Tab; android.app.FragmentTransaction; android.util.Log; android.app.FragmentManager; android.app.Fragment;
public class ActionBarTabAppActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_action_bar_tab_app); Fragment createFragment = new CreateActivity(); Fragment updateFragment = new UpdateActivity(); ActionBar actionBar = getActionBar(); actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS); actionBar.setDisplayShowTitleEnabled(true);
Receptura: tworzenie paska zadań z zakładkami ActionBar.Tab CreateTab = actionBar.newTab(). setText("Utwórz"); ActionBar.Tab UpdateTab = actionBar.newTab(). setText("Zaktualizuj"); CreateTab.setTabListener(new MyTabsListener( createFragment)); UpdateTab.setTabListener(new MyTabsListener( updateFragment)); actionBar.addTab(CreateTab); actionBar.addTab(UpdateTab); } protected class MyTabsListener implements ActionBar. TabListener { Fragment fragment; public MyTabsListener(Fragment fragment){ this.fragment = fragment; } public void onTabSelected(Tab tab, FragmentTransaction ft) { ft.replace(R.id.fragment_container, fragment, null); } public void onTabUnselected(Tab tab, FragmentTransaction ft) { ft.remove(fragment); getFragmentManager().popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE); } public void onTabReselected(Tab tab, FragmentTransaction ft) { Log.d("Zakładka", String.valueOf(tab.getPosition()) + " wybrana ponownie"); } } }
W powyższym kodzie możesz zauważyć, że obiekt paska akcji jest tworzony za pomocą wywołania metody getActionBar(). Aby pasek akcji wyświetlał się z zakładkami, musisz ustawić jego tryb nawigacji jako ActionBar.NAVIGATION_MODE_TABS. Możesz sprawić, aby tytuł aktywności był niewidoczny, przekazując wartość logiczną false do metody setDisplayShowTitleEnabled(). Następnie tworzysz dwie zakładki o nazwach Utwórz oraz Zaktualizuj i dodajesz je do paska akcji. Nasłuchiwacz zdarzeń TabListener jest powiązany z obiema zakładkami. Kiedy któraś z nich zostaje wybrana, wywoływane są metoda onTabSelected() i żądany fragment. Metoda onTabSelected() aktywuje odpowiedni fragment i wyświetla widok zdefiniowany w jego pliku układu. Po wybraniu jednej z zakładek wywoływana jest metoda onTabUnselected() i zakładka, która nie została wybrana, jest przekazywana do niej jako parametr. Fragment powiązany z niewybraną zakładką jest usuwany ze stosu, przez co staje się niewidoczny.
143
144
Rozdział 3. Paski akcji w działaniu
Metoda onTabUnselected() wyświetla pozycję niewybranej zakładki. Po uruchomieniu aplikacji widzisz pasek akcji z dwoma zakładkami — Utwórz i Zaktualizuj. Po wybraniu zakładki Utwórz wywoływany jest powiązany z nią fragment, wyświetlający komunikat To jest fragment tworzenia, co pokazano na rysunku 3.8 (górny obrazek). Analogicznie po wybraniu zakładki Zaktualizuj powiązany z nią fragment jest wywoływany i wyświetlana jest wiadomość tekstowa To jest fragment aktualizacji, co widać na rysunku 3.8 (dolny obrazek).
Rysunek 3.8. Dwie zakładki w pasku akcji oraz komunikaty dziennika wyświetlone po wybraniu zakładek akcji
Receptura: tworzenie paska akcji z rozwijaną listą
Receptura: tworzenie paska akcji z rozwijaną listą W pasku akcji z rozwijaną listą elementy akcji są wyświetlane w formie rozwijanej listy wyboru. Aby wyświetlić pasek akcji z rozwijaną listą, wywołujesz jego metodę setNavigationMode(), przekazując do niej jako parametr wartość ActionBar.NAVIGATION_MODE_LIST w następujący sposób: actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
Rozwijana lista jest wyświetlana w postaci zagnieżdżonego widoku dostępnych opcji, co pozwala użytkownikowi wybranie jednej z nich. Aby wyświetlić elementy akcji w rozwijanej liście, użyjesz adaptera, który implementuje interfejs SpinnerAdapter. Możesz np. użyć adapterów ArrayAdapter czy SimpleCursorAdapter lub dowolnego adaptera baseadapter. W aplikacji, którą będziesz tworzył, użyjesz adaptera ArrayAdapter, ponieważ jest to najprostszy z adapterów, działający jako źródło danych dla widżetów wyboru. Aby utworzyć taką aplikację, wykonaj następujące czynności. 1. Zdefiniuj tablicę ciągów znaków zawierającą ciągi, które chcesz wyświetlić
w rozwijanej liście. 2. Utwórz adapter ArrayAdapter, który wyświetla elementy tablicy w formie
rozwijanych pozycji. W tym celu musisz zawinąć lub zrzucić elementy tablicy w zagnieżdżoną listę rozwijanych pozycji. 3. Przydziel ArrayAdapter do paska akcji, aby wyświetlić elementy akcji.
By przypisać ArrayAdapter do paska akcji i dołączyć nasłuchiwacz zdarzeń do rozwijanych elementów, które mają być wyświetlane, wywołaj metodę setListNavigationCallbacks(), przekazując do niej w formie parametrów dany adapter oraz nasłuchiwacz OnNavigationListener w następujący sposób. String[] items = new String[] { "Utwórz", "Wstaw", "Zaktualizuj", "Szukaj" }; ArrayAdapter adapter = new ArrayAdapter(this, android.R.layout. simple_spinner_dropdown_item, items); ActionBar actionBar = getActionBar(); actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST); actionBar.setListNavigationCallbacks(adapter, onNavigationItemSelected);
W powyższym kodzie zdefiniowana została tablica ciągów znaków o nazwie items, składająca się z ciągów, które chcesz wyświetlić w rozwijanej liście paska akcji. Adapter ArrayAdapter o nazwie adapter jest tworzony do przechowywania elementów tablicy ciągów i zrzucania tych elementów tablicy na zagnieżdżone pozycje rozwijanej listy. Tworzony jest obiekt paska akcji actionBar, a jego tryb nawigacji jest ustawiany jako ActionBar.NAVIGATION_MODE_LIST. Metoda setListNavigationCallbacks() jest wywoływana na obiekcie actionBar poprzez przekazanie do niej w postaci parametrów adaptera ArrayAdapter o nazwie adapter oraz nasłuchiwacza onNavigationSelected. Wywołania zwrotne są przypisane do obsługi wyborów z rozwijanej listy. Kiedy użytkownik wybierze element akcji z rozwijanej listy, wywoływana jest procedura obsługi onNavigationItemSelected, w której możesz wpisać kod wykonujący żądane działanie.
145
146
Rozdział 3. Paski akcji w działaniu
Spróbuj teraz utworzyć pasek akcji z rozwijaną listą. Najpierw utwórz projekt Android o nazwie ActionBarListApp. W tej aplikacji będziesz wyświetlał kilka elementów akcji w formie rozwijanej listy wyboru. Kiedy wybrany zostanie któryś z elementów akcji, pokazany będzie odpowiedni komunikat dziennika. W pliku aktywności Java ActionBarListAppActivity.java wpisz kod przedstawiony w listingu 3.17. Listing 3.17. Kod wpisany w pliku aktywności Java ActionBarListAppActivity.java package com.androidtablet.actionbarlistapp; import import import import import import
android.os.Bundle; android.app.Activity; android.app.ActionBar.OnNavigationListener; android.app.ActionBar; android.widget.ArrayAdapter; android.util.Log;
public class ActionBarListAppActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); String[] items = new String[] { "Utwórz", "Wstaw", "Zaktualizuj", "Szukaj" }; ArrayAdapter adapter = new ArrayAdapter( this, android.R.layout.simple_spinner_dropdown_ item, items); ActionBar actionBar = getActionBar(); actionBar.setNavigationMode(ActionBar.NAVIGATION_ MODE_LIST); actionBar.setListNavigationCallbacks(adapter, onNavigationItemSelected); } OnNavigationListener onNavigationItemSelected = new OnNavigationListener() { @Override public boolean onNavigationItemSelected(int itemPosition, long itemId) { Log.d("Opcja ", String.valueOf(itemId) + " wybrałeś"); return true; } }; }
Jak możesz zauważyć, kiedy wybrany zostaje element akcji z rozwijanej listy, wywoływana jest metoda onNavigationItemSelected(). Parametry itemPosition i itemId w metodzie onNavigationItemSelected() zawierają informacje o pozycji (position) i identyfikatorze (ID) wybranego elementu akcji. Wyświetlany jest komunikat dziennika, czyli identyfikator wybranego elementu akcji. Identyfikatory są kolejno przypisane do elementów akcji w rozwijanej liście, począwszy od 0. Nie zapomnij w pliku AndroidManifest.xml ustawić wartość atrybutu android:minSdkVersion na 11 lub wyższą, aby włączyć pasek akcji.
Receptura: tworzenie paska akcji z rozwijaną listą
Po uruchomieniu tej aplikacji zobaczysz zagnieżdżony widok, co pokazano na rysunku 3.9 (pierwszy obrazek). Zagnieżdżony widok wyświetla pierwszy element z rozwijanej listy, czyli Utwórz. Domyślny styl wyświetla ten element w ciemnym kolorze, co jest niemal niewidoczne na ciemnym tle. Otwórz plik styles.xml w folderze res/values i za pomocą poniższej instrukcji dodaj do niego niestandardowy styl o nazwie MyActionBar:
Po dodaniu tego stylu plik styles.xml będzie wyglądał tak, jak przedstawiono w listingu 3.18. Listing 3.18. Kod wpisany w pliku styles.xml
Aby zaimplementować ten styl w swojej aplikacji, otwórz plik AndroidManifest.xml i ustaw wartość atrybutu android:theme w następujący sposób: android:theme="@style/MyActionBar"
Instrukcja spowoduje zastosowanie w Twojej aplikacji stylu MyActionBar. Efekt został przedstawiony na rysunku 3.9 (drugi obrazek). Po kliknięciu zagnieżdżonego widoku otwierana jest rozwijana lista, która wyświetla wszystkie dostępne elementy akcji (patrz rysunek 3.9, trzeci obrazek). Wybierz element akcji Zaktualizuj, a pojawi się on w nagłówku zagnieżdżonego widoku (tak jak pokazano na rysunku 3.9, czwarty obrazek), informując użytkownika, że został wskazany przy dokonywaniu poprzedniego wyboru. Na rysunku 3.10 przedstawiono komunikaty dziennika, które są wyświetlane po wybraniu z rozwijanej listy elementów akcji Zaktualizuj i Utwórz. Identyfikator wybranego elementu rozwijanej listy jest wyświetlany przy wykorzystaniu parametru itemId z metody onNavigationItemSelected().
147
148
Rozdział 3. Paski akcji w działaniu
Rysunek 3.9. Pierwszy element zagnieżdżonego widoku jest prawie niewidoczny (pierwszy obrazek). Pierwszy element zagnieżdżonego widoku jest widoczny (drugi obrazek). Wszystkie akcje są wyświetlane po kliknięciu listy (trzeci obrazek). Wybrany element listy jest wyświetlany na początku listy (czwarty obrazek)
Podsumowanie
Rysunek 3.10. Komunikaty dziennika wyświetlone po wybraniu akcji z paska akcji
Podsumowanie W tym rozdziale poznałeś zastosowanie paska akcji do wyświetlania kluczowych akcji aplikacji. Nauczyłeś się przełączać widoczność paska akcji, zobaczyłeś, jak działają różne komponenty, i odkryłeś procedury wyświetlania elementów akcji oraz widoków akcji w pasku akcji. Dowiedziałeś się, jak wyświetlać podmenu w pasku akcji i w jaki sposób tworzyć paski akcji z zakładkami i z rozwijaną listą. W kolejnym rozdziale poznasz nowe widżety, które są dostępne od czasu opublikowania API 11. Nauczysz się wyświetlać w aplikacji Android kalendarz za pomocą CalendarView oraz zakres liczb przy użyciu NumberPicker. Dowiesz się także, jak wyświetlać stos obrazów, wykorzystując widżet StackView. Na koniec nauczysz się wyświetlać listę opcji z zastosowaniem ListPopupWindow oraz wyświetlać sugestie za pomocą PopupMenu.
149
150
Rozdział 3. Paski akcji w działaniu
4 Nowe widżety W
tym rozdziale poznasz nowe widżety, które są dostępne od API poziomu 11. Nauczysz się wyświetlać w aplikacji Android kalendarz za pomocą widżetu CalendarView oraz zakres liczb przy użyciu widżetu NumberPicker. Dowiesz się także, jak wyświetlać stos obrazów, wykorzystując widżet StackView. Na koniec nauczysz się wyświetlać listę opcji, stosując widżet ListPopupWindow, oraz sugestie za pomocą widżetu PopupMenu.
Receptura: wyświetlanie kalendarza w aplikacji Android Aby wyświetlić kalendarz w aplikacji Android, skorzystasz z widżetu CalendarView. Jest to konfigurowalny widżet, który wyświetla i wybiera daty. Domyślnie wyświetlany jest kalendarz na bieżący miesiąc, ale możesz przewinąć do konkretnej daty. Kiedy chcesz wybrać datę, po prostu ją kliknij. Aby zapoznać się z kalendarzem, utwórz projekt Android o nazwie CalendarViewApp. Aplikacja domyślnie będzie wyświetlać kalendarz na bieżący miesiąc. Użytkownik może przewinąć kalendarz, aby zobaczyć daty z innego, wybranego miesiąca. Wybrana data będzie wyświetlana za pomocą komunikatu Toast. Aplikacja będzie również zawierać kontrolkę Button; po jej kliknięciu pokaże się okno dialogowe DatePickerDialog, umożliwiające użytkownikowi wyświetlenie kalendarza na wybrany miesiąc. Ponieważ w Twojej aplikacji potrzebne są zarówno przycisk, jak i kalendarz, musisz w pliku układu aktywności zdefiniować kontrolkę Button oraz widżet CalendarView. Po zdefiniowaniu tych dwóch elementów plik układu aktywności activity_calendar_view_app.xml będzie wyglądał tak, jak przedstawiono w listingu 4.1. Listing 4.1. Kod wpisany w pliku aktywności activity_calendar_view_app.xml
152
Rozdział 4. Nowe widżety android:layout_height="match_parent" >
Dla celów dostępu i identyfikacji w kodzie Java kontrolkom Button i CalendarView zostały przypisane odpowiednio identyfikatory date_picker_button i calendar_view. Teraz musisz napisać kod Java, który będzie: wyświetlał widżet CalendarView zdefiniowany w pliku układu aktywności, wiązał nasłuchiwacz zdarzeń setOnClickListener z kontrolką Button w celu
wyświetlenia okna dialogowego DatePickerDialog, wiązał nasłuchiwacz OnDateSetListener z oknem dialogowym
DatePickerDialog, by możliwe było wyświetlenie kalendarza dla wybranej daty za pomocą widżetu CalendarView, wiązał nasłuchiwacz zdarzeń z widżetem CalendarView, żeby wyświetlać
na ekranie wybraną datę. Aby zrealizować poniższe zadania, wpisz w pliku aktywności Java CalendarViewAppActivity.java kod przedstawiony w listingu 4.2. Listing 4.2. Kod wpisany w pliku aktywności Java CalendarViewAppActivity.java package com.androidtablet.calendarviewapp; import import import import import import import import import import import
android.os.Bundle; android.app.Activity; android.widget.CalendarView; android.widget.CalendarView.OnDateChangeListener; android.widget.Toast; java.util.Calendar; android.app.DatePickerDialog; android.widget.DatePicker; android.widget.Button; android.view.View; android.view.View.OnClickListener;
public class CalendarViewAppActivity extends Activity { private CalendarView calendarView; private int yr, mon, dy; private Calendar selectedDate; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState);
Receptura: wyświetlanie kalendarza w aplikacji Android setContentView(R.layout.activity_calendar_view_app); Calendar c = Calendar.getInstance(); yr = c.get(Calendar.YEAR); mon = c.get(Calendar.MONTH); dy = c.get(Calendar.DAY_OF_MONTH); Button datePickerButton = (Button) findViewById( R.id.date_picker_button); calendarView = (CalendarView) findViewById( R.id.calendar_view); datePickerButton.setOnClickListener(new OnClickListener() { public void onClick(View v) { new DatePickerDialog(CalendarViewAppActivity. this, dateListener, yr, mon, dy).show(); } }); calendarView.setOnDateChangeListener(new OnDateChangeListener() { @Override public void onSelectedDayChange(CalendarView view, int year, int month, int dayOfMonth) { Toast.makeText(getApplicationContext(),"Wybrałeś datę "+dayOfMonth+"."+(month+1)+"."+ year, Toast.LENGTH_SHORT). show(); } }); } private DatePickerDialog.OnDateSetListener dateListener = new DatePickerDialog.OnDateSetListener() { public void onDateSet(DatePicker view, int year, int monthOfYear, int dayOfMonth){ selectedDate=Calendar.getInstance(); yr=year; mon=monthOfYear; dy=dayOfMonth; selectedDate.set(yr, mon, dy); calendarView.setDate(selectedDate.getTimeInMillis()); } }; }
Jak możesz zauważyć w powyższym kodzie, dostęp do widżetu CalendarView uzyskiwany jest z pliku układu, a sam widżet mapowany jest na obiekt calendarView klasy CalendarView. Ponadto dostęp do kontrolki Button o identyfikatorze date_picker_button jest uzyskiwany z pliku układu, a sama kontrolka mapowana na obiekt datePickerButton klasy Button. Nasłuchiwacz setOnClickListener został skojarzony z kontrolką Button, a jego metoda wywołania zwrotnego onClick jest wykonywana po kliknięciu tej kontrolki. W metodzie wywołania zwrotnego onClick wywoływane jest okno dialogowe DatePickerDialog w celu wyświetlenia bieżącej daty. Nasłuchiwacz OnDateSetListener jest powiązany z oknem dialogowym kontrolki daty (ang. Date Picker), więc kiedy jakaś data zostanie wybrana w tym oknie dialogowym, widżet CalendarView będzie wyświetlał kalendarz na dany miesiąc i rok.
153
154
Rozdział 4. Nowe widżety
Nasłuchiwacz setOnDateChangeListener jest powiązany z widżetem CalendarView. Kiedy jakaś data zostaje wybrana lub zmieniona w tym widżecie, wywoływana jest metoda wywołania zwrotnego onSelectedDayChange(). Wykorzystując tę metodę, wyświetlasz wybraną datę za pomocą komunikatu Toast. Należy pamiętać, że miesiące liczone są od 0, więc przed wyświetleniem wartości danego miesiąca należy dodać do jego liczby 1. Po uruchomieniu tej aplikacji zobaczysz, że widżet CalendarView wyświetla kalendarz na bieżący miesiąc (patrz rysunek 4.1, górny obrazek). Aby wyświetlić kalendarz na żądany miesiąc, wybierz przycisk Otwórz kontrolkę daty, który otwiera okno dialogowe DatePickerDialog. W tym oknie dialogowym możesz wybierać datę z kalendarza (patrz rysunek 4.1, środkowy obrazek). Po wybraniu daty i kliknięciu Gotowe wyświetlony zostanie kalendarz dla tej daty. Data wybrana z widżetu CalendarView jest wyświetlana za pomocą komunikatu Toast, co pokazano na rysunku 4.1 (dolny obrazek).
Receptura: wyświetlanie i wybieranie liczb za pomocą widżetu NumberPicker Podczas czytania tej receptury nauczysz się wyświetlać widżet NumberPicker, który pokazuje liczby z określonego przedziału. Liczba wybrana z widżetu NumberPicker jest wyświetlana za pomocą kontrolki TextView. Utwórz nowy projekt Android o nazwie NumberPickerApp. W tej aplikacji będziesz chciał po prostu wyświetlić kontrolkę TextView i widżet NumberPicker. Widżet NumberPicker będzie wyświetlał liczby z określonego przedziału, a kontrolka TextView — liczbę wybraną z widżetu NumberPicker. Aby zdefiniować kontrolkę TextView i widżet NumberPicker, wpisz w pliku układu aktywności activity_number_picker_app.xml kod przedstawiony w listingu 4.3. Listing 4.3. Kod wpisany w pliku układu aktywności activity_number_picker_app.xml
Receptura: wyświetlanie i wybieranie liczb za pomocą widżetu NumberPicker
Rysunek 4.1. Widżet CalendarView wyświetlający kalendarz na bieżący miesiąc (górny obrazek). Okno dialogowe DatePickerDialog otwarte po kliknięciu przycisku Otwórz kontrolkę daty (środkowy obrazek). Widżet CalendarView wyświetlający kalendarz dla daty wybranej z DatePicker (dolny obrazek)
155
156
Rozdział 4. Nowe widżety
Jak widać, kontrolka TextView, do której przypisany został identyfikator numberview, jest inicjowana w celu wyświetlenia tekstu Wybierz liczbę z widżetu NumberPicker. Tekst wyświetlany za pomocą kontrolki TextView będzie prezentowany pogrubioną czcionką o rozmiarze zdefiniowanym w zasobie wymiarów text_size. W celu uzyskania dostępu do kodu Java i identyfikacji tego kodu widżetowi NumberPicker należy przypisać identyfikator numberpicker. W głównym pliku aktywności Java musisz wpisać kod, który będzie wykonywał następujące zadania. Będzie uzyskiwał dostęp do kontrolki TextView i widżetu NumberPicker z pliku
układu i mapował je na odpowiednie obiekty. Będzie ustawiał maksymalne i minimalne wartości liczbowe, które mają być
wyświetlane za pomocą widżetu NumberPicker. Będzie powiązywał z widżetem NumberPicker nasłuchiwacz zdarzeń, który
ma nasłuchiwać, czy zmienia się bieżąca wartość w tym widżecie. Będzie wyświetlał za pomocą kontrolki TextView liczbę wybraną z widżetu
NumberPicker.
Aby wykonać powyższe zadania, wpisz w pliku aktywności Java NumberPickerAppActivity.java kod przedstawiony w listingu 4.4. Listing 4.4. Kod wpisany w pliku aktywności Java NumberPickerAppActivity.java package com.androidtablet.numberpickerapp; import import import import
android.os.Bundle; android.app.Activity; android.widget.NumberPicker; android.widget.TextView;
public class NumberPickerAppActivity extends Activity { TextView numberView; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_number_picker_app); numberView = (TextView)findViewById(R.id.numberview); NumberPicker numberPicker = (NumberPicker) findViewById(R.id.numberpicker); numberPicker.setMaxValue(100); #1 numberPicker.setMinValue(0); #2 numberPicker.setWrapSelectorWheel(true); numberPicker.setOnValueChangedListener( new NumberPicker. OnValueChangeListener() { @Override public void onValueChange(NumberPicker picker, int oldVal, int newVal) { numberView.setText("Wybrałeś liczbę "+ newVal); }
Receptura: wyświetlanie i wybieranie liczb za pomocą widżetu NumberPicker }); } }
Jak możesz zauważyć, z pliku układu uzyskiwany jest dostęp do kontrolki TextView o identyfikatorze numberview, a sama kontrolka jest mapowana na obiekt TextView o nazwie numberView. Analogicznie z pliku układu uzyskiwany jest dostęp do widżetu NumberPicker o identyfikatorze numberpicker, a sam widżet jest mapowany na obiekt NumberPicker o nazwie numberPicker. Minimalne i maksymalne wartości, które maja być wyświetlane za pomocą widżetu NumberPicker, zostały ustawione odpowiednio na 0 i 100. Metoda setWrapSelectorWheel() ma ustawioną wartość true, aby kółko selektora obejmowało minimalne i maksymalne wartości, które są wyświetlane za pomocą widżetu NumberPicker. Kiedy zakres wartości (czyli wartość maksymalna – wartość minimalna) wyświetlany za pomocą widżetu NumberPicker jest większy niż wartość liczbowa wyświetlana w kółku selektora, obejmowanie zakresu jest włączone domyślnie. (Kółko selektora obejmuje maksymalne i minimalne wartości domyślnie). Nasłuchiwacz setOnValueChangedListener jest powiązany z widżetem NumberPicker. Kiedy w tym widżecie zmienia się bieżąca wartość, wywoływana jest metoda wywołania zwrotnego onValueChange. W tej metodzie za pomocą kontrolki TextView wyświetlana jest nowo wybrana liczba z widżetu NumberPicker. Po uruchomieniu danej aplikacji kontrolka TextView będzie wyświetlać użytkownikowi komunikat tekstowy o treści Wybierz liczbę z widżetu NumberPicker. Widżet ten pokazuje przypisaną wartość minimalną w możliwej do edycji formie. Im mniejsza wartość pokazana powyżej, tym większą wartość widać poniżej (patrz rysunek 4.2, górny obrazek). Możesz zmienić liczbę, przewijając w górę lub w dół i klikając mniejszą lub większą wartość pokazaną powyżej lub poniżej. Kiedy klikniesz wybraną liczbę, jest ona wyświetlana za pomocą kontrolki TextView, tak jak pokazano na rysunku 4.2 (dolny obrazek). Za pomocą widżetu NumberPicker możesz wyświetlić dowolny zakres wartości. W celu wyświetlenia np. wartości nieparzystych z zakresu od 1 do 19, możesz zamienić w listingu 4.4 instrukcje #1 i #2 na następujący kod: String[] stringArray = new String[10]; int n=1; for(int i=0; i<10; i++){ stringArray[i] = Integer.toString(n); n+=2; } numberPicker.setMaxValue(stringArray.length-1); numberPicker.setMinValue(0); numberPicker.setDisplayedValues(stringArray);
157
158
Rozdział 4. Nowe widżety
Rysunek 4.2. Widżet NumberPicker wyświetlający liczby od ustalonej wartości minimalnej (górny obrazek) oraz wybrana liczba wyświetlona za pomocą kontrolki TextView (dolny obrazek)
Jak widać, zdefiniowana została tablica String o nazwie stringArray, w której przechowywane są wartości 1, 3, 5... 19. Wartość min widżetu NumberPicker jest ustawiona na 0. Wartość max tego widżetu ma być równa długości stringArray -1, ponieważ chcesz wyświetlić wszystkie elementy tablicy stringArray. Następnie za pomocą metody setDisplayedValues() wartości z tablicy stringArray wyświetlane są przez widżet NumberPicker. Ponieważ bieżący motyw w danej aplikacji Android wywodzi się z motywu Theme_Holo lub Theme_Holo_Light, widżet NumberPicker wygląda tak, jak pokazano na rysunku 4.2 (czyli bieżąca wartość może być edytowana za pomocą mniejszej lub większej wartości wyświetlonej odpowiednio powyżej i poniżej widżetu NumberPicker). Jeśli zmienisz motyw swojej aplikacji, możesz zmienić wygląd widżetu NumberPicker. Przykładowo poniższe instrukcje zastosowane w pliku AndroidManifest.xml spowodują, że bieżący motyw aplikacji wywodził się będzie z motywu Theme.
Receptura: wyświetlanie i wybieranie liczb za pomocą widżetu NumberPicker
Przedstawione instrukcje zmienią motyw aplikacji na Theme.Black.NoTitleBar i dlatego zmianie ulegnie wygląd widżetu NumberPicker. Innymi słowy, widżet NumberPicker wyświetli w edytowalnej postaci bieżącą wartość wraz z przyciskami zwiększania i zmniejszania wartości odpowiednio powyżej i poniżej (patrz rysunek 4.3, górny obrazek). Zmieniona wartość bieżąca zostanie wyświetlona za pomocą kontrolki TextView, tak jak pokazano na rysunku 4.3 (dolny obrazek).
Rysunek 4.3. Po zmianie motywu aplikacji widżet NumberPicker z czarnym tłem, przyciskami zwiększenia i zmniejszania wartości (górny obrazek) oraz wybrana liczba wyświetlona za pomocą kontrolki TextView (dolny obrazek)
159
160
Rozdział 4. Nowe widżety
Receptura: tworzenie stosu obrazów za pomocą widżetu StackView Widżet StackView pomaga aranżować elementy w formie stosu kart, w którym znajdująca się na wierzchu karta może zostać przełożona i odsłoni kartę leżącą pod nią. W stos, poza obrazami, możesz układać również obiekty składające się tekstu i innych danych. Czytając tę recepturę, nauczysz się układać w stos obrazy w widżecie StackView. Utwórz projekt Android o nazwie StackViewApp. Jedyną kontrolką do zdefiniowania w pliku układu aktywności jest widżet StackView. Po zdefiniowaniu tego widżetu plik układu aktywności activity_stack_view_app.xml będzie wyglądał tak, jak pokazano w listingu 4.5. Listing 4.5. Kod wpisany w pliku układu aktywności activity_stack_view_app.xml
Dla celów identyfikacji i uzyskiwania dostępu do widżetu StackView w kodzie Java widżetowi przydzielony został identyfikator stackview. Wartość atrybutu android:animateLayoutChanges została ustawiona na true, więc zmiany pojawiające się w układzie nie będą przeszkadzały w uruchomieniu klasy LayoutTransition. Aby reprezentować element stosu, który chcesz układać w widżecie StackView, musisz w folderze res/layout zdefiniować plik XML. Kliknij prawym przyciskiem myszy folder res/layout w oknie Package Explorer i dodaj plik XML o nazwie item.xml. Ponieważ chcesz układać w stos jedynie obrazy, w pliku item.xml zdefiniowana zostanie tylko kontrolka ImageView. Po zdefiniowaniu tej kontrolki będzie wyglądał tak, jak przedstawiono w listingu 4.6. Listing 4.6. Kod wpisany w pliku item.xml
Receptura: tworzenie stosu obrazów za pomocą widżetu StackView android:layout_height="match_parent" android:src="@drawable/ic_launcher" />
Możesz zauważyć, że kontrolka ImageView, której przypisano identyfikator imageview, jest inicjowana w celu wyświetlenia pliku ic_launcher.png. Ogólnie rzecz biorąc, chcesz wyświetlić pięć obrazów za pomocą kontrolki StackView. Te pięć obrazów to tutaj pliki o nazwach prod1.png, prod2.png, prod3.png, prod4.png oraz prod5.png; skopiuj je do folderów res/drawable. Nadszedł czas, aby w pliku aktywności Java wpisać kod, który będzie: uzyskiwał dostęp do widżetu StackView z pliku układu i mapował ten widżet
na obiekt StackView, definiował tablicę zawierającą identyfikatory zasobów dla obrazów, które
skopiowałeś do folderów res/drawable. Tablica będzie działać jako źródło danych, dostarczając obrazy, które chcesz wyświetlać, definiował niestandardowy adapter o nazwie ImageAdapter rozszerzający klasę
abstrakcyjną BaseAdapter w celu zdefiniowania zawartości, która ma być wyświetlona za pomocą kontrolki StackView, wyświetlał zawartość adaptera (obrazy) za pomocą StackView; wykorzystując
metodę setAdapter(), będzie ustawiał adapter ImageAdapter dla obiektu StackView. Aby wykonać wymienione zadania, w pliku aktywności Java StackViewAppActivity.java wpisz kod przedstawiony w listingu 4.7. Listing 4.7. Kod wpisany w pliku aktywności Java StackViewAppActivity.java package com.androidtablet.stackviewapp; import import import import import import import import import
android.os.Bundle; android.app.Activity; android.content.Context; android.view.LayoutInflater; android.view.View; android.view.ViewGroup; android.widget.ImageView; android.widget.StackView; android.widget.BaseAdapter;
public class StackViewAppActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_stack_view_app); StackView stackView = (StackView)this.findViewById( R.id.stackview); stackView.setAdapter(new ImageAdapter(this)); } public class ImageAdapter extends BaseAdapter { private Context contxt;
161
162
Rozdział 4. Nowe widżety Integer[] images = { R.drawable.prod1, R.drawable.prod2, R.drawable.prod3, R.drawable.prod4, R.drawable.prod5 }; public ImageAdapter(Context c) { contxt = c; } public int getCount() { return images.length; } public Object getItem(int position) { return position; } public long getItemId(int position) { return position; } public View getView(int position, View view, ViewGroup parent) { if (view == null) { LayoutInflater vi = (LayoutInflater) getBaseContext().getSystemService( Context.LAYOUT_INFLATER_SERVICE); view = vi.inflate(R.layout.item, null, false); } ImageView imageView = (ImageView) view.findViewById( R.id.imageview); imageView.setImageResource(images[position]); return view; } } }
Adapter ImageAdapter jest przypisany do kontrolki StackView, aby mogła ona uzyskiwać dostęp do metod tego adaptera w celu wyświetlania zawartości (obrazów). Metody adaptera — getCount(), getItem() oraz getItemId() — są wykorzystywane do określenia liczby obrazów, które mają być wyświetlone, oraz unikatowego identyfikatora konkretnego obrazu. Metoda getView() jest stosowana do pobrania właściwego widoku lub obrazu w określonej pozycji. Uzyskiwany jest dostęp do kontrolki ImageView zdefiniowanej w pliku item.xml, a kontrolka ta wykorzystywana do wyświetlenia obrazów za pomocą StackView. Po uruchomieniu tej aplikacji zobaczysz stos elementów, tu obrazów (patrz rysunek 4.4, lewy obrazek). Kiedy przełożysz obrazek znajdujący się na froncie, obrazki znajdujące się dalej przesuną się ku przodowi, tak jak pokazano na rysunku 4.4 (prawy obrazek).
Receptura: tworzenie stosu obrazów za pomocą widżetu StackView
Rysunek 4.4. Widżet StackView wyświetlający obrazy (lewy obrazek) oraz ukryte obrazy wyświetlone z przodu po przełożeniu frontowych obrazów (prawy obrazek)
Po uruchomieniu tej aplikacji na telefonie rozmiar obrazów może być odpowiedni. Jednak na ekranie tabletu obrazy będą bardzo małe. Aby skalować obrazy zgodnie z rozmiarem ekranu urządzenia, musisz zmodyfikować plik item.xml. Otwórz plik item.xml znajdujący się w folderze res/layout i zmodyfikuj go według listingu 4.8. Zmodyfikowane zostały jedynie fragmenty kodu zaznaczone pogrubioną czcionką. Reszta pozostaje taka sama jak w listingu 4.6. Listing 4.8. Kod wpisany w pliku item.xml
163
164
Rozdział 4. Nowe widżety
Obraz (lub obrazy), który będzie wyświetlany za pomocą widżetu StackView, ma przypisaną szerokość i wysokość za pomocą zasobów wymiarów odpowiednio image_width i image_height. Aby zdefiniować zasoby wymiarów image_width i image_height, otwórz plik dimens.xml znajdujący się w folderze res/values. Zakładamy, że plik wymiarów dimens.xml istnieje już w folderze res/values Twojej aplikacji. Zakładamy również, że zistnieją w folderze res dwa foldery o nazwach values-sw600dp i values-sw720dp i oba te foldery zawierają plik wymiarów o nazwie dimens.xml. Aby zdefiniować szerokość i wysokość dla aplikacji uruchamianej na telefonie, otwórz plik dimens.xml znajdujący się w folderze res/values i wpisz w nim następujący kod. 100dp 200dp
Jak możesz zauważyć, na telefonie widżet StackView będzie wyświetlał obrazy o szerokości i wysokości odpowiednio 100 dp i 200 dp. Następnie w celu zdefiniowania szerokości i wysokości dla obrazów w aplikacji uruchamianej na 7-calowym tablecie otwórz plik wymiarów dimens.xml znajdujący się w folderze res/values-sw600dp i wpisz w nim następujący kod. 200dp 300dp
Powyższy kod będzie przydzielał szerokość 200 dp oraz wysokość 300 dp obrazom wyświetlanym za pomocą widżetu StackView na 7-calowych tabletach. W celu zdefiniowania wymiarów obrazów dla 10-calowych tabletów otwórz plik wymiarów dimens.xml znajdujący się w folderze res/values-sw720dp i wpisz w nim następujący kod. 300dp 400dp
Powyższy kod sprawi, że w aplikacji uruchamianej na 10-calowych tabletach obrazy w widżecie StackView będą miały szerokość 300 dp i wysokość 400 dp. Po uruchomieniu tej aplikacji na 10-calowym tablecie widżet StackView będzie wyglądał tak, jak pokazano na rysunku 4.5 (lewy obrazek). Porównując rysunek 4.5 z rysunkiem 4.4 (górny obrazek), możesz zauważyć, że na tablecie obrazy są większe i wyraźniejsze. Kiedy przełożysz frontowy obrazek, obrazy znajdujące się z tyłu przesuną się do przodu, co zostało pokazane na rysunku 4.5 (prawy obrazek).
Receptura: wyświetlanie listy opcji za pomocą widżetu ListPopupWindow
Rysunek 4.5. Widżet StackView wyświetlający powiększone obrazy (po lewej). Obrazy z tyłu przesuwają się do przodu, kiedy przekładasz frontowy obraz (po prawej)
Receptura: wyświetlanie listy opcji za pomocą widżetu ListPopupWindow Możesz użyć widżetu ListPopupWindow, aby zakotwiczyć go w widoku hosta i wyświetlić listę opcji. Po przeczytaniu tej receptury będziesz umiał kotwiczyć widżet ListPopupWindow w kontrolce EditText. Kiedy użytkownik kliknie kontrolkę EditText, pojawi się widżet ListPopupWindow wyświetlający listę opcji. Po wybraniu przez użytkownika opcji z ListPopupWindow opcja ta zostanie przypisana do kontrolki EditText. Utwórz nowy projekt Android o nazwie ListPopupWindowApp. Skoro chcesz zakotwiczyć widżet ListPopupWindow w kontrolce EditText, zdefiniuj tę kontrolkę w pliku układu. Po jej zdefiniowaniu plik układu aktywności activity_list_popup_window_app.xml będzie wyglądał tak, jak przedstawiono w listingu 4.9. Listing 4.9. Kod wpisany w pliku układu aktywności activity_list_popup_window_app.xml
165
166
Rozdział 4. Nowe widżety android:layout_width="match_parent" android:layout_height="match_parent">
Jak możesz zauważyć, kontrolce EditText został przypisany identyfikator product_name. W tej aplikacji będziesz w kontrolce EditText prosił użytkownika o wpisanie nazwy produktu. W kontrolce wyświetlany jest tekst Wprowadź nazwę produktu. Tekst ten będzie prezentowany czcionką o rozmiarze zdefiniowanym w zasobie wymiarów text_size. Domyślny rozmiar elementów listy wyświetlanych w kontrolce ListView jest odpowiedni dla telefonów, ale za mały dla tabletów. Aby zmienić rozmiar elementów listy kontrolki ListView zgodnie z rozmiarem ekranu danego urządzenia, dodaj w pliku res/layout jeszcze jeden plik XML o nazwie list_item.xml. W tym pliku wpisz kod przedstawiony w listingu 4.10. Listing 4.10. Kod wpisany w pliku list_item.xml
Zgodnie z powyższym kodem elementy listy kontrolki ListView zostaną oddzielone spacjami o szerokości 6 dp i będą wyświetlane pogrubioną czcionką o rozmiarze zdefiniowanym w zasobie wymiarów text_size. Następnie musisz napisać kod Java, który wykona następujące zadania. Będzie uzyskiwał dostęp do kontrolki EditText z pliku układu i mapował ją
na obiekt EditText. Będzie definiował obiekt widżetu ListPopupWindow. Będzie definiował adapter ArrayAdapter i wiązał go z listą produktów,
które chcesz wyświetlić za pomocą widżetu ListPopupWindow. Będzie kojarzył adapter ArrayAdapter z widżetem ListPopupWindow w celu
wyświetlenia listy produktów zdefiniowanej w tym adapterze. Będzie ustawiał wysokość i szerokość widżetu ListPopupWindow.
Receptura: wyświetlanie listy opcji za pomocą widżetu ListPopupWindow Będzie przypisywał naturę modalną do widżetu ListPopupWindow (co oznacza,
że kontrolka nie będzie wracać do elementu wywołującego, dopóki widżet ListPopupWindow nie zostanie zwolniony). Widżet ListPopupWindow może zostać zwolniony przez wybranie produktu z ListPopupWindow lub kliknięcie dowolnego obszaru poza widżetem ListPopupWindow. Będzie kotwiczył ListPopupWindow w kontrolce EditText. Będzie wiązał nasłuchiwacz setOnItemClickListener z kontrolką EditText,
aby po kliknięciu tej kontrolki otwierał widżet ListPopupWindow pokazujący listę produktów. Będzie ustawiał klasę aktywności w taki sposób, aby implementowała
nasłuchiwacz OnItemClickListener. Wybrana z ListPopupWindow opcja ma być przydzielana do kontrolki EditText. Aby wykonać wymienione zadania, wpisz w głównym pliku aktywności Java ListPopupWindowAppActivity.java kod przedstawiony w listingu 4.11. Listing 4.11. Kod wpisany w pliku aktywności Java ListPopupWindowAppActivity.java package com.androidtablet.listpopupwindowapp; import import import import import import import import import
android.os.Bundle; android.app.Activity; android.widget.ListPopupWindow; android.view.View; android.widget.ArrayAdapter; android.widget.EditText; android.widget.AdapterView.OnItemClickListener; android.widget.AdapterView; android.view.View.OnClickListener;
public class ListPopupWindowAppActivity extends Activity implements OnItemClickListener { EditText productName; ListPopupWindow listPopupWindow; String[] products={"Aparat", "Laptop", "Zegarek","Smartfon", "Telewizor"}; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_list_popup_window_app); productName = (EditText) findViewById( R.id.product_name); listPopupWindow = new ListPopupWindow( ListPopupWindowAppActivity.this); listPopupWindow.setAdapter(new ArrayAdapter( ListPopupWindowAppActivity.this, R.layout.list_item, products)); listPopupWindow.setAnchorView(productName); listPopupWindow.setWidth(300);
167
168
Rozdział 4. Nowe widżety listPopupWindow.setHeight(400); listPopupWindow.setModal(true); listPopupWindow.setOnItemClickListener( ListPopupWindowAppActivity.this); productName.setOnClickListener(new OnClickListener() { public void onClick(View v) { listPopupWindow.show(); } }); } @Override public void onItemClick(AdapterView> parent, View view, int position, long id) { productName.setText(products[position]); listPopupWindow.dismiss(); } }
Jak możesz zauważyć, w powyższym kodzie użyty został adapter ArrayAdapter, który działa jak źródło danych dla widżetu ListPopupWindow. Adapter ArrayAdapter wykorzystuje kontrolkę TextView do reprezentowania w widoku widoków potomnych (czyli wyświetla elementy tablicy products za pomocą kontrolki TextView). Wykorzystany wcześniej konstruktor ArrayAdapter składa się z niżej wymienionych elementów. ListPopupWindowAppActivity.this — bieżący kontekst. R.layout.list_item — wskazuje kontrolkę TextView, którą zdefiniowałeś
w pliku list_item.xml. Kontrolka TextView będzie wykorzystana do wyświetlenia każdego z elementów w widżecie ListPopupWindow. Elementy tablicy products są pakowane w widok przed tym, zanim zostaną przypisane do danego widżetu w celu wyświetlenia. Dlatego też R.layout.list_item po prostu zamienia ciągi zdefiniowane w tablicy products w kontrolkę TextView w celu wyświetlenia w widżecie ListPopupWindow. products — działa jako źródło danych.
Po uruchomieniu tej aplikacji otrzymujesz kontrolkę EditText z komunikatem Wprowadź nazwę produktu (patrz rysunek 4.6, górny obrazek). Kliknij tę kontrolkę, a wyświetlony zostanie widżet ListPopupWindow z listą produktów (patrz rysunek 4.6, środkowy obrazek). Produkt wybrany z ListPopupWindow pojawi się w kontrolce EditText (patrz rysunek 4.6, dolny obrazek).
Receptura: wyświetlanie listy opcji za pomocą widżetu ListPopupWindow
Rysunek 4.6. Kontrolka EditText z prośbą o podanie nazwy produktu (górny obrazek). Pokazujący dostępne opcje widżet ListPopupWindow, który został wyświetlony po kliknięciu kontrolki EditText (środkowy obrazek). Produkt wybrany z ListPopupWindow pojawia się w kontrolce EditText (dolny obrazek)
169
170
Rozdział 4. Nowe widżety
Receptura: sugerowanie opcji za pomocą widżetu PopupMenu Widżet PopupMenu wyświetla menu w modalnym wyskakującym okienku (ang. pop-up window). Możesz zakotwiczyć go w widoku i wykorzystać do wyświetlania wymaganych elementów menu lub opcji. W tej recepturze zakotwiczymy widżet PopupMenu w kontrolce EditText w celu wyświetlania sugestii podczas wpisywania danych w tej kontrolce. Różnica pomiędzy poprzednią recepturą a tą jest taka, że lista opcji jest wyświetlana za pomocą widżetu PopupMenu, a nie ListPopupWindow. Utwórz nowy projekt Android o nazwie PopupMenuApp. Ponieważ chcesz zakotwiczyć PopupMenu w kontrolce EditText, należy ją zdefiniować w pliku układu activity_popup_menu_app.xml, wykorzystując kod przedstawiony w listingu 4.12. Listing 4.12. Kod wpisany w pliku układu aktywności activity_popup_menu_app.xml
Jak możesz zauważyć, kontrolka EditText, której przypisano identyfikator product_name, jest skonfigurowana do wyświetlania komunikatu Wprowadź nazwę produktu. Elementy menu lub opcje dla widżetu PopupMenu będziesz definiował w pliku XML. Innymi słowy, menu będzie wypełniane za pomocą pliku XML. Do folderu res/menu dodaj plik XML o nazwie popupmenu.xml. Ponieważ chcesz wyświetlić nazwy produktów w formie sugestii w kontrolce EditText, musisz zdefiniować elementy menu w postaci nazw produktów w pliku popupmenu.xml. Te elementy menu są definiowane w pliku popupmenu.xml w sposób przedstawiony w listingu 4.13. Listing 4.13. Kod wpisany w pliku popupmenu.xml
Receptura: sugerowanie opcji za pomocą widżetu PopupMenu android:title="Zegarek" android:textSize="@dimen/text_size" />
Jak możesz zauważyć, produkty Aparat, Laptop, Zegarek, Smartfon i Telewizor są zdefiniowane jako elementy menu w pliku popupmenu.xml. Każdej nazwie produktu przypisany został również unikatowy identyfikator. Musisz teraz napisać kod Java wykonujący zadania, takie jak: uzyskanie dostępu do kontrolki EditText zdefiniowanej w pliku układu
i zmapowanie jej na obiekt EditText, zdefiniowanie obiektu PopupMenu i wypełnienie elementów menu lub nazw
produktów zdefiniowanych w pliku popupmenu.xml w celu wyświetlenia za pomocą widżetu PopupMenu, powiązanie nasłuchiwacza setOnClickListener z kontrolką EditText w celu
nasłuchiwania zdarzeń kliknięcia w tej kontrolce, wyświetlenie PopupMenu, kiedy użytkownik kliknie kontrolkę EditText, powiązanie nasłuchiwacza setOnMenuItemClickListener z PopupMenu, przypisanie elementów menu (produktów) do kontrolki EditText, kiedy któryś
z nich zostanie wybrany z PopupMenu. Aby wykonać wymienione zadania, wpisz w głównym pliku aktywności Java PopupMenuAppActivity.java kod przedstawiony w listingu 4.14. Listing 4.14. Kod wpisany w pliku aktywności Java PopupMenuAppActivity.java package com.androidtablet.popupmenuapp; import import import import import import import
android.os.Bundle; android.app.Activity; android.widget.EditText; android.view.View.OnClickListener; android.view.View; android.widget.PopupMenu; android.view.MenuItem;
public class PopupMenuAppActivity extends Activity { EditText productName; PopupMenu popupMenu; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState);
171
172
Rozdział 4. Nowe widżety setContentView(R.layout.activity_popup_menu_app); productName = (EditText) findViewById( R.id.product_name); popupMenu = new PopupMenu(PopupMenuAppActivity.this, productName); popupMenu.getMenuInflater().inflate( R.menu.popupmenu, popupMenu.getMenu()); productName.setOnClickListener(new OnClickListener() { public void onClick(View v) { popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { @Override public boolean onMenuItemClick(MenuItem item) { productName.setText(item.toString()); return true; } }); popupMenu.show(); } }); } }
Po uruchomieniu tej aplikacji najpierw pojawia się kontrolka EditText. Wyświetla ona komunikat z prośbą, aby użytkownik wprowadził nazwę produktu (patrz rysunek 4.7, górny obrazek). Kiedy użytkownik kliknie kontrolkę EditText, pojawi się widżet PopupMenu wyświetlający nazwy produktów w formie elementów menu (patrz rysunek 4.7, środkowy obrazek). Użytkownik wybiera z PopupMenu produkt, który jest przypisywany do kontrolki EditText (patrz rysunek 4.7, dolny obrazek). Możesz zauważyć, że PopupMenu pojawia się pod widokiem kotwicy (kontrolką EditText), ponieważ pod tą kontrolką znajduje się dużo miejsca. Jeśli nie byłoby wystarczająco dużo wolnej przestrzeni pod kontrolką, widżet PopupMenu pojawiłby się ponad widokiem kotwicy.
Podsumowanie W tym rozdziale nauczyłeś się wyświetlać w aplikacji Android kalendarz za pomocą widżetu CalendarView i zobaczyłeś, w jaki sposób wyświetlana jest data wybrana z tego kalendarza. Dowiedziałeś się również, jak wyświetlać zakres liczb za pomocą widżetu NumberPicker. Poznałeś procedurę wyświetlania stosu obrazów przy wykorzystaniu widżetu StackView. Na koniec nauczyłeś się wyświetlać listę opcji za pomocą widżetu ListPopupWindow oraz wyświetlać sugestię przy użyciu widżetu PopupMenu. Kolejny rozdział koncentruje się na przedstawieniu klas ClipData i DragEvent. Dowiesz się, czym jest schowek systemowy oraz poznasz procedurę przeciągania i upuszczania tekstu i obrazów.
Podsumowanie
Rysunek 4.7. Kontrolka EditText z prośbą o podanie nazwy produktu (górny obrazek). Pokazujący dostępne opcje widżet PopupMenu, który został wyświetlony po kliknięciu kontrolki EditText (środkowy obrazek). Produkt wybrany z PopupMenu pojawia się w kontrolce EditText (dolny obrazek)
173
174
Rozdział 4. Nowe widżety
Część II Zarządzanie zawartością
5 Schowek systemowy oraz operacja przeciągnij i upuść O
peracja przeciągnij i upuść (ang. drag and drop) jest typowa dla prawie wszystkich aplikacji. Jest ona często wykorzystywana bez względu na to, czy chodzi o wybranie produktu do zakupu w sklepie online, czy też aranżację elementów w określonej kolejności. W tym rozdziale nauczysz się wykonywać operację przeciągnij i upuść na tekście, a także na obrazach. Dowiesz się, jak należy nasłuchiwać zdarzeń przeciągania, jak tworzy się cień przeciągania oraz dodaje upuszczony widok do widoku docelowego. Nauczysz się również wykorzystywać schowek systemowy do implementacji w aplikacjach Android operacji wytnij, kopiuj i wklej.
Receptura: operacja przeciągnij i upuść Operacja przeciągnij i upuść odnosi się do procedury kliknięcia i przeniesienia jednego widoku do innego w bieżącej aktywności. Widoki te mogą być w różnych układach. Aby wybrać widok do przeciągnięcia, zazwyczaj przyciskasz ten widok. Kiedy przeciągasz dany widok, wyświetlany jest cień reprezentujący czynność przeciągania widoku. Operacja przeciągania kończy się, kiedy przeciągany cień jest uwalniany w strefach upuszczania. Strefy upuszczania (ang. drop zones) odnoszą się do widoków, które mają akceptować widok przeciągany. Podczas wykonywania operacji przeciągnij i upuść zdarzenie przeciągania jest wysyłane do nasłuchiwacza tych zdarzeń lub metod wywołania zwrotnego wszystkich widoków w bieżącym układzie. Kiedy więc implementujesz operację przeciągnij i upuść, musisz zdefiniować widoki dla stref upuszczania lub miejsce, w którym chcesz upuszczać przeciągane widoki. Ponadto musisz też zdefiniować obiekty nasłuchiwacza zdarzeń przeciągania. Kiedy dane są upuszczane nad obiektem View, zostają wysyłane do nasłuchiwacza lub metody wywołania zwrotnego w zdarzeniu przeciągania.
178
Rozdział 5. Schowek systemowy oraz operacja przeciągnij i upuść
Uwaga Każdy obiekt View posiada własną metodę wywołania zwrotnego onDragEvent().
Do przeprowadzania operacji przeciągnij i upuść wykorzystywane są następujące klasy. ClipData — obiekt zawierający widok, który ma zostać przekazany do widoków
stref upuszczania lub w nich upuszczony. DragEvent — obiekt generowany podczas przeciągania widoku. DragShadowBuilder — wyświetla graficzną reprezentację procesu przeciągania
widoku. Jeśli ta klasa nie jest zaimplementowana, domyślnie pojawi się reprezentacja źródłowego widoku. Kiedy rozpoczyna się operacja przeciągania, wywoływana jest metoda startDrag(). Metoda ta inicjuje generowanie zdarzeń przeciągania. Metoda startDrag() może być wywoływana dla dowolnego widoku w bieżącym układzie. Widok może odbierać zdarzenie przeciągania za pomocą nasłuchiwacza zdarzeń przeciągania, który implementuje interfejs View.OnDragListener lub przy użyciu własnej metody wywołania zwrotnego onDragEvent(DragEvent). Zdarzenie przeciągania jest generowane w postaci obiektu DragEvent. Obiekt ten zawiera typ akcji, który informuje nasłuchiwacz o konkretnej akcji odbywającej w procesie przeciągnij i upuść. Metodą wykorzystywaną do poznania typu akcji jest getAction(). W klasie DragEvent zdefiniowanych jest z wykorzystaniem stałych sześć możliwych typów akcji, które zostały zestawione w tabeli 5.1. Tabela 5.1. Krótki opis stałych dla klasy DragEvent
Typ akcji
Opis
DragEvent.ACTION_DRAG_STARTED
Wskazuje, że rozpoczęło się zdarzenie przeciągania.
DragEvent.ACTION_DRAG_ENTERED
Wskazuje, że cień przeciągania wkroczył w pole ograniczające widok docelowy (widok gotowy do zaakceptowania widoku przeciąganego).
DragEvent.ACTION_DRAG_EXITED
Wskazuje, że cień przeciągania został przeciągnięty poza pole ograniczające widok docelowy.
DragEvent.ACTION_DRAG_LOCATION
Wskazuje, że operacja przeciągania jest w trakcie, a cień przeciągania jest wewnątrz pola ograniczającego widok docelowy.
DragEvent.ACTION_DROP
Wskazuje, że cień przeciągania został uwolniony lub upuszczony w obrębie pola ograniczającego widok docelowy.
DragEvent.ACTION_DRAG_ENDED
Wskazuje, że operacja przeciągania została zakończona, a cień przeciągania został uwolniony poza polem ograniczającym widok docelowy.
Receptura: przeciąganie i upuszczanie tekstu
W trakcie procesu przeciągnij i upuść mają miejsce następujące konkretne zdarzenia. Kiedy użytkownik rozpoczyna operację przeciągania, wywoływana jest metoda
startDrag(), która z kolei generuje zdarzenie przeciągania. Zdarzenie przeciągania z typem akcji ACTION_DRAG_STARTED jest wysyłane do nasłuchiwacza zdarzeń przeciągania dla wszystkich obiektów View w bieżącym układzie. Takie
zdarzenie przeciągania zawiera dane, które są przekazywane do metody startDrag() w trakcie rozpoczynania operacji przeciągania. Jedynie te nasłuchiwacze zdarzeń przeciągania, które zwracają wartość logiczną
true (prawda), są w stanie odbierać zdarzenia przeciągania. Nasłuchiwacze zdarzeń przeciągania zwracające wartość false (fałsz) nie będą odbierać zdarzeń
przeciągania dla bieżącej operacji. Oznacza to, że dopóki bieżąca operacja przeciągnij i upuść nie zostanie ukończona, te nasłuchiwacze zdarzeń przeciągania nie będą w stanie odbierać zdarzeń przeciągania. Po zakończeniu operacji przeciągnij i upuść generowane jest zdarzenie przeciągania z typem akcji ACTION_DRAG_ENDED. Nasłuchiwacz zdarzeń przeciągania może zmieniać wygląd swojego obiektu
View w odpowiedzi na określone zdarzenie. Kiedy przykładowo cień przeciągania
wkracza w pole ograniczające widok docelowy, nasłuchiwacz zdarzeń przeciągania może podświetlić ten widok, aby wskazać, że jest on gotowy na przyjęcie widoku przeciąganego. Kiedy cień przeciągania wkracza w pole ograniczające widok docelowy, generowane jest zdarzenie przeciągania z typem akcji ACTION_DRAG_ENTERED. Gdy cień przeciągania jest uwalniany w obrębie pola ograniczającego widok docelowy, generowane jest zdarzenie przeciągania z typem akcji ACTION_DROP. Jeśli cień przeciągania jest uwalniany w innej lokalizacji niż widok docelowy, nie jest generowane żadne zdarzenie przeciągania ACTION_DROP. Po wygenerowaniu zdarzenia przeciągania z typem akcji ACTION_DROP generowane jest z kolei zdarzenie przeciągania z typem akcji ACTION_DRAG_ENDED, które wskazuje, że zakończyła się operacja przeciągnij i upuść. Kiedy rozpoczyna sie zdarzenie przeciągania, możesz utworzyć reprezentację przeciąganego widoku, rozszerzając klasę DragShadowBuilder.
Receptura: przeciąganie i upuszczanie tekstu W trakcie lektury tej receptury nauczysz się przeciągać i upuszczać tekst. Utworzysz dwie kontrolki ListView. Jedna będzie wyświetlać pewne nazwy elementów, a druga będzie pusta. Kiedy klikniesz i przeciągniesz dowolną nazwę elementu wyświetloną w pierwszej kontrolce ListView, a potem upuścisz ją w drugiej kontrolce ListView, dana nazwa elementu zostanie dodana do tej drugiej kontrolki. Możesz kliknąć i przeciągnąć dowolną ilość tekstu lub dowolną liczbę nazw elementów z jednej kontrolki ListView do drugiej.
179
180
Rozdział 5. Schowek systemowy oraz operacja przeciągnij i upuść
Aby zrozumieć działanie całej procedury w praktyce, utwórz projekt Android o nazwie DragDropListApp. W tej aplikacji potrzebujesz dwóch kontrolek TextView i dwóch kontrolek ListView. Kontrolka ListView zawierająca pewien tekst do przeciągnięcia będzie miała nazwę Source List, a ta, która jest pusta, będzie się nazywać Target List. Każda z kontrolek TextView będzie wyświetlana nad jedną z kontrolek ListView, by wskazać jej typ poniżej. Aby zdefiniować dwie kontrolki TextView i dwie kontrolki ListView, wpisano w pliku układu aktywności activity_drag_drop_list_app.xml kod przedstawiony w listingu 5.1. Pary kontrolek TextView i ListView zostały zawarte w kontenerach LinearLayout, aby je uszeregować. Oba kontenery LinearLayout są zdefiniowane z orientacją pionową, w której kontrolki TextView i ListView są wyświetlane jedna pod drugą. Oba kontenery LinearLayout są z kolei zagnieżdżone w zewnętrznym kontenerze LinearLayout z orientacją poziomą, co ma na celu wyświetlenie wspomnianych dwóch kontenerów LinearLayout obok siebie. Listing 5.1. Kod wpisany w pliku układu aktywności activity_drag_drop_list_app.xml
Receptura: przeciąganie i upuszczanie tekstu android:id="@+id/targetlist" android:layout_width="match_parent" android:layout_height="wrap_content"/>
Jak możesz zauważyć, dwóm kontrolkom ListView przypisano identyfikatory sourcelist i targetlist. Dwie kontrolki TextView widoczne nad kontrolkami ListView są inicjowane w celu wyświetlenia odpowiednio tekstów Lista źródłowa i Lista docelowa. Jest to informacja, czy kontrolka ListView wyświetlana poniżej jest kontrolką źródłową, czy docelową. Tekst wyświetlany za pomocą kontrolek TextView będzie prezentowany czcionką o rozmiarze zdefiniowanym w zasobie wymiarów text_size. Kontenerowi LinearLayout zawierającemu docelową kontrolkę ListView przypisano identyfikator targetlayout. Domyślny rozmiar elementów listy wyświetlanych w kontrolce ListView jest odpowiedni dla telefonów, ale za mały dla tabletów. Aby zmienić rozmiar elementów listy kontrolki ListView według rozmiaru ekranu danego urządzenia, dodaj do folderu res/layout kolejny plik XML o nazwie list_item.xml. Wpisz w tym pliku następujący kod:
Powyższy kod wypełni elementy listy kontrolki ListView spacjami o szerokości 6 dp. Tekst będzie prezentowany pogrubioną czcionką o rozmiarze zdefiniowanym w zasobie wymiarów text_size. Następnie musisz napisać kod Java, który będzie wykonywał następujące zadania. Będzie definiował tablicę oraz adapter ArrayAdapter w celu wyświetlania
elementów w źródłowej kontrolce ListView. Będzie powiązywał nasłuchiwacz setOnItemLongClickListener ze źródłową
kontrolką ListView w celu nasłuchiwania długich kliknięć, które występują podczas wybierania elementów z tej kontrolki. Będzie definiował niestandardową klasę DragEventListener, która
implementuje interfejs View.OnDragListener w celu nasłuchiwania zdarzeń przeciągania (DragEvent) i podejmowania niezbędnych działań. Będzie definiował niestandardową klasę ShadowBuilder, która rozszerza
View.DragShadowBuilder w celu zdefiniowania wymiarów cienia przeciągania. Będzie dodawał do docelowej kontrolki ListView element, który jest nad nią
upuszczany.
181
182
Rozdział 5. Schowek systemowy oraz operacja przeciągnij i upuść
Aby wykonać wymienione zadania, wpisz w głównym pliku aktywności Java DragDropListAppActivity.java kod przedstawiony w listingu 5.2. Listing 5.2. Kod wpisany w pliku aktywności Java DragDropListAppActivity.java package com.androidtablet.dragdroplistapp; import import import import import import import import import import import import import import import import import import import import
android.os.Bundle; android.app.Activity; java.util.ArrayList; java.util.List; android.content.ClipData; android.content.ClipDescription; android.graphics.Canvas; android.graphics.Color; android.graphics.Point; android.graphics.drawable.ColorDrawable; android.graphics.drawable.Drawable; android.view.DragEvent; android.view.View; android.view.View.DragShadowBuilder; android.widget.AdapterView; android.widget.AdapterView.OnItemLongClickListener; android.widget.ArrayAdapter; android.widget.LinearLayout; android.widget.ListView; android.util.Log;
public class DragDropListAppActivity extends Activity { LinearLayout targetLayout; ListView sourceListView, targetListView; DragEventListener dragEventListener = new DragEventListener(); String[] food ={"Pizza","Hot dog","Chińska zupka","Hamburger", "Kanapka","Frytki","Napój chłodzący","Lody" }; List targetArrayList; ArrayAdapter targetAdapter; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_drag_drop_list_app); sourceListView = (ListView)findViewById( R.id.sourcelist); targetListView = (ListView)findViewById( R.id.targetlist); targetLayout = (LinearLayout)findViewById( R.id.targetlayout); sourceListView.setTag("Źródłowa kontrolka ListView"); targetListView.setTag("Docelowa kontrolka ListView"); targetLayout.setTag("Docelowy układ"); sourceListView.setAdapter(new ArrayAdapter( this, R.layout.list_item, food)); sourceListView.setOnItemLongClickListener( sourceListItemLongClickListener); targetArrayList = new ArrayList();
#1 #2 #3 #4 #5 #6 #7 #8
Receptura: przeciąganie i upuszczanie tekstu targetAdapter = new ArrayAdapter(this, R.layout.list_item, targetArrayList); targetListView.setAdapter(targetAdapter); sourceListView.setOnDragListener(dragEventListener); targetLayout.setOnDragListener(dragEventListener);
#9 #10
} OnItemLongClickListener sourceListItemLongClickListener = new OnItemLongClickListener(){ @Override public boolean onItemLongClick(AdapterView> l, View v, int position, long id) { ClipData.Item foodItem = new ClipData.Item( food[position]); String[] clipDescription = {ClipDescription. MIMETYPE_TEXT_PLAIN}; ClipData dragData = new ClipData((CharSequence) v.getTag(), clipDescription, foodItem); DragShadowBuilder foodItemShadow = new ShadowBuilder(v); v.startDrag(dragData, foodItemShadow, food[position], 0); return true; } }; private static class ShadowBuilder extends View.DragShadowBuilder { private static Drawable shadow; public ShadowBuilder(View v) { super(v); shadow = new ColorDrawable(Color.CYAN); } @Override public void onProvideShadowMetrics (Point size, Point touch){ int width = getView().getWidth(); int height = getView().getHeight(); shadow.setBounds(0, 0, width, height); size.set(width, height); touch.set(width / 2, height / 2); } @Override public void onDrawShadow(Canvas canvas) { shadow.draw(canvas); }
#11 #12
#13 #14
#15
#16
} protected class DragEventListener implements View.OnDragListener { @Override public boolean onDrag(View v, DragEvent event) { switch(event.getAction()) { case DragEvent.ACTION_DRAG_STARTED: if (event.getClipDescription().hasMimeType(
#17 #18 #19
183
184
Rozdział 5. Schowek systemowy oraz operacja przeciągnij i upuść ClipDescription. MIMETYPE_TEXT_PLAIN)) { Log.d((String) v.getTag(), "akcja ACTION_DRAG_STARTED zaakceptowana"); return true; } else{ Log.d((String) v.getTag(), "akcja ACTION_DRAG_STARTED odrzucona"); return false; } case DragEvent.ACTION_DRAG_ENTERED: Log.d((String) v.getTag(), "akcja ACTION_DRAG_ENTERED"); return true; case DragEvent.ACTION_DRAG_LOCATION: Log.d((String) v.getTag(), "akcja ACTION_DRAG_LOCATION " + event.getX() + " : " + event.getY()); return true; case DragEvent.ACTION_DRAG_EXITED: Log.d((String) v.getTag(), "akcja ACTION_DRAG_EXITED"); return true; case DragEvent.ACTION_DROP: ClipData.Item foodItem = event.getClipData().getItemAt(0); Log.d((String) v.getTag(), "akcja ACTION_DROP"); if(v == targetLayout){ String droppedItem = foodItem.getText(). toString(); Log.d("Upuszczony przedmiot to ", droppedItem); targetArrayList.add(droppedItem); targetAdapter.notifyDataSetChanged(); return true; } else return false; case DragEvent.ACTION_DRAG_ENDED: if (event.getResult()) Log.d((String) v.getTag(), "akcja ACTION_DRAG_ENDED powiodła się"); else Log.d((String) v.getTag(), "Niepowodzenie akcji: ACTION_DRAG_ENDED"); return true; default: Log.d((String) v.getTag(), "Akcja nieznana"); return false; } } } }
Receptura: przeciąganie i upuszczanie tekstu
Oto sposób działania kodu przedstawionego w listingu 5.2. Instrukcje #1 i #2 uzyskują dostęp do kontrolek ListView z pliku układu,
posiadających identyfikatory sourcelist oraz targetlist i mapują te kontrolki na obiekty ListView o nazwach odpowiednio sourceListView oraz targetListView. Instrukcja #3 uzyskuje dostęp do docelowego kontenera LinearLayout z pliku
układu, przechowującego docelową kontrolkę ListView, i mapuje ten kontener na obiekt LinearLayout o nazwie targetLayout. Instrukcje #4, #5 i #6 kojarzą metadane w formie znaczników z dwoma
kontrolkami ListView oraz docelowym kontenerem LinearLayout. Znaczniki te pomagają wyświetlać informacje na temat widoków i kontenerów zaangażowanych w operację przeciągnij i upuść. Instrukcja #7 definiuje tablicę wraz z adapterem ArrayAdapter i ustawia ją
dla kontrolki źródłowej ListView, która ma wyświetlać określone elementy. Elementy wyświetlane za pomocą źródłowej kontrolki ListView są tymi, które użytkownik może przeciągnąć i upuścić w docelowej kontrolce ListView. Instrukcja #8 kojarzy nasłuchiwacz setOnItemLongClickListener ze źródłową
kontrolką ListView. Kiedy któryś z elementów ze źródłowej kontrolki ListView zostanie kliknięty i przytrzymany (długie kliknięcie), wywoływana jest metoda wywołania zwrotnego onItemLongClick. Instrukcje #9 i #10 kojarzą nasłuchiwacz setOnDragListener ze kontrolkami
ListView źródłową i docelową. W metodzie onItemLongClick (instrukcja #11) wywoływanej, kiedy któryś
z elementów zostanie kliknięty i przytrzymany, definiujesz obiekt ClipData o nazwie dragData, który ma reprezentować element wybrany ze źródłowej kontrolki ListView. Do zdefiniowania obiektu ClipData wymagane są trzy rzeczy: znacznik lub metadane widoku, typ MIME opisujący dane oraz tekst przeciąganego elementu. Z tego powodu definiowana jest tablica ciągów znaków o nazwie clipDescription, w której przechowywany jest typ MIME opisujący dane z wycinka. Instrukcja #12 definiuje obiekt DragShadowBuilder o nazwie foodItemShadow
w celu wyświetlenia cienia przeciągania lub obrazu elementu przeciąganego w trakcie operacji przeciągnij i upuść. Domyślnie cień przeciągania reprezentuje obraz, który przypomina przeciągany element. Aby utworzyć cień przeciągania, musisz zdefiniować niestandardową klasę
ShadowBuilder (instrukcja #13), która rozszerza klasę View.DragShadowBuilder. Konstruktor klasy ShadowBuilder (instrukcja #14) tworzy obraz cienia elementu
w kolorze CYAN (cyjan). Domyślnie wymiary oraz wygląd cienia będą takie same jak przeciągany widok i punkt kontaktu nad centrum widoku.
185
186
Rozdział 5. Schowek systemowy oraz operacja przeciągnij i upuść Metoda onProvideShadowMetrics() (instrukcja #15) jest nadpisywana w celu
określenia metryk dla cienia przeciągania. Oznacza to, że możesz za pomocą tej metody określić wymiary cienia przeciągania oraz punktu, który powinien być umieszczony centralnie pod punktem kontaktu podczas przeciągania. W tej metodzie obliczasz szerokość i wysokość przeciąganego widoku oraz używasz metody setBounds() do ustawienia rozmiaru cienia przeciągania. Rozmiar cienia przeciągania został ustawiony w taki sposób, aby był taki sam jak szerokość i wysokość przeciąganego widoku. Ponadto punkt kontaktu został zdefiniowany jako środek widoku w celu wyśrodkowania cienia pod punktem dotyku. Wymiary widoku obliczone w tej metodzie są wykorzystywane przy konstrukcji obiektu Canvas. Dlatego też metoda onDrawShadow() (instrukcja #16) jest nadpisywana w celu
narysowania w obiekcie Canvas cienia o wymiarach określonych za pomocą metody onProvideShadowMetrics(). W instrukcji #17 definiowana jest klasa DragEventListener, która implementuje
interfejs View.OnDragListener. Kiedy element lub widok zostanie wybrany i przeciągnięty, generowany jest
obiekt DragEvent i wywoływana jest metoda onDrag() (instrukcja #18). Metoda onDrag() posiada dwa parametry: View, który odbiera zdarzenie przeciągania, oraz DragEvent, który reprezentuje zdarzenie generowane podczas operacji przeciągnij i upuść oraz zawiera informacje na temat przeciąganego widoku i dane związane z operacją przeciągania. Metoda onDrag() zwraca wartość logiczną true (prawda), jeśli zdarzenie przeciągania zostanie obsłużone z powodzeniem. W przeciwnym razie zwraca wartość false (fałsz). W metodzie onDrag() wywoływana jest metoda getAction() na obiekcie DragEvent (instrukcja #19) w celu poznania typu akcji, który określa stan operacji przeciągnij i upuść. Po uruchomieniu tej aplikacji wyświetlane są dwie kontrolki ListView: jedna pokazuje listę elementów, a druga jest pusta. Nad każdą z kontrolek ListView wyświetlana jest kontrolka TextView. Wspomniane dwie kontrolki TextView są inicjowane w celu wyświetlenia odpowiednio tekstu Lista źródłowa i Lista docelowa, który określa funkcję kontrolki ListView znajdującej się poniżej (patrz rysunek 5.1, górny obrazek). Po kliknięciu elementu ze źródłowej kontrolki ListView i rozpoczęciu przeciągania go do docelowej kontrolki ListView cień przeciągania będzie poruszał się wraz ze wskaźnikiem myszy, tak jak pokazano na rysunku 5.1 (środkowy obrazek). Element upuszczony w docelowej kontrolce ListView jest do niej dodawany (patrz rysunek 5.1, dolny obrazek).
Receptura: przeciąganie i upuszczanie tekstu
Rysunek 5.1. Lista źródłowa i lista docelowa wyświetlone po uruchomieniu aplikacji (górny obrazek). Cień przeciągania pojawiający się po kliknięciu i przeciągnięciu elementu ze źródłowej kontrolki ListView (środkowy obrazek). Upuszczony element pojawia się w docelowej kontrolce ListView (dolny obrazek)
187
188
Rozdział 5. Schowek systemowy oraz operacja przeciągnij i upuść
Różne zdarzenia, które mają miejsce w źródłowej kontrolce ListView i układzie docelowym w trakcie operacji przeciągnij i upuść, są wyświetlane za pomocą komunikatów dziennika pokazanych na rysunku 5.2. Na tym rysunku możesz zauważyć nie tylko komunikaty dotyczące rozpoczęcia i zakończenia operacji przeciągania, ale również informacje o lokalizacjach, przez które przechodził cień przeciągania.
Rysunek 5.2. Komunikaty dziennika informujące o różnych akcjach wykonywanych w trakcie operacji przeciągnij i upuść przeprowadzanej na źródłowej kontrolce ListView i układzie docelowym
Receptura: przeciąganie i upuszczanie obrazów Podczas czytania opisu tej receptury dowiesz się, jak przeciągać i upuszczać obrazy. Nauczysz się wyświetlać dwie kontrolki GridView: jedną zawierającą kilka obrazów, a drugą początkowo pustą. Kiedy któryś obraz zostanie kliknięty i przeciągnięty z pierwszej kontrolki GridView, cień przeciągania będzie poruszał się wraz ze wskaźnikiem myszy, reprezentując przeciągany obraz. Obraz upuszczony w drugiej kontrolce GridView zostanie do niej dodany. Utwórz nowy projekt Android o nazwie DragAndDropImage. W tej aplikacji będziesz chciał wyświetlić dwie kontrolki GridView oraz dwie kontrolki TextView. Kontrolka TextView będzie wyświetlana na każdej kontrolce GridView. Dwie kontrolki TextView będą inicjowane w celu wyświetlenia odpowiednio tekstu Źródłowa kontrolka siatki i Docelowa kontrolka siatki, który informuje o funkcji kontrolki GridView wyświetlonej poniżej. Dla uszeregowania pary kontrolek TextView i GridView są zagnieżdżone w kontenerach LinearLayout. Aby wyświetlić kontrolki GridView i TextView w pożądanym układzie, w pliku układu aktywności activity_drag_and_drop_image.xml należy wpisać kod przedstawiony w listingu 5.3. Listing 5.3. Kod wpisany w pliku układu aktywności activity_drag_and_drop_image.xml
Receptura: przeciąganie i upuszczanie obrazów android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" >
Jak możesz zauważyć, dwóm kontrolkom GridView przypisano kolejno identyfikatory sourcegrid_view i targetgrid_view. Dwie kontrolki TextView wyświetlane nad kontrolkami GridView są inicjowane w celu wyświetlenia odpowiednio tekstu Źródłowa kontrolka siatki i Docelowa kontrolka siatki. Obie pary kontrolek TextView i GridView zostały umieszczone w pionowo zorientowanych (vertical) kontenerach LinearLayout, które z kolei są umieszczone w poziomo zorientowanym (horizontal) kontenerze zewnętrznym LinearLayout.
189
190
Rozdział 5. Schowek systemowy oraz operacja przeciągnij i upuść
Po zdefiniowaniu układu musisz skopiować obrazy do folderów res/drawable. W tej aplikacji wykorzystasz siedem różnych obrazów. Załóżmy, że nazwy plików obrazów to image1.jpg, image2.jpg, image3.jpg, image4.jpg, image5.jpg, image6.jpg oraz image7.jpg. Skopiuj te obrazy do wszystkich czterech folderów res/drawable. Ponieważ aplikacja ma być kompatybilna zarówno z telefonami, jak i tabletami, musisz zmieniać rozmiar układu na podstawie rozmiarów ekranu, na którym jest uruchamiana. Zmiana rozmiaru kontrolki GridView powoduje automatyczną zmianę rozmiarów wyświetlanych w niej obrazów. Mówiąc bardziej konkretnie, obrazy mają wyglądać na odpowiednio mniejsze na telefonie w porównaniu z tabletem. Możesz to osiągnąć, definiując zasoby wymiarów. Zakładamy, że w folderze res/values istnieje już plik wymiarów dimens.xml. Możesz zatem dodać w nim wymiary, które będą zmieniać wielkość obrazów na podstawie rozmiarów ekranu urządzenia wykorzystywanego do uruchomienia tej aplikacji. W pliku dimens.xml wpisz następujący kod: 14sp 100dp 120dp
Trzy zasoby wymiarów text_size, layout_width oraz layout_height definiują odpowiednio rozmiar tekstu, szerokość układu oraz wysokość układu. Powyższe zasoby wymiarów przeznaczone są dla urządzeń o normalnych rozmiarach ekranu (telefonów). Aby zdefiniować zasoby wymiarów dla 7-calowego tabletu, otwórz znajdujący się w folderze res/values-sw600dp plik dimens.xml i wpisz w nim następujący kod: 24sp 140dp 160dp
Na koniec, aby zdefiniować wymiary dla urządzeń z ekstradużymi ekranami (10-calowych tabletów), otwórz znajdujący się w folderze values-sw720dp plik dimens.xml i wpisz w nim następujący kod: 32sp 180dp 200dp
Porównując zasoby wymiarów dla telefonów, 7-calowych tabletów i 10-calowych tabletów, możesz zauważyć, że wielkość tekstu i układy kontrolki GridView są zmieniane na podstawie rozmiaru ekranu urządzenia.
Receptura: przeciąganie i upuszczanie obrazów
Następnie musisz napisać kod Java, który będzie wykonywał następujące zadania. Będzie uzyskiwał dostęp do źródłowej i docelowej kontrolki GridView
i mapował te kontrolki na odpowiednie obiekty GridView. Będzie definiował tablicę ArrayList służącą do przechowywania informacji
o obrazach, które mają być wyświetlane za pomocą źródłowej kontrolki GridView. Będzie definiował klasę ImageAdapter, która rozszerza klasę BaseAdapter.
Klasa ImageAdapter jest ustawiana dla źródłowej kontrolki GridView w celu wyświetlania obrazów zdefiniowanych w tablicy ArrayList. Ponadto zdefiniować należy również TargetAdapter, który będzie wyświetlał obrazy upuszczone w docelowej kontrolce GridView. Będzie kojarzył nasłuchiwacz setOnItemLongClickListener ze źródłową
kontrolką GridView w celu nasłuchiwania, czy któryś obraz został kliknięty i przytrzymany, oraz podjęcia niezbędnych akcji. Będzie kojarzył nasłuchiwacz setOnDragListener z obiema kontrolkami
GridView w celu nasłuchiwania zdarzeń DragEvent. Będzie definiował obiekt ClipData w celu reprezentowania obrazu wybranego
ze źródłowej kontrolki GridView. Będzie definiował klasę ShadowBuilder, która rozszerza View.DragShadowBuilder
w celu zdefiniowania wymiarów cienia przeciągania i narysowania tego cienia. Będzie definiował klasę DragEventListener, która implementuje interfejs
View.OnDragListener służący do nasłuchiwania wygenerowanych zdarzeń DragEvent oraz różnych akcji podejmowanych w trakcie operacji przeciągnij i upuść. Będzie dodawał obraz upuszczony w docelowej kontrolce GridView do jej
adaptera ImageAdapter, aby dany obraz pojawił się w tej kontrolce. Aby wykonać wymienione zadania, wpisz w pliku aktywności Java DragAndDropImageActivity.java kod przedstawiony w listingu 5.4. Listing 5.4. Kod wpisany w pliku aktywności Java DragAndDropImageActivity.java package com.androidtablet.draganddropimage; import import import import import import import import import import import import
android.os.Bundle; android.app.Activity; java.util.ArrayList; android.widget.BaseAdapter; android.widget.ImageView; android.widget.GridView; android.widget.AdapterView.OnItemLongClickListener; android.view.DragEvent; android.view.View; android.content.ClipDescription; android.content.ClipData; android.widget.AdapterView;
191
192
Rozdział 5. Schowek systemowy oraz operacja przeciągnij i upuść import import import import import
android.view.ViewGroup; android.view.View.DragShadowBuilder; android.graphics.Point; android.graphics.Canvas; android.util.Log;
public class DragAndDropImageActivity extends Activity { GridView sourceGridView; GridView targetGridView; private ArrayList drawables; private ArrayList targetdrawables = new ArrayList (); DragEventListener dragEventListener = new DragEventListener(); TargetAdapter targetAdapter; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_drag_and_drop_image); drawables = new ArrayList(); drawables.add(R.drawable.image1); drawables.add(R.drawable.image2); drawables.add(R.drawable.image3); drawables.add(R.drawable.image4); drawables.add(R.drawable.image5); drawables.add(R.drawable.image6); drawables.add(R.drawable.image7); sourceGridView = (GridView) findViewById( R.id.sourcegrid_view); targetGridView = (GridView) findViewById( R.id.targetgrid_view); sourceGridView.setAdapter(new ImageAdapter()); sourceGridView.setOnItemLongClickListener( sourceGridLongClickListener); sourceGridView.setOnDragListener(dragEventListener); targetGridView.setOnDragListener(dragEventListener); targetAdapter=new TargetAdapter(); targetGridView.setAdapter(targetAdapter); sourceGridView.setTag("Źródłowa kontrolka GridView"); targetGridView.setTag("Docelowa kontrolka GridView"); } OnItemLongClickListener sourceGridLongClickListener = new OnItemLongClickListener() { @Override public boolean onItemLongClick(AdapterView> l, View v, int position, long id) { ClipData.Item item = new ClipData.Item(drawables. get(position).toString()); String[] clipDescription = {ClipDescription.MIMETYPE_TEXT_PLAIN}; ClipData dragData = new ClipData((CharSequence) v.getTag(), clipDescription,item); DragShadowBuilder itemShadow = new ShadowBuilder(v); v.startDrag(dragData, itemShadow, drawables. get(position), 0);
Receptura: przeciąganie i upuszczanie obrazów return true; } }; private static class ShadowBuilder extends View.DragShadowBuilder { private static View view; public ShadowBuilder(View v) { super(v); view=v; } @Override public void onProvideShadowMetrics (Point size, Point touch){ int width = getView().getWidth(); int height = getView().getHeight(); size.set(width, height); touch.set(width / 2, height / 2); } @Override public void onDrawShadow(Canvas canvas) { view.draw(canvas); } } protected class ImageAdapter extends BaseAdapter{ @Override public View getView(int position, View convertView, ViewGroup gridView) { ImageView imageView = new ImageView( DragAndDropImageActivity.this); imageView.setImageResource((Integer) drawables. get(position)); int layout_width = (int) getResources(). getDimension(R.dimen.layout_width); int layout_height = (int) getResources(). getDimension(R.dimen.layout_height); imageView.setLayoutParams(new GridView.LayoutParams( layout_width, layout_height)); imageView.setLongClickable(true); imageView.setTag(String.valueOf(position)); return imageView; } @Override public long getItemId(int position) { return position; } @Override public Object getItem(int position) { return drawables.get(position); }
193
194
Rozdział 5. Schowek systemowy oraz operacja przeciągnij i upuść @Override public int getCount() { return drawables.size(); } } protected class TargetAdapter extends BaseAdapter{ @Override public View getView(int position, View convertView, ViewGroup gridView) { ImageView imageView = new ImageView( DragAndDropImageActivity.this); imageView.setImageResource((Integer) targetdrawables.get(position)); int layout_width = (int) getResources(). getDimension(R.dimen.layout_width); int layout_height = (int) getResources(). getDimension(R.dimen.layout_height); imageView.setLayoutParams(new GridView.LayoutParams( layout_width, layout_height)); imageView.setLongClickable(true); imageView.setTag(String.valueOf(position)); return imageView; } @Override public long getItemId(int position) { return position; } @Override public Object getItem(int position) { return targetdrawables.get(position); } @Override public int getCount() { return targetdrawables.size(); } } protected class DragEventListener implements View. OnDragListener { @Override public boolean onDrag(View v, DragEvent event) { switch (event.getAction()) { case DragEvent.ACTION_DRAG_STARTED: if (event.getClipDescription().hasMimeType( ClipDescription.MIMETYPE_TEXT_PLAIN)) { Log.d((String) v.getTag(), "akcja ACTION_DRAG_STARTED zaakceptowana"); return true; }else{ Log.d((String) v.getTag(), "akcja ACTION_DRAG_STARTED odrzucona"); return false; }
Receptura: przeciąganie i upuszczanie obrazów case DragEvent.ACTION_DRAG_ENTERED: Log.d((String) v.getTag(), "akcja ACTION_DRAG_ENTERED"); return true; case DragEvent.ACTION_DRAG_EXITED: Log.d((String) v.getTag(), "akcja ACTION_DRAG_EXITED"); return true; case DragEvent.ACTION_DRAG_LOCATION: return true; case DragEvent.ACTION_DROP: if(v == targetGridView){ ClipData.Item item = event. getClipData().getItemAt(0); Log.d((String) v.getTag(), "akcja ACTION_DROP"); String droppedItem = item.getText(). toString(); targetdrawables.add(Integer.parseInt( droppedItem)); targetAdapter.notifyDataSetChanged(); return true; } else return false; case DragEvent.ACTION_DRAG_ENDED: if (event.getResult()) Log.d((String) v.getTag(), "akcja ACTION_DRAG_ENDED powiodła się"); else Log.d((String) v.getTag(), "Niepowodzenie akcji: ACTION_DRAG_ENDED"); return true; default: Log.d((String) v.getTag(), "Akcja nieznana"); return false; } } } }
Po uruchomieniu tej aplikacji na ekranie będą widoczne dwie kontrolki GridView. Jedna z tych kontrolek wyświetla obrazy, a druga jest pusta. Dwie kontrolki TextView wyświetlają odpowiednio tekst Źródłowa kontrolka siatki i Docelowa kontrolka siatki, który określa funkcję danej kontrolki GridView. Z powodu ograniczonej przestrzeni obrazy w drugim rzędzie źródłowej kontrolki siatki są widoczne jedynie częściowo (patrz rysunek 5.3, górny obrazek). Kiedy przewiniesz siatkę, obrazy z drugiego rzędu staną się widoczne (patrz rysunek 5.3, dolny obrazek). Kiedy w źródłowej kontrolce GridView któryś obraz zostanie kliknięty i przeciągnięty, cień przeciągania reprezentujący ten obraz porusza się wraz ze wskaźnikiem myszy (patrz rysunek 5.4, górny obrazek). Po upuszczeniu cienia obrazu w docelowej kontrolce GridView obraz zostaje dodany do tej kontrolki (patrz rysunek 5.4, środkowy obrazek). Po przeciągnięciu i upuszczeniu kilku obrazów docelowa kontrolka GridView może wyglądać tak, jak przedstawiono na rysunku 5.4 (dolny obrazek).
195
196
Rozdział 5. Schowek systemowy oraz operacja przeciągnij i upuść
Rysunek 5.3. Źródłowa kontrolka siatki i docelowa kontrolka siatki wyświetlone po uruchomieniu aplikacji (górny obrazek). Ukryte obrazy pojawiają się w źródłowej kontrolce siatki po jej przewinięciu (dolny obrazek)
Receptura: przeciąganie i upuszczanie obrazów
Rysunek 5.4. Cień przeciągania obrazu pojawia się podczas przeciągania tego obrazu ze źródłowej kontrolki siatki do docelowej kontrolki siatki (górny obrazek). Upuszczony obraz pojawia się w docelowej kontrolce siatki (środkowy obrazek). Obrazy wyświetlone w docelowej kontrolce siatki po wykonaniu kilku operacji przeciągnij i upuść (dolny obrazek)
197
198
Rozdział 5. Schowek systemowy oraz operacja przeciągnij i upuść
Receptura: wycinanie, kopiowanie i wklejanie tekstu przy wykorzystaniu schowka systemowego Aby wyciąć, skopiować i wkleić zawartość z jednego widoku do drugiego w danej aplikacji Android, a nawet pomiędzy aplikacjami, musisz skorzystać z klasy ClipboardManager. Klasa ClipboardManager reprezentuje schowek systemowy. Aby użyć tej klasy, nie musisz jej instancjonować. Wystarczy uzyskać referencje do niej za pomocą wywołania metody getSystemService(CLIPBOARD_SERVICE). Przykładowo poniższa instrukcja uzyskuje referencje do klasy ClipboardManager: ClipboardManager clipManager= (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
Oto dwie metody klasy ClipboardManager często wykorzystywane w operacjach wycinania, kopiowania i wklejania. setPrimaryClip() — ustawia bieżący podstawowy wycinek w schowku.
Wycinek podstawowy jest wykorzystywany w operacjach wycinania, kopiowania i wklejania. getPrimaryClip() — zwraca bieżący podstawowy wycinek schowka.
Dane kopiowane lub wycinane do schowka są określane jako wycinek (ang. clip) i reprezentowane za pomocą obiektu ClipData. Oznacza to, że do reprezentowania danych skopiowanych lub wyciętych do schowka tworzony jest obiekt ClipData składający się z dwóch obiektów. Obiekt ClipDescription — zawiera opis (metadane) wycinka. Posiada tablicę
typów MIME dla danych wycinka. Typy MIME są sprawdzane przed wklejeniem wycinka, aby zapewnić, że dostępne typy MIME mogą być obsłużone. Obiekt ClipData.Item — zawiera rzeczywiste dane wycięte lub skopiowane
do schowka. Jest to obiekt interfejsu CharSequence zawierający formatowanie. Obiekt ten może przechowywać tekst, identyfikator URI lub dane intencji. Klasa ClipData zapewnia następujące metody dla tworzenia obiektów ClipData zawierających różne typy danych. newPlainText(label, text) — tworzy obiekt ClipData, w którym obiekt
ClipData.Item zawiera dostarczony argument text. Dostarczony argument label zostanie przypisany do etykiety obiektu ClipDescription, a typ MIME ustawiony na MIMETYPE_TEXT_PLAIN. newUri(content_resolver, label, URI) — tworzy obiekt ClipData, w którym
obiekt ClipData.Item zawiera dostarczony identyfikator URI. Argument label zostanie przypisany do etykiety obiektu ClipDescription, a typ MIME ustawiony na MIMETYPE_TEXT_URILIST. Aby pobrać informacje o URI, użyj argumentu content_resolver.
Receptura: wycinanie, kopiowanie i wklejanie tekstu newIntent(label, intent) — tworzy obiekt ClipData, w którym obiekt
ClipData.Item zawiera dostarczoną intencję. Dostarczony argument label zostanie przypisany do etykiety obiektu ClipDescription, a typ MIME ustawiony na MIMETYPE_TEXT_INTENT.
Uwaga Schowek może przechowywać w danym momencie tylko jeden obiekt ClipData. Obiekt ClipData zawiera obiekt ClipDescription oraz jeden lub kilka obiektów ClipData.Item.
Aby zobaczyć, jak w praktyce wycina się, kopiuje i wkleja tekst, utwórz nowy projekt Android o nazwie CopyPasteApp. W tej aplikacji zbudujesz dwie kontrolki EditText i trzy kontrolki Button. Trzem kontrolkom Button przypisane zostaną odpowiednio nagłówki Wytnij, Kopiuj i Wklej. Sam tekst jest wpisany w pierwszej kontrolce EditText. Po kliknięciu przycisku Kopiuj tekst z pierwszej kontrolki EditText jest kopiowany do schowka systemowego w postaci obiektu ClipData. Oznacza to, że tekst z kontrolki EditText jest przypisywany do obiektu ClipData.Item należącego do obiektu ClipData. Kiedy klikniesz przycisk Wklej, tekst przechowywany w obiekcie ClipData.Item należącym do obiektu ClipData jest przypisywany do drugiej kontrolki EditText. Podobnie kiedy klikniesz przycisk Wytnij, tekst z pierwszej kontrolki EditText jest przypisywany do obiektu ClipData.Item należącego do obiektu ClipData i usuwany z tej kontrolki. Następnie kiedy klikniesz przycisk Wklej, tekst z obiektu ClipData.Item jest przypisywany do drugiej kontrolki EditText. Po zdefiniowaniu dwóch kontrolek EditText i trzech kontrolek Button plik układu aktywności activity_copy_paste_app.xml będzie wyglądał tak, jak przedstawiono w listingu 5.5. Listing 5.5. Kod wpisany w pliku układu aktywności activity_copy_paste_app.xml
199
200
Rozdział 5. Schowek systemowy oraz operacja przeciągnij i upuść android:textSize="@dimen/text_size" android:layout_below="@id/edittext1" />
Jak możesz zauważyć, wysokość obu kontrolek EditText została ustawiona tak, aby wyświetlać minimum pięć wierszy. Dla celów uzyskiwania dostępu i identyfikacji kontrolkom EditText przypisane zostały odpowiednio identyfikatory edittext1 i edittext2. Trzem kontrolkom Button przypisano odpowiednio identyfikatory cut_button, copy_button oraz paste_button. Tekst wpisywany w kontrolkach EditText oraz nagłówki kontrolek Button będą wyświetlane czcionką o rozmiarze zdefiniowanym w zasobie wymiarów text_size. Teraz napisz kod Java, który będzie wykonywał następujące zadania. Będzie uzyskiwał dostęp do dwóch kontrolek EditText i trzech kontrolek
Button zdefiniowanych w pliku układu aktywności i mapował je na odpowiednie obiekty. Będzie kojarzył nasłuchiwacz setOnClickListener z trzema kontrolkami
Button, aby po kliknięciu którejś z kontrolek Button wywoływana była odpowiednia metoda wywołania zwrotnego onClick(). Będzie definiował obiekt ClipboardManager, który ma reprezentować schowek
systemowy. Będzie definiował obiekt ClipData, który ma zawierać kopiowany lub wycinany
tekst.
Receptura: wycinanie, kopiowanie i wklejanie tekstu Będzie przypisywał podstawowy wycinek schowka do obiektu ClipData,
kiedy kliknięte zostaną przyciski Wytnij lub Kopiuj. Będzie uzyskiwał dostęp do wycinka podstawowego w celu pobrania obiektu
ClipData, kiedy kliknięty zostanie przycisk Wklej. Będzie uzyskiwał dostęp do obiektu ClipData.Item z obiektu ClipData
oraz wypełniał drugą kontrolkę EditText tekstem zawartym w obiekcie ClipData.Item. Aby wykonać wymienione zadania, wpisz w głównym pliku aktywności Java CopyPasteAppActivity.java kod przedstawiony w listingu 5.6. Listing 5.6. Kod wpisany w pliku aktywności Java CopyPasteAppActivity.java package com.androidtablet.copypasteapp; import import import import import import import import
android.os.Bundle; android.app.Activity; android.content.ClipboardManager; android.widget.EditText; android.widget.Button; android.view.View.OnClickListener; android.view.View; android.content.ClipData;
public class CopyPasteAppActivity extends Activity { EditText editText1, editText2; ClipboardManager clipManager; Button cutButton, copyButton, pasteButton; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_copy_paste_app); editText1 = (EditText) findViewById(R.id.edittext1); editText2 = (EditText) findViewById(R.id.edittext2); clipManager= (ClipboardManager) getSystemService( CLIPBOARD_SERVICE); cutButton = (Button)this.findViewById(R.id.cut_button); cutButton.setOnClickListener(new OnClickListener(){ public void onClick(View view) { ClipData clipData = ClipData.newPlainText( "dane", editText1.getText()); clipManager.setPrimaryClip(clipData); editText1.setText(""); } }); copyButton = (Button)this.findViewById(R.id.copy_button); copyButton.setOnClickListener(new OnClickListener(){ public void onClick(View view) { ClipData clipData = ClipData.newPlainText( "dane", editText1.getText()); clipManager.setPrimaryClip(clipData); }
201
202
Rozdział 5. Schowek systemowy oraz operacja przeciągnij i upuść }); pasteButton = (Button)this.findViewById( R.id.paste_button); pasteButton.setOnClickListener(new OnClickListener(){ public void onClick(View view) { if (clipManager.hasPrimaryClip()){ ClipData clipData = clipManager. getPrimaryClip(); editText2.setText(clipData.getItemAt(0). getText()); } } }); } }
Po uruchomieniu tej aplikacji wyświetlone zostają dwie puste kontrolki EditText i trzy kontrolki Button (patrz rysunek 5.5, górny obrazek). Kiedy wpiszesz dowolny tekst w pierwszej kontrolce EditText i klikniesz przycisk Kopiuj, a potem przycisk Wklej, tekst ten zostanie skopiowany i wklejony do drugiej kontrolki EditText (patrz rysunek 5.5, środkowy obrazek). Jeśli wybierzesz przycisk Wytnij, tekst z pierwszej kontrolki EditText zostanie wykasowany i przechowany w postaci obiektu ClipData. Po kliknięciu przycisku Wklej tekst z obiektu ClipData zostanie wklejony w drugiej kontrolce EditText, tak jak pokazano na rysunku 5.5 (dolny obrazek).
Podsumowanie W tym rozdziale nauczyłeś się stosować operacje przeciągnij i upuść w aplikacji Android. Dowiedziałeś się, jak nasłuchiwać zdarzeń DragEvent, śledzić różne akcje mające miejsce podczas operacji przeciągnij i upuść, tworzyć cień przeciągania oraz dodawać widok do strefy upuszczania. Przeciągałeś i upuszczałeś tekst oraz obrazy. Odkryłeś, w jaki sposób uzyskać dostęp do schowka systemowego i stosować w aplikacjach Android operacje wycinania, kopiowania i wklejania. W kolejnym rozdziale nauczysz się korzystać z intencji oczekujących w celu rozpoczęcia aktywności oraz dowiesz się, jak rozgłaszać intencję. Będziesz również tworzył odbiorniki rozgłaszania w celu nasłuchiwania rozgłaszanej intencji oraz uzyskiwania informacji o systemie, aby powiadamiać użytkowników.
Podsumowanie
Rysunek 5.5. Dwie kontrolki EditText i trzy kontrolki Button wyświetlone po uruchomieniu aplikacji (górny obrazek). Tekst z górnej kontrolki EditText skopiowany i wklejony w dolnej kontrolce EditText (środkowy obrazek). Tekst wycięty z górnej kontrolki EditText i wklejony w dolnej kontrolce EditText (dolny obrazek)
203
204
Rozdział 5. Schowek systemowy oraz operacja przeciągnij i upuść
6 Powiadomienia oraz intencje oczekujące P
owiadomienia (ang. notifications), jak wskazuje nazwa, odnoszą się do komunikatów lub zdarzeń, które wymagają uwagi użytkownika. Powiadomienia w systemie Honeycomb i w nowszych wersjach systemu Android są wyświetlane w formie ikony w obszarze powiadomień. Użytkownik może otworzyć szufladę powiadomień, aby sprawdzić szczegóły. W tabletach obszar powiadomień jest zintegrowany z paskiem systemowym znajdującym się na dole ekranu. Szufladę powiadomień można otworzyć, dotykając ekranu w dowolnym miejscu wewnątrz obszaru powiadomień. Po kliknięciu danego powiadomienia wywoływana jest wyznaczona aktywność intencji oczekującej w celu wykonania żądanej akcji. Dlatego też powiadomienia oraz intencje oczekujące są w pewien sposób połączone. W tym rozdziale poznasz szczegóły dotyczące intencji oczekujących, nauczysz się rozgłaszać intencję oraz tworzyć odbiorniki rozgłoszeniowe (ang. broadcast receivers), które mają nasłuchiwać rozgłaszanych intencji. Poznasz także system powiadomień Androida i nauczysz się za pomocą tego systemu wykorzystywać intencję oczekującą w celu rozpoczęcia aktywności.
Receptura: intencje oczekujące Intencja oczekująca (ang. pending intent) jest odmianą regularnej intencji. W rozdziale 1., „Przegląd aplikacji na tablety z systemem Android”, dowiedziałeś się, że intencja regularna jest definiowana jako struktura wykorzystywana do rozpoczęcia, zatrzymania oraz zaimplementowania przechodzenia pomiędzy aktywnościami w ramach aplikacji. Intencja oczekująca, jak sugeruje nazwa, jest intencją przechowywaną i oczekującą na wywołanie w przyszłości. Intencja oczekująca jest tworzona za pomocą klasy PendingIntent i obejmuje regularną intencję, która ma być wywołana w przyszłości, kiedy pojawi się określone zdarzenie. Przy założeniu, że bieżącą aktywnością (kontekstem aplikacji) jest PendingIntentAppActivity, w zamieszczonym poniżej kodzie powstaje intencja oczekująca o nazwie pendIntent:
206
Rozdział 6. Powiadomienia oraz intencje oczekujące Intent intent = new Intent(PendingIntentAppActivity.this, TargetActivity.class); PendingIntent pendIntent = PendingIntent.getActivity(PendingIntentAppActivity.this, 0, intent, 0);
Obiekt intencji możesz utworzyć, podając bieżący kontekst aplikacji oraz nazwę aktywności TargetActivity.class, czyli aktywności, którą chcesz uruchomić. Wtedy tworzony jest obiekt pendIntent klasy PendingIntent poprzez dostarczenie do metody getActivity() czterech parametrów. Oto one. Bieżący kontekst aplikacji, w którym intencja oczekująca rozpocznie określoną
aktywność. Kod żądania dla nadawcy; jest on zazwyczaj wykorzystywany do rozróżnienia
dwóch intencji oczekujących. Kiedy kod żądania nie jest wykorzystywany, dla tego parametru podawana jest wartość 0. Intencja aktywności, która ma być uruchomiona. Flagi określające akcje podejmowane w konkretnych warunkach. Flagi pomagają
np. określić akcję, która ma być podjęta, jeśli intencja oczekująca już istnieje. Aby podjąć akcję domyślną, dla tego parametru należy podać wartość 0. Poniżej wymienione zostały stałe, które są używane dla flag: FLAG_CANCEL_CURRENT — anuluje bieżącą intencję oczekującą (jeśli taka istnieje)
przed utworzeniem nowej, FLAG_NO_CREATE — zwraca wartość null (pustą), jeśli dana intencja jeszcze
nie istnieje; nie tworzy nowej intencji, FLAG_ONE_SHOT — określa, że utworzona intencja oczekująca zostanie użyta raz, FLAG_UPDATE_CURRENT — zastępuje dodatkowe dane istniejącej intencji
oczekującej danymi nowej intencji.
Uwaga Intencja oczekująca może być wywołana nawet wtedy, kiedy proces wywołujący zostanie „zabity”.
Intencja regularna może uruchomić aktywność, usługę lub odbiornik rozgłaszania, co określają trzy następujące metody: startActivity(intencja), startService(intencja), sendBroadcast(intencja).
Odpowiednie metody dla tworzenia intencji oczekującej dla aktywności, usługi oraz odbiornika rozgłoszeniowego są następujące: PendingIntent.getActivity(kontekst, 0, intencja, flaga), PendingIntent.getService(kontekst, 0, intencja, flaga), PendingIntent.getBroadcast(kontekst, 0, intencja, flaga).
Receptura: rozgłaszanie intencji
Przykład: Przy założeniu, że bieżącą aktywnością jest PendingIntentAppActivity (kontekst aplikacji), poniższe instrukcje tworzą intencję oczekującą w celu rozpoczęcia aktywności: int requestCode = 0; int flags = 0; Intent activityIntent = new Intent(PendingIntentAppActivity.this, TargetActivity.class); PendingIntent.getActivity(PendingIntentAppActivity.this, requestCode, activityIntent, flags);
Receptura: rozgłaszanie intencji Za pomocą intencji wysyłamy komunikaty o zorganizowanej strukturze. Możesz użyć intencji do wysyłania komunikatów, takich jak nadejście nowej poczty elektronicznej, niski poziom baterii czy zakończenie pobierania pliku. Innymi słowy, takie komunikaty mogą być rozgłaszane jako obiekt intencji.
Uwaga Rozgłaszane intencje powiadamiają aplikacje o zdarzeniach systemowych lub zdarzeniach aplikacji.
Aby rozgłosić intencję, trzeba utworzyć obiekt intencji, przypisać do niego określoną akcję i dołączyć dane lub komunikat do odbiornika rozgłoszeniowego. Następnie należy rozgłosić intencję. Opcjonalnie można w intencji umieścić dodatkowe dane lub komunikat. Metody zaangażowane do rozgłaszania intencji zostały przedstawione w tabeli 6.1. Tabela 6.1. Metody zaangażowane do rozgłaszania intencji
Metoda
Opis
putExtra()
Używana w celu dodania do intencji danych lub komunikatu, które chcesz wysłać do odbiornika rozgłaszania. Składnia: putExtra(String nazwa, String wartość)
Parametr nazwa jest kluczem lub nazwą wartości, które chcesz przekazać wraz z intencją. Parametr wartość jest używany do identyfikacji wartości. setAction()
Używana do ustawienia akcji, która ma być wykonana na danych lub komunikacie, które są wysyłane z intencją. Składnia: setAction(String akcja)
Odbiornik rozgłaszania wykorzystuje metodę getAction() do pobrania akcji, która ma być wykonana na otrzymanych danych.
207
208
Rozdział 6. Powiadomienia oraz intencje oczekujące Tabela 6.1. Metody zaangażowane do rozgłaszania intencji (ciąg dalszy)
Metoda
Opis
sendBroadcast()
Dostępna w klasie Context. Metoda ta wysyła rozgłaszaną intencję do wszystkich zarejestrowanych odbiorników intencji. Składnia: void sendBroadcast(intencja_do_rozgłoszania)
Parametr intencja_do_rozgłaszania reprezentuje intencję, którą chcesz rozgłosić.
Przy użyciu poniższego kodu rozgłaszana jest intencja: public static String BROADCAST_STRING = "com.androidtablet.broadcastingintent"; Intent broadcastIntent = new Intent(); broadcastIntent.putExtra("message", "Otrzymałeś nową wiadomość"); broadcastIntent.setAction(BROADCAST_STRING); sendBroadcast(broadcastIntent);
Jak widać, tworzony jest obiekt intencji o nazwie broadcastIntent. Dane lub komunikat przekazywany wraz z tą intencją to Otrzymałeś nową wiadomość, a nazwa lub klucz przypisane do tego komunikatu to message. Ciąg akcji jest unikatowy dzięki zastosowaniu przestrzeni nazw podobnej do klasy Java. Jak widzisz, ciąg com.androidtablet.broadcastingintent jest przypisywany do intencji jako akcja. Na koniec obiekt intencji broadcastIntent jest wysyłany lub rozgłaszany, a następnie odbierany przez odbiorniki rozgłoszeniowe. Do nasłuchiwania i odpowiadania na rozgłaszaną intencję należy zaimplementować odbiorniki rozgłoszeniowe. Rozgłaszana intencja może wywoływać więcej niż jeden odbiornik rozgłaszania. Odbiornik rozgłoszeniowy jest klasą, która implementuje klasę BroadcastReceiver. Musi również zostać zarejestrowany jako odbiornik w aplikacji Android za pomocą AndroidManifest.xml lub przy użyciu kodu w trakcie działania aplikacji. Jeśli nie zostanie zarejestrowany w pliku manifestu, nie będzie działał. Klasa odbiornika rozgłoszeniowego musi implementować metodę onReceive(). Poniżej zamieszczono przykładowy kod dla metody onReceive(). public void onReceive(Context context, Intent intent) { String actionName = intent.getAction(); if(actionName != null && actionName.equals("com.androidtablet. broadcastingintent")) { String msg = intent.getStringExtra("message"); Log.d("Otrzymana wiadomość: ",msg); } }
Metody getAction() i getStringExtra() wykonują następujące zadania. getAction() pobiera akcję, która ma być wykonana na obiekcie intencji. Jest to akcja
określająca zadanie do wykonania na danych przekazanych wraz z intencją. Składnia: getAction()
Receptura: rozgłaszanie intencji getStringExtra() pobiera rozszerzone dane z intencji.
Składnia: getStringExtra(String nazwa)
Tutaj parametr nazwa reprezentuje klucz lub nazwę, które zostały przypisane do wartości podczas dodawania danych do intencji za pomocą metody putExtra(). W metodzie onReceive() uzyskujesz dostęp do obiektu intencji przekazywanego jako parametr. Z tego obiektu intencji pobierasz akcję, która ma zostać wykonana. Jeśli akcją, która ma być wykonana, nie jest wartość null i akcja ta odpowiada akcji wysłanej przez aktywność nadawcy, komunikat lub dane przekazywane wraz z intencją są z niej pobierane i rejestrowane. Aby lepiej zrozumieć, w jaki sposób rozgłaszana jest intencja, utwórz projekt Android o nazwie BroadcastingIntent. W aplikacji wyświetlany będzie przycisk z nagłówkiem Rozgłoś intencję. Po kliknięciu tego przycisku rozgłoszona zostanie intencja z określonym komunikatem. Rozgłoszona intencja jest następnie odbierana za pomocą odbiornika intencji, a komunikat wysłany z intencją jest wydobywany i wyświetlany. Aby zdefiniować kontrolkę Button, w pliku układu activity_broadcasting_intent.xml wpisz kod przedstawiony w listingu 6.1. Listing 6.1. Kod wpisany w pliku activity_broadcasting_intent.xml
Jak możesz zauważyć, kontrolce Button przypisany został identyfikator broadcast_button, który będzie identyfikował tę kontrolkę w kodzie Java. Nagłówek przypisany kontrolce Button to Rozgłoś intencję. Następnie w pliku aktywności Java musisz wpisać kod, który będzie definiował obiekt intencji, przypisywał akcję, dodawał do obiektu określony komunikat, a następnie rozgłaszał intencję. W tym celu w pliku aktywności Java BroadcastingIntentActivity.java wpisz kod przedstawiony w listingu 6.2. Listing 6.2. Kod wpisany w pliku BroadcastingIntentActivity.java package com.androidtablet.broadcastingintent; import android.app.Activity; import android.os.Bundle; import android.content.Intent;
209
210
Rozdział 6. Powiadomienia oraz intencje oczekujące import android.widget.Button; import android.view.View; public class BroadcastingIntentActivity extends Activity { public static String BROADCAST_STRING = "com.androidtablet.broadcastintent"; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_broadcasting_intent); Button broadcastButton = (Button) this.findViewById( R.id.broadcast_button); broadcastButton.setOnClickListener(new Button. OnClickListener(){ public void onClick(View v) { Intent broadcastIntent = new Intent(); broadcastIntent.putExtra("message", "Otrzymałeś nową wiadomość"); broadcastIntent.setAction(BROADCAST_STRING); sendBroadcast(broadcastIntent); } }); } }
Możesz tutaj zauważyć, że do kontrolki Button o identyfikatorze broadcast_button uzyskiwany jest dostęp z pliku układu, a sama kontrolka mapowana jest na obiekt klasy Button o nazwie broadcastButton. Nasłuchiwacz setOnClickListener jest kojarzony z kontrolką Button. Po kliknięciu kontrolki Button wywoływana jest metoda wywołania zwrotnego onClick(). W metodzie onClick() definiowany jest obiekt intencji o nazwie broadcastIntent. Do obiektu broadcastIntent dodawany jest komunikat Otrzymałeś nową wiadomość z kluczem message. Za pomocą statycznego ciągu znaków BROADCAST_STRING unikatowa akcja com.androidtablet.broadcastintent przypisywana jest do obiektu intencji broadcastIntent. Na koniec dana intencja jest rozgłaszana poprzez wywołanie metody sendBroadcast(). Następnym krokiem jest zdefiniowanie aktywności, która będzie pełnić rolę odbiornika rozgłaszania. Dodaj więc do paczki com.androidtablet.broadcastingintent swojej aplikacji plik Java o nazwie ReceiveBroadcastActivity.java. Aby odpowiadać na rozgłaszaną intencję i uzyskiwać dostęp do danych lub komunikatu przekazywanych wraz z nią, w pliku Java ReceiveBroadcastActivity.java wpisz kod przedstawiony w listingu 6.3. Listing 6.3. Kod wpisany w pliku ReceiveBroadcastActivity.java package com.androidtablet.broadcastingintent; import import import import
android.content.BroadcastReceiver; android.content.Intent; android.content.Context; android.util.Log;
Receptura: rozgłaszanie intencji public class ReceiveBroadcastActivity extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { String actionName = intent.getAction(); if(actionName != null && actionName.equals( "com.androidtablet.broadcastintent")) { String msg = intent.getStringExtra("message"); Log.d("Tekst z rozgłaszanej intencji: ", msg); } } }
Jak wspomniano wcześniej, w celu odbierania rozgłaszanej intencji dana klasa Java musi rozszerzać klasę BroadcastReceiver. Klasa ta również nadpisuje metodę onReceive(). W metodzie onReceive() wykorzystujesz parametr intencji, który reprezentuje odbierany obiekt intencji. Z tego obiektu uzyskujesz dostęp do akcji, jaka ma być wykonana na danych przekazanych wraz z intencją. Sprawdzasz, czy dana akcja nie ma wartości null (pusta) i porównujesz tę akcję z akcją dostarczoną podczas rozgłaszania intencji. Następnie uzyskiwany jest dostęp do danych z obiektu intencji i dane te są wyświetlane. Plik ReceiveBroadcastActivity.java, będący odbiornikiem rozgłoszeniowym, musi zostać zarejestrowany w pliku manifestu. Poniżej zamieszczono kod służący do zarejestrowania tej aktywności.
Jak widać, znacznik jest wykorzystywany w pliku manifestu do zarejestrowania danego odbiornika rozgłaszania. Znacznik ten również wyznacza jako odbiorcę intencji klasę ReceiveBroadcastActivity.class, której akcją jest com.androidtablet. broadcastintent. W listingu 6.4 przedstawiono kod z pliku AndroidManifest.xml. Dodane zostały tylko fragmenty kodu zaznaczone pogrubioną czcionką. Reszta to domyślny kod generowany automatycznie przez SDK Androida. Listing 6.4. Kod wpisany w pliku AndroidManifest.xml File
211
212
Rozdział 6. Powiadomienia oraz intencje oczekujące
Po uruchomieniu tej aplikacji zobaczysz wyświetlony na ekranie przycisk z nagłówkiem Rozgłoś intencję, co pokazano na rysunku 6.1 (górny obrazek). Po kliknięciu tego przycisku rozgłaszana jest intencja z komunikatem Otrzymałeś nową wiadomość. Klasa ReceiveBroadcastActivity.class odbiera rozgłaszaną intencję, pobiera z niej komunikat Otrzymałeś nową wiadomość i rejestruje go. Zarejestrowany komunikat zostaje wyświetlony w oknie dziennika LogCat, tak jak pokazano na rysunku 6.1 (dolny obrazek).
Uwaga Rozgłaszaną intencję może odbierać więcej niż jeden odbiornik rozgłaszania.
Odbiornik rozgłaszania możesz nawet dodawać dynamicznie. W tym celu do pliku aktywności Java BroadcastingIntentActivity.java dodaj kod przedstawiony w listingu 6.5. Dodany został jedynie kod zaznaczony pogrubioną czcionką. Reszta pozostaje bez zmian, tak jak w listingu 6.2. Listing 6.5. Kod wpisany w pliku BroadcastingIntentActivity.java package com.androidtablet.broadcastingintent; import import import import import import import import import
android.app.Activity; android.os.Bundle; android.content.Intent; android.widget.Button; android.view.View; android.content.BroadcastReceiver; android.content.IntentFilter; android.content.Context; android.util.Log;
public class BroadcastingIntentActivity extends Activity { public static String BROADCAST_STRING = "com.androidtablet.broadcastintent"; @Override
Receptura: rozgłaszanie intencji
Rysunek 6.1. Aplikacja wyświetlająca po uruchomieniu przycisk Rozgłoś intencję (górny obrazek) oraz komunikaty dziennika wyświetlone w oknie LogCat (dolny obrazek) public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_broadcasting_intent); Button broadcastButton = (Button) this.findViewById( R.id.broadcast_button); broadcastButton.setOnClickListener(new Button. OnClickListener(){ public void onClick(View v) { Intent broadcastIntent = new Intent(); broadcastIntent.putExtra("message", "Otrzymałeś nową wiadomość"); broadcastIntent.setAction(BROADCAST_STRING); sendBroadcast(broadcastIntent); } }); } private BroadcastReceiver myBroadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String actionName = intent.getAction(); if(actionName != null && actionName.equals(
213
214
Rozdział 6. Powiadomienia oraz intencje oczekujące "com.androidtablet.broadcastintent")) { String msg = intent.getStringExtra("message"); Log.d("Tekst z rozgłaszanej intencji: ", msg); } } }; public void onResume() { super.onResume(); IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction("com.androidtablet.broadcastintent"); registerReceiver(myBroadcastReceiver, intentFilter); } public void onPause() { super.onPause(); unregisterReceiver(myBroadcastReceiver); } }
W powyższym kodzie możesz zauważyć, że intencja przenosząca komunikat tekstowy Otrzymałeś nową wiadomość jest rozgłaszana po kliknięciu kontrolki Button. Do odpowiedzi
na rozgłaszaną intencję i w celu uzyskania dostępu do danych przekazywanych wraz z nią definiowany jest odbiornik rozgłoszeniowy o nazwie myBroadcastReceiver. W metodzie onReceive() tego odbiornika wykorzystywany jest parametr intencji reprezentujący otrzymany obiekt intencji. Z obiektu intencji uzyskiwany jest dostęp do akcji, która porównywana jest z akcją dostarczaną podczas rozgłaszania intencji. Następnie uzyskiwany jest dostęp do danych z obiektu intencji i dane te są wyświetlane. W metodzie onResume() danej aktywności definiowany jest filtr intencji, a dynamicznie dodany odbiornik rozgłoszeniowy o nazwie myBroadcastReceiver jest rejestrowany jako odbiorca intencji, której akcją jest com.androidtablet.broadcastintent.
Receptura: system powiadomień systemu Android System powiadomień w systemie Android oferuje kilka sposobów ostrzegania użytkowników. Użytkownik może np. zostać powiadomiony za pomocą informacji tekstowej, wibracji, migających światełek oraz sygnałów dźwiękowych. Powiadomienie pojawia się w postaci ikony w obszarze powiadomień. W tabletach obszar powiadomień jest zintegrowany z paskiem systemowym znajdującym się na dole ekranu. Użytkownik może otworzyć szufladę powiadomień, aby sprawdzić szczegóły powiadomienia. Szuflada powiadomień jest otwierana po dotknięciu ekranu w dowolnym miejscu wewnątrz obszaru powiadomień. Po kliknięciu danego powiadomienia użytkownicy są kierowani do intencji zdefiniowanej przez to powiadomienie. Powiadomienie nigdy nie uruchomi aktywności automatycznie, ale po prostu poinformuje użytkownika i uruchomi daną
Receptura: tworzenie powiadomień
aktywność tylko wtedy, kiedy powiadomienie zostanie wybrane. Powiadomienie, poza ikoną oraz tekstem na pasku, może posiadać tytuł oraz treść główną, które są wyświetlane po wyświetleniu pełnego powiadomienia. W porównaniu z komunikatami Toast, powiadomienia mają persystentną naturę. Do tworzenia powiadomień używane są dwie klasy. Notification — obiekt definiujący informację do wyświetlenia. Informacją
może być tekst wyświetlany na pasku statusu lub rozszerzonym pasku statusu, ikona wyświetlana wraz z tekstem, wskaźnik liczby uruchomień danego powiadomienia itd. NotificationManager — obiekt bazowy, za pomocą którego obsługiwane są
powiadomienia. Wyświetla on informację zhermetyzowaną w obiekcie Notification, wykorzystując w tym celu metodę notify().
Receptura: tworzenie powiadomień Pierwszym krokiem przy budowaniu powiadomień jest utworzenie obiektu Notification i skonfigurowanie go poprzez zdefiniowanie właściwości powiadomienia. W poniższym kodzie pokazano, jak to zrobić. Notification notification = new Notification(); notification.icon = R.drawable.glowingbulb; notification.tickerText = "Otrzymałeś nową wiadomość"; notification.when = System.currentTimeMillis(); notification.flags |= Notification.FLAG_AUTO_CANCEL;
Możesz tutaj zauważyć, że tworzony jest obiekt notification klasy Notification i jej publiczne elementy są wykorzystywane do jego skonfigurowania. I tak: icon — przypisuje ikonę powiadomienia, tickerText — przypisuje krótki tekst powiadomienia, when — przypisuje czas, w którym powiadomienie ma się pojawić; czas ten
określany jest z wykorzystaniem czasu systemowego, flag — przypisuje stałą określającą późniejszą akcję, która jest wykonywana
po wybraniu danego powiadomienia z okna powiadomień. Do tej zmiennej publicznej zazwyczaj przydziela się stałą FLAG_AUTO_CANCEL, która określa, że powiadomienie ma zostać automatycznie anulowane po wybraniu go z okna powiadomień. Ikonę powiadomienia, tekst paska oraz czas wystąpienia możesz również przypisywać za pomocą konstruktora obiektu Notification, w sposób pokazany poniżej: Notification notification = new Notification(R.drawable.glowingbulb, "Otrzymałeś nową wiadomość", System.currentTimeMillis());
215
216
Rozdział 6. Powiadomienia oraz intencje oczekujące
Po odebraniu powiadomienia możesz zdecydować o podjęciu niezbędnej akcji. Wykorzystujesz klasę PendingIntent w celu przełączenia się na żądaną intencję po kliknięciu określonego powiadomienia. Klasa PendingIntent pozwala tworzyć intencje, które mogą być wyzwalane przez Twoją aplikację, kiedy ma miejsce jakieś zdarzenie. Przy założeniu, że bieżącą aktywnością jest PendingIntentAppActivity, poniższy kod tworzy obiekt pendIntent klasy PendingIntent: Intent intent = new Intent(PendingIntentAppActivity.this, TargetActivity.class); PendingIntent pendIntent = PendingIntent.getActivity(PendingIntentAppActivity.this, 0, intent, 0);
Aby wyświetlić tekst po rozwinięciu danego powiadomienia oraz określić intencję oczekującą, którą chcesz uruchomić, skorzystaj z klasy konstruktora Notification.Builder.
Receptura: wykorzystanie klasy Notification.Builder Notification.Builder jest klasą konstruktora dla obiektów powiadomień, oferującą kilka metod do konfiguracji powiadomień. Metody te przedstawione zostały w tabeli 6.2. Tabela 6.2. Metody klasy Notification.Builder
Metoda
Opis
setSmallIcon()
Wykorzystywana do dostarczenia zasobu małej ikony, która będzie użyta do reprezentowania powiadomienia w pasku statusu. Składnia: setSmallIcon(int identyfikator_ikony)
Parametr identyfikator_ikony reprezentuje źródłowy identyfikator szuflady, który ma być wykorzystany jako ikona powiadomienia. setAutoCancel()
Wykorzystywana do określenia, czy powiadomienie ma znikać po jego kliknięciu. Dostarczona do tej metody wartość logiczna true (prawda) powoduje znikanie powiadomienia. Składnia: setAutoCancel(boolean automatyczne_anulowanie)
setTicker()
Wykorzystywana w celu dostarczenia tekstu paska, który ma być wyświetlany w pasku statusu po otrzymaniu powiadomienia. Składnia: setTicker(CharSequence wiadomość_tekstowa)
setWhen()
Wykorzystywana w celu dostarczenia czasu, w którym pojawić ma się powiadomienie. Składnia: setWhen(long czas_wystąpienia)
Receptura: wykorzystanie klasy Notification.Builder Tabela 6.2. Metody klasy Notification.Builder (ciąg dalszy)
Metoda
Opis
setContentTitle()
Wykorzystywana w celu dostarczenia tytułu powiadomienia, który jest widoczny po rozwinięciu paska statusu. Składnia: setContentTitle(CharSequence tytuł)
setContentText()
Wykorzystywana w celu dostarczenia tekstu powiadomienia. Składnia: setContentText(CharSequence tekst)
setContentIntent()
Wykorzystywana w celu dostarczenia obiektu PendingIntent, który ma być wysłany po kliknięciu powiadomienia. Składnia: setContentIntent(PendingIntent intencja)
Przy założeniu, że bieżącą aktywnością jest PendingIntentAppActivity, poniższy kod pokazuje, jak zastosować przedstawione w tabeli 6.3 metody klasy Notification.Builder do skonfigurowania powiadomienia. Notification.Builder builder = new Notification.Builder(PendingIntentAppActivity.this) .setSmallIcon(R.drawable.ic_launcher) .setAutoCancel(true) .setTicker("Otrzymałeś nowe powiadomienie") .setWhen(System.currentTimeMillis()) .setContentTitle("Nowy E-mail") .setContentText("Masz jedną nieprzeczytaną wiadowość.") .setContentIntent(pendIntent); notification = builder.build();
Powyższy kod konfiguruje powiadomienie w następujący sposób. Ustawia obrazek ic_launcher.png jako ikonę powiadomienia. Powoduje, że powiadomienie znika po jego kliknięciu. Przypisuje tekst Otrzymałeś nowe powiadomienie jako tekst paska
dla powiadomienia. Ustawia bieżący czas jako czas pojawienia się powiadomienia. Ustawia tytuł powiadomienia jako Nowy E-mail. Ustawia treść główną powiadomienia jako Masz jedną nieprzeczytaną wiadomość. Po kliknięciu powiadomienia uruchamia określoną intencję oczekującą pendIntent.
Konfiguracja powiadomienia utworzona za pomocą obiektu Notification.Builder jest przypisywania do obiektu notification klasy Notification.
217
218
Rozdział 6. Powiadomienia oraz intencje oczekujące
Receptura: pozyskiwanie obiektu klasy NotificationManager Klasa NotificationManager wykonuje wszystkie powiadomienia dotyczące statusu i zarządza tymi powiadomieniami. Aby uzyskać prawidłowy obiekt klasy NotificationManager, trzeba skorzystać z metody getSystemService(): NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
Po uzyskaniu obiektu klasy NotificationManager możesz wywołać jej metodę notify() w celu poinformowania użytkowników poprzez wyświetlenie powiadomienia. Składnia: notify(unikatowe_ID, obiekt_powiadomienia)
Parametr unikatowe_ID reprezentuje unikatowy identyfikator aplikacji, a parametr obiekt_powiadomienia reprezentuje obiekt Notification, który chcesz wyświetlić. Parametr unikatowe_ID może być wykorzystywany do aktualizacji lub usunięcia powiadomienia. Przykład: notificationManager.notify(0, notification);
Receptura: tworzenie powiadomienia i wykorzystywanie intencji oczekującej w celu rozpoczęcia aktywności Podczas lektury tej receptury nauczysz się tworzyć intencję oczekującą, która uruchamia aktywność w momencie wyświetlenia powiadomienia. Będziesz wyświetlał kontrolkę Button, która w przypadku kliknięcia tworzy powiadomienie. Tekst paska informujący o pojawieniu się powiadomienia będzie wyświetlany w pasku systemowym. Po rozwinięciu powiadomienia wyświetlone zostaną szczegółowe informację, w tym ikona, tytuł i treść główna. W momencie kliknięcia powiadomienia intencja oczekująca uruchomi wyznaczoną aktywność. Aktywność ta będzie wyświetlać komunikat Uruchomiłeś aktywność powitania poprzez intencję oczekującą. Receptura ta pozwoli zrozumieć system powiadomień Androida oraz jego połączenia z intencjami oczekującymi. Utwórz projekt Android o nazwie PendingIntent App. Rozpoczniesz od zdefiniowania układu aktywności docelowej, która zostanie uruchomiona przy użyciu intencji oczekującej. Ponieważ aktywność docelowa wyświetla po prostu komunikat tekstowy informujący, że dana aktywność została uruchomiona, musisz jedynie zdefiniować kontrolkę TextView w pliku układu aktywności docelowej. Dodaj więc do folderu res/layout plik XML o nazwie welcome.xml. W tym pliku zdefiniuj kontrolkę TextView w sposób przedstawiony w listingu 6.6.
Receptura: tworzenie powiadomienia i wykorzystywanie intencji oczekującej Listing 6.6. Kod wpisany w pliku welcome.xml
Jak możesz zauważyć, kontrolka TextView jest inicjowana w celu wyświetlenia tekstu Uruchomiłeś aktywność powitania poprzez intencję oczekującą. Tekst ten będzie prezentowany pogrubioną czcionką o rozmiarze zdefiniowanym w zasobie wymiarów text_size. Dla docelowej aktywności, która będzie uruchamiana przez intencję oczekującą, dodaj plik Java do paczki com.androidtablet.pendingintentapp swojego projektu. Nowo utworzony plik nazwij WelcomeActivity.java. Zadaniem nowej aktywności jest jedynie wyświetlenie kontrolki TextView zdefiniowanej w pliku układu welcome.xml. Kod wpisany w pliku nowej aktywności WelcomeActivity.java został przedstawiony w listingu 6.7. Listing 6.7. Kod wpisany w pliku Java WelcomeActivity.java package com.androidtablet.pendingintentapp; import android.app.Activity; import android.os.Bundle; public class WelcomeActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.welcome); } }
Możesz zauważyć, że plik welcome.xml jest ustawiony jako ContentView nowej aktywności WelcomeActivity.java. Następnie musisz zdefiniować widoki dla pliku układu aktywności. Ponieważ w Twojej aplikacji powiadomienie będzie tworzone po kliknięciu kontrolki Button, musisz zdefiniować tę kontrolkę w pliku układu. Po zdefiniowaniu kontrolki Button plik układu aktywności activity_pending_intent_app.xml będzie wyglądał tak, jak przedstawiono w listingu 6.8.
219
220
Rozdział 6. Powiadomienia oraz intencje oczekujące Listing 6.8. Kod wpisany w pliku układu aktywności activity_pending_intent_app.xml
Możesz zauważyć, że kontrolce Button przypisane zostały identyfikator createbutton i nagłówek Utwórz powiadomienie. Nagłówek tego przycisku ma mieć rozmiar zdefiniowany w zasobie wymiarów text_size. Identyfikator kontrolki Button będzie identyfikował ją w kodzie Java. Dla celów reprezentowania powiadomienia za pomocą ikony skopiuj plik obrazu glowingbulb.png do folderów res/drawable. Następnie w pliku aktywności Java endingIntentAppActivity.java musisz napisać kod wykonujący następujące zadania. Tworzenie obiektu Notification i skonfigurowanie go do wyświetlania ikony,
tytułu oraz tekstu. Tworzenie obiektu PendingIntent w celu uruchomienia aktywności
po kliknięciu powiadomienia. Tworzenie obiektu NotificationManager do wyświetlenia powiadomienia
i zarządzania tym powiadomieniem. Aby wykonać wymienione zadania, w pliku głównej aktywności PendingIntentAppActivity.java wpisz kod przedstawiony w listingu 6.9. Listing 6.9. Kod wpisany w pliku aktywności Java PendingIntentAppActivity.java package com.androidtablet.pendingintentapp; import import import import import import import import import
android.os.Bundle; android.app.Activity; android.content.Intent; android.app.PendingIntent; android.app.NotificationManager; android.app.Notification; android.widget.Button; android.view.View.OnClickListener; android.view.View;
public class PendingIntentAppActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState);
Receptura: tworzenie powiadomienia i wykorzystywanie intencji oczekującej setContentView(R.layout.activity_pending_intent_app); Button createButton = (Button) findViewById(R.id.createbutton); createButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { Intent intent = new Intent( PendingIntentAppActivity. this, WelcomeActivity.class); PendingIntent pendIntent = PendingIntent. getActivity(PendingIntentAppActivity.this,0, intent, 0); NotificationManager notificationManager = (NotificationManager) getSystemService( NOTIFICATION_SERVICE); Notification notification = new Notification(); Notification.Builder builder = new Notification. Builder(PendingIntentAppActivity.this) .setSmallIcon(R.drawable.glowingbulb) .setAutoCancel(true) .setTicker("Powiadomienie przy uruchamianiu intencji oczekującej") .setWhen(System.currentTimeMillis()) .setContentTitle("Wiadomość") .setContentText("Uruchommy intencję oczekującą") .setContentIntent(pendIntent); notification = builder.build(); notificationManager.notify(0, notification); } }); }
}
Aplikacja Android nie rozpozna nowo dodanej aktywności, dopóki nie zostanie ona wymieniona w pliku konfiguracyjnym AndroidManifest.xml. Aby poinformować aplikację Android o nowo dodanej aktywności WelcomeActivity, dodaj w tym pliku poniższą instrukcję:
Powyższa instrukcja musi być zagnieżdżona w elemencie pliku AndroidManifest.xml. Nie zapomnij także ustawić wartości atrybutu android:minSdkVersion na 16, ponieważ metoda build() klasy Notification.Builder działa tylko z API poziomu 16. We wcześniejszych poziomach API zamiast metody build() wykorzystywana była metoda getNotification(). Metoda ta została dodana w API poziomu 11. i jest już przestarzała. Poniższa instrukcja zastosowana w pliku AndroidManifest.xml potwierdza, że minimalnym wymaganym poziomem API dla uruchomienia tej aplikacji jest poziom 16., a preferowanym poziomem API, dla którego aplikacja została zaprojektowana jest poziom 17.:
221
222
Rozdział 6. Powiadomienia oraz intencje oczekujące
Teraz Twoja aplikacja jest gotowa do uruchomienia. Po uruchomieniu wyświetlona zostanie kontrolka Button z nagłówkiem Utwórz powiadomienie (patrz rysunek 6.2, pierwszy obrazek). Po kliknięciu tej kontrolki zobaczysz powiadomienie wraz z tekstem paska Powiadomienie przy uruchomieniu intencji oczekującej wyświetlonym na górze ekranu w pasku statusu (rysunek 6.2, drugi obrazek). Po przeciągnięciu w dół paska statusu wyświetlone zostaną tytuł powiadomienia, jego ikona oraz treść główna, tak jak zostało pokazane na rysunku 6.2 (trzeci obrazek). Tytuł powiadomienia to Wiadomość, a treść główna to Uruchommy intencję oczekującą. Po wybraniu powiadomienia intencja oczekująca uruchomi nową aktywność, co zostanie potwierdzone przez wyświetlenie za pomocą kontrolki TextView komunikatu Uruchomiłeś aktywność powitania poprzez intencję oczekującą (patrz rysunek 6.2, czwarty obrazek).
Podsumowanie W tym rozdziale poznałeś powiadomienia i dowiedziałeś się, jaka jest ich rola w informowaniu użytkowników o występowaniu różnych wydarzeń. Zobaczyłeś, jak tworzy się powiadomienia oraz definiuje ich ikonę, tytuł, pasek tekstu oraz treść główną. Poznałeś także intencje oczekujące i nauczyłeś się wykorzystywać je w połączeniu z systemem powiadomień Androida. Wreszcie prześledziłeś procedurę rozgłaszania intencji i tworzenia odbiorników rozgłoszeniowych w celu nasłuchiwania rozgłaszanej intencji. W kolejnym rozdziale poznasz klasę Loader i jej metody wywołania zwrotnego. Nauczysz się także korzystać z ładowarek kursora przy uzyskiwaniu dostępu do informacji z tabel bazy danych. Na koniec zobaczysz, jaka jest rola ładowarek w dostawcach treści.
Podsumowanie
Rysunek 6 2. Aplikacja wyświetlająca kontrolkę Button za nagłówkiem Utwórz powiadomienie (pierwszy obrazek). Powiadomienie utworzone i zaprezentowane za pomocą tekstu paska w pasku statusu (drugi obrazek). Szczegóły powiadomienia wyświetlone po jego rozwinięciu (trzeci obrazek). Aktywność WelcomeActivity uruchomiona przez intencję oczekującą po kliknięciu powiadomienia (czwarty obrazek)
223
224
Rozdział 6. Powiadomienia oraz intencje oczekujące
7 Ładowarki W
iele zadań aplikacji Android nie jest wykonywanych w głównym wątku, ale asynchronicznie w osobnych wątkach. Zwiększa to wydajność interfejsu użytkownika takiej aplikacji. Ładowarki również asynchronicznie ładują dane i monitorują bazowe źródło danych pod kątem zmian. W tym rozdziale dowiesz się, jak działają ładowarki, a szczególnie klasa CursorLoader. Z klasy CursorLoader będziesz korzystał w celu uzyskania dostępu do informacji zawartych w dostawcy treści Contacts. Na koniec nauczysz się tworzyć własnych niestandardowych dostawców treści.
Receptura: ładowarki Jak wskazuje nazwa, ładowarki (ang. loaders) są wykorzystywane do asynchronicznego ładowania danych. Dostęp do nich uzyskuje się w aktywności i fragmencie za pomocą klasy LoaderManager. Klasa LoaderManager obsługuje cykl życia ładowarek oraz podstawowe kwerendy i kursory. Ładowarki mogą ładować dowolny rodzaj danych źródłowych, ale skupimy się na ładowaniu kursora. Klasa CursorLoader jest wykorzystywana do zarządzania kursorami. Zarządza cyklami życia kursora, wykonuje asynchroniczne kwerendy na dostawcach zawartości, monitoruje zmiany w załączonym zapytaniu itd. Potwierdza również, czy kursor jest zamknięty, kiedy aktywność zostanie zakończona. Aby zastosować klasę CursorLoader, musisz utworzyć nową implementację interfejsu LoaderManager.LoaderCallbacks: LoaderManager.LoaderCallbacks loaderCallback = new LoaderManager.LoaderCallbacks()
Dostęp do klasy LoaderManager uzyskujesz, wywołując metodę getLoaderManager(). W celu zainicjowania nowej ładowarki wywołujesz metodę initLoader() klasy LoaderManager. Oto składnia wykorzystywana podczas stosowania metody initLoader(): getLoaderManager().initLoader(ID_ładowarki, pakiet, wywołania_zwrotne_ładowarki);
Parametr ID_ładowarki reprezentuje identyfikator ładowarki.
226
Rozdział 7. Ładowarki Parametr pakiet reprezentuje pakiet opcjonalnych argumentów. Jeśli parametr
ten nie jest wymagany, możesz przekazać do niego wartość null. Parametr wywołania_zwrotne_ładowarki jest referencją do implementacji
wywołania zwrotnego ładowarki. Przykład: Bundle args = null; getLoaderManager().initLoader(0, args,this);
Identyfikatorem ładowarki jest tutaj wartość 0. Jeśli ładowarka odpowiadająca określonemu identyfikatorowi nie istnieje, zostaje utworzona. Za każdym razem przy wywołaniu metody initLoader() zwracana jest istniejąca ładowarka. Aby odtworzyć ładowarkę, wywołaj metodę restartLoader(). Oto składnia dla zastosowania tej metody: getLoaderManager().restartLoader(ID_ładowarki, pakiet, wywołania_zwrotne_ładowarki);
Wywołania zwrotne ładowarki składają się z trzech procedur obsługi. onCreateLoader() — procedura obsługi wywoływana, kiedy inicjowana jest
ładowarka. Tworzy i zwraca nowy obiekt CursorLoader. Obiekt CursorLoader przechowuje kolumny, które są określone w tablicy planowania String. onLoadFinished() — kiedy klasa LoaderManager zakończy asynchroniczne
zapytanie, wywoływana jest procedura obsługi onLoadFinished z kursorem danych przekazanym jako parametr. onLoaderReset() — kiedy LoaderManager resetuje CursorLoader, wywoływana
jest procedura obsługi onLoaderReset. Procedura ta uwalnia wszelkie referencje do danych zwracanych przez zapytanie. Pamiętaj, że LoaderManager zamyka kursor automatycznie, nie trzeba więc bezpośrednio go zamykać. Najlepszym sposobem na zrozumienie działania klasy CursorLoader jest wykorzystanie jej w celu uzyskania dostępu do informacji pochodzących od dostawców treści.
Receptura: dostawca treści Dostawca treści działa jak magazyn danych i oferuje interfejs umożliwiający dostęp do jego zawartości. W przeciwieństwie do bazy danych, gdzie dostęp do informacji może być uzyskiwany tylko przez paczkę, w której zostały utworzone, w dostawcach treści informacje mogą być udostępniane pomiędzy paczkami. Poniżej wymieniono kilka charakterystycznych cech dostawców treści. Podobnie jak w przypadku bazy danych, dane w dostawcy treści mogą być
kwerendowane, dodawane, edytowane, usuwane i aktualizowane.
Receptura: dostawca treści
Dane mogą być przechowywane w bazach danych, plikach lub w strukturze
sieciowej. Dostawca treści działa jak program pośredniczący (ang. wrapper) dla magazynu
danych, aby przypominał usługi sieciowe. Oznacza to, że dane w dostawcach treści są udostępniane jako usługa. W systemie Android znajdziemy wbudowanych dostawców treści. Najpopularniejsi zostali przedstawieni w tabeli 7.1. Tabela 7.1. Wbudowani dostawcy treści
Dostawca treści
Zastosowanie
Contacts
Przechowuje szczegóły dotyczące kontaktów.
Media Store
Przechowuje pliki multimedialne, takie jak audio, wideo i obrazy.
Settings
Przechowuje ustawienia i preferencje urządzenia.
Browser
Przechowuje dane, takie jak zakładki i historia przeglądarki.
CallLog
Przechowuje dane, takie jak nieodebrane połączenia i szczegóły dotyczące połączeń.
Aby pobrać dane z dostawcy treści, określasz ciąg zapytania w postaci jednolitego identyfikatora zasobu (ang. uniform resource identifier — URI). Składnia zapytania URI wygląda następująco: ://<ścieżka_danych>/
Znaczenia poszczególnych znaczników używanych w zapytaniu URI zostały opisane w tabeli 7.2. Tabela 7.2. Znaczniki wykorzystywane w zapytaniu URI
Znacznik
Znaczenie
standardowy_prefiks
W przypadku dostawców treści standardowym prefiksem jest content://.
upoważnienie
Określa nazwę dostawcy treści. Ma postać nazwy domenowej dla dostawcy treści. W pełni kwalifikowana nazwa nie jest istotna dla uzyskania dostępu do dostawców treści wbudowanych w system Android. W przypadku zewnętrznych dostawców treści zalecana jest jednak w pełni kwalifikowana nazwa. Przykładowo dostęp do wbudowanego w system Android dostawcy treści contacts jest uzyskiwany za pomocą nazwy com.google.android.contacts. Do zewnętrznych dostawców treści należy odwoływać się za pomocą w pełni kwalifikowanej nazwy, takiej jak com.bmharwani.provider.
227
228
Rozdział 7. Ładowarki Tabela 7.2. Znaczniki wykorzystywane w zapytaniu URI (ciąg dalszy)
Znacznik
Znaczenie
ścieżka_danych
Określa rodzaj żądanych danych. Aby przykładowo uzyskać dostęp do samouczka systemu Android z dostawcy treści bmharwani, adres URI powinien wyglądać następująco content://com.bmharwani.provider.AndroidTutorial.
identyfikator
Określa konkretną żądaną zawartość. Aby przykładowo uzyskać dostęp z dostawcy treści bmharwani do samouczka systemu Android o identyfikatorze 2, adres URI powinien wyglądać następująco content://com.bmharwani.provider.AndroidTutorial/2.
Poniższe przykłady przybliżają koncepcję adresu URI dostawcy treści. Adres URI identyfikujący katalog lub zbiór samouczków systemu Android
znajdujący się w bazie danych bmharwani: content://com.bmharwani.provider.AndroidTutorial
Adres URI identyfikujący konkretny samouczek: content://com.bmharwani.provider.AndroidTutorial/#
Znak # jest identyfikatorem konkretnego samouczka. Adres URI identyfikujący zbiór osób w bazie danych contacts: content://contacts/people/
Ponieważ contacts to wbudowany w system Android dostawca treści, nie potrzebujesz w pełni kwalifikowanego adresu URI do zidentyfikowania konkretnej osoby. Adres URI dla osoby o identyfikatorze 10 w bazie danych contacts będzie wyglądał następująco: content://contacts/people/10
Receptura: zastosowanie klasy CursorLoader w celu uzyskania dostępu do informacji przechowywanych przez dostawcę treści Contacts Zanim nauczysz się uzyskiwać dostęp do informacji o kontaktach przechowywanych przez dostawcę treści Contacts, musisz nauczyć się, jak umieszczać w nim te informacje, korzystając z urządzenia fizycznego lub emulatora. W emulatorze kontakty mają nazwę Osoby. Aby uzyskać dostęp do informacji o kontaktach, otwórz listę aplikacji na urządzeniu fizycznym lub emulatorze i kliknij ikonę Osoby (patrz rysunek 7.1, górny obrazek). Wyświetlona zostanie strona z informacją, że na danym urządzeniu fizycznym lub emulatorze nie ma żadnych kontaktów. Na ekranie widoczne będą trzy przyciski:
Receptura: zastosowanie klasy CursorLoader
Utwórz nowy kontakt, Zaloguj się na konto oraz Importuj kontakty (patrz rysunek 7.1, dolny obrazek).
Rysunek 7.1. Wyświetlone na ekranie emulatora ikony reprezentujące różne aplikacje (górny obrazek). Wyświetlony po kliknięciu ikony Osoby ekran pokazujący dostępne opcje (dolny obrazek)
Po wybraniu przycisku Utwórz nowy kontakt pojawi się okno dialogowe informujące, że kopia zapasowa nowego kontaktu nie zostanie utworzona. Zostaniesz również zapytany, czy chcesz dodać konto, na którym kopie zapasowe będą tworzone online. Wyświetlone zostaną dwie opcje: Przechowuj lokalnie oraz Dodaj konto. Wybierz Przechowuj lokalnie. Wyświetlony zostanie pusty formularz, w którym należy wpisać informacje na temat nowego kontaktu, takie jak nazwa kontaktu, organizacja, opis, adres, adres domowy oraz adres e-mail (patrz rysunek 7.2, górny obrazek). Po wpisaniu informacji profilu osobistego
229
230
Rozdział 7. Ładowarki
kliknij widoczny w lewym górnym rogu przycisk Gotowe, aby je zapisać. Utworzony zostanie nowy kontakt wyświetlający zapisane informacje oraz umieszczoną na górze strzałkę skierowaną w lewo. Po wybraniu strzałki przejdziesz do ekranu, który pokazuje listę kontaktów w porządku alfabetycznym. Aby dodać więcej kontaktów, kliknij w prawym górnym rogu ikonę Osoby, pod którą widoczny jest znak plus. Po dodaniu dwóch kontaktów lista kontaktów może wyglądać tak, jak przedstawiono na rysunku 7.2 (dolny obrazek). Możesz konfigurować informacje o kontaktach użytkownika, klikając opcję Konfiguruj profil. Po wybraniu kontaktu z listy wyświetlone zostaną informacje profilowe.
Rysunek 7.2. Formularz do wprowadzania informacji o nowym kontakcie (górny obrazek) oraz wyświetlona lista kontaktów (dolny obrazek)
Receptura: zastosowanie klasy CursorLoader
Dostęp do informacji o kontaktach przechowywanych w Twoim urządzeniu możesz uzyskiwać z poziomu aplikacji Android. Utwórz nowy projekt Android o nazwie AccessContactsApp. Aplikacja będzie uzyskiwać dostęp do informacji o kontaktach i wyświetlać je za pomocą kontrolki ListView. Aby zdefiniować kontrolkę ListView, do pliku układu activity_access_contacts_app.xml dodaj kod przedstawiony w listingu 7.1. Listing 7.1. Kod wpisany w pliku activity_access_contacts_app.xml
Jak widać, kontrolka ListView została zdefiniowana z identyfikatorem contactslist. Będzie on wykorzystywany w celu uzyskania dostępu do kontrolki ListView w kodzie Java. Domyślny rozmiar elementów listy wyświetlanych w kontrolce ListView jest odpowiedni dla telefonów, ale za mały dla tabletów. Aby zmienić rozmiar elementów listy kontrolki ListView zgodnie z rozmiarem ekranu danego urządzenia, dodaj do folderu res/layout kolejny plik XML o nazwie list_item.xml. Wpisz w tym pliku następujący kod:
Powyższy kod będzie wypełniał elementy listy kontrolki ListView spacjami o szerokości 6 dp. Elementy te będą wyświetlane pogrubioną czcionką o rozmiarze zdefiniowanym w zasobie wymiarów text_size. Aby uzyskać dostęp do informacji o kontaktach i wyświetlić je za pomocą kontrolki ListView, wpisz w pliku aktywności Java AccessContactsAppActivity.java kod przedstawiony w listingu 7.2. Listing 7.2. Kod wpisany w pliku AccessContactsAppActivity.java package com.androidtablet.accesscontactsapp; import import import import import
android.app.Activity; android.os.Bundle; android.net.Uri; android.database.Cursor; android.content.CursorLoader;
231
232
Rozdział 7. Ładowarki import import import import
android.provider.ContactsContract; android.widget.ListView; java.util.ArrayList; android.widget.ArrayAdapter;
public class AccessContactsAppActivity extends Activity { ArrayList contactRows=new ArrayList(); final String[] nocontact={"Nie masz żadnych kontaktów w urządzeniu"}; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_access_contacts_app); final ListView contactsList=(ListView) findViewById( R.id.contactslist); Uri contactsUri = Uri.parse("content:// contacts/people"); String[] projection = new String[] {ContactsContract.Contacts._ID, ContactsContract.Contacts.DISPLAY_NAME }; Cursor c; CursorLoader cursorLoader = new CursorLoader(this, contactsUri, projection, null, null, null); c = cursorLoader.loadInBackground(); contactRows.clear(); c.moveToFirst(); while(c.isAfterLast()==false){ String contactID = c.getString(c.getColumnIndex( ContactsContract.Contacts._ID)); String contactDisplayName = c.getString( c.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME)); contactRows.add(contactID+ " "+contactDisplayName); c.moveToNext(); } if (c != null && !c.isClosed()) { c.close(); } if(contactRows.isEmpty()) { ArrayAdapter arrayAdpt=new ArrayAdapter (this, R.layout.list_item, nocontact); contactsList.setAdapter(arrayAdpt); } else { ArrayAdapter arrayAdpt=new ArrayAdapter (this, R.layout.list_item, contactRows); contactsList.setAdapter(arrayAdpt); } } }
Na początku określasz adres URI contactsUri dla dostawcy kontaktów. Następnie definiowana jest tablica ciągów znaków projection, aby ustalić kolumny, które chcesz pobrać z bazy danych contacts. Za pomocą klasy CursorLoader ładujesz wiersze z dostawcy kontaktów i przypisujesz je do kursora c. Dalej przy zastosowaniu pętli while wydobywane są informacje z kursora. Ponieważ chcesz wyświetlić jedynie identyfikator i nazwę kontaktu, uzyskiwany jest dostęp do informacji z kolumn ContactsContract.Contacts._ID
Receptura: Tworzenie niestandardowego dostawcy treści
i ContactsContract.Contacts.DISPLAY_NAME, a informacje te są przypisywane do obiektu contactRows klasy ArrayList. Jeśli obiekt contactRows nie jest pusty, za jego pomocą definiowany jest obiekt arrayAdpt klasy ArrayAdapter. Na koniec kontrolka ListView jest wypełniania informacjami z obiektu arrayAdpt klasy ArrayAdapter. Aby w swojej aplikacji uzyskać dostęp do dostawcy kontaktów, musisz w pliku AndroidManifest.xml dodać następujące zezwolenie:
Do pliku AndroidManifest.xml dodaj wiersze kodu zaznaczone w listingu 7.3 pogrubioną czcionką. Listing 7.3. Kod wpisany w pliku AndroidManifest.xml
Jeśli w Twojej aplikacji znajdą się powyższe wiersze kodu, będzie ona uzyskiwać dostęp do informacji przechowywanych w dostawcy treści Contacts. Przy założeniu, że w Twoim urządzeniu lub emulatorze zapisane są dwa kontakty, po uruchomieniu aplikacji uzyskany zostanie dostęp do informacji o kontaktach i informacje te zostaną wyświetlone za pomocą kontrolki ListView, tak jak pokazano na rysunku 7.3.
Receptura: Tworzenie niestandardowego dostawcy treści W trakcie lektury tej i dwóch kolejnych receptur nauczysz się, jak utworzyć swojego własnego dostawcę treści, który przechowuje informacje związane z różnymi produktami. W tym dostawcy treści będziesz mógł wprowadzić informacje o nowym produkcie,
233
234
Rozdział 7. Ładowarki
Rysunek 7.3. Uzyskany dostęp do informacji o kontaktach z urządzenia fizycznego lub emulatora, a następnie wyświetlenie ich za pomocą kontrolki ListView
wyświetlić listę produktów, zaktualizować informacje o produktach, a nawet usunąć produkty, które nie są już potrzebne. Mówiąc ściślej, aplikacja ta (dostawca treści) będzie pozwalała użytkownikom na wprowadzanie nazwy i ceny dla różnych produktów, wyświetlanie przewijanej listy przechowywanych produktów oraz usuwanie i aktualizowanie informacji o produktach. Aby łatwiej było Ci zrozumieć procedurę tworzenia niestandardowego dostawcy treści, podzielimy ją na trzy następujące fazy: wprowadzanie informacji o produkcie, wyświetlanie informacji o produkcie, edytowanie informacji o produkcie.
W tej recepturze skoncentrujemy się na pierwszej fazie związanej z wprowadzaniem informacji o produkcie. Utwórz nowy projekt Android o nazwie CustomContentProviderApp. W celu zakończenia pierwszej fazy tworzenia własnego niestandardowego dostawcy treści, musisz wykonać trzy kroki. Utworzyć formularz służący do wprowadzania informacji o różnych
produktach. Utworzyć plik klasy Java, który będzie pełnił rolę Twojego niestandardowego
dostawcy treści. W tej klasie będziesz definiował bazę danych dla swojego dostawcy treści, adres URI zawartości służący do pobierania danych z dostawcy treści oraz typy MIME oraz inne elementy dostawcy treści. Napisać w głównym pliku aktywności Java kod, który umożliwi wstawianie
wierszy w dostawcy treści.
Receptura: Tworzenie niestandardowego dostawcy treści
W ramach pierwszego kroku będziesz wyświetlał formularz umożliwiający użytkownikom wpisanie informacji na temat różnych produktów. Formularz ten będzie zawierał kontrolki EditText, w których użytkownicy będą wpisywać nazwę produktu i jego cenę. Formularz będzie też wyświetlał dwie kontrolki Button: jedną dla dodania informacji o produkcie, a drugą do wyświetlenia listy produktów, które są już zapisane w dostawcy treści. W celu wyświetlenia takiego interfejsu użytkownika, wpisz w pliku układu activity_custom_content_provider_app.xml kod przedstawiony w listingu 7.4. Listing 7.4. Kod wpisany w pliku activity_custom_content_provider_app.xml
235
236
Rozdział 7. Ładowarki
Jak możesz tutaj zauważyć, zdefiniowane zostały trzy kontrolki TextView, którym przypisano odpowiednio teksty: Wprowadź informację o produktach, Nazwa produktu i Cena. Dwóm kontrolkom EditText przypisane zostały identyfikatory product_name i price. Dwóm kontrolkom Button przypisano identyfikatory add_productinfo i list_productinfo. Nagłówki przypisane do dwóch kontrolek Button to Dodaj informację o produkcie i Pokaż informację o produkcie. Identyfikatory przypisane tym kontrolkom pozwalają uzyskiwać do nich dostęp w kodzie Java. Jak widać, kontrolce TextView wyświetlającej nagłówek formularza przydzielono rozmiar czcionki zdefiniowany w zasobie wymiarów heading_size. Rozmiar czcionki pozostałych kontrolek TextView jest definiowany za pomocą zasobu wymiarów text_size. Podobnie zasoby wymiarów product_width i price_width definiują odpowiednio szerokość dwóch kontrolek EditText. Powinieneś zdefiniować zasoby wymiarów dla wszystkich trzech typów urządzeń z systemem Android: telefonów, 7-calowych tabletów oraz 10-calowych tabletów. Zasoby wymiarów dla telefonów są zdefiniowane w folderze res/values, natomiast dla tabletów wykorzystywane są foldery res/values-sw600dp i res/values-sw720dp. Dla telefonów otwórz plik zasobu wymiarów dimens.xml znajdujący się w folderze res/values i zdefiniuj cztery zasoby — heading_size, text_size, product_width oraz price_width — wpisując w nim następujący kod: 18sp 14sp 250dp 100dp
Aby zdefiniować zasoby wymiarów dla 7-calowych tabletów, otwórz plik dimens.xml znajdujący się w folderze res/values-sw600dp i wpisz w nim następujący kod:
Receptura: Tworzenie niestandardowego dostawcy treści 28sp 24sp 400dp 150dp
Analogicznie zdefiniuj zasoby wymiarów dla 10-calowych tabletów, otwierając znajdujący się w folderze res/values-sw720dp plik dimens.xml i wpisując w nim następujący kod: 36sp 32sp 500dp 200dp
Następnie dodaj do swojego projektu plik klasy Java, który będzie pełnił rolę Twojego niestandardowego dostawcy treści. Wywołaj dodaną klasę ProductsProvider. W tej klasie będziesz wykonywał opisane niżej zadania. Zdefiniujesz bazę danych dla Twojego dostawcy treści. Do przechowywania
nazw produktów i odpowiadających im cen utworzysz bazę danych o nazwie Products. W tej bazie danych zbudujesz pojedynczą tabelę o nazwie productinfo. Tabela productinfo będzie składała się z dwóch kolumn, product i price, które będą przechowywać nazwy produktów i odpowiadające im ceny. Zdefiniujesz adres URI zawartości w celu pobierania danych z dostawcy treści.
Obiekt UriMatcher będzie definiowany i wypełniany za pomocą dwóch adresów URI. Adres URI kończący się na productinfo będzie reprezentował żądanie wszystkich wierszy, a adres kończący się na productinfo/# będzie reprezentował żądanie pojedynczego wiersza. Zdefiniowany zostanie również mechanizm ContentResolver, za pomocą którego pobierane będą dane z wykorzystaniem adresu URI zawartości. Zdefiniujesz typy MIME dla pojedynczego wiersza i dla zbiorów wierszy.
Dostawca treści zwraca typ MIME danych, które przesyła. Zaimplementujesz metody getType(), query(), insert(), update() oraz
delete(), aby dostawca treści był funkcjonalny. W celu identyfikacji typu danych dostawcy treści zdefiniuj metodę getType(). Aby umożliwić użytkownikom przeprowadzanie kwerendy dotyczącej pożądanego wiersza (lub wierszy) po dostawcy treści, zdefiniuj metodę query(). Utwórz obiekt klasy SQLiteQueryBuilder — klasy pomocniczej, która jest wymagana do tworzenia i wykonywania zapytań SQL na instancji bazy danych SQLite. Do wstawiania nowego wiersza w dostawcy treści zdefiniuj metodę insert(). Po wstawieniu wiersza wywołaj metodę notifyChange() z klasy ContentResolver w celu
237
238
Rozdział 7. Ładowarki
powiadomienia wszelkich zarejestrowanych obserwatorów o operacji wstawiania. Zdefiniuj metodę update() do aktualizacji informacji w dostawcy treści. Zdefiniuj również metodę delete() do usuwania informacji z dostawcy treści. Aby wykonać wymienione zadania, wpisz w pliku ProductsProvider.java kod przedstawiony w listingu 7.5. Listing 7.5. Kod wpisany w pliku ProductsProvider.java package com.androidtablet.customcontentproviderapp; import import import import import import import import import import import import import
android.content.ContentProvider; android.content.UriMatcher; android.net.Uri; android.database.sqlite.SQLiteOpenHelper; android.database.sqlite.SQLiteDatabase; android.content.Context; android.content.ContentValues; android.content.ContentUris; android.database.SQLException; android.database.Cursor; android.text.TextUtils; android.database.sqlite.SQLiteQueryBuilder; android.content.ContentResolver ;
public class ProductsProvider extends ContentProvider { static final String DB_NAME = "Products.db"; static final String DB_TABLE = "productinfo"; static final int DB_VERSION = 1; static final String CREATE_TABLE ="CREATE TABLE " + DB_TABLE + " (_id INTEGER PRIMARY KEY AUTOINCREMENT, product TEXT not null, price TEXT not null);"; static final String ID = "_id"; static final String PRODUCT = "product"; static final String PRICE = "price"; static final String AUTHORITY="com.bmharwani.provider.Products"; static final Uri CONTENT_URI =Uri.parse("content://"+ AUTHORITY+"/productinfo"); static final int ALLROWS = 1; static final int SINGLEROW = 2; private static final UriMatcher URIMATCHER; static{ URIMATCHER = new UriMatcher(UriMatcher.NO_MATCH); URIMATCHER.addURI(AUTHORITY, "productinfo", ALLROWS); URIMATCHER.addURI(AUTHORITY, "productinfo/#", SINGLEROW); } SQLiteDatabase ProductsDB; public static final String CONTENT_ITEM_TYPE = ContentResolver.CURSOR_ITEM_BASE_TYPE+"/productinfo"; @Override public boolean onCreate() { Context context = getContext(); SQHelper helper = new SQHelper(context);
Receptura: Tworzenie niestandardowego dostawcy treści ProductsDB = helper.getWritableDatabase(); return (ProductsDB == null)? false:true; } @Override public String getType(Uri uri) { switch (URIMATCHER.match(uri)){ case ALLROWS: return "vnd.android.cursor.dir/vnd.products.productinfo"; case SINGLEROW: return "vnd.android.cursor.item/vnd.products.productinfo"; default: throw new IllegalArgumentException("Nieobsługiwane URI: " + uri); } } @Override public Cursor query(Uri uri, String[] projection, String criteria, String[] criteriaValues, String sortColumn) { SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder(); queryBuilder.setTables(DB_TABLE); if (URIMATCHER.match(uri) == SINGLEROW) queryBuilder.appendWhere(ID + " = " + uri.getPathSegments().get(1)); if (sortColumn==null || sortColumn=="") sortColumn = "product"; Cursor c = queryBuilder.query(ProductsDB,projection, criteria,criteriaValues,null,null,sortColumn); c.setNotificationUri(getContext().getContentResolver(), uri); return c; } @Override public Uri insert(Uri uri, ContentValues contentValues) { long rowID = ProductsDB.insert(DB_TABLE,null, contentValues); if (rowID >0) { Uri _uri = ContentUris.withAppendedId(CONTENT_URI, rowID); getContext().getContentResolver().notifyChange(_uri, null); return _uri; } throw new SQLException("Błąd: Nowy wiersz nie może zostać wprowadzony "); } @Override public int update(Uri uri, ContentValues contentValues, String criteria, String[] criteriaValues) { int count = 0; switch (URIMATCHER.match(uri)){ case ALLROWS: count = ProductsDB.update(DB_TABLE, contentValues, criteria,criteriaValues); break;
239
240
Rozdział 7. Ładowarki case SINGLEROW: count = ProductsDB.update(DB_TABLE, contentValues, ID + " = " + uri. getPathSegments().get(1) +(! TextUtils. isEmpty(criteria) ? " AND (" +criteria + ')': ""),criteriaValues); break; default: throw new IllegalArgumentException("Nie znaleziono URI: " + uri); } getContext().getContentResolver().notifyChange(uri, null); return count; } @Override public int delete(Uri rowUri, String criteria, String[] criteriaValues) { int count=0; switch (URIMATCHER.match(rowUri)){ case ALLROWS: count = ProductsDB.delete(DB_TABLE, criteria, criteriaValues); break; case SINGLEROW: String id = rowUri.getPathSegments().get(1); count = ProductsDB.delete(DB_TABLE, ID + " = " + id +(!TextUtils.isEmpty(criteria) ? " AND (" +criteria + ')': ""),criteriaValues); break; default: throw new IllegalArgumentException("Nie znaleziono URI: " + rowUri); } getContext().getContentResolver().notifyChange(rowUri, null); return count; } private static class SQHelper extends SQLiteOpenHelper { SQHelper(Context context) { super(context, DB_NAME, null, DB_VERSION); } @Override public void onCreate(SQLiteDatabase db) { db.execSQL(CREATE_TABLE); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { db.execSQL("DROP TABLE IF EXISTS "+ DB_TABLE); onCreate(db); } } }
Receptura: Tworzenie niestandardowego dostawcy treści
Jak widać, metoda onCreate() jest nadpisywana w celu inicjowania źródła danych, aby źródło to mogło być dostępne poprzez dostawcę treści. W tej metodzie definiujesz obiekt SQHelper i otwierasz bazę danych Products.db w trybie zapisu. Aby wstawiać wiersze w dostawcy treści i uzyskiwać dostęp do zapisanych w nim informacji, wpisz w pliku aktywności Java CustomContentProviderAppActivity.java kod przedstawiony w listingu 7.6. Listing 7.6. Kod wpisany w pliku aktywności Java CustomContentProviderAppActivity.java package com.androidtablet.customcontentproviderapp; import import import import import import import import
android.app.Activity; android.os.Bundle; android.view.View; android.content.ContentValues; android.widget.Toast; android.widget.EditText; android.widget.Button; android.content.Intent;
public class CustomContentProviderAppActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_custom_content_provider_app); Button addProductButton = (Button) this.findViewById( R.id.add_productinfo); addProductButton.setOnClickListener(new Button. OnClickListener(){ @Override public void onClick(View v) { ContentValues contentValues = new ContentValues(); EditText productName = (EditText) findViewById(R.id.product_name); EditText productPrice = (EditText) findViewById(R.id.price); contentValues.put(ProductsProvider.PRODUCT, productName.getText().toString()); contentValues.put(ProductsProvider.PRICE, productPrice.getText().toString()); getContentResolver().insert(ProductsProvider.CONTENT_URI, contentValues); Toast.makeText(CustomContentProviderAppActivity. this, "Dodałeś wiersz", Toast.LENGTH_SHORT). show(); productName.setText(""); productPrice.setText(""); } }); Button listProductButton = (Button)this.findViewById( R.id.list_productinfo); listProductButton.setOnClickListener(new Button.OnClickListener(){ public void onClick(View v) {
241
242
Rozdział 7. Ładowarki startActivity(new Intent( CustomContentProviderAppActivity.this, ShowProductActivity.class));
#1
} }); } }
W instrukcji #1 powyższego kodu uzyskiwany jest dostęp do klasy, która uzyskuje dostęp do zawartości dostawcy treści i wyświetla tę zawartość. Jednak w tej recepturze koncentrujemy się na wprowadzaniu treści do dostawcy treści, a nie listowaniu tej treści. Procedury uzyskiwania dostępu do zawartości z dostawcy treści i listowania tej zawartości nauczysz się w kolejnej recepturze. Tymczasem wykomentuj instrukcję #1 z listingu 7.6. Możesz zauważyć, że do kontrolek Button o identyfikatorach add_productinfo i list_productinfo uzyskiwany jest dostęp z pliku układu, a same kontrolki są mapowane na obiekty addProductButton i listProductButton klasy Button. Przycisk addProductButton wykonuje operację wstawiania, a przycisk listProductButton pokazuje zapisane w dostawcy treści informacje. Aby wstawić wiersz w dostawcy treści, pobierasz go najpierw do obiektu ContentValues. Obiekt ContentValues jest katalogiem par klucz-wartość, w którym możesz gromadzić informacje pojedynczego rekordu. Następnie zadaniem mechanizmu ContentResolver jest wstawienie tego rekordu do dostawcy treści przy wykorzystaniu adresu URI. Mechanizm ContentResolver rozwiązuje referencję URI na odpowiedniego dostawcę i wstawia do niego informację przechowywaną w obiekcie ContentValues. Aby dodać informację o produkcie, pobierasz dane, które użytkownik wpisał w dwóch kontrolkach EditText. Następnie tworzysz nowy obiekt ContentValues i zapełniasz go informacjami wprowadzonymi w kontrolkach EditText. Ponieważ Twój dostawca treści znajduje się w tej samej paczce, do której uzyskujesz dostęp, wykorzystujesz stałe ProductsProvider.PRODUCT i ProductsProvider.PRICE, aby odwołać się do kolumn product i price tabeli bazy danych. Żeby ContentResolver mógł odnaleźć Twojego dostawcę treści, musisz zarejestrować go w pliku manifestu aplikacji. Znacznik oraz jego atrybuty android:name i android:authorities są wykorzystywane do zarejestrowania dostawcy treści. Atrybut android:name jest nazwą klasy dostawcy treści, a atrybut android:authorities definiuje bazowy adres URI upoważnienia dostawcy treści. W Twojej aplikacji nazwą klasy dostawcy jest ProductsProvider. Format wykorzystywany do definiowania upoważnienia dostawcy treści jest następujący: com..provider.
Jeśli założymy, że nazwa_przedsiębiorstwa to bmharwani, a nazwa_aplikacji to Products, podstawowym adresem URI upoważnienia Twojego dostawcy będzie com.bmharwani.provider.Products. Dlatego też kompletny znacznik, który zostanie wykorzystany do zarejestrowania Twojego dostawcy treści, to:
Receptura: wyświetlanie informacji z niestandardowego dostawcy treści
Otwórz plik AndroidManifest.xml i zarejestruj swojego dostawcę treści, wpisując następujący kod w elemencie :
W ten sposób zakończyłeś pierwszą fazę tworzenia niestandardowego dostawcy treści. Po uruchomieniu aplikacji wyświetlony zostanie formularz, w którym należy wpisać nazwę i cenę produktu. Wprowadź informacje o produkcie i kliknij przycisk Dodaj informację o produkcie, a informacje zostaną zapisane w dostawcy treści. Na ekranie pojawi się komunikat Toast o treści Dodałeś wiersz, co potwierdza udaną operację wstawienia informacji o produkcie — patrz rysunek 7.4.
Rysunek 7.4. Komunikat Toast o treści Dodałeś wiersz pojawia się po udanej operacji dodania informacji o produkcie do dostawcy treści
Po kliknięciu przycisku Pokaż informację o produkcie nie zostanie wykonana żadna akcja. Aby włączyć ten przycisk i wyświetlić informacje zapisane w dostawcy treści, musisz przejść do drugiej fazy tworzenia niestandardowego dostawcy treści, czyli fazy wylistowania informacji o produkcie.
Receptura: wyświetlanie informacji z niestandardowego dostawcy treści Przycisk Pokaż informację o produkcie, widoczny na rysunku 7.4, powinien wyświetlać informacje o produktach przechowywanych w Twoim dostawcy treści. W tym celu utworzysz nową aktywność Java o nazwie ShowProductActivity, która będzie uzyskiwała dostęp do zawartości dostawcy treści. Aktywność ShowProductActivity będzie uruchamiana
243
244
Rozdział 7. Ładowarki
lub inicjowana po kliknięciu przycisku Pokaż informację o produkcie. Ta informacja (wiersze pobrane z dostawcy treści) będzie wyświetlana za pomocą kontrolki ListView. Aby przy użyciu kontrolki ListView wyświetlić informacje z dostawcy treści, dodaj do folderu res/layout plik układu o nazwie showproduct.xml. By zdefiniować kontrolkę ListView, dodaj w pliku showproduct.xml kod przedstawiony w listingu 7.7. Listing 7.7. Kod wpisany w pliku showproduct.xml
Jak widać, w pliku układu została zdefiniowana kontrolka ListView z identyfikatorem list. Domyślny rozmiar elementów listy wyświetlanych w kontrolce ListView jest odpowiedni dla telefonów, ale za mały dla tabletów. Aby zmienić rozmiar elementów listy kontrolki ListView według rozmiaru ekranu danego urządzenia, dodaj do folderu res/layout kolejny plik XML o nazwie list_item.xml. W tym pliku wpisz kod przedstawiony w listingu 7.8. Listing 7.8. Kod wpisany w pliku list_item.xml
Powyższy kod wypełni elementy listy kontrolki ListView spacjami o szerokości 6 dp. Elementy te będą wyświetlane pogrubioną czcionką o rozmiarze zdefiniowanym w zasobie wymiarów text_size.
Receptura: wyświetlanie informacji z niestandardowego dostawcy treści
Aby załadować kontrolkę ListView zdefiniowaną w pliku showproduct.xml, dodaj do paczki projektu com.androidtablet.customcontentproviderapp plik klasy Java o nazwie ShowProductActivity.java. W celu pobrania wszystkich wierszy z dostawcy treści i wyświetlania ich w kontrolce ListView za pomocą klasy CursorLoaders wpisz w pliku ShowProductActivity.java kod przedstawiony w listingu 7.9. Listing 7.9. Kod wpisany w pliku ShowProductActivity.java package com.androidtablet.customcontentproviderapp; import import import import import import import import import import import
android.app.ListActivity; android.os.Bundle; android.widget.SimpleCursorAdapter; android.app.LoaderManager.LoaderCallbacks; android.content.CursorLoader; android.content.Loader; android.database.Cursor; android.widget.ListView; android.content.Intent; android.net.Uri; android.view.View;
public class ShowProductActivity extends ListActivity implements LoaderCallbacks { private SimpleCursorAdapter adapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.showproduct); String[] columns = new String[] { ProductsProvider.PRODUCT}; int[] toIds = new int[] {R.id.product_name}; getLoaderManager().initLoader(0, null,this); adapter = new SimpleCursorAdapter(this, R.layout.list_item, null, columns, toIds, 0); setListAdapter(adapter); } @Override public Loader onCreateLoader(int id, Bundle args) { String[] projection = new String[] {ProductsProvider.ID, ProductsProvider.PRODUCT, ProductsProvider.PRICE} ; CursorLoader cursorLoader = new CursorLoader(this, ProductsProvider.CONTENT_URI, projection, null, null, null); return cursorLoader; } @Override public void onLoadFinished(Loader loader, Cursor data) { adapter.swapCursor(data); }
245
246
Rozdział 7. Ładowarki @Override public void onLoaderReset(Loader loader) { adapter.swapCursor(null); } @Override protected void onListItemClick(ListView l, View v, int position, long id) { super.onListItemClick(l, v, position, id); Intent intent = new Intent(this, MaintainProductActivity.class); Uri uri = Uri.parse(ProductsProvider.CONTENT_URI + "/" + id); intent.putExtra(ProductsProvider.CONTENT_ITEM_TYPE, uri); startActivity(intent); }
#1 #2 #3 #4 #5 #6 #7 #8
}
Aby skorzystać z klasy CursorLoader, musisz utworzyć nową implementację interfejsu LoaderManager.LoaderCallbacks: public class ShowProductActivity extends ListActivity implements LoaderCallbacks
Jak możesz zauważyć, dostęp do klasy LoaderManager jest uzyskiwany po wywołaniu metody getLoaderManager(). W celu zainicjowania nowej ładowarki wywoływana jest metoda initLoader() klasy LoaderManager. Za pomocą klasy SimpleCursorAdapter uzyskiwany jest dostęp do danych z kolumny ProductsProvider.PRODUCT i dane te są wyświetlane w kontrolce ListView.
Uwaga W listingu 7.9 wykomentuj całą metodę onListItemClick() (instrukcje od #1 do #8), ponieważ będzie ona wykorzystywana w kolejnej recepturze do utrzymywania zawartości w dostawcy treści. Ponieważ bieżąca receptura koncentruje się tylko na wylistowaniu zawartości z dostawcy treści, a nie na utrzymywaniu tej zawartości, wykomentuj metodę onListItemClick() tymczasowo. Ponadto, aby włączyć wyświetlanie informacji z dostawcy treści, usuń wykomentowanie instrukcji #1 z listingu 7.6, które zastosowałeś w poprzedniej recepturze.
Aby Twoja aplikacja mogła identyfikować klasę ShowProductActivity dodaną w tej recepturze, w pliku AndroidManifest.xml dodaj w elemencie poniższą instrukcję:
Druga faza tworzenia Twojego niestandardowego dostawcy treści (wylistowanie informacji z dostawcy treści) jest zakończona. Po uruchomieniu aplikacji otrzymasz formularz umożliwiający wprowadzenie informacji o nowym produkcie. W tym celu wypełnij puste pola i kliknij przycisk Dodaj informację o produkcie. Aby zobaczyć listę produktów przechowywanych w Twoim dostawcy treści, kliknij przycisk Pokaż
Receptura: wyświetlanie informacji z niestandardowego dostawcy treści
informację o produkcie (patrz rysunek 7.5, górny obrazek). Na rysunku 7.5 (dolny obrazek) pokazano informacje o produktach pobrane z dostawcy treści i wyświetlone za pomocą kontrolki ListView.
Rysunek 7.5. Przyciski umożliwiające dodanie i wylistowanie zawartości z dostawcy treści (górny obrazek) oraz informacje pobrane z dostawcy treści i wyświetlone za pomocą kontrolki ListView (dolny obrazek)
Kiedy w tym momencie klikniesz któryś z produktów wyświetlonych w kontrolce ListView, nic się nie stanie. W trakcie lektury kolejnej receptury nauczysz się edytować i usuwać informacje o produkcie wybranym z kontrolki ListView.
Zakończone zostały dwie pierwsze fazy: tworzenie Twojego niestandardowego dostawcy treści i wyświetlanie zawartych w nim informacji. Teraz przejdziemy do fazy utrzymywania informacji, czyli aktualizowania i usuwania informacji przechowywanych w dostawcy treści.
247
248
Rozdział 7. Ładowarki
Receptura: aktualizowanie i usuwanie informacji przechowywanych w niestandardowym dostawcy treści Utrzymywanie dostawcy treści ma na celu umożliwienie użytkownikom aktualizowania i usuwania znajdującej się tam zawartości. W tym celu dodaj do swojego projektu klasę Java MaintainProductActivity.class. Klasa ta będzie przekazywać adres URI danego wiersza, czyli produkt wybrany z kontrolki ListView. W aktywności MaintainProductActivity będziesz usuwał i aktualizował zawartość dostawcy treści. Oznacza to, że będziesz wyświetlał nazwę produktu i jego cenę za pomocą kontrolek EditText oraz dostarczał użytkownikowi dwie kontrolki Button o nagłówkach Aktualizuj i Usuń. Użytkownik będzie mógł modyfikować informacje zawarte w dostawcy treści poprzez aktualizowanie danych wyświetlanych w kontrolkach EditText i zatwierdzanie zmian przyciskiem Aktualizuj. Użytkownik będzie mógł również usunąć informacje z dostawcy treści, klikając przycisk Usuń. Aby wyświetlić informacje o produkcie wybranym z kontrolki ListView, dodaj do folderu res/layout plik układu o nazwie maintainproduct.xml i wpisz w tym pliku kod przedstawiony w listingu 7.10. Listing 7.10. Kod wpisany w pliku maintainproduct.xml
Receptura: aktualizowanie i usuwanie informacji w niestandardowym dostawcy treści android:layout_marginLeft="20dip" android:layout_toRightOf="@id/price_view" android:layout_below="@id/product_name" />
Jak możesz zauważyć, dwóm kontrolkom EditText, które będą wykorzystywane do wyświetlenia nazw produktów i cen, przydzielono identyfikatory product_name i price. Dwóm kontrolkom Button o nagłówkach Usuń i Aktualizuj przydzielono identyfikatory delete_productinfo i update_productinfo. Będą one identyfikować dane kontrolki w kodzie Java w celu uzyskania do nich dostępu. Aby załadować widoki zdefiniowane w powyższym pliku układu, do paczki projektu com.androidtablet.customcontentproviderapp dodaj plik klasy Java o nazwie MaintainProductActivity.java. Aby usuwać i aktualizować informacje zawarte w dostawcy treści, wpisz w pliku MaintainProductActivity.java kod przedstawiony w listingu 7.11. Listing 7.11. Kod wpisany w pliku MaintainProductActivity.java package com.androidtablet.customcontentproviderapp; import import import import import import import import import
android.os.Bundle; android.app.Activity; android.widget.EditText; android.widget.Button; android.net.Uri; android.content.ContentValues; android.database.Cursor; android.view.View; android.widget.Toast;
public class MaintainProductActivity extends Activity { EditText productName, price; Uri uri; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.maintainproduct);
249
250
Rozdział 7. Ładowarki productName = (EditText) findViewById(R.id.product_name); price = (EditText) findViewById(R.id.price); Bundle extras = getIntent().getExtras(); uri = (extras == null) ? null: (Uri) extras. getParcelable(ProductsProvider.CONTENT_ITEM_TYPE); if (extras != null) { uri = extras.getParcelable(ProductsProvider.CONTENT_ITEM_TYPE); String[] projection = new String[] {ProductsProvider.ID, ProductsProvider.PRODUCT, ProductsProvider.PRICE} ; Cursor cursor = getContentResolver().query(uri, projection, null, null, null); if (cursor != null) { cursor.moveToFirst(); productName.setText(cursor.getString(cursor. getColumnIndexOrThrow (ProductsProvider.PRODUCT))); price.setText(cursor.getString(cursor. getColumnIndexOrThrow (ProductsProvider.PRICE))); cursor.close(); } } Button deleteProductInfo = (Button) findViewById( R.id.delete_productinfo); Button updateProductInfo = (Button) findViewById( R.id.update_productinfo); deleteProductInfo.setOnClickListener(new Button.OnClickListener(){ @Override public void onClick(View v) { int count = getContentResolver().delete(uri,null, null); if(count >0) Toast.makeText(MaintainProductActivity.this, "Usunąłeś wiersz", Toast.LENGTH_SHORT).show(); } }); updateProductInfo.setOnClickListener(new Button.OnClickListener(){ @Override public void onClick(View v) { ContentValues contentValues = new ContentValues(); contentValues.put(ProductsProvider.PRODUCT, productName.getText().toString()); contentValues.put(ProductsProvider.PRICE, price.getText().toString()); getContentResolver().update(uri, contentValues, null,null); Toast.makeText(MaintainProductActivity.this, "Zaktualizowałeś wiersz", Toast.LENGTH_SHORT).show(); } }); }
}
Do kontrolek EditText o identyfikatorach product_name i price uzyskujesz dostęp z pliku układu maintainproduct.xml i mapujesz te kontrolki na obiekty EditText o nazwach productName i price. Uzyskujesz dostęp do adresu URI nazwy produktu wybranego z kontrolki ListView, który jest przekazywany za pomocą obiektu Bundle. Następnie
Receptura: aktualizowanie i usuwanie informacji w niestandardowym dostawcy treści
wywołujesz metodę query(), aby uzyskać dostęp do wiersza z dostawcy treści o określonym adresie URI. Metoda query() jest wywoływana w celu uzyskania ceny wybranego produktu, aby nazwa produktu i jego cena mogły zostać przypisane do kontrolek EditText i za ich pomocą wyświetlone. Aby usunąć wiersz, wywołujesz metodę delete() z klasy ContentResolver. Do tej metody przekazujesz adres URI zawartości wiersza, który ma być usunięty. Aby zaktualizować wiersz, wywołujesz metodę update()z klasy ContentResolver. Do tej metody przekazujesz adres URI wiersza, który ma być zaktualizowany, oraz obiekt ContentValues zawierający zaktualizowaną lub nową zawartość odpowiednich kolumn. Aby Twoja aplikacja mogła zidentyfikować klasę MaintainProductActivity dodaną w tej recepturze, dodaj do pliku AndroidManifest.xml w elemencie następującą instrukcję:
Po dodaniu powyższej instrukcji plik AndroidManifest.xml będzie wyglądał tak, jak przedstawiono w listingu 7.12. Nowo dodany został jedynie kod zaznaczony pogrubioną czcionką. Reszta to kod domyślny. Dodany fragment kodu rejestruje Twojego niestandardowego dostawcę treści oraz definiuje dla celów identyfikacji aplikacji aktywności ShowProductActivity i MaintainProductActivity, które do tej aplikacji dodałeś. Listing 7.12. Kod wpisany w pliku AndroidManifest.xml
251
252
Rozdział 7. Ładowarki
Ostatnia faza tworzenia Twojego niestandardowego dostawcy treści została zakończona, usuń więc komentarz z metody onListItemClick() w klasie ShowProductActivity (patrz listing 7.9). Jak zapewne pamiętasz, wykomentowałeś tę metodę w poprzedniej recepturze. Twój niestandardowy dostawca treści jest kompletny i gotowy do uruchomienia. Po uruchomieniu aplikacji otrzymasz ekran umożliwiający wprowadzenie informacji o produkcie. Wpisz nazwę produktu i jego cenę, a następnie kliknij przycisk Dodaj informację o produkcie, aby zapisać dane w dostawcy treści. Po udanym wstawieniu informacji pojawi się komunikat Toast z wiadomością Dodałeś wiersz (patrz rysunek 7.6, górny obrazek). Kiedy już wstawisz wiersze w dostawcy treści i klikniesz przycisk Pokaż informację o produkcie, zobaczysz wszystkie produkty wylistowane w kontrolce ListView (patrz rysunek 7.6, środkowy obrazek). Po wybraniu produktu z kontrolki ListView zostaną wyświetlone szczegóły na jego temat. Aby zmodyfikować nazwę lub cenę produktu, po prostu zaktualizuj zawartość w kontrolkach EditText i kliknij przycisk Aktualizuj. Informacje w dostawcy treści zostaną zaktualizowane. Na rysunku 7.6 (dolny obrazek) został pokazany zaktualizowany wiersz. Komunikat Toast z wiadomością Zaktualizowałeś wiersz pojawia się, kiedy aktualizacja przebiegnie z powodzeniem. Po kliknięciu przycisku Usuń wybrany produkt i jego cena zostaną usunięte z dostawcy treści, a na ekranie pojawi się komunikat Toast z informacją Usunąłeś wiersz, tak jak pokazano na rysunku 7.7 (górny obrazek). Rysunek 7.7 (dolny obrazek) potwierdza, że wybrany wiersz został usunięty z dostawcy treści, ponieważ nie jest już widoczny w kontrolce ListView.
Podsumowanie W tym rozdziale dowiedziałeś się, jaką rolę pełnią ładowarki przy ładowaniu danych z bazowego źródła danych w sposób asynchroniczny. Nauczyłeś się korzystać z klasy CursorLoader w celu uzyskiwania informacji zawartych w dostawcy treści Contacts. Na koniec prześledziłeś procedurę tworzenia własnego niestandardowego dostawcy treści. W kolejnym rozdziale koncentrujemy się na animacjach. Dowiesz się z niego, czym są animacje właściwości i zobaczysz, w jaki sposób klasy ValueAnimator i ObjectAnimator są wykorzystywane przy animowaniu widoków i obiektów. Nauczysz się również, jak zaimplementować wiele animacji jednocześnie.
Podsumowanie
Rysunek 7.6. Wprowadzanie nazwy i ceny produktu (górny obrazek). Pobrane i wyświetlone informacje z dostawcy treści (środkowy obrazek). Aktualizacja informacji dla wybranego produktu (dolny obrazek)
253
254
Rozdział 7. Ładowarki
Rysunek 7.7. Usuwanie wybranego produktu z dostawcy treści (górny obrazek). Informacje o aktualnych produktach pobrane z dostawcy treści i wyświetlone (dolny obrazek)
Część III Techniki multimedialne
8 Animacje A
nimacje sprawiają, że aplikacja staje się bardziej atrakcyjna i dynamiczna. Możesz wykorzystywać animacje podczas przygotowywania gier, samouczków edukacyjnych oraz demonstracji. W tym rozdziale poznasz różne typy animacji obsługiwane przez system Android. Zaimplementujesz animację właściwości za pomocą klas ValueAnimator i ObjectAnimator. Zaimplementujesz również wiele animacji, wykorzystując klasę AnimatorSet. Ponadto będziesz korzystał z animacji poklatkowej, animacji generującej klatki pośrednie oraz animacji układu. Na koniec nauczysz się gromadzić i wyświetlać sekwencje animacji za pomocą klasy AnimationSet.
Receptura: typy animacji Zasadniczo system Android obsługuje dwa typy animacji, czyli animację właściwości i animację widoku. Animacja właściwości to solidny framework, dzięki któremu możesz animować
właściwości dowolnego obiektu. Oznacza to, że możesz zdefiniować animację, która będzie zmieniać dowolną właściwość danego obiektu w określonym przedziale czasu. Animacja właściwości umożliwia również, jeśli trzeba, odwracanie i powtarzanie animacji. Ponadto dla uzyskania płynnych przejść w animacji właściwości można wykorzystać interpolatory. Poniżej wymieniono dwa animatory, które możesz stosować do animowania wartości właściwości. ValueAnimator — śledzi bieżącą wartość animowanej właściwości oraz czas trwania animacji. Aby skorzystać z klasy ValueAnimator, musisz podać
początkowe i końcowe wartości animowanej właściwości oraz czas trwania animacji. Uwzględniając podane wartości i czas trwania, ValueAnimator oblicza wartości animacji. Za pomocą nasłuchiwaczy pobierane są aktualizacje wartości stosowane do właściwości żądanych obiektów. ObjectAnimator — to podklasa klasy ValueAnimator powszechnie wykorzystywana
do przetwarzania i zastosowania wygenerowanych wartości animacji na żądanych
258
Rozdział 8. Animacje
obiektach. Aby użyć klasy ObjectAnimator, wybierasz obiekt, właściwość obiektu, która ma być animowana, oraz zestaw wartości do zastosowania do właściwości obiektu w określonym czasie trwania. Animacja widoku to framework umożliwiający animowanie obiektów View.
Istnieją trzy rodzaje animacji, które możesz wykonać za pomocą tego frameworku. Animacja poklatkowa — to seria obrazów ułożonych w klatkach
i wyświetlanych w regularnych interwałach. Każdy z obrazów różni się nieco od poprzedniego i jest wyświetlany przez krótki czas, aby dać wrażenie ruchu. Animacja generowania klatek pośrednich — taka animacja zakłada
istnienie dwóch obrazów kluczowych: początkowego i końcowego stanu obrazu. Pomiędzy tymi obrazami automatycznie generowane są stopniowe przekształcenia od stanu początkowego do stanu końcowego. Generowanie klatek pośrednich może być zastosowane do dowolnej grafiki, tekstu lub innego widoku. System Android obsługuje kilka powszechnych przekształceń obrazu, w tym alpha, rotate, scale oraz translate. Animacja układu — jak wskazuje nazwa, animacja ta jest wykorzystywana
w danym układzie. Animacja ma zastosowanie do każdego widoku potomnego w układzie, kiedy jest on dodawany lub usuwany. Nie ma konieczności bezpośredniego uruchamiania tej aplikacji, ponieważ jest uruchamiana automatycznie po wyświetleniu danego układu. W tabeli 8.1 opisano w skrócie kilka metod i stałych wykorzystywanych do przeprowadzania animacji. Tabela 8.1. Krótki opis metod i stałych wykorzystywanych w animacji
Metoda lub stała
Opis
setDuration(long długość_animacji)
Określa długość animacji. Parametr długość_animacji jest podawany w milisekundach. Domyślna długość animacji to 300 milisekund.
setRepeatCount(int liczba_powtórzeń)
Określa liczbę powtórzeń animacji. Jeśli parametr liczba_powtórzeń wynosi 0, animacja nie jest powtarzana. Animacja jest powtarzana, jeśli parametr liczba_powtórzeń ma wartość dodatnią lub INFINITE (nieskończona liczba powtórzeń). Domyślna wartość dla tego parametru wynosi 0.
public void setRepeatMode(int wartość)
Określa akcję dla animacji, kiedy ta osiągnie koniec. Parametr wartość może mieć wartość RESTART (ponowne uruchomienie) lub REVERSE (odwrócenie). Metoda działa tylko wtedy, kiedy liczba powtórzeń ma wartość dodatnią lub INFINITE.
INFINITE
Stała wykorzystywana do powtarzania animacji w nieskończoność.
Receptura: korzystanie z klasy ValueAnimator Tabela 8.1. Krótki opis metod i stałych wykorzystywanych w animacji (ciąg dalszy)
Metoda lub stała
Opis
RESTART
Restartuje animację od początku, kiedy ta osiągnie koniec. Animacja jest restartowana tylko wtedy, kiedy liczba powtórzeń ma wartość dodatnią lub INFINITE.
REVERSE
Odwraca kierunek wykonywania animacji, kiedy animacja osiągnie koniec (czyli animacja odwraca swój kierunek po każdej iteracji). Animacja jest odwracana tylko wtedy, kiedy liczba powtórzeń ma wartość dodatnią lub INFINITE.
Receptura: korzystanie z klasy ValueAnimator Procedura animowania właściwości dowolnego widoku za pomocą klasy ValueAnimator jest prosta. Musisz jedynie dostarczyć początkową i końcową wartość dla animowanej właściwości oraz czas trwania animacji, a następnie wywołać metodę start(), aby rozpocząć animację. W tabeli 8.2 opisano w skrócie kilka metod wykorzystywanych w klasie ValueAnimator. Tabela 8.2. Krótki opis metod wykorzystywanych w klasie ValueAnimator
Metoda
Opis
ofInt(int wartość1, wartość2...)
Tworzy i zwraca obiekt klasy ValueAnimator, która wykonuje animację w zakresie podanych wartości całkowitych.
ofFloat(float wartość1, wartość2...)
Tworzy i zwraca obiekt klasy ValueAnimator, która wykonuje animację w zakresie podanych wartości zmiennoprzecinkowych.
ofObject(Object obiekt_docelowy, String właściwość, TypeEvaluator ewaluator, Object wartość1, wartość2...)
Tworzy i zwraca obiekt klasy ValueAnimator, która wykonuje animację w zakresie podanych wartości obiektów. Parametr obiekt_docelowy reprezentuje obiekt, którego właściwość ma być animowana. Parametr właściwość reprezentuje nazwę właściwości, która ma być animowana (listę nazw właściwości znajdziesz w tabeli 8.3). Parametr ewaluator reprezentuje interfejs TypeEvaluator, który będzie wywoływany przy każdej klatce animacji. Wartości wartość1, wartość2... reprezentują zestaw wartości, które będą animowane w określonym przedziale czasu.
getAnimatedValue()
Zwraca najbardziej aktualną wartość obliczoną przez ValueAnimator. Metoda jest wykorzystywana, kiedy istnieje tylko jedna właściwość, która ma być animowana.
getAnimatedValue(property)
Zwraca najbardziej aktualną wartość obliczoną przez ValueAnimator dla określonej właściwości.
259
260
Rozdział 8. Animacje
Uwaga Interfejs TypeEvaluator zapewnia niezbędną interpolację pomiędzy właściwościami obiektów w celu wyprowadzenia wartości animowanej. Oznacza to, że w wyniku interpolacji dostarczonych wartości obiektów TypeEvaluator zwraca wartość reprezentującą proporcje pomiędzy danymi wartościami obiektów.
W tabeli 8.3 opisano w skrócie różne właściwości obiektów, które mogą być animowane. Tabela 8.3. Krótki opis nazw właściwości, które mogą być animowane
Właściwość
Opis
translationX oraz translationY
Określa lokalizację obiektu w jego kontenerze układu za pomocą współrzędnych od lewej i od góry.
rotation, rotationX oraz rotationY
Określa rotację obiektu wokół określonego punktu pivota.
scaleX oraz scaleY
Określa kierunek skalowania obiektu wokół jego punktu pivota.
pivotX oraz pivotY
Określa lokalizację punktu pivota, wokół którego przebiegają transformacje rotacji i skalowania.
alpha
Reprezentuje przezroczystość alfa dla obiektu. Domyślnie wartość wynosi 1 (nieprzezroczysty), a wartość 0 reprezentuje pełną przezroczystość (obiekt niewidoczny).
Uwaga Po wykonaniu animacji lub zmianie właściwości dowolnego obiektu automatycznie wywoływana jest metoda invalidate() w celu odświeżenia ekranu.
W tej recepturze skoncentrujemy się na objaśnieniu, w jaki sposób klasa ValueAnimator oblicza wartości animacji w zakresie dostarczonego przedziału wartości w określonym czasie trwania. Za pomocą klasy ValueAnimator wygenerowane zostaną wartości całkowite z zakresu od 10 do 20 w czasie 3000 milisekund. W tym celu utwórz nowy projekt Android o nazwie ValueAnimatorApp. Ponieważ chcesz, aby aplikacja generowała wartości całkowite po kliknięciu określonego przycisku, zdefiniuj w pliku układu aktywności kontrolkę Button. Po zdefiniowaniu tej kontrolki plik układu aktywności activity_value_animator_app.xml będzie wyglądał tak, jak przedstawiono w listingu 8.1. Listing 8.1. Kod wpisany w pliku układu aktywności activity_value_animator_app.xml
Receptura: korzystanie z klasy ValueAnimator
Jak możesz zauważyć, kontrolka Button, której przypisano identyfikator value_anim_button, została skonfigurowana do wyświetlania nagłówka Uruchom obiekt klasy ValueAnimator. Nagłówek ten będzie wyświetlany czcionką o rozmiarze zdefiniowanym w zasobie wymiarów text_size. Ponadto przycisk ten ma być umieszczony w odległości 50 dp od górnego brzegu ekranu oraz wyśrodkowany w linii poziomej. Następnie musisz napisać kod Java, który będzie wykonywał poniższe zadania. Uzyskiwanie dostępu do kontrolki Button zdefiniowanej w pliku układu
i mapowanie tej kontrolki na obiekt Button. Powiązanie nasłuchiwacza setOnClickListener z kontrolką Button w celu
nasłuchiwania wystąpień zdarzeń kliknięcia tej kontrolki. Definiowanie obiektu ValueAnimator oraz ustawienie zakresu jego wartości
i czasu trwania animacji. Powiązanie nasłuchiwacza addUpdateListener z obiektem ValueAnimator w celu
nasłuchiwania aktualizacji zdarzeń, które mogą wystąpić w trakcie wykonywania animacji. Wyświetlanie w formie komunikatów dziennika wartości animacji
wygenerowanych przez obiekt ValueAnimator. Aby wykonać wymienione zadania, wpisz w pliku aktywności Java ValueAnimatorAppActivity.java kod przedstawiony w listingu 8.2. Listing 8.2. Kod wpisany w pliku aktywności Java ValueAnimatorAppActivity.java package com.androidtablet.valueanimatorapp; import import import import import import import
android.app.Activity; android.os.Bundle; android.view.View; android.view.View.OnClickListener; android.widget.Button; android.animation.ValueAnimator; android.util.Log;
public class ValueAnimatorAppActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_value_animator_app);
261
262
Rozdział 8. Animacje Button valueAnimatorButton = (Button) findViewById( R.id.value_anim_button); valueAnimatorButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { ValueAnimator anim = ValueAnimator.ofInt(10, 20); anim.setDuration(3000); anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener(){ public void onAnimationUpdate( ValueAnimator animation) { Integer value = Integer.parseInt(animation. getAnimatedValue().toString()); Log.d("wartość: ",value.toString()); } }); anim.start(); } }); } }
W powyższym kodzie definiujesz obiekt klasy ValueAnimator o nazwie anim, który będzie generował wartości całkowite animacji w zakresie od 10 do 20. Czas trwania animacji został ustawiony na 3 sekundy. Domyślnie czas trwania animacji wynosi 300 milisekund. Nasłuchiwacz addUpdateListener jest powiązany z obiektem anim klasy ValueAnimator w celu nasłuchiwania aktualizacji zdarzeń, które mają miejsce w trakcie wykonywania animacji. Domyślnie wywołanie zwrotne onAnimationUpdate jest wywoływane co 10 milisekund. Dlatego też metoda onAnimationUpdate będzie w ciągu 3 sekund wielokrotnie wywoływana, wyświetlając wartości całkowite z zakresu od 10 do 20. Po uruchomieniu tej aplikacji otrzymasz kontrolkę Button o nagłówku Uruchom obiekt klasy ValueAnimator (patrz rysunek 8.1, górny obrazek). Po kliknięciu kontrolki Button obiekt ValueAnimator wygeneruje wartości całkowite z zakresu od 10 do 20 w czasie 3 sekund, tak jak pokazano na rysunku 8.1 (dolny obrazek). Jak wspomniano wcześniej, za pomocą obiektów ValueAnimator możesz animować dowolny obiekt. Aby zobaczyć, w jaki sposób wykorzystać obiekt ValueAnimator do animacji pewnego obiektu, musisz zmodyfikować swoją bieżącą aplikację Android o nazwie ValueAnimatorApp. Poza kontrolką Button, będziesz w tej aplikacji wyświetlał kontrolkę TextView. Po kliknięciu przycisku kontrolka TextView będzie na przemian rozciągać się i kurczyć w nieskończoność. Mówiąc ściślej, kontrolka TextView rozciągnie się w poziomie do wielkości równej 3,5-krotności oryginalnego rozmiaru, a następnie skurczy się na powrót do rozmiaru początkowego. Po skurczeniu się kontrolki do oryginalnego rozmiaru nastąpi ponowne jej rozciągnięcie w poziomie. W ten sposób animacja będzie wykonywana w nieskończoność.
Receptura: korzystanie z klasy ValueAnimator
Rysunek 8.1. Kontrolka Button z nagłówkiem Uruchom obiekt klasy ValueAnimator wyświetlona po uruchomieniu aplikacji (górny obrazek) oraz komunikaty dziennika pokazujące wartości całkowite z zakresu od 10 do 20 (dolny obrazek)
Aby dodać kontrolkę TextView, zmodyfikuj kod z pliku układu aktywności activity_value_animator_app.xml w sposób przedstawiony w listingu 8.3. Dodane zostały w nim jedynie fragmenty kodu zaznaczone pogrubioną czcionką. Reszta pozostaje taka sama jak w listingu 8.1. Listing 8.3. Kod wpisany w pliku układu aktywności activity_value_animator_app.xml
263
264
Rozdział 8. Animacje
Jak możesz zauważyć, kontrolka TextView jest inicjowana w celu wyświetlenia tekstu To jest tekst przykładowy, który ma być pokazany horyzontalnie w centrum ekranu. Do kontrolki tej przypisany został identyfikator textview i ma być ona widoczna w odległości 10 dp od górnego brzegu ekranu. Ponadto tekst kontrolki ma być prezentowany pogrubioną czcionka o rozmiarze zdefiniowanym w zasobie wymiarów text_size. Następnie musisz zmodyfikować plik aktywności Java ValueAnimatorAppActivity.java, aby wykonywał poniższe zadania. Musisz uzyskać dostęp do kontrolki TextView z pliku układu i zmapować tę
kontrolkę na obiekt TextView. Ponieważ chcesz rozciągnąć kontrolkę TextView do 3,5-krotności jej
oryginalnego rozmiaru, musisz skonfigurować obiekt ValueAnimator w taki sposób, aby generował wartości zmiennoprzecinkowe w przedziale od 1 do 3,5. Ponieważ chcesz, aby animacja była odwracana lub przełączana, kiedy
dobiegnie końca (innymi słowy, chcesz, aby kontrolka TextView powracała po rozciągnięciu do swojego oryginalnego rozmiaru), tryb powtarzania dla animacji musi być ustawiony na REVERSE. Ponieważ chcesz, aby animacja była kontynuowana w nieskończoność,
musisz ustawić liczbę powtórzeń dla animacji na INFINITE. Musisz uzyskać w metodzie wywołania zwrotnego onAnimationUpdate dostęp
do wygenerowanych wartości animacji i zastosować je do właściwości setTextScaleX kontrolki TextView w celu jej rozciągania i skurczania.
Aby wykonać wymienione zadania, zmodyfikuj plik aktywności Java ValueAnimatorAppActivity.java w sposób przedstawiony w listingu 8.4.
Receptura: korzystanie z klasy ValueAnimator Listing 8.4. Kod wpisany w pliku aktywności Java ValueAnimatorAppActivity.java package com.androidtablet.valueanimatorapp; import import import import import import import import
android.app.Activity; android.os.Bundle; android.view.View; android.view.View.OnClickListener; android.widget.Button; android.animation.ValueAnimator; android.widget.TextView; android.animation.ValueAnimator.AnimatorUpdateListener;
public class ValueAnimatorAppActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_value_animator_app); final TextView textView = (TextView)this.findViewById( R.id.textview); Button valueAnimatorButton = (Button) findViewById( R.id.value_anim_button); valueAnimatorButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { ValueAnimator anim = ValueAnimator.ofFloat(1f, 3.5f); anim.setRepeatCount(ValueAnimator.INFINITE); anim.setRepeatMode(ValueAnimator.REVERSE); anim.setDuration(3000); anim.addUpdateListener(new AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { float value = (Float)animation.getAnimatedValue(); textView.setTextScaleX(value); } }); anim.start(); } }); } }
Jak możesz zauważyć w powyższym kodzie, metoda setTextScaleX() określa wartość skali, według której musi zostać rozciągnięta w poziomie kontrolka TextView. Za pomocą metody ofFloat() generowane będą wartości zmiennoprzecinkowe w zakresie od 1 do 3,5. Dlatego też kontrolka TextView będzie rozciągana poziomo do 3,5-krotności swojego oryginalnego rozmiaru w czasie 3 sekund. Ponieważ na obiekcie ValueAnimator została użyta stała REVERSE, animacja kontrolki będzie odwracana, po tym jak osiągnie koniec. (Innymi słowy, kiedy kontrolka TextView osiągnie w poziomie 3,5-krotność oryginalnego rozmiaru, skurczy się na powrót do pierwotnej szerokości). Animacja będzie kontynuowana w nieskończoność, ponieważ na obiekcie ValueAnimator zastosowano stałą INFINITE.
265
266
Rozdział 8. Animacje
Po uruchomieniu tej aplikacji wyświetlone zostaną kontrolki TextView i Button. Kontrolka TextView jest inicjowana w celu wyświetlenia tekstu To jest tekst przykładowy, a nagłówek kontrolki Button to Uruchom obiekt klasy ValueAnimator (patrz rysunek 8.2, górny obrazek). Po kliknięciu kontrolki Button kontrolka TextView zostanie rozciągnięta w poziomie do 3,5-krotności swojego oryginalnego rozmiaru w czasie 3 sekund (patrz rysunek 8.2, dolny obrazek). Po rozciągnięciu kontrolka TextView skurczy się na powrót do pierwotnego rozmiaru, a następnie ponownie zostanie rozciągnięta. Proces ten będzie powtarzał się w nieskończoność.
Rysunek 8 2. Kontrolki TextView i Button wyświetlone po uruchomieniu aplikacji (górny obrazek) oraz kontrolka TextView rozciągnięta do 3,5-krotności swojego pierwotnego rozmiaru (dolny obrazek)
Receptura: wykorzystanie klasy ObjectAnimator do animowania widoków
Receptura: wykorzystanie klasy ObjectAnimator do animowania widoków Najprostszą techniką tworzenia właściwości animacji jest wykorzystanie klasy ObjectAnimator. Klasa ta obejmuje statyczne metody ofFloat, ofInt oraz ofObject. Metody te generują wymagane wartości z określonego zakresu które mogą być zastosowane do danej właściwości obiektu docelowego. Oznacza to, że klasa ObjectAnimator zapewnia metody, które tworzą animację, implementując przejście pomiędzy dostarczonymi wartościami w określonej właściwości obiektu docelowego. Aby zrozumieć, w jaki sposób klasa ObjectAnimator jest wykorzystywana w animowaniu obiektów, utwórz nowy projekt Android o nazwie ObjectAnimatorApp. W aplikacji będziesz definiował kontrolki TextView i ToggleButton. Po każdym kliknięciu kontrolki ToggleButton kontrolka TextView będzie na przemian rozjaśniana i przyciemniana. Wcelu zdefiniowania kontrolek TextView i ToggleButton wpisz w pliku układu aktywności activity_object_animator_app.xml kod przedstawiony w listingu 8.5. Listing 8.5. Kod wpisany w pliku układu aktywności activity_object_animator_app.xml
Jak możesz zauważyć, kontrolka TextView jest inicjowana w celu wyświetlenia tekstu Demo klasy ObjectAnimator i została skonfigurowana w taki sposób, aby była wyświetlana horyzontalnie pośrodku ekranu. Kontrolce tej przypisano identyfikator textview i ma ona być umieszczona w odległości 200 dp od górnego brzegu ekranu. Wyświetlany przez tę kontrolkę tekst ma być prezentowany pogrubioną czcionka o rozmiarze zdefiniowanym w zasobie wymiarów text_size. Poniżej kontrolki TextView zdefiniowana została kontrolka ToggleButton, której przypisano identyfikator toggle_button. Kontrolka ta ma być wyśrodkowana w poziomie i wyświetlana w odległości 200 dp pod kontrolką TextView.
267
268
Rozdział 8. Animacje
W pliku aktywności Java musisz zdefiniować kod, który będzie wykonywał następujące zadania. Uzyskanie dostępu do kontrolek TextView i ToggleButton z pliku układu
oraz mapowanie tych kontrolek na odpowiednie obiekty. Powiązanie nasłuchiwacza setOnClickListener z kontrolką Button w celu
nasłuchiwania wystąpień zdarzenia kliknięcia tej kontrolki. Zdefiniowanie obiektu fadeOut klasy ObjectAnimator, który zmienia właściwość
alfa kontrolki TextView z bieżącej wartości na wartość 0 (czyli sprawia, że kontrolka TextView staje się przezroczysta lub niewidoczna) w czasie 5 sekund. Zdefiniowanie kolejnego obiektu klasy ObjectAnimator noszącego nazwę
fadeIn, który zmienia właściwość alfa kontrolki z jej bieżącej wartości na wartość 1 (czyli sprawia, że kontrolka TextView staje się nieprzezroczysta
lub w pełni widoczna) w czasie 5 sekund. Uruchamianie na przemian obiektów fadeOut i fadeIn klasy ObjectAnimator
przy każdym kliknięciu kontrolki ToggleButton. Kontrolka TextView będzie rozjaśniana przy pierwszym kliknięciu kontrolki ToggleButton i przyciemniana przy kolejnym kliknięciu tej kontrolki. Aby wykonać wymienione zadania, wpisz w pliku aktywności Java ObjectAnimatorAppActivity.java kod przedstawiony w listingu 8.6. Listing 8.6. Kod wpisany w pliku aktywności Java ObjectAnimatorAppActivity.java package com.androidtablet.objectanimatorapp; import import import import import import import
android.os.Bundle; android.app.Activity; android.widget.ToggleButton; android.view.View.OnClickListener; android.widget.TextView; android.view.View; android.animation.ObjectAnimator;
public class ObjectAnimatorAppActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_object_animator_app); final TextView textView = (TextView)this.findViewById( R.id.textview); final ToggleButton toggleButton = (ToggleButton) findViewById(R.id.toggle_button); toggleButton.setText("Rozjaśnij"); toggleButton.setOnClickListener(new OnClickListener() { public void onClick(View v) { if (toggleButton.isChecked()) { toggleButton.setText("Przyciemnij"); ObjectAnimator fadeOut =ObjectAnimator.ofFloat( textView, "alpha", 0f);
Receptura: wykorzystanie klasy ObjectAnimator do animowania widoków fadeOut.setDuration(5000); fadeOut.start(); } else { toggleButton.setText("Rozjaśnij"); ObjectAnimator fadeIn =ObjectAnimator.ofFloat( textView, "alpha", 1f); fadeIn.setDuration(5000); fadeIn.start(); } } }); } }
Wykorzystana w powyższym kodzie metoda ofFloat wymaga nieco wyjaśnień. Metoda ofFloat tworzy i zwraca obiekt ObjectAnimator, który animuje określoną właściwość obiektu docelowego w zakresie ustalonych wartości zmiennoprzecinkowych. Składnia dla stosowania tej metody jest następująca: public static ObjectAnimator.ofFloat(Object obiekt_docelowy, String właściwość, float wartość1, wartość2...) Parametr obiekt_docelowy reprezentuje obiekt, którego właściwość ma być
animowana. Parametr właściwość reprezentuje nazwę właściwości, która ma być animowana.
Listę nazw właściwości, które mogą być animowane, znajdziesz w tabeli 8.3. Parametr wartość reprezentuje zbiór wartości, w zakresie których wykonywana
będzie animacja w określonym czasie trwania. Jeśli dostarczona zostanie pojedyncza wartość, będzie ona reprezentować wartość końcową. Dwie wartości będą określały wartość początkową i wartość końcową, czyli zakres wartości, w którym wykonywana będzie animacja. Jeśli dostarczone zostaną więcej niż dwie wartości, będą one określały wartość początkową, wartości animowane w trakcie wykonywania animacji oraz wartość końcową. Jak możesz zauważyć w poprzednim kodzie, metoda ofFloat() została wykorzystana do zdefiniowania obiektów klasy ObjectAnimator, które zmieniają właściwość alfa kontrolki TextView, a tym samym implementują operacje rozjaśniania i ściemniania. Po uruchomieniu tej aplikacji na początek wyświetlone zostaną kontrolki TextView i ToggleButton. Kontrolka TextView jest inicjowana w celu wyświetlenia tekstu Demo klasy ObjectAnimator. Początkowy nagłówek przypisany do kontrolki ToggleButton to Rozjaśnij, tak jak pokazano na rysunku 8.3 (górny obrazek). Po kliknięciu kontrolki ToggleButton kontrolka TextView zacznie się rozjaśniać, a proces ten będzie trwał 5 sekund. Ponadto zmieniony zostanie nagłówek kontrolki ToggleButton na Przyciemnij, co pokazano na rysunku 8.3 (dolny obrazek).
269
270
Rozdział 8. Animacje
Rysunek 8.3. Kontrolki TextView i ToggleButton wyświetlone po uruchomieniu aplikacji (górny obrazek). Kontrolka TextView jest stopniowo rozjaśniana po pierwszym kliknięciu kontrolki ToggleButton (dolny obrazek)
Nagłówek Przyciemnij kontrolki ToggleButton wskazuje, że jeśli kontrolka ta zostanie kliknięta ponownie, kontrolka TextView stopniowo się przyciemni lub stanie się ponownie widoczna (patrz rysunek 8.4, górny obrazek). Po zakończeniu cyklu animacji kontrolka TextView będzie całkowicie widoczna, a nagłówek kontrolki ToggleButton zmieni się na Rozjaśnij (patrz rysunek 8.4, dolny obrazek).
Receptura: wykorzystanie klasy ObjectAnimator do animowania widoków
Rysunek 8.4. Kontrolka TextView jest stopniowo przyciemniana po ponownym kliknięciu kontrolki ToggleButton (górny obrazek). Stan kontrolek TextView i ToggleButton po zakończeniu cyklu animacji (dolny obrazek)
Za pomocą klasy ObjectAnimator możesz animować dowolną właściwość danego obiektu. Spróbuj w bieżącej aplikacji Android ObjectAnimatorApp wykonać za pomocą klasy ObjectAnimator animację właściwości rotacji kontrolki TextView. Innymi słowy, zmodyfikuj tę aplikację w taki sposób, aby obrócić kontrolkę TextView zgodnie z kierunkiem ruchu wskazówek zegara i przeciwnie do kierunku ruchu wskazówek zegara. W tym celu musisz zmodyfikować plik aktywności Java ObjectAnimatorAppActivity.java w sposób przedstawiony w listingu 8.7. Zmienione zostały jedynie fragmenty kodu zaznaczone pogrubioną czcionką. Reszta pozostaje taka sama jak w listingu 8.6.
271
272
Rozdział 8. Animacje Listing 8.7. Kod wpisany w pliku aktywności Java ObjectAnimatorAppActivity.java package com.androidtablet.objectanimatorapp; import import import import import import import
android.os.Bundle; android.app.Activity; android.widget.ToggleButton; android.view.View.OnClickListener; android.widget.TextView; android.view.View; android.animation.ObjectAnimator;
public class ObjectAnimatorAppActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_object_animator_app); final TextView textView = (TextView)this.findViewById(R.id.textview); final ToggleButton toggleButton = (ToggleButton) findViewById(R.id.toggle_button); toggleButton.setText("Obróć zgodnie z ruchem wskazówek zegara"); final ObjectAnimator objAnim = ObjectAnimator.ofFloat(textView, "rotation", 0f, 360f).setDuration(5000); toggleButton.setOnClickListener(new OnClickListener() { public void onClick(View v) { if (toggleButton.isChecked()) { toggleButton.setText("Obróć zgodnie z ruchem wskazówek zegara, a następnie w przeciwnym kierunku"); objAnim.setRepeatMode(ObjectAnimator.RESTART); } else { toggleButton.setText("Obróć zgodnie z ruchem wskazówek zegara"); objAnim.setRepeatMode(ObjectAnimator.REVERSE); } objAnim.setRepeatCount(ObjectAnimator.INFINITE); objAnim.start(); } }); } }
Jak możesz zauważyć, kontrolka ToggleButton jest początkowo ustawiona do wyświetlania nagłówka Obróć zgodnie z ruchem wskazówek zegara. Definiowany jest obiekt objAnim klasy ObjectAnimator. Obiekt ten zmienia właściwość rotacji kontrolki TextView od 0 do 360° w czasie 5 sekund. Za pomocą klasy ObjectAnimator przy użyciu metody ofFloat() generowane są zmiennoprzecinkowe wartości z zakresu od 0 do 360 i są one stosowane do właściwości rotacji kontrolki TextView w celu obrócenia jej zgodnie z kierunkiem ruchu wskazówek zegara. Aby kontrolka TextView obracała się w obu kierunkach (zgodnie z ruchem wskazówek zegara i przeciwnie), w metodzie wywołania zwrotnego onClick kontrolki ToggleButton ustaw tryb powtarzania dla obiektu ObjectAnimator na REVERSE. Aby animować kontrolkę TextView w nieskończoność, ustaw liczbę powtórzeń dla obiektu ObjectAnimator na INFINITE.
Receptura: uruchamianie wielu animacji za pomocą klasy AnimatorSet
Po uruchomieniu tej aplikacji na początku wyświetlane są kontrolki TextView i ToggleButton. Kontrolka TextView wyświetla tekst Demo klasy ObjectAnimator, a początkowy nagłówek przypisany do kontrolki ToggleButton to Obróć zgodnie z ruchem wskazówek zegara, tak jak pokazano na rysunku 8.5 (górny obrazek). Po kliknięciu kontrolki ToggleButton kontrolka TextView rozpocznie stopniową rotację zgodnie z ruchem wskazówek zegara, a proces ten będzie trwał 5 sekund. Ponadto nagłówek kontrolki ToggleButton zmieni się na Obróć zgodnie z ruchem wskazówek zegara, a następnie w przeciwnym kierunku, co widać na rysunku 8.5 (środkowy obrazek). Nagłówek ten wskazuje, że jeśli ponownie klikniesz kontrolkę ToggleButton, kontrolka TextView obróci się zgodnie z ruchem wskazówek zegara, a następnie obróci się w przeciwnym kierunku (patrz rysunek 8.5, dolny obrazek).
Receptura: uruchamianie wielu animacji za pomocą klasy AnimatorSet AnimatorSet to klasa, którą możesz wykorzystać do odegrania zestawu obiektów animatora w określonej kolejności. W tabeli 8.4 przedstawiono metody służące do dodawania animacji do klasy AnimatorSet. Tabela 8.4. Krótki opis metod wykorzystywanych w celu dodawania animacji do klasy AnimatorSet
Metoda
Opis
play(Animator animacja)
Metoda tworzy obiekt Builder, który konstruuje obiekt AnimatorSet na podstawie określonych zależności. Przykładowo poniższa instrukcja spowoduje, że AnimatorSet odtworzy animacje animacja1 i animacja2 jednocześnie: play(animacja1).with(animacja2)
Analogicznie poniższa instrukcja spowoduje, że AnimatorSet odtworzy najpierw animację animacja1, a następnie animację animacja2: play(animacja1).before(animacja2)
Kolejna instrukcja spowoduje, że AnimatorSet odtworzy animację animacja1 po animacji animacja2: play(animacja1).after(animacja2) playSequentially (Animator animacja1, animacja2...)
Metoda sprawia, że AnimatorSet odtwarza każdą ze wskazanych animacji w kolejności jedna po drugiej. Do tej metody możesz również przekazać jako parametr List animacje, aby wskazać listę animacji do odtworzenia.
playTogether (Animator animacja1, animacja2...)
Metoda sprawia, że AnimatorSet odtwarza wskazane animacje jednocześnie. Dla tej metody możesz również wskazać jako parametr Collection animacje, aby określić zbiór animacji do odtworzenia.
273
274
Rozdział 8. Animacje
Rysunek 8.5. Kontrolki TextView i ToggleButton widoczne po uruchomieniu aplikacji (górny obrazek). Kontrolka TextView obraca się zgodnie z ruchem wskazówek zegara po jednokrotnym kliknięciu kontrolki ToggleButton (środkowy obrazek). Po powtórnym kliknięciu kontrolki ToggleButton kontrolka TextView obraca się zgodnie z ruchem wskazówek zegara, a następnie w kierunku przeciwnym (dolny obrazek)
Receptura: uruchamianie wielu animacji za pomocą klasy AnimatorSet
Żeby zobaczyć, w jaki sposób za pomocą klasy AnimatorSet można odtworzyć dwie animacje jednocześnie, utwórz projekt Android o nazwie MultiAnimApp. W tej aplikacji wykorzystasz kontrolki TextView i ImageView. Zastosujesz dwie animacje — skalowanie i rotację — na kontrolce ImageView. (Kontrolka ImageView będzie się obracać i jednocześnie będzie skalowana). Ponadto zmodyfikujesz konsekutywnie właściwość alfa kontrolki TextView. Kiedy kontrolka TextView stanie się całkowicie niewidoczna, zacznie stopniowo się pojawiać. Animacje rozjaśniania i przyciemniania będą odgrywane jedna po drugiej. Aby zdefiniować kontrolki TextView i ImageView, wpisz w pliku układu aktywności activity_multi_anim_app.xml kod przedstawiony w listingu 8.8. Listing 8.8. Kod wpisany w pliku układu aktywności activity_multi_anim_app.xml
Jak możesz zauważyć, zdefiniowana została kontrolka ImageView, której przypisano identyfikator imgview. Kontrolka ta ma być wyświetlana w odległości 50 dp od górnego brzegu kontenera i w odległości 50 dp od lewego brzegu ekranu. Kontrolka ImageView jest inicjowana w celu wyświetlenia domyślnie obrazu ic_launcer.png. Szerokość i wysokość obrazu wyświetlanego za pomocą kontrolki ImageView jest określana odpowiednio przez zasoby wymiarów image_width oraz image_height. Pod kontrolką ImageView inicjowana jest kontrolka TextView w celu wyświetlenia tekstu Demo klasy AnimatorSet. Ma być ona wyśrodkowana w poziomie. Kontrolce TextView przypisano identyfikator textview i ma ona być umieszczona w odległości 50 dp od górnego brzegu ekranu oraz w odległości 50 dp na prawo od kontrolki ImageView. Tekst wyświetlany przez kontrolkę TextView ma być prezentowany pogrubioną czcionką o rozmiarze zdefiniowanym w zasobie wymiarów text_size.
275
276
Rozdział 8. Animacje
Zakładamy, że w folderze res/values istnieje już plik wymiarów dimens.xml. Dodaj do tego pliku wymiary do zmiany rozmiaru obrazów w zależności od ekranu urządzenia, na którym uruchamiana jest aplikacja. Wpisz w pliku dimens.xml następujący kod: 14sp 100dp 120dp
Trzy zasoby wymiarów, text_size, image_width oraz image_height, definiują odpowiednio rozmiar czcionki tekstu, szerokość obrazu oraz wysokość obrazu. Zdefiniowane w powyższym kodzie trzy zasoby wymiarów są przeznaczone dla urządzeń o normalnych ekranach (telefonów). Aby zdefiniować zasoby wymiarów dla 7-calowych tabletów, otwórz znajdujący się w folderze res/values-sw600dp plik dimens.xml i wpisz w nim następujący kod: 24sp 140dp 160dp
Na koniec, aby zdefiniować wymiary dla urządzeń o ekstradużych ekranach (10-calowych tabletów), otwórz znajdujący się w folderze values-sw720dp plik dimens.xml i wpisz w nim następujący kod: 32sp 180dp 200dp
Porównując zasoby wymiarów telefonów, 7-calowych tabletów i 10-calowych tabletów, możesz zauważyć, że rozmiary tekstu i obrazów w aplikacji zmieniają się na podstawie rozmiaru ekranu urządzenia. W pliku aktywności Java musisz wpisać kod, który będzie wykonywał następujące zadania. Uzyskiwanie dostępu do kontrolek TextView oraz ImageView zdefiniowanych
w pliku układu i mapowanie tych kontrolek na odpowiednie obiekty. Zdefiniowanie obiektu AnimatorSet. Wykorzystanie metody playTogether() na obiekcie AnimatorSet w celu
wykonania symultanicznie na kontrolce ImageView animacji skalowania i rotacji. Czas trwania połączonych animacji ma być ustawiony na 20 sekund. Kontrolka ImageView będzie obracać się o 360° zgodnie z ruchem wskazówek zegara i jednocześnie będzie skalowana horyzontalnie do dwukrotności pierwotnego rozmiaru.
Receptura: uruchamianie wielu animacji za pomocą klasy AnimatorSet Wykorzystanie metody playSequentially() na obiekcie AnimatorSet w celu
zastosowania na kontrolce TextView animacji rozjaśniania (animacja alfa), po której następuje animacja przyciemniania. Właściwość alfa kontrolki TextView jest zmieniana na 0. (Innymi słowy, kontrolka TextView najpierw staje się niewidoczna). Kiedy kontrolka TextView stanie się całkowicie niewidoczna, właściwość alfa jest zmieniana na 1. (Kontrolka TextView jest animowana do stanu widzialnego). Animacje rozjaśniania i przyciemniania będą odbywały się jedna po drugiej w czasie 5 sekund. Aby wykonać wymienione zadania, wpisz w pliku aktywności Java MultiAnimAppActivity.java kod przedstawiony w listingu 8.9. Listing 8.9. Kod wpisany w pliku aktywności Java MultiAnimAppActivity.java package com.androidtablet.multianimapp; import import import import import import
android.os.Bundle; android.app.Activity; android.widget.ImageView; android.animation.ObjectAnimator; android.animation.AnimatorSet; android.widget.TextView;
public class MultiAnimAppActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_multi_anim_app); ImageView imgView = (ImageView)findViewById(R.id.imgview); final TextView textView = (TextView)this.findViewById( R.id.textview); AnimatorSet animatorSet = new AnimatorSet(); animatorSet.playTogether(ObjectAnimator.ofFloat(imgView, "scaleX",1f, 2f), ObjectAnimator.ofFloat(imgView, "rotation", 0f, 360f)); animatorSet.setDuration(20000); animatorSet.start(); ObjectAnimator fadeOut = ObjectAnimator.ofFloat( textView, "alpha", 0f); ObjectAnimator fadeIn = ObjectAnimator.ofFloat( textView, "alpha", 1f); animatorSet.playSequentially(fadeOut,fadeIn); animatorSet.setDuration(5000); animatorSet.start(); } }
Po uruchomieniu tej aplikacji pojawią się początkowo kontrolki ImageView i TextView, tak jak przedstawiono na rysunku 8.6 (pierwszy obrazek). Na kontrolce ImageView zostaną jednocześnie zastosowane animacje rotacji i skalowania (patrz rysunek 8.6, drugi obrazek). Ponadto kontrolka TextView zacznie się rozjaśniać (patrz rysunek 8.6, trzeci obrazek).
277
278
Rozdział 8. Animacje
Rysunek 8.6. Kontrolki ImageView i TextView po uruchomieniu aplikacji (pierwszy obrazek). Kontrolka ImageView po zastosowaniu jednocześnie animacji skalowania i rotacji (drugi obrazek). Kontrolka TextView jest rozjaśniana (trzeci obrazek). Kontrolki ImageView i TextView po zastosowaniu kilku animacji (czwarty obrazek)
Receptura: animacja poklatkowa
Na rysunku 8.6 (czwarty obrazek) przedstawiono kontrolkę ImageView po zastosowaniu na niej jednocześnie animacji skalowania i rotacji oraz kontrolkę TextView po zastosowaniu na niej konsekutywnie operacji rozjaśniania i przyciemniania.
Receptura: animacja poklatkowa Animacje poklatkowe są tworzone za pomocą rysowania serii kolejnych obrazów, z których każdy jest wyświetlany przez określony czas. Aby pokazać ruch, każdy obraz różni się nieco od poprzedniego. Żeby wykonać taką animację w praktyce, utwórz projekt Android o nazwie FrameAnimationApp i skopiuj do folderów res/drawable obrazy, które chcesz animować. Na rysunku 8.7 przedstawiono obrazy dwg1.png, dwg2.png, dwg3.png oraz dwg4.png, które zostaną wykorzystane w tej animacji poklatkowej. Zauważ, że każdy z kolejnych obrazów różni się nieco od poprzedniego, co pozwoli pokazać ruch.
Rysunek 8.7. Cztery obrazy użyte w animacji poklatkowej
Skopiuj te cztery obrazy do folderów res/drawable. Aby kontrolować animację, musisz zdefiniować kontrolkę ToggleButton, która posłuży do uruchomienia i zatrzymania animacji. W celu zdefiniowania kontrolek ImageView i ToggleButton wpisz w pliku układu activity_frame_animation_app.xml kod przedstawiony w listingu 8.10. Listing 8.10. Kod z pliku activity_frame_animation_app.xml po zdefiniowaniu kontrolek ImageView i ToggleButton
279
280
Rozdział 8. Animacje
W tym kodzie kontrolka ImageView określa, na której klatce zostanie zastosowana animacja. Kontrolka ToggleButton będzie wykorzystywana do włączania i wyłączania animacji. Kontrolkom ImageView i ToggleButton przypisano odpowiednio identyfikatory imgview oraz startstop_button. Szerokość i wysokość obrazu wyświetlanego za pomocą kontrolki ImageView są zdefiniowane odpowiednio w zasobach wymiarów image_width i image_height. Tekst wyświetlany w kontrolce ToggleButton zostanie zaprezentowany czcionką o rozmiarze zdefiniowanym w zasobie wymiarów text_size. Dostęp do obu kontrolek w kodzie Java będzie uzyskiwany za pomocą ich identyfikatorów. Teraz musisz zdefiniować zasoby wymiarów, dzięki którym widoki Twojej aplikacji będą kompatybilne z telefonami, 7-calowymi tabletami i 10-calowymi tabletami. Najpierw w pliku wymiarów dimens.xml znajdującym się w folderze res/values zdefiniuj zasoby wymiarów odpowiadające rozmiarowi ekranu telefonów. Wpisz w tym pliku następujący kod: 14sp 100dp 120dp
Trzy zasoby wymiarów, text_size, image_width oraz image_height, definiują odpowiednio rozmiar czcionki tekstu, szerokość obrazu oraz wysokość obrazu. Zdefiniowane w powyższym kodzie wspomniane trzy zasoby wymiarów są przeznaczone dla urządzeń o normalnych ekranach (telefonów). Aby zdefiniować zasoby wymiarów dla 7-calowych tabletów, otwórz znajdujący się w folderze res/values-sw600dp plik dimens.xml i wpisz w nim następujący kod: 24sp 140dp 160dp
Na koniec, aby zdefiniować wymiary dla urządzeń o ekstradużych ekranach (10-calowych tabletów), otwórz znajdujący sie w folderze values-sw720dp plik dimens.xml i wpisz w nim następujący kod: 32sp 180dp 200dp
Porównując zasoby wymiarów dla telefonów, 7-calowych tabletów i 10-calowych tabletów, możesz zauważyć, że rozmiar tekstu i obrazów w tej aplikacji jest zmieniany na podstawie rozmiaru ekranu urządzenia.
Receptura: animacja poklatkowa
Następnie w celu zdefiniowania obrazów, które będą brały udział w tej animacji, oraz określenia czasu trwania ich wyświetlania musisz do folderu res/anim dodać plik XML. Najpierw w folderze /res utwórz folder anim. Potem w folderze /res/anim utwórz plik o nazwie frame_anim.xml. Każdy z obrazów skopiowanych do folderu res/drawable będzie pełnił funkcję klatki. Oznacza to, że do każdego z obrazów będzie uzyskiwany dostęp za pomocą identyfikatora jego zasobu, a potem obrazy te będą wyświetlane sekwencyjnie. Aby wyświetlić prostą animację przechodzącą przez serię dostarczonych obrazów, z których każdy jest wyświetlany przez 100 milisekund, wpisz w pliku frame_anim.xml kod przedstawiony w listingu 8.11. Listing 8.11. Kod wpisany w pliku frame_anim.xml
W tabeli 8.5 zawarto krótki opis atrybutów opisanych w tym pliku. Tabela 8.5. Atrybuty wykorzystane w pliku frame_anim.xml
Atrybut
Opis
android:drawable
Definiuje zasób drawable, który będzie wyświetlany w danej animacji.
android:duration
Określa czas trwania animacji. Czas ten podawany jest w milisekundach. Przykładowo wartość 5000 spowoduje, że animacja będzie trwała 5 sekund: android:duration="5000"
android:oneshot
Określa, czy animacja ma być odtwarzana w pętli. Wartość logiczna false (fałsz) sprawi, że animacja będzie powtarzana w nieskończoność. Wartość logiczna true (prawda) spowoduje, że obrazy będą animowane tylko raz.
Możesz zauważyć, że obrazy, które chcesz animować, są zebrane w liście animacji. Znacznik animation-list jest konwertowany na obiekt AnimationDrawable reprezentujący zbiór obrazów. Klasa AnimationDrawable jest dostępna w paczce grafiki, która wyświetla klatki sekwencyjnie przez określony czas. Ponieważ chcesz, aby animacja działała w nieskończonej pętli, do atrybutu android:oneshot w kodzie z listingu 8.11 musisz przydzielić wartość logiczną false. Następnie trzeba napisać kod Java, który będzie wykonywał następujące zadania. Uzyskiwanie dostępu do obrazów wpisanych w pliku frame_anim.xml
i rysowanie tych obrazów za pomocą kontrolki ImageView.
281
282
Rozdział 8. Animacje Tworzenie obiektu klasy AnimationDrawable i uzyskiwanie dostępu do jego
metod start() oraz stop() w celu uruchamiania i zatrzymywania aplikacji. Aby wykonać wymienione zadania, wpisz w pliku aktywności FrameAnimationAppActivity.java kod przedstawiony w listingu 8.12. Listing 8.12. Kod wpisany w pliku aktywności FrameAnimationAppActivity.java package com.androidtablet.frameanimationapp; import import import import import import
android.app.Activity; android.os.Bundle; android.widget.ToggleButton; android.widget.ImageView; android.view.View; android.graphics.drawable.AnimationDrawable;
public class FrameAnimationAppActivity extends Activity { AnimationDrawable animation; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_frame_animation_app); final ToggleButton startStopButton = (ToggleButton) findViewById(R.id.startstop_button); final ImageView imgView = (ImageView)findViewById( R.id.imgview); startStopButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (startStopButton.isChecked()) { imgView.setBackgroundResource(R.anim.frame_anim); animation = (AnimationDrawable) imgView.getBackground(); animation.start(); } else animation.stop(); } }); } }
Możesz tutaj zauważyć, że dostęp do kontrolek ToggleButton i ImageView jest uzyskiwany z pliku układu, a same kontrolki są mapowane na swoje instancje o nazwach odpowiednio startStopButton i imgView. Nasłuchiwacz ClickListener zostaje powiązany z kontrolką ToggleButton. Po kliknięciu tej kontrolki uruchamiana jest jej metoda wywołania zwrotnego onClick(). W metodzie onClick() odwołujesz się do pliku frame_anim.xml, który zawiera zbiór obrazów animation-list. Ustawiasz Drawable jako podstawowy zasób dla kontrolki ImageView.
Receptura: animacja generująca klatki pośrednie
W kodzie tworzony jest obiekt klasy AnimationDrawable i wywoływana animacja. Uzyskiwany jest dostęp do metod start() i stop() klasy AnimationDrawable, które uruchamiają i zatrzymują animację. Po uruchomieniu aplikacji widoczna jest kontrolka ToggleButton z nagłówkiem Uruchom animację (patrz rysunek 8.8, górny obrazek). Kiedy klikniesz przycisk Uruchom animację, wywoływana jest metoda start() klasy AnimationDrawable w celu rozpoczęcia animacji poklatkowej (patrz rysunek 8.8, środkowy obrazek). Nagłówek kontrolki ToggleButton zmienia się wtedy na Zatrzymaj animację. Po kliknięciu przycisku Zatrzymaj animację kontrolka ToggleButton wraca do stanu sprzed kliknięcia i wywoływana jest metoda stop() klasy AnimationDrawable w celu zatrzymania animacji. Kontrolka ImageView pokazuje obraz klatki, na której animacja została zatrzymana, a nagłówek kontrolki ToggleButton to ponownie Uruchom animację (rysunek 8.8, dolny obrazek).
Receptura: animacja generująca klatki pośrednie Istnieją cztery rodzaje animacji generującej klatki pośrednie. Oto one. Animacja alfa — wykorzystywana do zmiany stopnia przyciemnienia lub
przezroczystości widoku. Animacja rotacji — wykorzystywana do obracania widoku o określony kąt
wokół danej osi lub punku pivota. Animacja skalowania — wykorzystywana do zmniejszania lub powiększania
widoku na osi X, osi Y lub na obu osiach jednocześnie. Możesz również określić punkt pivota, wokół którego chcesz skalować dany widok. Animacja przesunięcia — wykorzystywana do przesuwania widoku wzdłuż osi
X lub Y. Aby zrozumieć, jak działa animacja generująca klatki pośrednie, utwórz nowy projekt Android o nazwie TweeningAnimApp. W tej aplikacji będziesz wyświetlał kontrolkę ImageView oraz cztery kontrolki Button. Kontrolka ImageView będzie wykorzystywana do wyświetlania animacji. Cztery kontrolki Button posłużą do inicjowania czterech rodzajów animacji generującej klatki pośrednie: animacji alfa, animacji rotacji, animacji skalowania oraz animacji przesunięcia. Aby zdefiniować kontrolkę ImageView oraz cztery kontrolki Button, wpisz w pliku układu activity_tweening_anim_app.xml kod przedstawiony w listingu 8.13.
283
284
Rozdział 8. Animacje
Rysunek 8.8. Ekran początkowy (górny obrazek). Animacja odtwarzana po kliknięciu przycisku Uruchom animację (środkowy obrazek). Animacja zatrzymana po kliknięciu przycisku Zatrzymaj animację (dolny obrazek)
Receptura: animacja generująca klatki pośrednie Listing 8.13. Kod wpisany w pliku układu activity_tweening_anim_app.xml
Jak możesz zauważyć, kontrolce ImageView przypisany został identyfikator imgview, który będzie wykorzystywany w celu uzyskania dostępu do tej kontrolki w kodzie Java. Ponadto kontrolka ta ma początkowo wyświetlać obraz ic_launcher.png. Szerokość i wysokość przypisane do kontrolki ImageView zostały zdefiniowane za pomocą zasobów wymiarów odpowiednio image_width oraz image_height. Czterem kontrolkom Button przypisane zostały teksty Dodaj kanał alfa, Obróć, Skaluj i Przenieś; wskazują one, jaki rodzaj animacji zostanie uruchomiony po kliknięciu każdej z nich. Nagłówki kontrolek Button będą wyświetlane czcionką o rozmiarze zdefiniowanym w zasobie wymiarów text_size. W celu identyfikacji oraz uzyskania dostępu do kontrolek Button w kodzie Java przypisano im identyfikatory alpha_button, rotate_button, scale_button oraz translate_button. Do pliku wymiarów dimens.xml, znajdującego się w folderze res/values, dodaj kod zmieniający rozmiary obrazów według rozmiaru ekranu urządzenia, na którym uruchamiana jest dana aplikacja:
285
286
Rozdział 8. Animacje 14sp 100dp 120dp
Trzy zasoby wymiarów text_size, image_width oraz image_height definiują odpowiednio rozmiar czcionki tekstu, szerokość obrazu oraz wysokość obrazu. Te zasoby wymiarów są przeznaczone dla urządzeń o normalnych ekranach (telefonów). Aby zdefiniować zasoby wymiarów dla 7-calowych tabletów, otwórz znajdujący się w folderze res/values-sw600dp plik dimens.xml i wpisz w nim poniższy kod: 24sp 140dp 160dp
Na koniec, aby zdefiniować wymiary dla urządzeń o ekstradużych ekranach (10-calowych tabletów), otwórz znajdujący się w folderze values-sw720dp plik dimens.xml i wpisz w nim następujący kod: 32sp 180dp 200dp
Porównując zasoby wymiarów dla telefonów, 7-calowych tabletów i 10-calowych tabletów, możesz zauważyć, że rozmiar tekstu i obrazów w danej aplikacji jest zmieniany na podstawie rozmiaru ekranu urządzenia. Za każdy rodzaj animacji generującej klatki pośrednie odpowiada konkretna klasa. Klasy te zostały opisane w tabeli 8.6. Tabela 8.6. Klasy wykorzystywane w animacjach generujących klatki pośrednie
Klasa
Zadanie
AlphaAnimation
Definiuje stopień przyciemnienia lub przezroczystości widoku (lub widoków).
TranslateAnimation
Definiuje animację przesunięcia i pozwala zastosować ruch do powiązanych widoków.
RotateAnimation
Definiuje animację rotacji i sprawia, że widoki obracają się o określoną liczbę stopni wokół danego punktu pivota lub osi, w kierunku zgodnym z ruchem wskazówek zegara albo w kierunku przeciwnym.
ScaleAnimation
Definiuje animację skalowania oraz powiększa lub zmniejsza określony widok poziomo, pionowo lub w obu aspektach.
Receptura: animacja generująca klatki pośrednie
Korzystanie z klasy AlphaAnimation Klasa ta określa stopień przyciemnienia lub przezroczystości widoku (albo widoków). Składnia konstruktora tej klasy jest następująca: public AlphaAnimation(float od_alpha, float do_alpha)
Zastosowanie dwóch wymienionych parametrów jest następujące. od_alpha — definiuje startową wartość alpha dla rozpoczęcia animacji. Dla tego
parametru można dostarczyć wartości z zakresu od 0.0 do 1.0, gdzie 1.0 oznacza całkowitą nieprzezroczystość, a 0.0 oznacza całkowitą przezroczystość. do_alpha — definiuje końcową wartość alpha dla zakończenia animacji.
Podobnie jak w przypadku poprzedniego parametru, możliwe wartości pochodzą z zakresu od 0.0 do 1.0. Przykładowo poniższa instrukcja spowoduje animowanie widoku od stanu widzialności do stanu niewidzialności: Animation animation = new AlphaAnimation(1.0f, 0.1f);
Korzystanie z klasy TranslateAnimation Klasa definiuje animację przesunięcia i pozwala zastosować ruch do powiązanego widoku. Składnia konstruktora tej klasy jest następująca: public TranslateAnimation(float zmiana_od_X, float zmiana_do_X, float zmiana_od_Y, float zmiana_do_Y)
Opis parametrów zastosowanych w tym konstruktorze przedstawiono w tabeli 8.7. Tabela 8.7. Parametry wykorzystane w konstruktorze klasy TranslateAnimation
Parametr
Opis
zmiana_od_X
Reprezentuje zmianę współrzędnej X, która ma być zastosowana przy rozpoczynaniu animacji. Wartość 0 rozpoczyna animację od bieżącego położenia współrzędnej X.
zmiana_do_X
Reprezentuje zmianę współrzędnej X dla zakończenia animacji. Dla przesunięcia w prawo wartość tego parametru jest dodatnia, a dla przesunięcia w lewo wartość jest ujemna.
zmiana_od_Y
Reprezentuje zmianę współrzędnej Y, która ma być zastosowana przy rozpoczynaniu animacji. Wartość 0 rozpoczyna animację od bieżącego położenia współrzędnej Y.
zmiana_do_Y
Reprezentuje zmianę współrzędnej Y dla zakończenia animacji. Dla przesunięcia w dół wartość tego parametru jest dodatnia, a dla przesunięcia w górę wartość jest ujemna.
287
288
Rozdział 8. Animacje
Przykładowo poniższa instrukcja powoduje przesunięcie danego widoku z jego bieżącej lokalizacji w lewo o 150 punktów: Animation animation = new TranslateAnimation(0,-150,0,0);
Analogicznie kolejna instrukcja wykona animację widoku w prawo o 150 punktów i w dół o 200 punktów (czyli widok zostanie przesunięty po przekątnej): Animation animation = new TranslateAnimation(0,150,0,200);
Korzystanie z klasy RotateAnimation Klasa definiuje animację rotacji i powoduje obrócenie widoku wokół określonego punktu pivota lub osi w kierunku zgodnym z ruchem wskazówek zegara lub w kierunku przeciwnym. Składnia konstruktora tej klasy jest następująca: public RotateAnimation(float od_kąta, float do_kąta, int typ_pivota_X, float pivot_X, int typ_pivota_Y, float pivot_Y)
W tabeli 8.8 opisano parametry wykorzystywane w tym konstruktorze. Tabela 8.8. Parametry wykorzystane w konstruktorze klasy RotateAnimation
Parametr
Opis
od_kąta
Definiuje w stopniach kąt, w którym ma rozpocząć się animacja rotacji.
do_kąta
Definiuje kąt, w którym ma zakończyć się animacja rotacji. Jeśli dany kąt jest liczbą dodatnią, widok będzie obracać się zgodnie z kierunkiem ruchu wskazówek zegara. Jeśli wartość kąta jest liczbą ujemną, widok będzie obracać się w kierunku przeciwnym do ruchu wskazówek zegara.
typ_pivota_X
Określa sposób interpretacji wartości współrzędnej pivot_X. Parametr ten może przyjmować poniższe wartości. Animation.ABSOLUTE — interpretuje wartość współrzędnej pivot_X jako bezwzględną liczbę pikseli. Animation.RELATIVE_TO_SELF — interpretuje wartość współrzędnej pivot_X w relacji do bieżącej współrzędnej X animowanego widoku. Wartość współrzędnej pivot_X może być podawana w procentach. Wartość procentowa jest skalowana w zakresie od 0.0 do 1.0, gdzie 1.0 reprezentuje 100%. Wartość 0.5 przypisana do współrzędnej pivot_X
reprezentuje punkt środkowy szerokości danego widoku. Animation.RELATIVE_TO_PARENT — interpretuje wartość współrzędnej pivot_X w relacji do współrzędnej X dla przodka widoku animowanego.
Receptura: animacja generująca klatki pośrednie Tabela 8.8. Parametry wykorzystane w konstruktorze klasy RotateAnimation (ciag dalszy)
Parametr
Opis
pivot_X
Definiuje współrzędną X punktu pivota lub osi, wokół których obracany ma być widok. Jeśli parametr typ_pivota_X ma wartość Animation.ABSOLUTE, do tego parametru dostarczana jest wartość bezwzględna. Wartość 0 reprezentuje lewy brzeg widoku. Jeśli do parametru typ_pivota_X przypisano wartość Animation.RELATIVE_TO_SELF lub Animation.RELATIVE_TO_PARENT, parametr ten przyjmuje wartości procentowe. Wartości procentowe są skalowane w zakresie od 0.0 do 1.0, gdzie 1.0 reprezentuje 100%.
typ_pivota_Y
Określa sposób interpretacji wartości współrzędnej pivot_Y. Parametr ten może przyjmować następujące wartości: Animation.ABSOLUTE, Animation.RELATIVE_TO_SELF oraz Animation.RELATIVE_TO_PARENT.
pivot_Y
Definiuje współrzędną Y punktu pivota lub osi, wokół których obracany ma być widok. Jeśli parametr typ_pivota_Y ma wartość Animation.ABSOLUTE, do tego parametru dostarczana jest wartość bezwzględna. Wartość 0 reprezentuje górny brzeg widoku. Jeśli do parametru typ_pivota_Y przypisano wartość Animation.RELATIVE_TO_SELF lub Animation.RELATIVE_TO_PARENT, parametr ten przyjmuje wartości procentowe. Wartości procentowe są skalowane w zakresie od 0.0 do 1.0, gdzie 1.0 reprezentuje 100%. Jeśli współrzędna pivot_Y ma wartość 0.5, oznacza to, że jest traktowana jako punkt środkowy wysokości widoku.
Przykładowo poniższa instrukcja spowoduje zastosowanie do widoku animacji rotacji, która obróci go o 360° wokół jego osi środkowej zgodnie z kierunkiem ruchu wskazówek zegara: RotateAnimation animation = new RotateAnimation(0,360, Animation.RELATIVE_TO_ SELF,0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
Korzystanie z klasy ScaleAnimation Klasa ta definiuje animację skalowania i powoduje zmniejszenie lub zwiększenie danego widoku w pionie, w poziomie lub w obu aspektach. Składnia konstruktora tej klasy jest następująca: public ScaleAnimation(float od_X, float do_X, float od_Y, float do_Y, int typ_pivota_X, float pivot_X, int typ_pivota_Y, float pivot_Y)
W tabeli 8.9 opisano parametry wykorzystywane w konstruktorze klasy ScaleAnimation.
289
290
Rozdział 8. Animacje Tabela 8.9. Parametry wykorzystywane w konstruktorze klasy ScaleAnimation
Parametr
Opis
od_X
Definiuje początkową wartość skalowania w poziomie, która ma być zastosowania przy rozpoczynaniu animacji skalowania. Wartości dla tego parametru dostarczane są w formie skalowanej, np. 1.0 reprezentuje 100%, 2.0 reprezentuje 200%, 0.5 reprezentuje 50% itd.
do_X
Definiuje wartość skalowania w poziomie, która ma być zastosowana do zakończenia animacji skalowania.
od_Y
Definiuje początkową wartość skalowania w pionie, która ma być zastosowana przy rozpoczynaniu animacji skalowania.
do_Y
Definiuje wartość skalowania w pionie, która ma być zastosowana do zakończenia animacji skalowania.
typ_pivota_X
Określa sposób interpretacji współrzędnej pivot_X (czyli wartość absolutną lub procentową). Parametr ten może przyjmować następujące wartości: Animation.ABSOLUTE, Animation.RELATIVE_TO_SELF oraz Animation.RELATIVE_TO_PARENT. Znaczenie tych stałych jest takie same jak w przypadku animacji rotacji.
pivot_X
Definiuje współrzędną X punktu pivota lub osi, wokół których skalowany ma być dany widok. Jeśli parametr typ_pivota_X ma wartość Animation.ABSOLUTE, do tego parametru dostarczana jest wartość bezwzględna. Wartość 0 reprezentuje lewy brzeg widoku. Jeśli do parametru typ_pivot_X przypisano wartość Animation.RELATIVE_TO_SELF lub Animation.RELATIVE_TO_PARENT, parametr ten przyjmuje wartości procentowe. Wartości procentowe są skalowane w zakresie od 0.0 do 1.0, gdzie 1.0 reprezentuje 100%.
typ_pivota_Y
Określa sposób interpretacji współrzędnej pivot_Y. Parametr ten może przyjmować następujące wartości: Animation.ABSOLUTE, Animation.RELATIVE_TO_SELF oraz Animation.RELATIVE_TO_PARENT.
pivot_Y
Definiuje współrzędną Y punktu pivota lub osi, wokół których skalowany ma być dany widok. Jeśli parametr typ_pivota_Y ma wartość Animation.ABSOLUTE, do tego parametru dostarczana jest wartość bezwzględna. Wartość 0 reprezentuje górny brzeg widoku. Jeśli do parametru typ_pivota_Y przypisano wartość Animation.RELATIVE_TO_SELF lub Animation.RELATIVE_TO_PARENT, parametr ten przyjmuje wartości procentowe. Wartości procentowe są skalowane w zakresie od 0.0 do 1.0, gdzie 1.0 reprezentuje 100%. Jeśli współrzędna pivot_Y ma wartość 0.5, oznacza to, że jest traktowana jako punkt środkowy wysokości widoku.
Receptura: animacja generująca klatki pośrednie
Wartości dostarczane do parametrów od_X, do_X, od_Y oraz do_Y mają zawsze postać skalowaną. Wartości dostarczane do parametrów pivot_X oraz pivot_Y mogą być bezwzględne lub skalowane, w zależności od wartości przypisanych parametrom typ_pivota_X oraz typ_pivot_Y. Przykładowo poniższa instrukcja spowoduje skalowanie widoku w pionie i w poziomie do dwukrotności jego pierwotnego rozmiaru. Środek widoku został potraktowany jako oś lub punkt pivota animacji skalowania: Animation animation = new ScaleAnimation(1.0f, 2.0f, 1.0f, 2.0f,Animation.RELATIVE_TO_ SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
Wróć teraz do aplikacji TweeningAnimApp, aby zastosować odpowiednie klasy animacji, które właśnie poznałeś. W pliku aktywności musisz napisać kod Java, który będzie nasłuchiwał zdarzeń kliknięcia czterech kontrolek Button, będzie tworzył obiekty odpowiednich klas animacji i stosował je do kontrolki ImageView w celu wyświetlenia danej animacji na ekranie. Aby wykonać wymienione zadania, w pliku aktywności Java TweeningAnimAppActivity.java wpisz kod przedstawiony w listingu 8.14. Listing 8.14. Kod wpisany w pliku aktywności Java TweeningAnimAppActivity.java package com.androidtablet.tweeninganimapp; import import import import import import import import import import import
android.app.Activity; android.os.Bundle; android.widget.ImageView; android.view.animation.Animation; android.widget.Button; android.view.View; android.view.animation.TranslateAnimation; android.view.animation.RotateAnimation; android.view.animation.AlphaAnimation; android.view.animation.ScaleAnimation; android.view.animation.AnimationSet;
public class TweeningAnimAppActivity extends Activity { ImageView imgView; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_tweening_anim_app); Button alphaButton = (Button) findViewById( R.id.alpha_button); Button rotateButton = (Button) findViewById( R.id.rotate_button); Button scaleButton = (Button) findViewById( R.id.scale_button); Button translateButton = (Button) findViewById( R.id.translate_button); imgView = (ImageView)findViewById(R.id.imgview); rotateButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) {
291
292
Rozdział 8. Animacje
}
RotateAnimation animation = new RotateAnimation( 0,360, Animation.RELATIVE_TO_SELF,0.5f, Animation.RELATIVE_TO_SELF, 0.5f); animation.setDuration(3000); imgView.setAnimation(animation); animation.start();
}); alphaButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Animation animation = new AlphaAnimation(1.0f, 0.1f); animation.setDuration(3000); imgView.setAnimation(animation); animation.start(); } }); scaleButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { AnimationSet set = new AnimationSet(true); Animation animation1 = new ScaleAnimation(1.0f, 2.0f, 1.0f, 2.0f,Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); animation1.setDuration(3000); set.addAnimation(animation1); Animation animation2 = new ScaleAnimation(1.0f, 0.5f, 1.0f, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); animation2.setDuration(3000); animation2.setStartOffset(3000); set.addAnimation(animation2); imgView.startAnimation(set); } });
}
}
translateButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { AnimationSet set = new AnimationSet(true); Animation animation1 = new TranslateAnimation( 0,-150,0,0); animation1.setDuration(3000); animation1.setFillAfter(true); set.addAnimation(animation1); Animation animation2 = new TranslateAnimation( 0,0,0,200); animation2.setDuration(3000); animation2.setStartOffset(3000); animation2.setFillAfter(true); set.addAnimation(animation2); imgView.startAnimation(set); } });
Receptura: zastosowanie animacji układu
Jak możesz zauważyć, w powyższym kodzie uzyskiwany jest dostęp do czterech kontrolek Button zdefiniowanych w pliku układu z identyfikatorami alpha_button, rotate_button, scale_button oraz translate_button, a same kontrolki są mapowane na obiekty Button o nazwach odpowiednio alphaButton, rotateButton, scaleButton oraz translateButton. Z czterema kontrolkami Button powiązane zostały nasłuchiwacze ClickListener, dzięki czemu po kliknięciu którejś z kontrolek Button uruchamiana jest jej metoda wywołania zwrotnego onClick(). W metodzie powstają obiekty odpowiednich klas animacji, które są stosowane do kontrolki ImageView w celu rozpoczęcia i wyświetlenia animacji na ekranie. Po uruchomieniu tej aplikacji zobaczysz cztery kontrolki Button: Dodaj kanał alfa, Obróć, Skaluj oraz Przenieś. Widoczna będzie również kontrolka ImageView wyświetlająca obraz ic_launcher.png, który skopiowałeś do folderów res/drawable (patrz rysunek 8.9, górny obrazek). Po kliknięciu przycisku Dodaj kanał alfa zastosowana zostanie animacja alpha, która spowoduje, że w ciągu trzech sekund obraz stanie się prawie całkowicie przezroczysty (patrz rysunek 8.9, środkowy obrazek). Kiedy klikniesz kontrolkę Obróć, zastosowana zostanie animacja rotation, która spowoduje obrócenie obrazu o 360° zgodnie z ruchem wskazówek zegara. Osią rotacji będzie środek obrazu, a czas trwania animacji wyniesie 3 sekundy (patrz rysunek 8.9, dolny obrazek). Po kliknięciu przycisku Skaluj obraz zostanie przeskalowany w poziomie i w pionie do dwukrotności jego pierwotnego rozmiaru. Skalowanie będzie odbywać się względem środka obrazu, a animacja potrwa 3 sekundy (patrz rysunek 8.10, górny obrazek). Po wykonaniu skalowania obraz zacznie kurczyć się do pierwotnego rozmiaru w czasie 3 sekund. Kiedy klikniesz przycisk Przenieś, obraz zostanie przesunięty w lewo na osi X o 150 pikseli. Operacja ta potrwa 3 sekundy, a następnie obraz zacznie przesuwać się w dół na osi Y o 200 pikseli w czasie kolejnych 3 sekund (patrz rysunek 8.10, dolny obrazek).
Receptura: zastosowanie animacji układu Jak wskazuje nazwa, animacja układu jest wykorzystywana w celu animowania układów. Zasadniczo animacja układu ma zastosowanie do każdego dodawanego lub usuwanego widoku potomnego w danym układzie. Animacja układu może być stosowana nie tylko do każdego typu układu, ale również do każdego typu widoku AdapterView. Poza układami, takimi jak LinearLayout oraz RelativeLayout, animację układu można stosować do kontrolek ListView. Do zarządzania animacją układu możesz wykorzystać klasę LayoutAnimationController, która może być zdefiniowana w pliku Java lub w pliku XML. W tabeli 8.10 przedstawiono atrybuty klasy LayoutAnimationController.
293
294
Rozdział 8. Animacje
Rysunek 8.9. Aplikacja po uruchomieniu (górny obrazek). Obraz staje się przezroczysty po kliknięciu przycisku Dodaj kanał alfa (środkowy obrazek). Obraz obraca się po kliknięciu przycisku Obróć (dolny obrazek)
Receptura: zastosowanie animacji układu
Rysunek 8.10. Obraz przeskalowany po kliknięciu przycisku Skaluj (górny obrazek) oraz obraz przesunięty po kliknięciu przycisku Przenieś (dolny obrazek) Tabela 8.10. Krótki opis atrybutów klasy LayoutAnimationController
Atrybut
Opis
android:animation
Reprezentuje animację, która ma być zastosowana do każdego widoku potomnego w danym układzie.
android:animationOrder
Kolejność, w jakiej animacje są stosowane do każdego widoku potomnego. Atrybut ten może mieć wartość normal (normalna), reverse (odwrócona) lub random (przypadkowa). Wartość normal oznacza, że aplikacja będzie zastosowana najpierw do pierwszego widoku potomnego, a następnie do kolejnych. Wartość reverse oznacza, że animacja będzie zastosowana najpierw do ostatniego widoku potomnego. Wartość random oznacza, że animacja będzie zastosowana do dowolnego widoku potomnego w dowolnej kolejności.
295
296
Rozdział 8. Animacje Tabela 8.10. Krótki opis atrybutów klasy LayoutAnimationController (ciąg dalszy)
Atrybut
Opis
android:delay
Reprezentuje opóźnienie pomiędzy animacją każdego widoku potomnego. Atrybut ten może być określony jako wartość procentowa ogólnego czasu trwania animacji. Przykładowo 50 % oznacza, że pomiędzy animacją każdego widoku potomnego zastosowane będzie opóźnienie równe 50% czasu trwania animacji.
android:interpolator
Interpolator wykorzystywany do zmiany stopnia opóźnienia pomiędzy każdym widokiem potomnym. Domyślną wartością dla tego atrybutu jest interpolator liniowy.
Uwaga Kiedy wprowadzasz animację układu, nie musisz bezpośrednio rozpoczynać animacji, ponieważ rozpocznie się ona automatycznie po wyświetleniu układu.
Aby nauczyć się, w jaki sposób zastosować animację układu do kontrolki ListView, utwórz projekt Android o nazwie LayoutAnimApp. W celu zdefiniowania kontrolki ListView wpisz w pliku układu aktywności activity_layout_anim_app.xml kod przedstawiony w listingu 8.15. Listing 8.15. Kod wpisany w pliku układu aktywności activity_layout_anim_app.xml
Jak możesz zauważyć, kontrolce ListView przypisano identyfikator listview. Pamięć podręczna rysowania (ang. drawing cache) jest zachowywana po wykonaniu animacji, a także po przewinięciu widoku. Domyślny rozmiar elementów listy wyświetlonych w kontrolce ListView jest odpowiedni dla telefonów, ale za mały dla tabletów. Aby zmienić rozmiar elementów listy kontrolki ListView według wielkości ekranu danego urządzenia, dodaj do folderu res/layout kolejny plik XML o nazwie list_item.xml. Wpisz w tym pliku następujący kod:
Receptura: zastosowanie animacji układu android:layout_width="match_parent" android:layout_height="match_parent" android:padding="6dp" android:textSize="@dimen/text_size" android:textStyle="bold" />
Powyższy kod spowoduje, że elementy listy kontrolki ListView zostaną wypełnione spacjami o szerokości 6 dp. Tekst będzie prezentowany pogrubioną czcionką o rozmiarze zdefiniowanym w zasobie wymiarów text_size. Animacja układu dla kontrolki ListView będzie zastosowana z pliku list_layout_controller.xml, który utworzysz w folderze /res/anim.
Uwaga Atrybut android:layoutAnimation pomaga określić kontrolera układu, którego chcesz użyć do animowania widoków o obrębie danego układu.
W folderze /res utwórz folder o nazwie anim. Do folderu /res/anim dodaj dwa pliki XML o nazwach list_layout_controller.xml i slideleft.xml. W pliku list_layout_controller.xml wpisz kod przedstawiony w listingu 8.16. Listing 8.16. Kod wpisany w pliku list_layout_controller.xml
Jak możesz zauważyć, animacja zastosowana do każdego widoku potomnego kontrolki ListView zostanie pobrana z pliku slideleft.xml, który właśnie utworzyłeś w folderze /res/anim. Ponadto pomiędzy każdą animacją widoku potomnego zastosowane zostanie opóźnienie równe 50% czasu trwania animacji. Jeśli przykładowo czas trwania animacji to 1 sekunda, wtedy na początku animacji każdego widoku potomnego kontrolki ListView wstawione zostanie opóźnienie wynoszące 0,5 sekundy. W pliku slideleft.xml wpisz kod przedstawiony w listingu 8.17. Listing 8.17. Kod wpisany w pliku slideleft.xml
Jak widać, animacja przesunięcia została zastosowana do każdego widoku potomnego kontrolki ListView. Widok potomny kontrolki ListView ma zostać przesunięty od prawego brzegu ekranu w lewą stronę. Innymi słowy, widok potomny rozpocznie swoją
297
298
Rozdział 8. Animacje
animację w punkcie 100%p (prawy brzeg ekranu) i zostanie przesunięty w kierunku zerowego piksela na osi X (lewy brzeg ekranu). Wartość 100%p oznacza tutaj lokalizację piksela, która odpowiada szerokości widoku przodka, czyli prawej granicy ekranu. Czas trwania animacji wyniesie 1 sekundę. Ponieważ zastosowany został interpolator accelerate_interpolator, animacja rozpocznie się powoli, a następnie zacznie przyspieszać. Aby wyświetlić zawartość w kontrolce ListView, musisz w pliku aktywności Java LayoutAnimAppActivity.java wpisać kod przedstawiony w listingu 8.18. Listing 8.18. Kod wpisany w pliku aktywności Java LayoutAnimAppActivity.java package com.androidtablet.layoutanimapp; import import import import
android.os.Bundle; android.app.Activity; android.widget.ListView; android.widget.ArrayAdapter;
public class LayoutAnimAppActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_layout_anim_app); final String[] products={"Aparat", "Laptop", "Zegarek","Smartfon", "Telewizor"}; ListView listView = (ListView)findViewById(R.id.listview); ArrayAdapter arrayAdpt= new ArrayAdapter (getBaseContext(),R.layout.list_item, products); listView.setAdapter(arrayAdpt); } }
Jak możesz zauważyć, utworzona została tablica products w celu wylistowania nazw produktów, które chcesz wyświetlić za pomocą kontrolki ListView. Zdefiniowany został obiekt arrayAdpt klasy ArrayAdapter w celu określenia widoku dla każdego potomka kontrolki ListView i wyświetlenia tablicy zawierającej odpowiednie dane. Obiekt ArrayAdapter został skonfigurowany dla kontrolki ListView w celu wyświetlenia listy produktów. Po uruchomieniu tej aplikacji zauważysz, że jeden z widoków potomnych (produkt) najpierw pojawi się przy prawym brzegu ekranu i będzie animowany w kierunku lewego brzegu ekranu (patrz rysunek 8.11, górny obrazek). Czas potrzebny na przesunięcie każdego widoku potomnego od prawego do lewego brzegu ekranu wynosi 1 sekundę. Co każde kolejne 0,5 sekundy kolejny widok potomny pojawi się z prawej strony ekranu i będzie przesuwał się w lewą stronę (patrz rysunek 8.11, środkowy obrazek). Na rysunku 8.11 (dolny obrazek) przedstawiono kontrolkę ListView po tym, jak wszystkie widoki potomne zostały przesunięte w kierunku lewego brzegu ekranu.
Receptura: zastosowanie animacji układu
Rysunek 8.11. Widok potomny pojawia się po prawej stronie ekranu i przesuwa się w lewo (górny obrazek). Co każde 0,5 sekundy kolejny widok potomny pojawia się z prawego brzegu ekranu (środkowy obrazek). Wszystkie widoki potomne wylistowane w kontrolce ListView (dolny obrazek)
299
300
Rozdział 8. Animacje
Jak wspomniano wcześniej, klasa LayoutAnimationController może być zdefiniowana również w pliku Java. Poniższa instrukcja definiuje klasę LayoutAnimationController, ładuje animację układu zdefiniowaną w pliku list_layout_controller.xml przechowywanym w folderze /res/anim i powoduje zastosowanie tej animacji do obiektu ListView: LayoutAnimationController controller = AnimationUtils.loadLayoutAnimation(this, R.anim.list_layout_controller); listView.setLayoutAnimation(controller);
Ponieważ w tej aplikacji zastosowany został interpolator, warto dowiedzieć się czegoś więcej na jego temat. Interpolator definiuje współczynnik zmian w animacji. Może wpływać na wszystkie cztery typy animacji generującej klatki pośrednie. Interpolatory mogą sprawić, że animacja alfa, animacja skalowania, animacja przesunięcia i animacja rotacji będą przyspieszać, podskakiwać, wykonywać określony wzór itd. W ramach pakietu frameworku SDK Android dostępne są różne interpolatory. Niektóre z nich zostały opisane w tabeli 8.11. Tabela 8.11. Rodzaje interpolatorów
Interpolator
Opis
AccelerateDecelerateInterpolator
Animacja rozpoczyna się powoli, potem przyspiesza i kończy się powoli.
AccelerateInterpolator
Animacja rozpoczyna się powoli, a następnie przyspiesza.
AnticipateInterpolator
Animacja rozpoczyna się wstecz, a następnie wykonywana jest do przodu.
AnticipateOvershootInterpolator
Animacja rozpoczyna się wstecz, potem idzie do przodu, mija miejsce docelowe, a następnie wraca do wartości docelowej.
BounceInterpolator
Animacja pod koniec zaczyna podskakiwać.
CycleInterpolator
Powtarza animację przez określoną liczbę cykli przy płynnych przejściach.
DecelerateInterpolator
Animacja rozpoczyna się szybko, a następnie zwalnia.
LinearInterpolator
Prędkość animacji jest stała.
OvershootInterpolator
Animacja idzie do przodu, mija miejsce docelowe, a następnie wraca do docelowej wartości.
Receptura: gromadzenie i wyświetlanie sekwencji animacji za pomocą AnimationSet
Receptura: gromadzenie i wyświetlanie sekwencji animacji za pomocą klasy AnimationSet Aby połączyć animacje w grupę i odtworzyć je razem, możesz skorzystać z klasy AnimationSet. Do obiektu AnimationSet możesz dodać dowolną liczbę podklas animacji, takich jak AlphaAnimation, RotateAnimation, ScaleAnimation oraz TranslateAnimation. Po dodaniu do obiektu AnimationSet transformacje każdej podklasy tworzą pojedynczą transformację. Zbiór sekwencji animacji, które zostały dodane do obiektu AnimationSet, może być zastosowany do wymaganego widoku za pomocą metody startAnimation(). Poniższa instrukcja spowoduje, że sekwencje animacji dodane do obiektu animSet klasy AnimationSet zostaną zastosowane do obiektu imgView klasy ImageView: imgView.startAnimation(animSet);
Utwórz teraz aplikację pokazującą, w jaki sposób animacje są zbierane za pomocą klasy AnimationSet. W tym celu utwórz projekt Android o nazwie AnimationSetApp. W tej aplikacji będziesz skalował kontrolkę ImageView do dwukrotności jej pierwotnego rozmiaru. Po tym powiększeniu kontrolka ImageView skurczy się na powrót do wielkości początkowej. Aby zdefiniować kontrolkę ImageView, wpisz w pliku układu aktywności activity_animation_set_app.xml kod przedstawiony w listingu 8.19. Listing 8.19. Kod wpisany w pliku układu aktywności activity_animation_set_app.xml
Jak możesz zauważyć, zdefiniowana została kontrolka ImageView, której przypisano identyfikator imgview. Kontrolka ta ma być wyświetlana w odległości 100 dp od górnego brzegu ekranu oraz w odległości 100 dp od lewego brzegu ekranu. Kontrolka ImageView jest inicjowana w celu wyświetlenia obrazu ic_launcer.png, który jest dostarczany domyślnie. Szerokość i wysokość obrazu wyświetlonego za pomocą kontrolki ImageView zostały zdefiniowane odpowiednio w zasobach wymiarów image_width oraz image_height. Aby zdefiniować zasoby wymiarów, otwórz znajdujący się w folderze res/values plik wymiarów dimens.xml i określ w nim zasoby zmieniające rozmiary obrazu według
301
302
Rozdział 8. Animacje
wielkości ekranu urządzenia, na którym uruchamiana jest aplikacja. Wpisz w pliku dimens.xml następujący kod: 14sp 100dp 120dp
Trzy zasoby wymiarów, text_size, image_width oraz image_height, definiują odpowiednio rozmiar czcionki dla tekstu, szerokość obrazu oraz wysokość obrazu. Zdefiniowane powyżej zasoby wymiarów są przeznaczone dla urządzeń o normalnych ekranach (telefonów). Aby zdefiniować zasoby wymiarów dla 7-calowych tabletów, otwórz znajdujący się w folderze res/values-sw600dp plik dimens.xml i wpisz w nim następujący kod: 24sp 140dp 160dp
Na koniec, aby zdefiniować zasoby wymiarów dla urządzeń o ekstradużych ekranach (10-calowych tabletów), otwórz znajdujący się w folderze values-sw720dp plik dimens.xml i wpisz w nim następujący kod: 32sp 180dp 200dp
Porównując zasoby wymiarów dla telefonów, 7-calowych tabletów oraz 10-calowych tabletów, możesz zauważyć, że rozmiar tekstu i obrazów w danej aplikacji zmienia się według rozmiaru ekranu urządzenia. W pliku aktywności Java musisz wpisać kod wykonujący następujące zadania. Uzyskanie dostępu do kontrolki ImageView zdefiniowanej w pliku układu
i zmapowanie tej kontrolki na obiekt ImageView. Zdefiniowanie obiektu AnimationSet. Zdefiniowanie obiektu podklasy animacji ScaleAnimation w celu skalowania
kontrolki ImageView do dwukrotności jej rozmiaru w ciągu 3 sekund. Dodanie obiektu klasy ScaleAnimation do obiektu klasy AnimationSet. Zdefiniowanie kolejnego obiektu ScaleAnimation w celu zmniejszenia kontrolki
ImageView do połowy jej bieżącego rozmiaru (czyli zmniejszenia kontrolki ImageView do jej pierwotnego rozmiaru) w ciągu 3 sekund.
Receptura: gromadzenie i wyświetlanie sekwencji animacji za pomocą AnimationSet Dodanie drugiego obiektu ScaleAnimation do obiektu klasy AnimationSet
po 3 sekundach (kiedy zakończy się skalowanie kontrolki ImageView) w celu zmniejszenia jego rozmiaru. Uruchomienie w kontrolce ImageView animacji dodanych do obiektu
AnimationSet.
Aby wykonać wymienione zadania, wpisz w pliku aktywności Java AnimationSetAppActivity.java kod przedstawiony w listingu 8.20. Listing 8.20. Kod wpisany w pliku aktywności Java AnimationSetAppActivity.java package com.androidtablet.animationsetapp; import import import import import import import import
android.os.Bundle; android.app.Activity; android.widget.ImageView; android.view.animation.AccelerateInterpolator; android.view.animation.AnimationSet; android.view.animation.Animation; android.view.animation.ScaleAnimation; android.view.animation.TranslateAnimation;
public class AnimationSetAppActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_animation_set_app); ImageView imgView = (ImageView)findViewById(R.id.imgview); AnimationSet animSet = new AnimationSet(true); Animation animation1 = new ScaleAnimation(1.0f, 2.0f, 1.0f, 2.0f,Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); animation1.setDuration(3000); animSet.addAnimation(animation1); Animation animation2 = new ScaleAnimation(1.0f, 0.5f, 1.0f, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); animation2.setDuration(3000); animation2.setStartOffset(3000); animSet.addAnimation(animation2); imgView.startAnimation(animSet); } }
Po uruchomieniu tej aplikacji kontrolka ImageView będzie miała na początku swój pierwotny rozmiar (patrz rysunek 8.12, górny obrazek). Następnie w czasie 3 sekund kontrolka ta zostanie powiększona do dwukrotności pierwotnego rozmiaru (patrz rysunek 8.12, środkowy obrazek). Po wykonaniu tego skalowania kontrolka ImageView zacznie się kurczyć na powrót do oryginalnej wielkości w ciągu 3 sekund (patrz rysunek 8.12, dolny obrazek).
303
304
Rozdział 8. Animacje
Rysunek 8.12. Kontrolka ImageView po uruchomieniu aplikacji (górny obrazek). Kontrolka ImageView przeskalowana do dwukrotności pierwotnego rozmiaru (środkowy obrazek). Kontrolka ImageView zmniejszona do pierwotnego rozmiaru (dolny obrazek)
Receptura: gromadzenie i wyświetlanie sekwencji animacji za pomocą AnimationSet
W obiekcie klasy AnimationSet możesz zebrać dowolną liczbę animacji. Jeśli np. chcesz, aby w bieżącej aplikacji Android kontrolka ImageView podskakiwała jak piłka, możesz do obiektu AnimationSet dodać kilka obiektów klasy TranslateAnimation. W poniższym kodzie pokazano, w jaki sposób możesz dodać parę obiektów TranslateAnimation do obiektu AnimationSet, aby kontrolka ImageView zaczęła podskakiwać jak piłka. animSet.setInterpolator(new AccelerateInterpolator()); TranslateAnimation slide1 = new TranslateAnimation(0, 100, 0, 200); slide1.setStartOffset(0); slide1.setDuration(1000); animSet.addAnimation(slide1); TranslateAnimation slide2 = new TranslateAnimation(0, 100, 0, -200); slide2.setStartOffset(1000); slide2.setDuration(1000); animSet.addAnimation(slide2); TranslateAnimation slide3 = new TranslateAnimation(0, 100, 0, 200); slide3.setStartOffset(2000); slide3.setDuration(1000); animSet.addAnimation(slide3); animSet.setFillAfter(true); imgView.startAnimation(animSet);
Jak możesz zauważyć, w powyższym kodzie do obiektu animSet klasy AnimationSet zastosowano interpolator AccelerateInterpolator. Interpolator ten sprawi, że animacja rozpocznie się powoli, a następnie zacznie przyspieszać. Możesz również zauważyć, że zdefiniowane zostały trzy obiekty klasy TranslateAnimation o nazwach slide1, slide2 i slide3. Obiekt slide1 będzie powodował ruch kontrolki ImageView w dół, obiekt slide2 będzie powodował ruch kontrolki w górę, a obiekt slide3 będzie powodował ponowny ruch kontrolki w dół. Trzy obiekty klasy TranslateAnimation o nazwach slide1, slide2 oraz slide3 zostaną dodane do obiektu AnimationSet po określonym czasie. Czas ten został zdefiniowany w taki sposób, aby kolejne animacje klasy TranslateAnimation były stosowane dopiero wtedy, kiedy poprzednia animacja się zakończy (czyli obiekt slide2 rozpocznie swoje przesunięcie dopiero, kiedy obiekt slide1 skończy przesuwać kontrolkę ImageView do żądanej lokalizacji). Aby utrzymać kontrolkę ImageView w końcowej lokalizacji animacji, do metody setFillAfter() przekazywana jest wartość logiczna true (prawda). Po uruchomieniu tej aplikacji wyświetlona zostanie kontrolka ImageView w swojej pierwotnej lokalizacji (patrz rysunek 8.13, górny obrazek). Następnie kontrolka ImageView zacznie opadać stopniowo w czasie 1 sekundy (patrz rysunek 8.13, dolny obrazek). Po opadnięciu w dół o 200 pikseli kontrolka ImageView zacznie poruszać się w górę (patrz rysunek 8.14, górny obrazek). Po dotarciu do górnego położenia kontrolka ImageView ponownie zacznie opadać (patrz rysunek 8.14, dolny obrazek).
305
306
Rozdział 8. Animacje
Rysunek 8.13. Kontrolka ImageView w pierwotnej lokalizacji (górny obrazek) oraz kontrolka ImageView po przesunięciu w dół o 200 pikseli (dolny obrazek)
Podsumowanie W tym rozdziale nauczyłeś się, jak w aplikacjach Android wykorzystywać i stosować różne rodzaje animacji. Poznałeś również procedurę implementacji animacji właściwości za pomocą klas ValueAnimator oraz ObjectAnimator. Ponadto dowiedziałeś się, jak korzystać z wielu animacji jednocześnie za pomocą klasy AnimatorSet oraz stosować animację poklatkową, animację generującą klatki pośrednie i animację układu. Na koniec poznałeś procedurę gromadzenia aplikacji i wyświetlania ich w sekwencji przy użyciu klasy AnimationSet.
Podsumowanie
Rysunek 8.14. Kontrolka ImageView poruszająca się w górę (górny obrazek) oraz ponownie poruszająca się w dół (dolny obrazek)
W kolejnym rozdziale dowiesz się, czym jest sprzętowa akceleracja grafiki 2D. Poznasz także konfigurację i różne efekty. Na koniec zapoznasz się z transformacjami oraz typami warstw widoku.
307
308
Rozdział 8. Animacje
9 Sprzętowa akceleracja grafiki 2D G
rafiki, rysunki oraz animacje odgrywają istotną rolę w uatrakcyjnieniu aplikacji, bo sprawiają, że staje się dynamiczna i interesująca. Jednak zastosowanie dużej liczby grafik i rysunków oznacza wykorzystanie wielu zasobów, co może drastycznie zwolnić działanie aplikacji. Aby poprawić wydajność aplikacji opierających się na grafikach, możesz zastosować akcelerację sprzętową. W tym rozdziale nauczysz się włączać akcelerację sprzętową w aplikacjach. Dowiesz się również, jak zastosować akcelerację sprzętową do określonych widoków w trakcie ich animowania. Oznacza to, że nauczysz się stosować warstwy sprzętowe i programowe do poszczególnych widoków. W animacjach oraz innych zasobochłonnych aplikacjach wątek graficznego interfejsu użytkownika (ang. graphical user interface — GUI) jest zazwyczaj przeciążony wykonywaniem wielu zadań. Nauczysz się stosować klasę SurfaceView w celu wykorzystania osobnego wątku do wykonywania rysunków i aktualizacji, co redukuje obciążenie wątku GUI. Na koniec dowiesz się, jak wyświetlać strumień wideo za pomocą klasy TextureView oraz stosować transformacje do zawartości wyświetlanej przy użyciu tej klasy.
Receptura: akceleracja sprzętowa Akceleracja sprzętowa jest stosowana w celu zwiększenia wydajności systemów graficznych. W trakcie wykonywania akceleracji sprzętowej przyspieszany jest element sprzętowy zwany GPU, czyli procesor graficzny, który można znaleźć w większości najnowszych urządzeń z systemem Android. Ponieważ uruchomienie akceleracji sprzętowej pochłania więcej zasobów, w tym także większą ilość pamięci RAM, najrozsądniej stosować tę akcelerację jedynie do wybranych części aplikacji. Akcelerację sprzętową można włączyć lub wyłączyć dla aplikacji, aktywności, okna lub poziomów widoku.
310
Rozdział 9. Sprzętowa akceleracja grafiki 2D
Aby włączyć akcelerację sprzętową dla całej aplikacji, musisz na początku skonfigurować docelowe API aplikacji na poziomie 11. lub wyższym. W tym celu załącz w pliku AndroidManifest.xml poniższą instrukcję:
Następnie dodaj instrukcję android:hardwareAccelerated="true" do znacznika w pliku AndroidManifest.xml w następujący sposób:
Aby zastosować akcelerację sprzętową do poszczególnych aktywności, dodaj atrybut android:hardwareAccelerated do określonego elementu w sposób pokazany niżej:
By zastosować akcelerację sprzętową dla danego okna, możesz zastosować poniższy kod: getWindow().setFlags(WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED, WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
W celu wyłączenia akceleracji sprzętowej dla indywidualnego widoku w trakcie wykonywania aplikacji użyj poniższego kodu: view.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
Uwaga Obecnie nie możesz wyłączyć akceleracji sprzętowej na poziomie okna, ale możesz ją włączyć. Ponadto nie możesz włączyć akceleracji sprzętowej na poziomie widoku, ale możesz ją wyłączyć.
Z pewnością zastanawiasz się, w jaki sposób możesz potwierdzić, czy w aplikacji włączona jest akceleracja sprzętowa. W tym celu należy zastosować metodę isHardwareAccelerated() na obiekcie widoku lub jego bazowym obiekcie Canvas. View.isHardwareAccelerated() — metoda zwraca wartość true (prawda),
jeśli dany widok jest dołączony do okna przyspieszanego sprzętowo. Canvas.isHardwareAccelerated() — metoda zwraca wartość true, jeśli obiekt
Canvas jest przyspieszany sprzętowo.
Uwaga W systemie Android 4.0 (API poziomu 14. lub wyższego) akceleracja sprzętowa jest domyślnie włączona dla wszystkich aplikacji.
Utwórz projekt Android o nazwie HardwareAccApp. Aby domyślnie włączyć akcelerację sprzętową, ustaw dla atrybutów android:minSdkVersion oraz android:minSdkVersion odpowiednio wartości 14 i 17. W pliku aktywności Java HardwareAccAppActivity.java wpisz kod przedstawiony w listingu 9.1.
Receptura: akceleracja sprzętowa Listing 9.1. Kod wpisany w pliku aktywności Java HardwareAccAppActivity.java package com.androidtablet.hardwareaccapp; import import import import import import import
android.app.Activity; android.os.Bundle; android.graphics.Paint; android.graphics.Canvas; android.view.View; android.content.Context; android.graphics.Color;
public class HardwareAccAppActivity extends Activity { Paint paint = new Paint(); @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); MyView myView=new MyView(this); setContentView(myView); } public class MyView extends View{ public MyView(Context context){ super(context); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); paint.setColor(Color.RED); paint.setTextSize(getResources().getDimension( R.dimen.text_size)); if(canvas.isHardwareAccelerated()) canvas.drawText("Obiekt Canvas z akceleracją sprzętową", 10, 75, paint); else canvas.drawText("Obiekt Canvas bez akceleracji sprzętowej", 10, 75, paint); } } }
W tej aplikacji możesz zauważyć, że kolor obiektu Paint jest ustawiony na red (czerwony), a rozmiar wyświetlanego tekstu jest określony w zasobie wymiarów text_size. Metoda canvas.isHardwareAccelerated() jest wywoływana, aby dowiedzieć się, czy akceleracja sprzętowa jest włączona, czy wyłączona. Zgodnie z tą informacją na obiekcie Canvas wyświetlany jest odpowiedni komunikat sprzętowy. Ponieważ poziom API Twojej aplikacji został ustawiony na 14, akceleracja sprzętowa jest włączana automatycznie. Po uruchomieniu tej aplikacji otrzymasz komunikat wyjściowy przedstawiony na rysunku 9.1 (górny obrazek). Jeśli zmienisz wartości atrybutów android:minSdkVersion i android:targetSdkVersion na 11, akceleracja sprzętowa przestanie być uruchamiana domyślnie. Zmieni się również komunikat wyjściowy aplikacji na Obiekt Canvas bez akceleracji sprzętowej (patrz rysunek 9.1, dolny obrazek). Jeśli do elementu w pliku
311
312
Rozdział 9. Sprzętowa akceleracja grafiki 2D
Rysunek 9.1. Komunikat potwierdzający włączenie akceleracji sprzętowej (górny obrazek) oraz komunikat potwierdzający, że akceleracja sprzętowa jest wyłączona
AndroidManifest.xml dodasz instrukcję android:hardwareAccelerated="true", aplikacja sprzętowa będzie uruchamiana na poziomie aplikacji. Komunikat wyjściowy aplikacji zmieni się ponownie na Obiekt Canvas z akceleracją sprzętową, potwierdzając, że akceleracja sprzętowa została włączona. Element w pliku AndroidManifest.xml po dodaniu atrybutu android:hardwareAccelerated będzie wyglądał następująco:
Receptura: korzystanie z warstw widoku
Receptura: korzystanie z warstw widoku W celu poprawienia wydajności aplikacji widok może wykorzystywać jeden z poniższych trzech typów warstw. Oto one. LAYER_TYPE_NONE — widok jest renderowany normalnie. Innymi słowy,
nie zawiera warstwy. Jest to ustawienie domyślne. LAYER_TYPE_HARDWARE — jeśli aplikacja wykorzystuje akcelerację sprzętową,
ta warstwa będzie renderować widok do tekstury sprzętowej. Warstwa jest używana, by sprawić, żeby animacja stała się bardziej płynna, oraz do zwiększenia wydajności aplikacji opartych na rysowaniu elementów. Dzięki zastosowaniu tej warstwy animacja staje się bardziej płynna, ponieważ w trakcie jej wykonywania widok nie jest stale rysowany ponownie, a jedynie wtedy, kiedy zmieniają się jego właściwości lub wywoływana jest metoda invalidate(). Ponieważ warstwy sprzętowe pochłaniają pamięć wideo, są stosowane jedynie na czas trwania animacji, a następnie usuwane. Jeśli aplikacja nie jest przyspieszana sprzętowo, warstwa ta będzie zachowywać się jak warstwa typu LAYER_TYPE_SOFTWARE. LAYER_TYPE_SOFTWARE — widok w tej warstwie jest renderowany programowo.
Warstwa jest stosowana, aby wyłączyć akcelerację sprzętową dla wybranego widoku. W konsekwencji widok taki jest renderowany programowo. Aby określić typ warstw i czas ich zastosowania dla widoków, należy skorzystać z metody setLayerType(). Metoda ta przyjmuje dwa parametry, czyli typ warstwy oraz opcjonalny obiekt Paint. Obiekt Paint może być wykorzystany do zastosowania koloru i innych efektów dla danej warstwy. Możesz zmieniać kilka właściwości warstwy, w tym wymienione poniżej. alpha — wpływa na stopień przyciemnienia (widzialności) warstwy. x, y, translationX, translationY — wpływa na pozycję warstwy. scaleX, scaleY — skaluje warstwę w poziomie i w pionie. rotation, rotationX, rotation — rotuje warstwę. pivotX, pivotY — zmienia aspekt transformacji.
Uwaga Warstwy poprawiają wydajność aplikacji, ponieważ pozwalają uniknąć ponownego rysowania widoków, dopóki nie zostanie wywołana metoda invalidate().
Aby lepiej zrozumieć, w jaki sposób warstwy są stosowane do widoków, utwórz nową aplikację o nazwie ViewLayerApp. Wartość parametru android:minSdkVersion ustaw na 14, ponieważ zastosowanie różnych transformacji na warstwach wymaga co najmniej API poziomu 14. Ponadto wartość parametru android:targetSdkVersion ustaw na 17, aby wskazać, że aplikacja została zaprojektowana do uruchamiania na API poziomu 17.
313
314
Rozdział 9. Sprzętowa akceleracja grafiki 2D
Plik będzie zawierał poniższą instrukcję, deklarującą minimalną i docelową wersję SDK aplikacji.
W tej aplikacji będziesz wyświetlał kontrolkę TextView, która po kliknięciu będzie wywoływać klasę ObjectAnimator wpływającą na właściwość ALPHA danego widoku. W trakcie animacji do kontrolki TextView zostanie zastosowany typ warstwy LAYER_TYPE_HARDWARE. Warstwa ta zostanie usunięta z kontrolki TextView po zakończeniu animacji. Ponieważ chcesz zastosować warstwy do kontrolki TextView, kontrolka ta została zdefiniowana w pliku układu aktywności activity_view_layer_app.xml za pomocą kodu przedstawionego w listingu 9.2. Listing 9.2. Kod wpisany w pliku układu aktywności activity_view_layer_app.xml
Jak możesz zauważyć, kontrolka TextView jest inicjowana w celu wyświetlenia tekstu Demo warstw widoku. Tekst jest prezentowany pogrubioną czcionką o rozmiarze zdefiniowanym w zasobie wymiarów text_size i umieszczony horyzontalnie na środku szerokości ekranu. Do kontrolki TextView przypisano identyfikator textview w celach identyfikacji i uzyskiwania do niej dostępu w kodzie Java. Teraz musisz napisać kod Java, który będzie wykonywał następujące zadania. Uzyskanie dostępu do kontrolki TextView zdefiniowanej w pliku układu
i zmapowanie tej kontrolki na obiekt TextView. Powiązanie nasłuchiwacza setOnClickListener z kontrolką TextView, w celu
nasłuchiwania wystąpienia zdarzeń kliknięcia tej kontrolki. Zastosowanie typu warstwy LAYER_TYPE_HARDWARE do kontrolki TextView
w metodzie wywołania zwrotnego onClick. Zdefiniowanie i wywołanie obiektu klasy ObjectAnimator w celu zastosowania
operacji ALPHA na kontrolce TextView. Operacja ALPHA kontroluje widzialność kontrolki TextView (czyli powoduje rozjaśnianie i przyciemnianie).
Receptura: korzystanie z warstw widoku Ustawienie warstwy kontrolki TextView na typ LAYER_TYPE_NONE, w którym
po zakończeniu animacji warstwa jest usuwana, a zasoby uwalniane. Aby wykonać wymienione zadania, wpisz w pliku aktywności Java ViewLayerAppActivity.java kod przedstawiony w listingu 9.3. Listing 9.3. Kod wpisany w pliku aktywności Java ViewLayerAppActivity.java package com.androidtablet.viewlayerapp; import import import import import import import import
android.os.Bundle; android.app.Activity; android.view.View.OnClickListener; android.widget.TextView; android.view.View; android.animation.Animator; android.animation.AnimatorListenerAdapter; android.animation.ObjectAnimator;
public class ViewLayerAppActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_view_layer_app); final TextView textView = (TextView)this.findViewById( R.id.textview); textView.setOnClickListener(new OnClickListener() { public void onClick(View v) { textView.setLayerType(View.LAYER_TYPE_HARDWARE, null); ObjectAnimator animator =ObjectAnimator.ofFloat( textView, View.ALPHA, 0, 1); animator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { textView.setLayerType(View.LAYER_TYPE_NONE, null); } }); animator.setDuration(2000); animator.start(); } }); } }
Po uruchomieniu tej aplikacji otrzymasz kontrolkę TextView wyświetlającą tekst Demo warstw widoku (patrz rysunek 9.2, górny obrazek). Po kliknięciu kontrolki TextView zastosowana zostanie na niej warstwa sprzętowa, po czym wykonana transformacja ALPHA. Oznacza to, że kontrolka TextView zacznie się rozjaśniać (patrz rysunek 9.2, dolny obrazek), a następnie ponownie zostanie przyciemniona. Transformacja będzie trwała 2000 milisekund.
315
316
Rozdział 9. Sprzętowa akceleracja grafiki 2D
Rysunek 9.2. Po kliknięciu kontrolki TextView zastosowana zostanie na niej operacja ALPHA (górny obrazek). Kontrolka TextView jest rozjaśniana w trakcie animacji ALPHA (dolny obrazek)
Receptura: poprawa wydajności aplikacji opartych na grafice przy użyciu SurfaceView
Receptura: poprawa wydajności aplikacji opartych na grafice przy wykorzystaniu klasy SurfaceView W aplikacji Android wątek GUI jest odpowiedzialny nie tylko za rysowanie wszystkich widoków aplikacji (renderowanie widoków), ale również za obsługę interakcji z użytkownikiem. Wydajność aplikacji spada, jeśli wątek GUI jest zmuszony do szybkiej aktualizacji widoku (lub widoków). W przypadku gier, aplikacji z grafiką 3D lub aplikacji, w których wykonywane są szybkie aktualizacje widoku lub inne operacje o znacznym wykorzystaniu zasobów, wątek GUI jest przytłoczony wieloma zadaniami, co w znaczący sposób redukuje wydajność aplikacji. Aby poradzić sobie z tym problemem, możesz w takich aplikacjach zastosować klasę SurfaceView. Klasa SurfaceView posiada dedykowaną powierzchnię rysowania w ramach hierarchii widoków. Rysuje i aktualizuje widoki, wykorzystując wątki działające w tle, dzięki czemu uwalnia wątek GUI dla innych zadań aplikacji. Klasa SurfaceView kontroluje format powierzchni, jej rozmiar oraz lokalizację na ekranie.
Uwaga Klasa SurfaceView tworzy powierzchnię, na której rysować może każdy inny wątek niż wątek GUI. Koncepcja ta ma na celu poprawienie wydajności aplikacji poprzez wykonywanie zadań rysowania w osobnym wątku.
Aby skorzystać z klasy SurfaceView, musisz utworzyć klasę, która ją rozszerza i implementuje interfejs SurfaceHolder.Callback. Wywołanie zwrotne SurfaceHolder to powiadomienie danego widoku o tym, kiedy bazowa powierzchnia jest tworzona, niszczona lub modyfikowana. To właśnie SurfaceHolder zapewnia powierzchnię do pracy. Interfejs SurfaceHolder jest pobierany za pomocą wywołania metody SurfaceView.getHolder(). W klasie wdrażającej interfejs SurfaceHolder.Callback implementowane są trzy następujące metody. surfaceCreated(SurfaceHolder) — informuje, kiedy powierzchnia jest
tworzona i staje się dostępna do rysowania. surfaceDestroyed(SurfaceHolder) — informuje, kiedy powierzchnia jest
niszczona. surfaceChanged(SurfaceHolder holder, int format, int w, int h) —
informuje o zmianach w strukturze powierzchni, takich jak zmiany szerokości lub wysokości. Teraz nauczysz się stosować klasę SurfaceView w działającej aplikacji. W tym celu utwórz projekt Android o nazwie SurfaceViewApp. W tej aplikacji będziesz wyświetlał rysowanie za pomocą SurfaceView. Aplikacja będzie początkowo wyświetlać bitmapę i okrąg. Ponadto wyświetlane będą linie proste pomiędzy dwoma punktami
317
318
Rozdział 9. Sprzętowa akceleracja grafiki 2D
zdefiniowanymi przez użytkownika. Pierwszy punkt będzie znajdował się w miejscu, w którym użytkownik wciśnie przycisk myszy, a drugi punkt będzie znajdował sie w miejscu, w którym użytkownik zwolni przycisk myszy. W tej aplikacji można narysować dowolną liczbę linii prostych. Zasoby wymiarów będą definiować szerokość i wysokość bitmapy rysowanej w aplikacji. Zasoby wymiarów pomogą zmieniać rozmiary bitmapy na podstawie rozmiaru ekranu urządzenia, na którym uruchamiana jest aplikacja. Aby aplikacja była kompatybilna z telefonami, 7-calowymi tabletami oraz 10-calowymi tabletami, zdefiniujesz zasoby wymiarów o nazwach odpowiednio image_width oraz image_height. Przy założeniu, że w folderze res/values istnieje już plik o nazwie dimens.xml, dodaj w nim wymiary do zmiany wielkości obrazów dla ekranów telefonów. Wpisz w pliku dimens.xml następujący kod. 14sp 100dp 120dp
Trzy zasoby wymiarów text_size, image_width oraz image_height definiują odpowiednio rozmiar czcionki tekstu, szerokość obrazu oraz wysokość obrazu. Te zasoby wymiarów przeznaczone są dla urządzeń o normalnych ekranach (telefonów). Aby zdefiniować zasoby wymiarów dla 7-calowych tabletów, otwórz znajdujący się w folderze res/values-sw600dp plik dimens.xml i wpisz w nim następujący kod. 24sp 140dp 160dp
Na koniec, aby zdefiniować zasoby wymiarów dla urządzeń o ekstradużych ekranach (czyli dla 10-calowych tabletów), otwórz znajdujący się w folderze values-sw720dp plik dimens.xml i wpisz w nim następujący kod. 32sp 180dp 200dp
Porównując zasoby wymiarów dla telefonów, 7-calowych tabletów oraz 10-calowych tabletów, możesz zauważyć, że rozmiary tekstu i obrazów w aplikacji są zmieniane na podstawie rozmiaru ekranu danego urządzenia. Po zdefiniowaniu tych zasobów wymiarów możesz przejść do napisania kodu Java. W pliku aktywności Java SurfaceViewAppActivity.java wpisz kod pokazany w listingu 9.4.
Receptura: poprawa wydajności aplikacji opartych na grafice przy użyciu SurfaceView Listing 9.4. Kod wpisany w pliku aktywności Java SurfaceViewAppActivity.java package com.androidtablet.surfaceviewapp; import import import import import import import import import import import import import
android.app.Activity; android.content.Context; android.graphics.Canvas; android.graphics.Paint; android.os.Bundle; android.view.SurfaceHolder; android.view.SurfaceView; android.graphics.Color; android.graphics.BitmapFactory; android.graphics.Bitmap; android.view.MotionEvent; android.graphics.Point; android.util.DisplayMetrics;
public class SurfaceViewAppActivity extends Activity { MySurfaceView mySurfaceView; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mySurfaceView = new MySurfaceView(this); setContentView(mySurfaceView); } @Override protected void onResume() { super.onResume(); mySurfaceView.onResumeMySurfaceView(); } @Override protected void onPause() { super.onPause(); mySurfaceView.onPauseMySurfaceView(); } class MySurfaceView extends SurfaceView implements SurfaceHolder.Callback { private SurfaceHolder holder; private MySurfaceViewThread mySurfaceViewThread; private boolean hasSurface; private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); private Bitmap bitmap ; Point pt1 = new Point(); Point pt2 = new Point(); private boolean drawing; public MySurfaceView(Context context) { super(context); bitmap = BitmapFactory.decodeResource(context. getResources(), R.drawable.ic_launcher); holder = getHolder(); #1 holder.addCallback(this); #2 hasSurface = false;
319
320
Rozdział 9. Sprzętowa akceleracja grafiki 2D } public void onResumeMySurfaceView(){ if (mySurfaceViewThread == null) { #3 mySurfaceViewThread = new MySurfaceViewThread(); if (hasSurface == true) mySurfaceViewThread.start(); #4 } } public void onPauseMySurfaceView(){ boolean retry = true; while(retry){ try { mySurfaceViewThread.join(); #5 retry = false; } catch (InterruptedException e) { e.printStackTrace(); } } } public void surfaceCreated(SurfaceHolder holder) { #6 hasSurface = true; if (mySurfaceViewThread != null) mySurfaceViewThread.start(); } public void surfaceDestroyed(SurfaceHolder holder) { #7 hasSurface = false; onPauseMySurfaceView(); } public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) { #8 if (mySurfaceViewThread != null) mySurfaceViewThread.onWindowResize(w, h); } @Override public boolean onTouchEvent(MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_DOWN) #9 { pt1.x = (int) event.getX(); pt1.y = (int) event.getY(); drawing = false; } else if (event.getAction() == MotionEvent. ACTION_UP) { #10 pt2.x = (int) event.getX(); pt2.y = (int) event.getY(); drawing = true; } return true; } @Override public void onDraw(Canvas canvas) { #11 paint.setStrokeWidth(5); paint.setColor(Color.RED); int imageWidth=(int) getResources().getDimension( R.dimen.image_width); int imageHeight=(int)getResources().getDimension( R.dimen.image_height);
Receptura: poprawa wydajności aplikacji opartych na grafice przy użyciu SurfaceView canvas.drawBitmap(Bitmap.createScaledBitmap( bitmap, imageWidth, imageHeight, true), 50, 50, null); DisplayMetrics dm = new DisplayMetrics(); getWindowManager().getDefaultDisplay().getMetrics(dm); int width = dm.widthPixels; int height = dm.heightPixels; float centerX = width / 2; float centerY = height / 2; canvas.drawCircle(centerX,centerY,70,paint); if (drawing){ canvas.drawLine(pt1.x, pt1.y, pt2.x, pt2.y, paint); #12 } } class MySurfaceViewThread extends Thread { #13 private boolean done; MySurfaceViewThread() { super(); done = false; } @Override public void run() { #14 SurfaceHolder surfaceHolder = holder; while (!done) { Canvas canvas = surfaceHolder.lockCanvas(); #15 onDraw(canvas); #16 if(canvas !=null) surfaceHolder.unlockCanvasAndPost(canvas); #17 } } public void requestExitAndWait() { done = true; try { join(); } catch (InterruptedException ex) { } } public void onWindowResize(int w, int h) { } } } }
W powyższej aplikacji wywołujesz klasę SurfaceView, tworząc instancję niestandardowej klasy MySurfaceView w metodzie onCreate() danej aktywności. Z metod aktywności onResume() i onPause() wywoływane są metody onResumeMySurfaceView() i onPauseMySurfaceView() klasy MySurfaceView w celu odpowiednio wznawiania i wstrzymywania rysowania na danej powierzchni. W klasie MySurfaceView pobierany jest interfejs SurfaceHolder poprzez wywołanie metody SurfaceView.getHolder (instrukcja #1). Metoda addCallback(this) (instrukcja #2) jest wywoływana w celu powiadomienia SurfaceHolder, że należy wykorzystać daną aktywność jako procedurę
321
322
Rozdział 9. Sprzętowa akceleracja grafiki 2D
obsługi wywołania zwrotnego. Instrukcja #3 sprawdza, czy istnieje niestandardowy wątek. Jeśli taki wątek nie istnieje, jest instancjonowany. Instrukcja #4 sprawdza, czy istnieje określona powierzchnia. Jeśli powierzchnia istnieje, dany wątek jest uruchamiany. W metodzie onPauseMySurfaceView() instrukcja #5 wstrzymuje wykonanie niestandardowego wątku. Instrukcja #6 definiuje metodę surfaceCreated, która powiadamia o utworzeniu powierzchni. Ostatecznie metoda ta uruchamia wątek inicjujący rysowanie lub aktualizację powierzchni. Instrukcja #7 definiuje metodę surfaceDestroyed, która informuje o zniszczeniu powierzchni. Metoda ta ustawia wartość logiczną zmiennej hasSurface na false (fałsz), aby wskazać, że dana powierzchnia już nie istnieje. Wywołuje również metodę onPauseSurfaceView() w celu wstrzymania wykonywania danego wątku. Instrukcja #8 definiuje metodę surfaceChanged(SurfaceHolder holder, int format, int w, int h), która informuje o zmianach w strukturze powierzchni. Jeśli np. nastąpi zmiana w szerokości lub wysokości powierzchni albo zmieni się orientacja urządzenia, metoda ta wywołuje metodę onWindowResize niestandardowego wątku. Instrukcja #9 sprawdza, czy użytkownik kliknął przycisk myszy. Współrzędne X i Y, w których użytkownik wciska przycisk myszy, są zapisywane w Point pt1. Instrukcja #10 sprawdza, czy użytkownik zwolnił przycisk myszy. Współrzędne X i Y, w których przycisk myszy został zwolniony, są zapisywane w Point pt2. Ponadto wartość zmiennej logicznej drawing jest ustawiana na true (prawda), aby linia prosta pomiędzy punktami pt1 i pt2 mogła zostać narysowana za pomocą metody onDraw() (instrukcja #11). W metodzie onDraw kolor i szerokość obramowania obiektu Paint są ustawiane odpowiednio na red (czerwony) i 5 pikseli. Rysowane są również koło i bitmapa. Szerokość i wysokość bitmapy są pobierane odpowiednio z zasobów wymiarów image_width oraz image_height. Ponadto przeliczane są współrzędne środka ekranu i w tym miejscu rysowane jest koło. Jak wspomniano wcześniej, klasa SurfaceView dostarcza powierzchnię, na której działający w tle wątek może aktualizować i rysować widoki. Instrukcja #13 definiuje klasę, która rozszerza klasę Thread, pobiera referencję do bieżącego interfejsu SurfaceHolder i wykonuje niezależnie operacje rysowania i aktualizowania. Instrukcja #14 definiuje metodę run() danego wątku. W działającym wątku SurfaceHolder wywołuje metodę lockCanvas() (instrukcja #15), która zwraca obiekt Canvas do narysowania. Zwrócony obiekt Canvas jest przekazywany do metody onDraw() w celu narysowania obiektu na powierzchni lub jego zaktualizowania. Następnie po metodzie onDraw() interfejs SurfaceHolder wywołuje metodę unlockCanvasAndPost() (instrukcja #17), która uwalnia obiekt Canvas i sprawia, że rysunek staje się widoczny. Po uruchomieniu tej aplikacji zobaczysz bitmapę i narysowany na ekranie okrąg. Kiedy użytkownik kliknie dowolne miejsce ekranu, przeciągnie wskaźnik myszy, a następnie go uwolni, pomiędzy wskazanymi punktami zostanie narysowana linia prosta. Na rysunku 9.3 pokazano ekran po narysowaniu kilku linii prostych.
Receptura: zastosowanie transformacji z wykorzystaniem klasy TextureView
Rysunek 9.3. Wykorzystanie klasy SurfaceView do rysowania przy użyciu osobnego wątku
Receptura: zastosowanie transformacji z wykorzystaniem klasy TextureView Klasa TextureView spełnia tę samą rolę, co klasa SurfaceView, jest jednak preferowana z uwagi na określone wady klasy SurfaceView. Klasa SurfaceView poprawia wydajność aplikacji poprzez rysowanie i aktualizowanie obiektów w osobnym wątku, ale tworzy osobne okno i nie zachowuje się jak normalny widok. W rezultacie nie można wykonać na nim żadnej transformacji. Innymi słowy, widok SurfaceView nie może zostać przesunięty, przeskalowany ani obrócony. Ponadto na widoku SurfaceView nie można zastosować żadnej operacji aplha (operacji rozjaśniania i przyciemniania). Z kolei TextureView zachowuje się jak normalny widok i obsługuje standardowe operacje widoku (czyli widok TextureView może być przesuwany, przekształcany, animowany itd.). TextureView wymaga akceleracji sprzętowej oraz klasy SurfaceTexture. Widok TextureView może być użyty tylko w oknie z akceleracją sprzętową albo nic nie zostanie narysowane. Widok ten może wyświetlać i przekształcać strumień zawartości, taki jak podgląd obrazu z kamery wideo. Aby zastosować TextureView, musisz pobrać jego klasę SurfaceTexture, która jest następnie wykorzystywana do wyświetlenia zawartości. Klasę SurfaceTexture widoku TextureView można uzyskać, wywołując metodę getSurfaceTexture() lub wykorzystując interfejs TextureView.SurfaceTextureListener. Aby lepiej zrozumieć koncepcję klasy TextureView, utwórz nowy projekt Android o nazwie TextureViewApp. Ponieważ chcesz, aby akceleracja sprzętowa była włączona domyślnie, podczas tworzenia tej aplikacji ustaw wartości atrybutów android:minSdkVersion
323
324
Rozdział 9. Sprzętowa akceleracja grafiki 2D
i android:targetSdkVersion odpowiednio na 14 i 17. W systemie Android 4.0 (API poziomu 14.) i w nowszych wersjach akceleracja sprzętowa jest włączona domyślnie dla wszystkich aplikacji. W tej aplikacji będziesz używał widoku TextureView do wyświetlenia podglądu z domyślnej kamery. Widok TextureView zostanie obrócony o 180° i przeskalowany w poziomie do półtorakrotności swojego oryginalnego obrazu. W pliku aktywności Java TextureViewAppActivity.java wpisz kod przedstawiony w listingu 9.5. Listing 9.5. Kod wpisany w pliku aktywności TextureViewAppActivity.java package com.androidtablet.textureviewapp; import import import import import import import import
android.os.Bundle; android.app.Activity; android.hardware.Camera; android.view.TextureView; android.graphics.SurfaceTexture; android.widget.FrameLayout; java.io.IOException; android.view.Gravity;
public class TextureViewAppActivity extends Activity implements TextureView. SurfaceTextureListener { private Camera camera; private TextureView textureView; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); textureView = new TextureView(this); textureView.setSurfaceTextureListener(this); setContentView(textureView); } @Override public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { camera = Camera.open(); Camera.Size previewSize = camera.getParameters(). getPreviewSize(); textureView.setLayoutParams(new FrameLayout. LayoutParams(previewSize.width, previewSize.height, Gravity.CENTER)); try { camera.setPreviewTexture(surface); } catch (IOException t) { } camera.startPreview(); textureView.setScaleX(1.5f); textureView.setRotation(180.0f); } @Override public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { }
#1 #2 #3 #4 #5
Receptura: zastosowanie transformacji z wykorzystaniem klasy TextureView @Override public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { camera.stopPreview(); camera.release(); return true; } @Override public void onSurfaceTextureUpdated(SurfaceTexture surface){} }
Ponieważ ta aplikacja powinna renderować podgląd z kamery do widoku TextureView, tworzone są obiekty klas Camera i TextureView. W celu nasłuchiwania zdarzeń nasłuchiwacz setSurface-TextureListener zostaje powiązany z obiektem TextureView. Aby uzyskać teksturę SurfaceTexture, w danej aktywności należy zaimplementować interfejs TextureView.SurfaceTextureListener i dlatego definiuje się cztery następujące metody. onSurfaceTextureAvailable — metoda jest wywoływana, kiedy klasa
SurfaceTexture widoku TextureView jest gotowa do rysowania lub innych operacji. W tej metodzie definiowany jest obiekt Camera (instrukcja #1) w celu
uzyskania dostępu do znajdującej się z tyłu urządzenia kamery. Następnie pobierane są ustawienia wymiarów dla obrazów z poglądu (instrukcja #2) oraz definiowany układ widoku TextureView według szerokości i wysokości pobranych wymiarów (instrukcja #3). Potem tekstura SurfaceTexture jest ustawiana w celu zastosowania jej w podglądzie na żywo (instrukcja #4) do robienia zdjęć. Obrazy z podglądu zostaną wysłane do tekstury SurfaceTexture. Następnie rozpoczyna się procedura przechwytywania obrazów z kamery i rysowania klatek podglądu na ekranie (instrukcja #5). Widok TextureView jest skalowany w poziomie do półtorakrotności oryginalnego rozmiaru i obracany o 180°. onSurfaceTextureSizeChanged — metoda jest wywoływana, kiedy zmienia się
tekstura SurfaceTexture widoku TextureView (czyli kiedy zmienia się szerokość lub wysokość SurfaceTexture). onSurfaceTextureDestroyed — metoda jest wywoływana, kiedy tekstura
SurfaceTexture ma zostać zniszczona. W tej metodzie procedura przechwytywania
obrazów zostaje zatrzymana i uwalniane są alokowane zasoby. Metoda zwraca wartość true, aby wskazać, że po jej wykonaniu żadne renderowanie na SurfaceTexture nie powinno mieć miejsca. onSurfaceTextureUpdated — metoda wywoływana, kiedy określona tekstura
SurfaceTexture jest aktualizowana.
W tej aplikacji potrzebujesz zezwolenia na dostęp do kamery i zadeklarowania funkcji kamery wykorzystywanych przez aplikację. Musisz więc w pliku AndroidManifest.xml załączyć element manifestu , tak jak pokazano w poniższych instrukcjach.
325
326
Rozdział 9. Sprzętowa akceleracja grafiki 2D
Po uruchomieniu aplikacji podgląd z kamery będzie renderowany do widoku TextureView. Widok będzie obrócony o 180° i przeskalowany w poziomie do
półtorakrotności oryginalnego rozmiaru, tak jak pokazano na rysunku 9.4.
Rysunek 9.4. Podgląd z kamery obrócony o 180° i przeskalowany horyzontalnie do półtorakrotności jego oryginalnego rozmiaru
Podsumowanie W tym rozdziale nauczyłeś się poprawiać wydajność opartych na grafikach aplikacji Android za pomocą akceleracji sprzętowej na poziomie aplikacji i aktywności. Dowiedziałeś się również, jak poprawić wykonywanie animacji zastosowanych na widokach poprzez wykorzystanie w nich wymaganych warstw sprzętowych i programowych. Ponadto zobaczyłeś, jak zwiększyć wydajność aplikacji, stosując osobny wątek do wykonywania grafiki i aktualizacji zadań za pomocą klasy SurfaceView. Na koniec wykorzystałeś klasę TextureView do wyświetlenia strumienia wideo i zastosowania transformacji na zawartości wyświetlanej w widoku TextureView. W kolejnym rozdziale nauczysz się korzystać z interfejsu programowania aplikacji OpenGL ES do tworzenia grafik 2D i 3D. Poznasz interfejsy API wymagane do budowania i renderowania grafik w aplikacjach Android. Ponadto nauczysz się stosować jednoodcieniowe i wieloodcieniowe kolory do grafik. Na koniec dowiesz się, jak stosować transformacje, takie jak rotacja, skalowanie i przesunięcie grafiki.
10 Tworzenie i renderowanie grafiki T
worzenie i renderowanie grafiki odgrywa ważną rolę w aplikacjach, które koncentrują się wokół gier, rozrywki, edukacji, medycyny oraz innych znaczących dziedzin. Do przygotowywania aplikacji opartych na grafice Android oferuje implementację OpenGL ES. OpenGL ES to interfejs programowania aplikacji (ang. application programming interface — API) graficznych dla systemów wbudowanych. W tym rozdziale skoncentrowano się na wykorzystaniu OpenGL ES przy tworzeniu grafiki 2D i 3D. Zapoznasz się w nim z interfejsami API wymaganymi do tworzenia i renderowania grafiki w aplikacjach Android. Następnie będziesz tworzył i renderował grafiki według procedur krok po kroku. Nauczysz się także stosować do rysowania grafik kolory jednoodcieniowe i wieloodcieniowe. Ponadto dowiesz się, jak w wyświetlanych grafikach wykorzystać transformacje (rotację, skalowanie i przesunięcie).
Receptura: interfejsy API wymagane dla grafiki Do optymalnej obsługi grafiki urządzenia Android wykorzystują dedykowany procesor graficzny (ang. graphics processing unit — GPU). GPU odciąża główny procesor (CPU), który może wtedy wykonywać inne zadania obliczeniowe. Do renderowania grafiki 2D i 3D przez GPU wykorzystywany jest wszechstronny międzyplatformowy interfejs graficzny API znany jako OpenGL. Interfejs OpenGL (ang. Open Graphics Library) dostarczany przez system Android nazywa się OpenGL ES API, gdzie ES oznacza systemy wbudowane (ang. embedded systems). OpenGL ES jest specyfikacją OpenGL przeznaczoną dla urządzeń wbudowanych. Aby wyświetlić grafikę, potrzebujesz powierzchni oraz mechanizmu renderującego (renderera). Dlatego do tworzenia i modyfikowania grafiki za pomocą OpenGL ES API wykorzystujesz dwie klasy.
328
Rozdział 10. Tworzenie i renderowanie grafiki GLSurfaceView — klasa jest kontenerem widoku do rysowania grafiki za
pomocą wywołań OpenGL API. Mówiąc ściślej, klasa GLSurfaceView zapewnia powierzchnię rysowania dla renderowania grafiki 2D i 3D. Zapewnia również lokalizację dla wyświetlenia tej grafiki. Aby skorzystać z klasy GLSurfaceView, tworzysz jej instancję i dodajesz do niej mechanizm renderujący. GLSurfaceView.Renderer — klasa kontroluje grafikę rysowaną w instancji
GLSurfaceView OpenGL. Jest to interfejs definiujący metody, które określają, co i jak ma być rysowane na GLSurfaceView. Aby utrzymać wydajność
renderowania, wykorzystujesz dedykowany wątek inny niż wątek GUI. Klasa implementująca interfejs GLSurfaceView.Renderer musi implementować następujące metody. onSurfaceCreated(GL10 gl, EGLConfig config) — metoda będzie
wywoływana, kiedy tworzona lub odtwarzana jest instancja GLSurfaceView. Za każdym razem, kiedy urządzenie budzi się z trybu uśpienia, instancja GLSurfaceView jest odtwarzana. Ta metoda może konfigurować parametry środowiska OpenGL ES lub inicjować obiekty graficzne OpenGL. onDrawFrame(GL10 gl) — metoda wykonuje wszystkie zadania związane
z rysowaniem. Rysuje bieżącą klatkę i jest wywoływana przy każdym ponownym rysowaniu powierzchni GLSurfaceView. W rzeczywistości mechanizm renderujący rysuje klatki nieustannie. Oznacza to, że ta metoda jest wywoływana wielokrotnie, co umożliwia wykonywanie za jej pomocą dowolnych animacji. Choć wyjściowa grafika może wydawać się nieruchoma, wewnętrznie klatki są rysowane nieustannie. onSurfaceChanged(GL10 gl, int width, int height) — metoda jest
wywoływana, kiedy zmienia się geometria powierzchni GLSurfaceView w aspekcie jej rozmiaru lub kiedy zmienia się orientacja urządzenia. Ta metoda przenosi kod, który wpływa na Twoje okienko widoku (ang. viewport).
Uwaga System Android 1.0 obsługuje specyfikacje OpenGL ES 1.0 i 1.1 API. System Android 2.2 (API poziomu 8.) lub nowszy obsługuje specyfikację OpenGL ES 2.0 API. Dlatego korzystając z OpenGL ES 2.0, upewnij się, że ten lub wyższy interfejs API jest docelowy dla Twojego projektu Android.
Receptura: tworzenie i renderowanie prostokąta przy użyciu OpenGL Aby zrozumieć, w jaki sposób za pomocą interfejsu OpenGL API jest tworzona i renderowana grafika, utwórz projekt Android o nazwie OpenGLApp. W tej aplikacji będziesz renderował niebieski prostokąt na czarnym tle. W pliku aktywności Java OpenGLAppActivity.java wpisz kod przedstawiony w listingu 10.1.
Receptura: tworzenie i renderowanie prostokąta przy użyciu OpenGL Listing 10.1. Kod wpisany w pliku aktywności Java OpenGLAppActivity.java package com.androidtablet.openglapp; import import import import import import import import import
android.os.Bundle; android.app.Activity; android.opengl.GLSurfaceView; android.opengl.GLES20; javax.microedition.khronos.opengles.GL10; javax.microedition.khronos.egl.EGLConfig; java.nio.FloatBuffer; java.nio.ByteBuffer; java.nio.ByteOrder;
public class OpenGLAppActivity extends Activity { private GLSurfaceView myGLView; public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); myGLView = new GLSurfaceView(this); MyGLSurfRenderer renderer=new MyGLSurfRenderer(); myGLView.setRenderer(renderer); setContentView(myGLView); } @Override protected void onPause() { super.onPause(); myGLView.onPause(); } @Override protected void onResume() { super.onResume(); myGLView.onResume(); } public class MyGLSurfRenderer implements GLSurfaceView.Renderer { private FloatBuffer boxBuffer; public void onSurfaceCreated(GL10 gl, EGLConfig config) { gl.glClearColor(0.0f, 0.0f, 0.0f, 1.0f); defineGraphic(); gl.glEnableClientState( GL10.GL_VERTEX_ARRAY); gl.glVertexPointer(3, GL10.GL_FLOAT, 0, boxBuffer); } public void onDrawFrame(GL10 gl) { gl.glClear(GLES20.GL_COLOR_BUFFER_BIT); gl.glColor4f(0.0f, 0.0f, 1.0f, 1.0f); gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4); }
#1 #2 #3 #4
#5
#6
#7
#8 #9 #10 #11 #12 #13 #14 #15 #16
329
330
Rozdział 10. Tworzenie i renderowanie grafiki public void onSurfaceChanged(GL10 gl, int width, int height) { #17 gl.glViewport(0, 0, width, height); #18 } private void defineGraphic(){ float vertices[] = { #19 -0.5f, -0.5f, 0.0f, 0.5f, -0.5f, 0.0f, -0.5f, 0.5f, 0.0f, 0.5f, 0.5f, 0.0f }; ByteBuffer byteBuffer = ByteBuffer. allocateDirect(vertices.length * 4); byteBuffer.order(ByteOrder.nativeOrder()); boxBuffer = byteBuffer.asFloatBuffer(); boxBuffer.put(vertices); boxBuffer.position(0); }
#20 #21 #22 #23
} }
Instrukcja #1 definiuje instancję myGLView klasy GLSurfaceView oraz inicjuje tę instancję w określonym kontekście (czyli w Twojej aktywności). Instrukcja #2 definiuje obiekt renderer niestandardowej klasy MyGLSurfRenderer. Instrukcja #3 informuje klasę GLSurfaceView, że renderowanie wykona klasa MyGLSurfRenderer. Aplikacja wstrzymuje renderowanie, kiedy wstrzymana zostaje aktywność (instrukcja #5), i wznawia, kiedy aktywność jest kontynuowana (instrukcja #6). Niestandardowa klasa MyGLSurfRenderer implementuje interfejs GLSurfaceView.Renderer (instrukcja #7) i dlatego implementuje trzy poniższe metody. Metoda onSurfaceCreated() (instrukcja #8) jest wywoływana przy rozpoczęciu
renderowania i za każdym razem, kiedy kontekst rysowania interfejsu OpenGL ES jest odtwarzany (czyli kiedy aktywność jest wstrzymywana i wznawiana). Metoda glClearColor() jest wywoływana w celu określenia wartości czerwony,
zielony, niebieski i kanał_alfa, które metoda glClear() wykorzystuje
do wyczyszczenia buforów koloru. Podawane wartości muszą pochodzić z przedziału od 0 do 1. Instrukcja #9 ustawia kolor czarny dla buforów koloru. Wywoływana jest metoda defineGraphic() (instrukcja #10) w celu określenia kształtu grafiki, którą chcesz wyświetlić. Do metody defineGraphic() dostarczane są współrzędne wierzchołków prostokąta, który chcesz narysować, oraz informacje jego bufora. Instrukcja #11włącza funkcje klienckie. Włącza tablicę wierzchołków, która może być użyta podczas renderowania. W instrukcji #12 z wykorzystaniem metody glVertexPointer() określany jest bufor przechowujący współrzędne wierzchołków. Za pomocą tej metody określasz, że każdy wierzchołek posiada trzy współrzędne. Każdy wierzchołek jest typem danych GL_FLOAT. Wszystkie wierzchołki są ułożone bez offsetu bajtowego pomiędzy kolejnymi wierzchołkami, a pierwsza współrzędna pierwszego
Receptura: tworzenie i renderowanie prostokąta przy użyciu OpenGL
wierzchołka jest dostępna w buforze o nazwie boxBuffer. Zasadniczo bufor boxBuffer będzie użyty dla tablicy wierzchołków vertex, która została włączona w instrukcji #11. Metoda onDrawFrame (instrukcja #13) jest wywoływana w celu ponownego
narysowania klatki. Instrukcja #14 czyści bufory do wartości określonych przez metodę glClearColor(). Oznacza to, że metoda glClear() czyści bufor, aby wyświetlić kolor (czarny) zdefiniowany za pomocą metody glClearColor(). Przy użyciu metody glColor4f() w instrukcji #15 bieżący kolor (kolor pierwszego planu) jest ustawiany na niebieski. Z wykorzystaniem metody glDrawArrays() w instrukcji #16 grafika jest renderowana na podstawie włączonych tablic. Grafika jest rysowana z użyciem sekwencji wyrażeń elementarnych. Pierwszy parametr w metodzie glDrawArrays() wskazuje, co ma być narysowane. Za pomocą tej metody wskazujesz, że wierzchołki zdefiniowane w indeksach od 0 do 4 tablicy (lub bufora) mają być wykorzystane w renderowaniu wyrażeń pierwotnych. Dlatego wierzchołki zdefiniowane w tablicy wierzchołków vertices, poczynając od pierwszego elementu, zostaną wykorzystane do renderowania grafiki na ekranie. Przypomnijmy, że w instrukcji #11 zostały włączone funkcje klienckie tablicy wierzchołków. Metoda onSurfaceChanged() w instrukcji #17 jest wywoływana, kiedy powierzchnia zmienia swój rozmiar. W tej metodzie za pomocą instrukcji #18 definiujesz rozmiar okienka ekranu OpenGL. Pierwsze dwa parametry to współrzędne X i Y lewego dolnego rogu okienka ekranu. Ostatnie dwa parametry to szerokość i wysokość okienka ekranu. Szerokość i wysokość okienka ekranu są równe szerokości i wysokości okna, więc okienko ekranu pokrywa cały ekran od (0, 0) do (width-1, height-1). W metodzie defineGraphic() definiowana jest tablica zmiennoprzecinkowa vertices (instrukcja #19) zawierająca współrzędne wierzchołków prostokąta, który chcesz narysować. Bufor bajtów o nazwie byteBuffer jest definiowany w instrukcji #20. Bufor byteBuffer jest wykorzystywany jako wskaźnik do tablicy wierzchołków. Rozmiar bufora jest równy czterokrotnej liczbie wierzchołków zdefiniowanych w tablicy wierzchołków, ponieważ współrzędne wierzchołków są wartościami zmiennoprzecinkowymi, a te wymagają czterech bajtów. W instrukcji #21 bufor zmiennoprzecinkowy o nazwie boxBuffer jest definiowany na podstawie zawartości bufora bajtowego byteBuffer. Wszystkie wartości zmiennoprzecinkowe (współrzędne wierzchołków) z tablicy vertices są zapisywane do bufora zmiennoprzecinkowego boxBuffer w instrukcji #22. Bufor zmiennoprzecinkowy boxBuffer jest resetowany do pozycji 0 (czyli zostaje przewinięty do początkowej lokalizacji w instrukcji #23) w celu odczytu współrzędnych pierwszego wierzchołka. To z bufora boxBuffer metoda glDrawArrays() (instrukcja #16) pobiera współrzędne wierzchołków rysowanego prostokąta. Po uruchomieniu tej aplikacji wyświetlony zostanie niebieski prostokąt na czarnym tle, tak jak pokazano na rysunku 10.1.
331
332
Rozdział 10. Tworzenie i renderowanie grafiki
Rysunek 10.1. Tworzenie i renderowanie prostokąta za pomocą OpenGL
W tabeli 10.1 zawarto krótki opis metod użytych w kodzie przedstawionym w listingu 10.1. Tabela 10.1. Krótki opis metod wykorzystanych w listingu 10.1
Metoda
Opis
void glClearColor( GLfloat czerwony, GLfloat zielony, GLfloat niebieski, GLfloat kanał_alfa)
Określa wartości czyszczenia dla bufora koloru. Ustalone za pomocą parametrów wartości czerwony, zielony, niebieski i kanał_alfa są wykorzystywane do czyszczenia buforów koloru. Początkowa wartość dla wszystkich parametrów to 0.
void glEnableClientState (GLenum tablica)
Włącza funkcje klienckie. Parametr tablica reprezentuje funkcję, która ma zostać włączona. Parametr ten może przyjmować jedną z poniższych wartości. GL_COLOR_ARRAY — tablica kolorów jest włączana w celu zapisywania i wykorzystywana podczas renderowania. GL_NORMAL_ARRAY — tablica standardowa jest włączana w celu zapisywania i wykorzystywana podczas renderowania. GL_TEXTURE_COORD_ARRAY — tablica współrzędnych tekstury jest włączana w celu zapisywania i wykorzystywana podczas renderowania. GL_VERTEX_ARRAY — tablica wierzchołków jest włączana w celu zapisywania i wykorzystywana podczas renderowania.
Receptura: tworzenie i renderowanie prostokąta przy użyciu OpenGL Tabela 10.1. Krótki opis metod wykorzystanych w listingu 10.1 (ciąg dalszy)
Metoda
Opis
void glClear(GLbitfield maska)
Czyści bufory do wartości określonych przez metodę glClearColor. Parametr maska reprezentuje operator poziomu bitowego OR (lub) następujących wartości, które wskazują bufor do wyczyszczenia. GL_COLOR_BUFFER_BIT — reprezentuje aktualnie włączone bufory. GL_DEPTH_BUFFER_BIT — reprezentuje bufor głębi. GL_STENCIL_BUFFER_BIT — reprezentuje bufor szablonowy.
void glColor4f(GLfloat czerwony, GLfloat zielony, GLfloat niebieski, GLfloat kanał_alfa)
Ustawia bieżący kolor rysowania. Wartości z zakresu od 0 do 1 dostarczane są dla parametrów czerwony, zielony, niebieski i kanał_alfa w celu ustawienia bieżącego koloru. Początkowa wartość to 1.
void glVertexPointer(GLint wymiar, GLenum typ, GLsizei krok, const GLvoid * wskaźnik)
Definiuje tablicę współrzędnych wierzchołków. wymiar — określa liczbę współrzędnych pojedynczego wierzchołka. Możliwe wartości to 2, 3 lub 4. Początkowa wartość to 4. typ — określa typ danych każdego wierzchołka w tablicy. Symboliczne zmienne reprezentujące określone typy danych to GL_BYTE, GL_SHORT, GL_FIXED oraz GL_FLOAT. Początkową wartością jest GL_FLOAT. krok — określa offset bajtowy pomiędzy kolejnymi wierzchołkami. Jeśli parametr krok ma wartość 0, oznacza to, że wierzchołki są przechowywane w tablicy. Wartość początkowa wynosi 0. wskaźnik — określa wskaźnik do pierwszej współrzędnej pierwszego wierzchołka w tablicy. Wartość początkowa wynosi 0.
void glDrawArrays( GLenum tryb, GLint pierwszy_indeks, GLsizei liczba_elementów)
Renderuje określone wyrażenia pierwotne z dostarczonej tablicy. tryb — reprezentuje wyrażenie pierwotne do renderowania. Wyrażenie pierwotne może być reprezentowane przez dowolną z poniższych zmiennych symbolicznych: GL_POINTS, GL_LINE_STRIP, GL_LINE_LOOP, GL_LINES, GL_LINE_STRIP_ADJACENCY, GL_LINES_ADJACENCY, GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN, GL_TRIANGLES, GL_TRIANGLE_STRIP_ADJACENCY, GL_TRIANGLES_ADJACENCY oraz GL_PATCHES.
333
334
Rozdział 10. Tworzenie i renderowanie grafiki Tabela 10.1. Krótki opis metod wykorzystanych w listingu 10.1 (ciąg dalszy)
Metoda
Opis pierwszy_indeks — określa indeks początkowy we włączonej tablicy. liczba_elementów — określa liczbę elementów tablicy, które mają być wykorzystane do renderowania grafiki.
public abstract void onSurfaceCreated(GL10 gl, EGLConfig config)
Wywoływana, kiedy powierzchnia jest tworzona lub odtwarzana. Powierzchnia jest odtwarzana, gdy utracony zostaje kontekst EGL (czyli, kiedy urządzenie Android zostaje wybudzone ze stanu uśpienia). Parametr gl to interfejs GL, a parametr config reprezentuje klasę EGLConfig tworzonej powierzchni.
public abstract void onDrawFrame(GL10 gl)
Wywoływana w celu narysowania bieżącej klatki. Parametr gl to interfejs GL.
public abstract void onSurfaceChanged(GL10 gl, int width, int height)
Wywoływana, kiedy zmienia się rozmiar powierzchni. Parametr gl to interfejs GL.
Receptura: zastosowanie kolorów wieloodcieniowych W poprzedniej recepturze wykorzystałeś jednoodcieniowy niebieski kolor do narysowania prostokąta. Kolor jednoodcieniowy (ang. flat color) oznacza tutaj kolor jednolity. W tej recepturze nauczysz się stosować kolory wieloodcieniowe (ang. smooth coloring) do prostokąta narysowanego w poprzedniej recepturze. W tym celu musisz dla każdego wierzchołka grafiki zastosować określony kolor. Kolory pomiędzy wierzchołkami będą interpolowane przez interfejs OpenGL ES, co da efekt płynnego przechodzenia barw. Otwórz projekt Android OpenGLApp, który utworzyłeś wcześniej. Aby zastosować osobny kolor dla każdego wierzchołka prostokąta narysowanego w tej aplikacji, zmodyfikuj plik aktywności Java OpenGLAppActivity.java na podstawie listingu 10.2. Dodane zostały jedynie fragmenty kodu zaznaczone pogrubioną czcionką. Reszta pozostaje taka sama jak w listingu 10.1. Listing 10.2. Kod wpisany w pliku aktywności Java OpenGLAppActivity.java package com.androidtablet.openglapp; import android.os.Bundle; import android.app.Activity; import android.opengl.GLSurfaceView;
Receptura: zastosowanie kolorów wieloodcieniowych import import import import import import
android.opengl.GLES20; javax.microedition.khronos.opengles.GL10; javax.microedition.khronos.egl.EGLConfig; java.nio.FloatBuffer; java.nio.ByteBuffer; java.nio.ByteOrder;
public class OpenGLAppActivity extends Activity { private GLSurfaceView myGLView; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); myGLView = new GLSurfaceView(this); MyGLSurfRenderer renderer=new MyGLSurfRenderer(); myGLView.setRenderer(renderer); setContentView(myGLView); } @Override protected void onPause() { super.onPause(); myGLView.onPause(); } @Override protected void onResume() { super.onResume(); myGLView.onResume(); } public class MyGLSurfRenderer implements GLSurfaceView.Renderer { private FloatBuffer boxBuffer; private FloatBuffer colorBuffer; public void onSurfaceCreated(GL10 gl, EGLConfig config) { gl.glClearColor(0.0f, 0.0f, 0.0f, 1.0f); defineGraphic(); gl.glEnableClientState(GL10.GL_VERTEX_ARRAY); gl.glVertexPointer(3, GL10.GL_FLOAT, 0, boxBuffer); gl.glEnableClientState(GL10.GL_COLOR_ARRAY); gl.glColorPointer(4, GL10.GL_FLOAT, 0, colorBuffer); } public void onDrawFrame(GL10 gl) { gl.glClear(GLES20.GL_COLOR_BUFFER_BIT); gl.glColor4f(0.0f, 0.0f, 1.0f, 1.0f); gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4); } public void onSurfaceChanged(GL10 gl, int width, int height) { gl.glViewport(0, 0, width, height); } private void defineGraphic(){ float vertices[] = { -0.5f, -0.5f, 0.0f,
335
336
Rozdział 10. Tworzenie i renderowanie grafiki 0.5f, -0.5f, 0.0f, -0.5f, 0.5f, 0.0f, 0.5f, 0.5f, 0.0f }; float[] colors = { 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, };
1.0f, 1.0f, 1.0f, 1.0f
ByteBuffer byteBuffer = ByteBuffer.allocateDirect( vertices.length * 4); byteBuffer.order(ByteOrder.nativeOrder()); boxBuffer = byteBuffer.asFloatBuffer(); boxBuffer.put(vertices); boxBuffer.position(0); ByteBuffer colorBytes = ByteBuffer.allocateDirect( colors.length * 4); colorBytes.order(ByteOrder.nativeOrder()); colorBuffer = colorBytes.asFloatBuffer(); colorBuffer.put(colors); colorBuffer.position(0); } } }
W klasie renderera MyGLSurfRenderer definiowany jest kolejny bufor zmiennoprzecinkowy FloatBuffer o nazwie colorBuffer. Po stronie klienckiej uruchamiana jest tablica GL_COLOR_ARRAY, ponieważ może ona być wykorzystana do aplikowania kolorów. Za pomocą metody glColorPointer() definiowana jest tablica colorBuffer przechowująca wartości kolorów. Wykorzystując metodę glColorPointer(), określasz, że istnieją cztery wartości kolorów na wierzchołek (czerwony, zielony, niebieski i kanał alfa), każda wartość koloru jest typem danych GL_FLOAT, wszystkie wartości kolorów są ułożone z offsetem 0 pomiędzy tymi wartościami i wszystkie są dostępne w buforze colorBuffer. Definiowana jest tablica zmiennoprzecinkowa colors, zawierająca wartości kolorów każdego z wierzchołków prostokąta, który chcesz narysować. Definiowany jest bufor bajtowy o nazwie colorBytes. Rozmiar tego bufora jest ustawiany na czterokrotność liczby wartości kolorów zdefiniowanych w tablicy colors, ponieważ wartości kolorów są definiowane jako float, a każda wartość zmiennoprzecinkowa wymaga czterech bajtów. Na podstawie zawartości bufora bajtowego colorBytes definiowany jest bufor zmiennoprzecinkowy colorBuffer. Wszystkie wartości zmiennoprzecinkowe (czyli wartości kolorów) z tablicy colors są zapisywane do bufora zmiennoprzecinkowego colorBuffer. Bufor zmiennoprzecinkowy colorBuffer jest resetowany do pozycji 0, aby wartości kolorów były odczytywane od początku bufora. Po uruchomieniu tej aplikacji wyświetlony zostanie prostokąt z efektem płynnego przechodzenia kolorów na pierwszym planie i z czarnym tłem, tak jak przedstawiono na rysunku 10.2.
Receptura: rotacja grafiki
Rysunek 10.2. Efekt płynnego przechodzenia kolorów zastosowany do prostokąta narysowanego za pomocą OpenGL
Receptura: rotacja grafiki W tej recepturze nauczysz się obracać grafikę o określony kąt. Ponadto dowiesz się, jak ustawić grafikę, aby obracała się w sposób ciągły. Grafiką, którą wykorzystasz w tej aplikacji, jest prostokąt. Ponieważ wierzchołki, kolor i inne informacje na temat tego prostokąta są zawarte w osobnej klasie i nie są pomieszane z logiką rotacji, recepturę tę możesz wykorzystać do obracania dowolnej grafiki. Aby w tej recepturze obrócić np. sześcian zamiast prostokąta, musisz po prostu zastąpić klasę zawierającą wymiary prostokąta klasą zawierającą wymiary sześcianu. Utwórz więc projekt Android o nazwie OpenGLDemo. Aby narysować prostokąt, zastosować do niego kolory oraz obrócić go, wpisz w pliku aktywności Java OpenGLDemoActivity.java kod przedstawiony w listingu 10.3. Listing 10.3. Kod wpisany w pliku aktywności Java OpenGLDemoActivity.java package com.androidtablet.opengldemo; import import import import import import import
android.os.Bundle; android.app.Activity; android.opengl.GLSurfaceView; android.opengl.GLES20; javax.microedition.khronos.opengles.GL10; javax.microedition.khronos.egl.EGLConfig; java.nio.FloatBuffer;
337
338
Rozdział 10. Tworzenie i renderowanie grafiki import java.nio.ByteBuffer; import java.nio.ByteOrder; public class OpenGLDemoActivity extends Activity { private GLSurfaceView myGLView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); myGLView = new GLSurfaceView(this); MyGLSurfRenderer renderer=new MyGLSurfRenderer(); myGLView.setRenderer(renderer); setContentView(myGLView); } @Override protected void onPause() { super.onPause(); myGLView.onPause(); } @Override protected void onResume() { super.onResume(); myGLView.onResume(); } public class MyGLSurfRenderer implements GLSurfaceView.Renderer { private Rectangle rectangle; public MyGLSurfRenderer() { rectangle = new Rectangle(); } public void onSurfaceCreated(GL10 gl, EGLConfig config) { gl.glLoadIdentity(); } public void onDrawFrame(GL10 gl) { gl.glClear(GLES20.GL_COLOR_BUFFER_BIT); gl.glColor4f(0.0f, 0.0f, 1.0f, 1.0f); gl.glMatrixMode(GL10.GL_MODELVIEW); gl.glPushMatrix(); gl.glRotatef(45.0f, 0.0f, 0.0f, 1.0f); rectangle.draw(gl); gl.glPopMatrix(); } public void onSurfaceChanged(GL10 gl, int width, int height) { gl.glViewport(0, 0, width, height); } } public class Rectangle { private FloatBuffer boxBuffer; private FloatBuffer colorBuffer; float vertices[] = {
Receptura: rotacja grafiki -0.5f, -0.5f, 0.0f, 0.5f, -0.5f, 0.0f, -0.5f, 0.5f, 0.0f, 0.5f, 0.5f, 0.0f }; float[] colors = { 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, };
1.0f, 1.0f, 1.0f, 1.0f
public Rectangle() { ByteBuffer byteBuffer = ByteBuffer.allocateDirect ( vertices.length * 4); byteBuffer.order(ByteOrder.nativeOrder()); boxBuffer = byteBuffer.asFloatBuffer(); boxBuffer.put(vertices); boxBuffer.position(0); ByteBuffer colorBytes = ByteBuffer.allocateDirect ( colors.length * 4); colorBytes.order(ByteOrder.nativeOrder()); colorBuffer = colorBytes.asFloatBuffer(); colorBuffer.put(colors); colorBuffer.position(0); } public void draw(GL10 gl) { gl.glEnableClientState(GL10.GL_VERTEX_ARRAY); gl.glVertexPointer(3, GL10.GL_FLOAT, 0, boxBuffer); gl.glClearColor(0.0f, 0.0f, 0.0f, 1.0f); gl.glEnableClientState(GL10.GL_COLOR_ARRAY); gl.glColorPointer(4, GL10.GL_FLOAT, 0, colorBuffer); gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4); gl.glDisableClientState(GL10.GL_VERTEX_ARRAY); } } }
Jak możesz zauważyć, instancja myGLView klasy GLSurfaceView jest definiowana i inicjowana w kontekście danej aktywności. Dla celów renderowania grafiki definiowany jest obiekt renderer klasy renderującej MyGLSurfRenderer. Definiowane są metody onPause() i onResume() w celu wstrzymywania i wznawiania renderowania, kiedy aktywność jest odpowiednio wstrzymywana i wznawiana. Klasa renderująca MyGLSurfRenderer implementuje interfejs GLSurfaceView.Renderer i dlatego implementuje też trzy metody: onSurfaceCreated(), onDrawFrame() oraz onSurfaceChanged(). Definiowany jest obiekt rectangle klasy Rectangle. Wierzchołki, kolory i inne informacje o prostokącie, który chcesz narysować, określane są w klasie Rectangle. W metodzie onDrawFrame() metoda glClear() czyści bufor, aby pokazać kolor (czarny) zdefiniowany za pomocą metody glClearColor(). Z wykorzystaniem metody glColor4f() bieżący kolor pierwszego planu jest ustawiany na niebieski. Metoda glMatrixMode() jest stosowana do ustawienia bieżącego trybu matrycy na GL_MODELVIEW.
339
340
Rozdział 10. Tworzenie i renderowanie grafiki
Tryb GL_MODELVIEW zawiera matrycę, która jest wykorzystywana w modelowaniu i wyświetlaniu grafiki. Metoda glLoadIdentity() jest wywoływania w celu zresetowania matrycy. Jest ona wywoływana jednorazowo w metodzie onSurfaceCreated() i kilka razy w metodzie onDrawFrame() przy użyciu operacji Push i Pop matrycy. Po zmianie trybu matrycy niezbędne jest jej zresetowanie, aby upewnić się, że jakiekolwiek transformacje matrycy wykonane za pomocą poprzednich poleceń (jeśli takie były) nie komplikują bieżących poleceń i nie wpływają na nie. Metoda glRotatef() jest wywoływana w celu pomnożenia bieżącej matrycy modelview przez rotację. Metoda ta obraca dany prostokąt o 45° wokół określonego wektora (0, 0, 1). Na koniec prostokąt jest rysowany poprzez wywołanie metody draw() w klasie Rectangle. W metodzie onSurfaceChanged() definiujesz rozmiar okienka ekranu OpenGL. Szerokość i wysokość okienka ekranu są definiowane jako równe szerokości i wysokości okna, więc okienko ekranu pokrywa cały ekran. Aby określić kształt prostokąta, w klasie Rectangle definiowana jest tablica zmiennoprzecinkowa vertices zawierająca współrzędne wierzchołków prostokąta, który chcesz narysować. Kolejna tablica zmiennoprzecinkowa o nazwie colors jest definiowana do przechowywania wartości kolorów każdego z wierzchołków prostokąta. Definiowany jest bufor bajtowy o nazwie byteBuffer. Rozmiar tego bufora bajtowego jest ustawiany jako równy czterokrotnej liczbie wierzchołków zdefiniowanych w tablicy vertices, ponieważ współrzędne wierzchołka są wartościami zmiennoprzecinkowymi, a wartości zmiennoprzecinkowe wymagają czterech bajtów. Na podstawie rozmiaru bufora bajtowego byteBuffer definiowany jest bufor zmiennoprzecinkowy o nazwie boxBuffer. Wszystkie wartości zmiennoprzecinkowe (czyli współrzędne wierzchołków) z tablicy vertices są zapisywane do bufora zmiennoprzecinkowego boxBuffer. Bufor ten jest resetowany do pozycji 0 (czyli resetowany do odczytu współrzędnych pierwszego wierzchołka). W celu zastosowania kolorów definiowany jest kolejny bufor bajtowy o nazwie colorBytes i na podstawie rozmiaru tego bufora definiowany jest bufor zmiennoprzecinkowy o nazwie colorBuffer. Wszystkie wartości zmiennoprzecinkowe (kolory) z tablicy colors są zapisywane do bufora zmiennoprzecinkowego colorBuffer. Ponadto bufor colorBuffer jest resetowany do pozycji 0 w celu odczytu pierwszej wartości koloru. W metodzie draw() po stronie klienta włączana jest tablica GL_VERTEX_ARRAY, która może być użyta podczas renderowania. Za pomocą metody glVertexPointer() deklarowane jest, że bufor boxBuffer przechowuje współrzędne wierzchołków prostokąta, który chcesz narysować. Analogicznie w celu zastosowania kolorów po stronie klienta włączana jest tablica GL_COLOR_ARRAY. Za pomocą metody glColorPointer() deklarowane jest, że bufor colorBuffer przechowuje wartości kolorów, które mają być zastosowane do wierzchołków prostokąta. Z wykorzystaniem metody glDrawArrays() grafika (prostokąt) jest renderowana na podstawie włączonych tablic. Po uruchomieniu tej aplikacji wyświetlony zostanie obrócony o 45° prostokąt z efektem płynnego przechodzenia kolorów a pierwszym planie oraz z czarnym tłem, tak jak pokazano na rysunku 10.3.
Receptura: rotacja grafiki
Rysunek 10.3. Grafika obrócona o 45°
W tabeli 10.2 zawarto krótki opis metod wykorzystanych w listingu 10.3. Tabela 10.2. Krótki opis metod wykorzystanych w listingu 10.3
Metoda
Opis
void glMatrixMode (GLenum tryb)
Metoda ustawia bieżący tryb matrycy. Parametr tryb może przyjmować jedną z następujących wartości. GL_MODELVIEW — ten tryb matrycy jest wykorzystywany do modelowania i przeglądania grafiki. GL_PROJECTION — ten tryb matrycy jest wykorzystywany do transformacji projekcyjnej. GL_TEXTURE — ten tryb matrycy jest wykorzystywany do przeprowadzania operacji na teksturze. Początkową wartością parametru tryb jest GL_MODELVIEW.
glLoadIdentity()
Metoda resetuje matrycę bieżącego trybu matrycy w celu jej identyfikacji. Metoda ta powinna być zawsze wywoływana po zmianie trybu matrycy, aby transformacje matrycy wynikające z poprzednich poleceń nie wpływały na bieżące operacje.
void glRotatef (GLfloat kąt, GLfloat x, GLfloat y, GLfloat z)
Metoda mnoży bieżącą matrycę o matrycę obrotu. Innymi słowy, obraca matrycę wokół określonego wektora (x, y, z). Parametr kąt określa kąt rotacji w stopniach. Parametry x, y i z reprezentują współrzędne X, Y i Z wektora, wokół którego wykonana ma być rotacja.
341
342
Rozdział 10. Tworzenie i renderowanie grafiki
Aby prostokąt obracał się w sposób ciągły w kierunku przeciwnym do ruchu wskazówek zegara, zmodyfikuj kod w metodzie onDrawframe() w klasie MyGLSurfRenderer (patrz listing 10.3) w następujący sposób: public class MyGLSurfRenderer implements GLSurfaceView.Renderer { private Rectangle rectangle; public void onSurfaceCreated(GL10 gl, EGLConfig config) { gl.glLoadIdentity(); } public void onDrawFrame(GL10 gl) { gl.glClear(GLES20.GL_COLOR_BUFFER_BIT); gl.glColor4f(0.0f, 0.0f, 1.0f, 1.0f); gl.glMatrixMode(GL10.GL_MODELVIEW); gl.glRotatef(2.0f, 0.0f, 0.0f, 1.0f); rectangle.draw(gl); }
Jak możesz zauważyć, kąt rotacji został tak ustawiony, aby zwiększał się o dwa stopnie po każdym obrocie. Po uruchomieniu tej aplikacji prostokąt będzie obracał się w sposób ciągły w kierunku przeciwnym do ruchu wskazówek zegara. Na rysunku 10.4 przedstawiono obracający się prostokąt. Rysunek ten (górny i dolny obrazek) to zrzuty ekranu dla prostokąta znajdującego się pod różnymi kątami podczas obrotu.
Receptura: skalowanie grafiki Podczas czytania tej receptury nauczysz się skalować grafikę. Spowodujesz kurczenie się prostokąta, który rysowałeś się w poprzedniej recepturze. Nauczysz się zmniejszać ten prostokąt do 50% jego oryginalnego rozmiaru. Ponadto nauczysz się iteracyjnie zmniejszać prostokąt o 10% jego wielkości. Aby zmniejszyć prostokąt o 50%, zmodyfikuj metodę onDrawframe() w klasie MyGLSurfRenderer (patrz listing 10.3) w następujący sposób: public class MyGLSurfRenderer implements GLSurfaceView.Renderer { private Rectangle rectangle; public void onDrawFrame(GL10 gl) { gl.glClear(GLES20.GL_COLOR_BUFFER_BIT); gl.glColor4f(0.0f, 0.0f, 1.0f, 1.0f); gl.glMatrixMode(GL10.GL_MODELVIEW); gl.glPushMatrix(); gl.glScalef(0.5f,0.5f, 0.5f); rectangle.draw(gl); gl.glPopMatrix(); }
Receptura: skalowanie grafiki
Rysunek 10.4. Grafika obracająca się w sposób ciągły w kierunku przeciwnym do ruchu wskazówek zegara. Obrazki górny i dolny pokazują prostokąt pod różnymi kątami w trakcie obrotu
Przy użyciu metody glScalef() rozmiar danego prostokąta zostaje zmniejszony o 50% wzdłuż osi X, Y i Z. Metoda glLoadIdentity() jest wywoływana w celu zresetowania matrycy. Za pomocą operacji Push i Pop matrycy wywoływana jest metoda glLoadIdentity() w celu powstrzymania dalszego skalowania prostokąta. Ponieważ metoda onDrawFrame() jest wywoływana przy każdej klatce, rozmiar prostokąta będzie w sposób ciągły zmniejszał się o 50% przy każdej iteracji, jeśli metoda glLoadIdentity() nie zostanie wykorzystana do zresetowania matrycy.
343
344
Rozdział 10. Tworzenie i renderowanie grafiki
Wskazówka Jeśli trybem matrycy jest GL_MODELVIEW lub GL_PROJECTION, wszystkie obiekty narysowane po wywołaniu metody glScalef() zostaną przeskalowane. Rozsądnie jest więc zastosować metody glPushMatrix() i glPopMatrix() w celu odpowiednio zapisania i przywrócenia nieprzeskalowanego układu współrzędnych.
Aby zmniejszać prostokąt iteracyjnie o 10% do momentu, aż osiągnie 25% swojego pierwotnego rozmiaru, zastosuj następujący fragment kodu: public class MyGLSurfRenderer implements GLSurfaceView.Renderer { private Rectangle rectangle; private float xscale=0.9f; private float yscale=0.9f; private float zscale=0.9f; public void onDrawFrame(GL10 gl) { gl.glClear(GLES20.GL_COLOR_BUFFER_BIT); gl.glColor4f(0.0f, 0.0f, 1.0f, 1.0f); gl.glMatrixMode(GL10.GL_MODELVIEW); if(xscale <=.25) { xscale=1; yscale=1; zscale=1; } gl.glPushMatrix(); gl.glScalef(xscale,yscale, zscale); rectangle.draw(gl); gl.glPopMatrix(); xscale-=.1; yscale-=.1; zscale-=.1; }
Jak możesz zauważyć, metoda glScalef() zmniejsza rozmiar prostokąta o 10% wzdłuż osi X, Y i Z. Prostokąt ten jest zmniejszany w sposób ciągły do momentu, aż osiągnie 25% swojego pierwotnego rozmiaru. Kiedy prostokąt skurczy się do 25% rozmiaru lub bardziej, jest na powrót powiększany do rozmiaru początkowego. Po uzyskaniu rozmiaru początkowego prostokąt jest ponownie zmniejszany w sposób ciągły o 10% do momentu osiągnięcia 25% tego rozmiaru. Proces ten jest kontynuowany w nieskończoność. Wykorzystana w powyższym kodzie metoda glScalef() odpowiada za mnożenie bieżącej matrycy przez matrycę skalowania. Składnia jest następująca: void glScalef(GLfloat x, GLfloat y, GLfloat z)
Parametry x, y i z reprezentują odpowiednio współczynniki skalowania wzdłuż osi X, Y i Z. Po uruchomieniu tej aplikacji wyświetlony zostanie najpierw prostokąt w pierwotnym rozmiarze. Następnie będzie on w sposób ciągły zmniejszał swoją wielkość do momentu osiągnięcia jednej czwartej pierwotnego rozmiaru. Potem wróci do rozmiaru początkowego i ponownie zacznie kurczyć się stopniowo do jednej czwartej pierwotnego rozmiaru.
Receptura: skalowanie grafiki
Proces ten będzie kontynuowany w nieskończoność. Na rysunku 10.5 przedstawiono zrzuty ekranu dla prostokąta w różnych rozmiarach w trakcie fazy zmniejszania.
Rysunek 10.5. Grafika zmniejszana według współrzędnych X, Y i Z. Obrazki górny i dolny przedstawiają prostokąt o różnych rozmiarach w trakcie fazy zmniejszania
345
346
Rozdział 10. Tworzenie i renderowanie grafiki
Receptura: przesuwanie grafiki Grafika może być przenoszona (przesuwana w kierunkach X, Y i Z). Czytając tę recepturę, nauczysz się przesuwać prostokąt, który rysowałeś w poprzedniej recepturze. Sprawisz, że prostokąt będzie przenoszony w osi X poprzez przesunięcie poziome w prawą stronę. Prostokąt zniknie za prawym brzegiem ekranu i pojawi się ponownie w pierwotnej pozycji w celu powtórzenia przesunięcia. Aby przesunąć prostokąt w poziomie wzdłuż osi X, zmodyfikuj metodę onDrawframe() w klasie MyGLSurfRenderer (patrz listing 10.3) w następujący sposób: public class MyGLSurfRenderer implements GLSurfaceView.Renderer { private Rectangle rectangle; private float xdelta=0.0f; public void onDrawFrame(GL10 gl) { gl.glClear(GLES20.GL_COLOR_BUFFER_BIT); gl.glColor4f(0.0f, 0.0f, 1.0f, 1.0f); gl.glMatrixMode(GL10.GL_MODELVIEW); if(xdelta >=2)xdelta=0; gl.glPushMatrix(); gl.glTranslatef(xdelta, 0.0f, 0.0f); rectangle.draw(gl); gl.glPopMatrix(); xdelta+=.02; }
Zastosowanie metody glTranslatef sprawia, że prostokąt jest przesuwany w prawą stronę wzdłuż osi X. Bieżąca wartość xdelta jest zwiększana o 0,02 w metodzie onDrawFrame(). Ponieważ metoda onDrawFrame() jest wywoływana przy każdej klatce, wartość xdelta będzie stale rosnąć. Dlatego też prostokąt będzie w sposób ciągły poruszał się w poziomie w prawą stronę. Kiedy wartość xdelta przekroczy 2, jest resetowana do wartości 0, aby prostokąt ponownie pojawił się w początkowej pozycji i przesunięcie zostało ponownie uruchomione. Po uruchomieniu tej aplikacji prostokąt zacznie poruszać się poziomo w prawą stronę wzdłuż osi X. Na rysunku 10.6 (górny obrazek) przedstawiono prostokąt w pierwotnej lokalizacji. Na rysunku 10.6 (dolny obrazek) pokazano prostokąt znikający za prawym brzegiem ekranu. Grafikę możesz również przesuwać jednocześnie wzdłuż więcej niż jednej osi. Aby prostokąt przesuwał się jednocześnie wzdłuż osi X i Y, zmodyfikuj metodę onDrawframe() w klasie MyGLSurfRenderer (patrz listing 10.3) w następujący sposób: public class MyGLSurfRenderer implements GLSurfaceView.Renderer { private Rectangle rectangle; private float xdelta=0.0f; private float ydelta=0.0f; public void onDrawFrame(GL10 gl) { gl.glClear(GLES20.GL_COLOR_BUFFER_BIT); gl.glColor4f(0.0f, 0.0f, 1.0f, 1.0f); gl.glMatrixMode(GL10.GL_MODELVIEW); if(xdelta >=2)xdelta=0;
Receptura: przesuwanie grafiki
Rysunek 10.6. Prostokąt w pierwotnej pozycji po uruchomieniu aplikacji (górny obrazek) oraz prostokąt znikający za prawym brzegiem ekranu (dolny obrazek) if(ydelta >=2)ydelta=0; gl.glPushMatrix(); gl.glTranslatef(xdelta, ydelta, 0.0f); rectangle.draw(gl); gl.glPopMatrix(); xdelta+=.02; ydelta+=.02; }
347
348
Rozdział 10. Tworzenie i renderowanie grafiki
Jak możesz zauważyć, zmienne xdelta i ydelta są inicjowane z wartością 0. Ponadto wartości tych zmiennych są zwiększane o 0,02 w metodzie onDrawFrame(). Zwiększenie wartości dla współrzędnej X będzie przesuwać prostokąt w prawą stronę, a zwiększenie wartości dla współrzędnej Y będzie przesuwać prostokąt w górę. To połączenie spowoduje, że prostokąt będzie poruszał się w kierunku prawego górnego rogu ekranu. Kiedy wartość zmiennej xdelta lub ydelta przekroczy 2, wartość ta jest resetowana do 0, aby prostokąt pojawił się w pierwotnej lokalizacji i przesunięcie mogło być uruchomione ponownie. Po uruchomieniu tej aplikacji prostokąt zacznie poruszać się w kierunku prawego górnego rogu ekranu (patrz rysunek 10.7, górny obrazek). Na rysunku 10.7 (dolny obrazek) pokazano prostokąt znikający w prawym górnym rogu ekranu.
Rysunek 10.7. Prostokąt w pierwotnej pozycji po uruchomieniu aplikacji (górny obrazek) oraz prostokąt znikający w prawym górnym rogu ekranu (dolny obrazek)
Podsumowanie
Podsumowanie W tym rozdziale dowiedziałeś się, w jaki sposób interfejs OpenGL ES API jest wykorzystywany do tworzenia i renderowania grafiki. Poznałeś procedurę stosowania jednoodcieniowych i wieloodcieniowych kolorów do rysowania grafiki. Nauczyłeś się również różnych metod rotacji, skalowania i przesuwania grafiki. W kolejnym rozdziale omówiono przechwytywanie obrazów, wideo oraz audio w aplikacji Android. Zapoznasz się z intencjami wbudowanymi i wykorzystasz je do przechwytywania obrazów, wideo oraz audio. Ponadto nauczysz się wykonywać te zadania ręcznie, pisząc kod Java, który uzyskuje dostęp do kamery oraz mikrofonu urządzenia i wywołuje różne metody w celu przechwytywania odpowiednio obrazów, audio i wideo.
349
350
Rozdział 10. Tworzenie i renderowanie grafiki
11 Przechwytywanie audio, wideo i obrazów N
iemal wszystkie urządzenia z systemem Android są wyposażone w mikrofon i kamerę. Aby wykorzystać je w projektowanych aplikacjach, musisz poznać różne klasy i metody wymagane w celu uzyskania do nich dostępu. Uzyskując dostęp do kamery urządzenia, możesz przygotować aplikacje, które przechwytują obrazy i wideo. Analogicznie możesz dodać funkcję nagrywania audio w aplikacji Android, jeśli wiesz, w jaki sposób uzyskać dostęp do mikrofonu urządzenia. W tym rozdziale skoncentrowano się na omówieniu, w jaki sposób obrazy, wideo i audio są przechwytywane w aplikacji Android. Oczywiście, pakiet Android SDK oferuje pewne intencje i akcje, które znacznie ułatwiają przechwytywanie obrazów, wideo i audio. W tym rozdziale zapoznasz się z intencjami wbudowanymi i wykorzystasz je do przechwytywania obrazów, wideo i audio. Nauczysz się pisać kod Java uzyskujący dostęp do kamery i mikrofonu urządzenia oraz dowiesz się, jak wywoływać różne metody do przechwytywania odpowiednio obrazów, wideo i audio. Tu nauczysz się również uzyskiwać dostęp do informacji profilowych kamery urządzenia.
Receptura: przechwytywanie obrazu z wykorzystaniem wbudowanej intencji Istnieją dwa sposoby uzyskania dostępu do kamery urządzenia i wykorzystania jej do przechwytywania obrazu. Przygotowanie kodu Java, za pomocą którego uzyskasz dostęp do kamery urządzenia,
zapewnisz powierzchnię do wyświetlenia podglądu obrazu, przechwycisz obraz i zapiszesz go w przestrzeni do przechowywania danych w urządzeniu. Zdefiniowanie i wywołanie intencji wykorzystującej akcję
MediaStore.ACTION_IMAGE_CAPTURE systemu Android, przy użyciu której
automatycznie wykonasz wszystkie zadania związane z podglądaniem, przechwytywaniem i zapisywaniem obrazu.
352
Rozdział 11. Przechwytywanie audio, wideo i obrazów
W tej recepturze skorzystamy z drugiej i jednocześnie łatwiejszej metody przechwytywania obrazu, czyli ze zdefiniowania i wywołania intencji z predefiniowaną akcją. Ponadto nauczysz się wyświetlać przechwycony obraz na ekranie za pomocą kontrolki ImageView. W tym celu utwórz projekt Android o nazwie CameraApp. W tej aplikacji wykorzystasz kontrolki Button oraz ImageView. Kliknięcie kontrolki Button wywoła kamerę urządzenia. Gdy użytkownik kliknie obraz przez kamerę urządzenia, obraz zostanie wyświetlony za pomocą kontrolki ImageView. Aby zdefiniować kontrolki Button i ImageView, wpisz w pliku układu aktywności activity_camera_app.xml kod przedstawiony w listingu 11.1. Listing 11.1. Kod wpisany w pliku układu aktywności activity_camera_app.xml
W celu identyfikacji i uzyskiwania dostępu w kodzie Java kontrolkom Button i ImageView przypisano odpowiednio identyfikatory launch_btn i photo. Nagłówek wyświetlany na kontrolce Button to Uruchom aparat. Rozmiar czcionki nagłówka został zdefiniowany w zasobie wymiarów text_size. Teraz trzeba napisać kod Java, który będzie wykonywał następujące zadania. Uzyskanie dostępu do kontrolek Button i ImageView zdefiniowanych w pliku
układu aktywności i zmapowanie ich na odpowiednie obiekty. Powiązanie nasłuchiwacza setOnClickListener z kontrolką Button w celu
nasłuchiwania zdarzeń kliknięcia tej kontrolki. Kiedy na kontrolce Button wystąpi zdarzenie kliknięcia, wywołana zostanie metoda wywołania zwrotnego onClick. Zdefiniowanie intencji o nazwie captureIntent, która odwołuje się do
wbudowanej akcji MediaStore.ACTION_IMAGE_CAPTURE. Wywołanie tej intencji spowoduje uruchomienie kamery urządzenia. Uruchomienie danej aktywności w przypadku kliknięcia kontrolki Button,
co umożliwi użytkownikowi kliknięcie obrazu. Wykonanie analizy kodu wynikowego po zakończeniu uruchomionej aktywności,
czyli dostarczenie informacji o tym, czy zadanie przechwytywania obrazu zostało przeprowadzone z powodzeniem.
Receptura: przechwytywanie obrazu z wykorzystaniem wbudowanej intencji Uzyskanie przez kontrolkę ImageView dostępu do obrazu i wyświetlenie go,
jeśli przechwytywanie obrazu się powiodło. Aby wykonać wymienione zadania, wpisz w pliku aktywności Java CameraAppActivity.java kod przedstawiony w listingu 11.2. Listing 11.2. Kod wpisany w pliku aktywności Java CameraAppActivity.java package com.androidtablet.cameraapp; import import import import import import import import import import
android.os.Bundle; android.app.Activity; android.widget.Button; android.view.View; android.provider.MediaStore; android.content.Intent; android.content.ActivityNotFoundException; android.widget.Toast; android.graphics.Bitmap; android.widget.ImageView;
public class CameraAppActivity extends Activity { final int CAPTURE_PHOTO = 1; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_camera_app); Button captureBtn = (Button) findViewById( R.id.launch_btn); captureBtn.setOnClickListener(new Button.OnClickListener() { public void onClick(View v) { try { Intent captureIntent = new Intent( MediaStore.ACTION_IMAGE_CAPTURE); startActivityForResult(captureIntent, CAPTURE_PHOTO ); } catch(ActivityNotFoundException e){ Toast.makeText(CameraAppActivity.this, "Wystąpił błąd podczas przechwytywania obrazu", Toast.LENGTH_SHORT).show(); } } }); } protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (resultCode == RESULT_OK) { if(requestCode == CAPTURE_PHOTO ){ Bitmap photo = (Bitmap) data.getExtras(). get("data"); ImageView picView = (ImageView)findViewById( R.id.photo); picView.setImageBitmap(photo);
#1 #2
#3 #4 #5 #6 #7
353
354
Rozdział 11. Przechwytywanie audio, wideo i obrazów } } else Toast.makeText(CameraAppActivity.this, "Nie można przechwycić obrazu", Toast.LENGTH_SHORT).show(); } }
Niektóre instrukcje z listingu 11.2 wymagają objaśnienia. Instrukcja #1 definiuje intencję o nazwie captureIntent z akcją
MediaStore.ACTION_IMAGE_CAPTURE. Wywołanie tej intencji powoduje
uruchomienie kamery urządzenia. Instrukcja #2 uruchamia daną aktywność i oczekuje na wynik wywołania
kamery urządzenia (czy przechwycenie obrazu się powiodło, czy też wystąpił jakiś błąd). Wykorzystana w tej instrukcji zmienna CAPTURE_PHOTO służy do rozróżnienia intencji captureIntent od innych intencji w tej aplikacji (jeśli takie istnieją). Zmienna CAPTURE_PHOTO pomoże śledzić intencję uruchamiającą kamerę urządzenia. Instrukcja #3 definiuje procedurę obsługi onActivityResult, która pobiera
i analizuje wynik uruchomionej aktywności. Innymi słowy, uzyskuje dostęp do wyników wywołania kamery urządzenia. Instrukcja #4 potwierdza, czy wywołana intencja została wykonana z powodzeniem.
Inaczej mówiąc, potwierdza, czy odnaleziono kamerę urządzenia, czy kamera została wywołana oraz czy przechwytywanie obrazu się powiodło. Instrukcja #5 potwierdza, czy dany wynik pochodzi z żądanej intencji
captureIntent. Przypomnijmy, że intencja captureIntent została wywołana ze zmienną CAPTURE_PHOTO w instrukcji #2. W instrukcji #6 za pomocą parametru data Intent uzyskiwany jest dostęp
do obrazu przechwyconego z kamery urządzenia. Obraz, do którego uzyskano dostęp, jest przypisywany do obiektu photo klasy
Bitmap. Instrukcja #7 przypisuje obraz z obiektu photo do obiektu picView kontrolki ImageView, w celu wyświetlenia go na ekranie.
Po uruchomieniu tej aplikacji wyświetlona zostanie kontrolka Button z nagłówkiem Uruchom aparat (patrz rysunek 11.1, lewy górny obrazek). Po wybraniu tej kontrolki wywołana zostanie kamera urządzenia wyświetlająca podgląd obrazu. Na dole ekranu z podglądem wyświetlony będzie przycisk w postaci ikony symbolizującej migawkę lub aparat (patrz rysunek 11.1, prawy górny obrazek). Kliknięcie tego przycisku powoduje kliknięcie obrazu i wyświetlenie go za pomocą kontrolki ImageView zdefiniowanej w pliku układu aktywności (patrz rysunek 11.1, lewy dolny obrazek). Galeria obrazów urządzenia również pokazuje kliknięty obraz (patrz rysunek 11.1, prawy dolny obrazek), a to jest potwierdzeniem, że obraz został kliknięty i zapisany z powodzeniem.
Receptura: przechwytywanie obrazu z wykorzystaniem wbudowanej intencji
Rysunek 11.1. Kontrolka Button wyświetlona po uruchomieniu aplikacji (lewy górny obrazek). Wywołana kamera urządzenia wyświetlająca podgląd obrazu (prawy górny obrazek). Kliknięty obrazek wyświetlony w kontrolce ImageView (lewy dolny obrazek). Galeria obrazów urządzenia pokazująca kliknięte zdjęcie (prawy dolny obrazek)
355
356
Rozdział 11. Przechwytywanie audio, wideo i obrazów
Receptura: przechwytywanie obrazu za pomocą kodu Java W tej recepturze nauczysz się przechwytywać obraz ręcznie za pomocą kodu Java wykorzystującego bezpośrednio klasę Camera i wywołującego jej różne metody wymagane do podglądania, przechwytywania i zapisywania obrazu w przestrzeni przechowywania danych urządzenia. Do obsługi wyświetlania obrazu wykorzystana zostanie klasa SurfaceView oraz interfejs SurfaceHolder.Callback. Klasa SurfaceView dostarczy w hierarchii widoków powierzchnię, którą możesz wykorzystać do wyświetlenia podglądu i przechwyconego obrazu. Klasa SurfaceView poprawia wrażenia użytkownika dzięki zastosowaniu niezależnego wątku do rysowania i aktualizacji widoku. Interfejs SurfaceHolder przechowuje obiekt klasy SurfaceView i kontroluje rozmiar powierzchni, edytuje ją oraz formatuje i monitoruje powierzchnię.
Uwaga Klasa Camera pomaga w konfiguracji ustawień przechwytywania obrazu, w rozpoczynaniu i zatrzymywaniu podglądu, przechwytywaniu obrazów itd. Klasa ta jest klientem usługi Camera, która zarządza faktyczną kamerą sprzętową.
Aby przechwycić obraz za pomocą klasy Camera, wykonaj następujące czynności. 1. Utwórz obiekt Camera, wywołując metodę open. 2. Wywołaj metodę setPreviewDisplay, aby ustawić powierzchnię do wyświetlania
podglądu z kamery. 3. Wywołaj metodę startPreview, aby rozpocząć rysowanie klatek podglądu
na określonej powierzchni. 4. Wywołaj metodę takePicture, aby wywołać określoną procedurę obsługi
do zapisania przechwyconego obrazu w żądanym formacie wynikowym i w żądanej ścieżce. 5. Zdefiniuj obiekt File, aby określić nazwę pliku, pod którą zapisany zostanie
przechwycony obraz. 6. Zdefiniuj obiekt FileOutputStream, aby zapisać przechwycony obraz
do określonego pliku. 7. Zamknij obiekt FileOutputStream. 8. Wywołaj metodę stopPreview, aby zatrzymać podgląd wyświetlany
przez kamerę urządzenia. 9. Wywołaj metodę release, aby uwolnić zasoby przypisane do obiektu Camera.
Utwórz projekt Android o nazwie CaptureImageApp. W tej aplikacji po uruchomieniu automatycznie włączony zostanie podgląd z kamery. Element akcji o nazwie Przechwyć obraz zostanie wyświetlony w pasku akcji, aby użytkownik mógł przechwycić obraz
Receptura: przechwytywanie obrazu za pomocą kodu Java
za pomocą kliknięcia. Przechwycony obraz zostanie zapisany na karcie SD urządzenia i będzie dostępny w jego galerii obrazów. Do wyświetlenia podglądu z kamery wykorzystasz klasę SurfaceView. Aby zdefiniować tę klasę, wpisz w pliku układu aktywności activity_capture_image_app.xml kod przedstawiony w listingu 11.3. Listing 11.3. Kod wpisany w pliku układu aktywności activity_capture_image_app.xml
Dla celów identyfikacji i uzyskania dostępu w kodzie Java widokowi powierzchni SurfaceView przypisano identyfikator surfaceview. Teraz zdefiniujesz element akcji
w pliku menu activity_capture_image_app.xml, który znajduje się w folderze res/menu. Aby zdefiniować element akcji o nazwie Przechwyć obraz, wpisz w pliku menu activity_capture_image_app.xml kod przedstawiony w listingu 11.4. Listing 11.4. Kod wpisany w pliku menu activity_capture_image_app.xml
Dany element akcji ma być wyświetlany z tytułem Przechwyć obraz. Ponadto dla celów identyfikacji i uzyskiwania dostępu elementowi temu przypisano identyfikator capture_button. Teraz musisz napisać kod Java, który będzie wykonywał następujące zadania. Uzyskanie dostępu do widoku powierzchni SurfaceView zdefiniowanego
w pliku układu aktywności i zmapowanie go na obiekt SurfaceView. To właśnie za pomocą widoku SurfaceView będziesz uzyskiwał podgląd z kamery, aby wskazać obraz do przechwycenia. Skonfigurowanie klasy głównej aktywności, aby implementowała interfejs
SurfaceHolder.Callback. Interfejs ten powiadamia powiązany widok, kiedy
bazowa powierzchnia jest tworzona, niszczona lub modyfikowana.
357
358
Rozdział 11. Przechwytywanie audio, wideo i obrazów Zaimplementowanie trzech metod: surfaceCreated(SurfaceHolder),
surfaceDestroyed(SurfaceHolder) oraz surfaceChanged(SurfaceHolder holder, int format, int w, int h).
surfaceCreated(SurfaceHolder) informuje, kiedy powierzchnia zostaje
utworzona i jest dostępna dla podglądu z kamery. surfaceDestroyed(SurfaceHolder) informuje, kiedy powierzchnia jest
niszczona. surfaceChanged(SurfaceHolder holder, int format, int w, int h)
informuje, kiedy w strukturze powierzchni następuje zmiana, taka jak zmiana szerokości lub wysokości albo zmiana orientacji urządzenia. Wypełnienie lub scalenie menu zdefiniowanego w pliku menu
activity_capture_image_app.xml. Zdefiniowanie metody onOptionsItemSelected() do obsługi zdarzeń kliknięcia
elementu akcji. Zdefiniowanie funkcji picHandler, która jest wywoływana przez metodę
takePicture(), kiedy użytkownik kliknie element akcji Przechwyć obraz. Zdefiniowanie nazwy pliku, pod którą przechwycony obraz ma być zapisywany
na karcie SD. Zdefiniowanie obiektu FileOutputStream w celu zapisywania przechwyconego
obrazu pod określoną nazwą. Wywołanie metod stopPreview() i release() w celu zatrzymania podglądu
z kamery i uwolnienia alokowanych zasobów. Aby wykonać wymienione zadania, wpisz w pliku aktywności Java CaptureImageAppActivity.java kod przedstawiony w listingu 11.5. Listing 11.5. Kod wpisany w pliku aktywności Java CaptureImageAppActivity.java package com.androidtablet.captureimageapp; import import import import import import import import import import import import import import import import
android.os.Bundle; android.app.Activity; android.view.Menu; android.view.MenuItem; android.view.SurfaceHolder; android.view.SurfaceView; android.hardware.Camera; java.io.IOException; java.io.FileNotFoundException; java.io.FileOutputStream; android.hardware.Camera.PictureCallback; android.os.Environment; java.io.File; android.widget.Toast; java.text.SimpleDateFormat; java.util.Calendar;
Receptura: przechwytywanie obrazu za pomocą kodu Java public class CaptureImageAppActivity extends Activity implements SurfaceHolder. Callback { private SurfaceHolder surfaceHolder; private SurfaceView surfaceView; private Camera camera = null; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_capture_image_app); surfaceView = (SurfaceView) findViewById( R.id.surfaceview); surfaceHolder = surfaceView.getHolder(); surfaceHolder.addCallback(this); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.activity_capture_image_app, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.capture_button: camera.takePicture(null, null, picHandler); break; default: return super.onOptionsItemSelected(item); } return true; } PictureCallback picHandler = new PictureCallback() { @Override public void onPictureTaken(byte[] data, Camera camera) { File pictureFile = new File(Environment. getExternalStorageDirectory().getPath() + "/sampleimage.jpg"); try { FileOutputStream fos = new FileOutputStream( pictureFile.toString()); fos.write(data); fos.close(); Toast.makeText(CaptureImageAppActivity.this, "Zapisałeś obraz",Toast.LENGTH_SHORT).show(); } catch (FileNotFoundException e) { } catch (IOException e) { } } }; public void surfaceCreated(SurfaceHolder holder) { camera=Camera.open();
#1
359
360
Rozdział 11. Przechwytywanie audio, wideo i obrazów try { camera.setPreviewDisplay(holder); camera.startPreview(); } catch (IOException e) { } } public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { try { camera.setPreviewDisplay(holder); camera.startPreview(); } catch (Exception e) { } } public void surfaceDestroyed(SurfaceHolder holder) { camera.stopPreview(); camera.release(); camera = null; } }
Przechwycony obraz, w zależności od urządzenia, zostanie zapisany pod nazwą sampleimage.jpg na karcie SD lub w wewnętrznej przestrzeni do przechowywania danych urządzenia. Aby umożliwić aplikacji dostęp do kamery urządzenia 000000000 i zapisać przechwycony obraz na karcie SD, dodaj do pliku AndroidManifest.xml trzy następujące instrukcje zezwoleń:
Twoja aplikacja jest już gotowa do uruchomienia. Jedyną jej wadą jest to, że każdy przechwycony obraz jest zapisywany pod nazwą sampleimage.jpg, co powoduje usunięcie poprzednio przechwyconego obrazu. Oznacza to, że aplikacja będzie pokazywać tylko ostatni z przechwyconych obrazów. Uruchom aplikację i sprawdź, co się stanie. Na rysunku 11.2 (lewy górny obrazek) widać albumy istniejące w galerii urządzenia. Liczba albumów i obrazów może się zmieniać w zależności od urządzenia, ponieważ wynika z tego, ile zdjęć jest zapisanych w danym urządzeniu. Na rysunku 11.2 (prawy górny obrazek) pokazano ekran po przechwyceniu obrazu. Ekran ten potwierdza udane przechwycenie obrazu, wyświetlając komunikat Zapisałeś obraz. Teraz przechwyć kolejny obraz, aby zobaczyć, czy usunie on obraz przechwycony wcześniej. Na rysunku 11.2 (lewy dolny obrazek) pokazano ekran po przechwyceniu kolejnego obrazu, a na rysunku 11.2 (prawy dolny obrazek) widać dodatkowy album sdcard0 zawierający jedno zdjęcie. Kolejny przechwycony obraz nadpisuje poprzedni, ponieważ oba mają taką samą nazwę sampleimage.jpg.
Receptura: przechwytywanie obrazu za pomocą kodu Java
Rysunek 11.2. Galeria obrazów urządzenia przedstawiająca początkowo dwa albumy (lewy górny obrazek). Przechwytywanie pierwszego obrazu (prawy górny obrazek). Przechwytywanie drugiego obrazu (lewy dolny obrazek). Galeria obrazów urządzenia pokazująca w albumie sdcard0 tylko jedno zdjęcie (prawy dolny obrazek)
Jeśli chcesz, aby każdy z przechwytywanych obrazów był zapisywany i nie dochodziło do nadpisywania poprzednio przechwyconych obrazów, skorzystaj z bieżących systemowych daty i czasu. Wtedy na podstawie daty i czasu każdemu z przechwyconych obrazów zostanie przypisana unikatowa nazwa pliku. W tym celu instrukcję #1 z listingu 11.5 musisz zastąpić następującą instrukcją: Calendar currentDate = Calendar.getInstance(); String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss"). format(currentDate.getTime());
361
362
Rozdział 11. Przechwytywanie audio, wideo i obrazów File pictureFile = new File(Environment. getExternalStorageDirectory().getPath() + "/IMG_" + timeStamp + ".jpg");
Teraz w przypadku przechwytywania kolejnych obrazów poprzednie obrazy nie będą usuwane. Na rysunku 11.3 widać w albumie sdcard0 dwa zdjęcia, które zostały tam zapisane po wykonaniu dwóch przechwyceń obrazu.
Rysunek 11.3. Galeria obrazów urządzenia pokazująca dwa zdjęcia w albumie sdcard0
Receptura: nagrywanie audio z wykorzystaniem wbudowanej intencji Podobnie jak w przypadku przechwytywania obrazu, istnieją dwa sposoby nagrywania audio w aplikacjach Android. W pierwszym przypadku korzystasz z klasy MediaRecorder systemu Android
i wywołujesz jej różne metody w celu uzyskania dostępu do mikrofonu urządzenia, nagrania dźwięku w określonym formacie i stylu kodowania oraz zapisania pliku w przestrzeni przechowywania danych urządzenia. Druga, znacznie łatwiejsza metoda polega na zdefiniowaniu i wywołaniu
intencji wykorzystującej akcję MediaStore.Audio.Media.RECORD_SOUND_ACTION, która wykonuje automatycznie wszystkie zadania związane z nagrywaniem, pauzowaniem, odtwarzaniem i przechowywaniem audio.
Receptura: nagrywanie audio z wykorzystaniem wbudowanej intencji
W tej recepturze posłużymy się drugą metodą w celu nagrania audio (czyli wywołaniem intencji z predefiniowaną akcją). W kolejnej recepturze nauczysz się nagrywać dźwięk, wykorzystując klasę MediaRecorder. Utwórz nowy projekt Android o nazwie AudioRecordApp. W tej aplikacji będziesz używał kontrolki Button, która po kliknięciu wywoła rejestrator dźwięku urządzenia. Użytkownik może nagrywać, pauzować i odtwarzać nagrany dźwięk. Ponadto nagrany dźwięk pojawi się na liście odtwarzania muzyki urządzenia. Aby zdefiniować kontrolkę Button, wpisz w pliku układu aktywności activity_audio_record_app.xml kod przedstawiony w listingu 11.6. Listing 11.6. Kod wpisany w pliku układu aktywności activity_audio_record_app.xml
Dla celów identyfikacji i wykorzystania w kodzie Java kontrolce Button przypisano identyfikator record_btn. Nagłówek wyświetlany dla kontrolki Button to Rozpocznij nagrywanie. Nagłówek ten będzie prezentowany czcionką o rozmiarze zdefiniowanym w zasobie wymiarów text_size. Po zdefiniowaniu pliku układu aktywności musisz napisać kod Java, który będzie wykonywał następujące zadania. Uzyskanie dostępu do kontrolki Button zdefiniowanej w pliku układu
aktywności i zmapowanie tej kontrolki na obiekt Button. Powiązanie nasłuchiwacza setOnClickListener z kontrolką Button w celu
nasłuchiwania zdarzeń kliknięcia. Kiedy na kontrolce Button wystąpi zdarzenie kliknięcia, uruchomiona zostanie metoda wywołania zwrotnego onClick. Zdefiniowanie intencji o nazwie audioIntent, która odwołuje się do wbudowanej
aktywności MediaStore.Audio.Media.RECORD_SOUND_ACTION. Ta wbudowana aktywność odpowiada za uruchamianie rejestratora dźwięku urządzenia. Uruchomienie danej aktywności po kliknięciu kontrolki Button. Pozwoli
to użytkownikowi na nagranie i odtworzenie żądanego dźwięku. Analiza kodu wynikowego po zakończeniu uruchomionej aktywności w celu
zweryfikowania, czy zadanie nagrywania się powiodło. Wyświetlenie komunikatów o błędach, jeśli takie pojawiły się w trakcie
nagrywania dźwięku.
363
364
Rozdział 11. Przechwytywanie audio, wideo i obrazów
Aby wykonać wymienione zadania, w pliku aktywności Java AudioRecordAppActivity.java wpisz kod przedstawiony w listingu 11.7. Listing 11.7. Kod wpisany w pliku aktywności Java AudioRecordAppActivity.java package com.androidtablet.audiorecordapp; import import import import import import import import import
android.os.Bundle; android.app.Activity; android.widget.Button; android.view.View; android.provider.MediaStore; android.content.Intent; android.content.ActivityNotFoundException; android.widget.Toast; android.net.Uri;
public class AudioRecordAppActivity extends Activity { final int RECORD_AUDIO = 1; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_audio_record_app); Button captureBtn = (Button) findViewById( R.id.record_btn); captureBtn.setOnClickListener(new Button.OnClickListener() { public void onClick(View v) { try { Intent audioIntent = new Intent( MediaStore.Audio.Media.RECORD_SOUND_ACTION); startActivityForResult(audioIntent, RECORD_AUDIO); } catch(ActivityNotFoundException e){ Toast.makeText(AudioRecordAppActivity.this, "Wystąpił błąd podczas nagrywania dźwięku", Toast.LENGTH_SHORT).show(); } } }); } protected void onActivityResult(int requestCode, int resultCode, Intent intent) { if (resultCode == RESULT_OK) { if(requestCode == RECORD_AUDIO){ Uri audioUri = intent.getData(); } } } }
Receptura: klasa CamcorderProfile
Jak możesz zauważyć, zdefiniowany został obiekt intencji o nazwie audioIntent z akcją Audio.Media.RECORD_SOUND_ACTION. Intencja ta po wywołaniu nasłuchuje mikrofonu urządzenia. Następnie uruchamiana jest dana aktywność i zwracany wynik jest analizowany przez procedurę obsługi onActivityResult. Przy uruchamianiu aktywności wykorzystywana jest zmienna RECORD_AUDIO w celu identyfikacji i rozróżnienia intencji podczas analizowania zwróconego wyniku. Intencja zwraca jednolity identyfikator zasobu (URI — ang. uniform resource identifier) nagranego pliku audio. System Android automatycznie dodaje nagrany dźwięk do katalogu głównego karty SD. Po uruchomieniu tej aplikacji wyświetlona zostanie kontrolka Button z nagłówkiem Rozpocznij nagrywanie (patrz rysunek 11.4, lewy górny obrazek). Po wybraniu tej kontrolki wywołany zostanie rejestrator dźwięku urządzenia. W zależności od fizycznego urządzenia, wyświetlonych może zostać kilka przycisków umożliwiających rozpoczęcie nagrywania dźwięku, zatrzymanie nagrywania oraz odtworzenia nagranego dźwięku. Na rysunku 11.4. (prawy górny obrazek) widać trzy przyciski umożliwiające wykonanie zadań związanych z nagrywaniem. Po kliknięciu przycisku Rozpocznij nagrywanie (przycisk z kółkiem) urządzenie zainicjuje nagrywanie dźwięku. Wyświetlony zostanie również zegar pokazujący czas nagrywania, co pokazano na rysunku 11.4 (lewy dolny obrazek). Po kliknięciu przycisku Zatrzymaj nagrywanie (ostatni przycisk z kwadratem) użytkownik zostanie zapytany, czy nagrany dźwięk ma zostać zapisany, czy odrzucony. Na rysunku 11.4 (prawy dolny obrazek) widać przyciski Odrzuć i Gotowe, których możesz użyć odpowiednio do odrzucenia lub zapisania nagranego dźwięku. Środkowy przycisk (z trójkątem) możesz wykorzystać do odtworzenia nagranego dźwięku.
Receptura: klasa CamcorderProfile W kolejnych recepturach nauczysz się ręcznie przechwytywać audio i wideo. Podczas tej czynności będziesz musiał dostarczyć wymagane cechy zasadnicze, takie jak żądany styl kodowania audio lub wideo, wymagany format pliku wynikowego, częstotliwość próbkowania oraz czas trwania nagrania. Przed zastosowaniem tych cech w aplikacji Android musisz dowiedzieć się, czy urządzenie, na którym zamierzasz uruchomić swoją aplikację do przechwytywania audio i wideo, obsługuje żądane funkcje. System Android oferuje klasę CamcorderProfile, która pomaga pobrać ustawienia profilu kamery urządzenia. Pozwala to programistom określić i zastosować w danej aplikacji te funkcje, które są obsługiwane przez urządzenie klienckie. W tej recepturze nauczysz się pobierać ustawienia profilu kamery urządzenia i wyświetlać je na ekranie. W tym celu utwórz projekt Android o nazwie CamcorderApp. W tej aplikacji będziesz wyświetlał dwa elementy akcji zatytułowane Wysoka jakość i Niska jakość. Po wybraniu elementu akcji uzyskiwany będzie dostęp do ustawień profilu kamery związanych z danym poziomem jakości i informacje te będą wyświetlane. Kiedy np. wybierzesz element akcji Wysoka jakość, pobrane i wyświetlone zostaną ustawienia profilu kamery odpowiadające najwyższej dostępnej rozdzielczości.
365
366
Rozdział 11. Przechwytywanie audio, wideo i obrazów
Rysunek 11.4. Przycisk Rozpocznij nagrywanie wyświetlony po uruchomieniu aplikacji (lewy górny obrazek). Wywołany rejestrator dźwięku urządzenia (prawy górny obrazek). Rozpoczęcie nagrywania dźwięku po wybraniu przycisku Rozpocznij nagrywanie (lewy dolny obrazek). Okno dialogowe z opcjami zapisania lub odrzucenia nagranego dźwięku wyświetlone po kliknięciu przycisku Zatrzymaj nagrywanie (prawy dolny obrazek)
Receptura: klasa CamcorderProfile
Aby wyświetlić ustawienia profilu kamery, będziesz potrzebował kontrolki TextView. Po zdefiniowaniu tej kontrolki plik układu aktywności activity_camcorder_app.xml będzie wyglądał tak, jak przedstawiono w listingu 11.8. Listing 11.8. Kod wpisany w pliku układu aktywności activity_camcorder_app.xml
Dla celów identyfikacji i wykorzystania w kodzie Java kontrolce TextView przypisano identyfikator profile. Teraz musisz zdefiniować dwa elementy akcji w pliku menu activity_camcorder_app.xml, który znajduje się w folderze res/menu. Aby zdefiniować elementy akcji o tytułach Wysoka jakość i Niska jakość, wpisz w pliku menu activity_camcorder_app.xml kod przedstawiony w listingu 11.9. Listing 11.9. Kod wpisany w pliku menu activity_camcorder_app.xml
Dwóm elementom akcji przypisano odpowiednio wyświetlane tytuły Wysoka jakość i Niska jakość. Ponadto dla celów uzyskiwania dostępu oraz identyfikacji tych elementów akcji przypisano im odpowiednio identyfikatory high_quality_profile oraz low_quality_profile. Teraz musisz napisać kod Java, który będzie wykonywał następujące zadania. Uzyskanie dostępu do kontrolki TextView zdefiniowanej w pliku układu
aktywności i zmapowanie tej kontrolki na obiekt TextView. Wypełnienie lub scalenie menu zdefiniowanego w pliku menu
activity_camcorder_app.xml.
367
368
Rozdział 11. Przechwytywanie audio, wideo i obrazów Zdefiniowanie metody onOptionsItemSelected() do obsługi zdarzeń kliknięcia
elementów akcji. Wywołanie metod klasy CamcorderProfile w celu uzyskania dostępu,
w zależności od wybranego elementu akcji, do formatu kodowania audio lub wideo, przepływności audio lub wideo, liczby kanałów audio, częstotliwości próbkowania audio oraz formatu pliku wynikowego. Wyświetlenie ustawień profilu kamery za pomocą kontrolki TextView.
Aby wykonać wymienione zadania, wpisz w pliku aktywności Java CamcorderAppActivity.java kod przedstawiony w listingu 11.10. Listing 11.10. Kod wpisany w pliku aktywności Java CamcorderAppActivity.java package com.androidtablet.camcorderapp; import import import import import import import import import
android.app.Activity; android.media.CamcorderProfile; android.media.MediaRecorder.AudioEncoder; android.media.MediaRecorder.OutputFormat; android.media.MediaRecorder.VideoEncoder; android.os.Bundle; android.widget.TextView; android.view.Menu; android.view.MenuItem;
public class CamcorderAppActivity extends Activity { private static final int QUALITY_LOW = 0; private static final int QUALITY_HIGH = 1; TextView textProfile; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_camcorder_app); textProfile = (TextView)findViewById(R.id.profile); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.activity_camcorder_app, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.high_quality_profile: ProfileInfo(QUALITY_HIGH); break; case R.id.low_quality_profile: ProfileInfo(QUALITY_LOW); break; default: return super.onOptionsItemSelected(item);
Receptura: klasa CamcorderProfile } return true; } private void ProfileInfo(int quality_type) { CamcorderProfile camcorderProfile = CamcorderProfile.get(quality_type); String selectedProfile=""; String profileInfo; if(quality_type==QUALITY_HIGH) selectedProfile= "WYSOKA_JAKOŚĆ"; else if(quality_type==QUALITY_LOW) selectedProfile="NISKA_JAKOŚĆ"; profileInfo = selectedProfile + " : \n" + camcorderProfile.toString() +"\n"; profileInfo += "Szybkość transmisji dźwięku: " + String.valueOf(camcorderProfile.audioBitRate) +"\n" + "Kanały audio: " + String.valueOf( camcorderProfile.audioChannels) +"\n" + "Kodek audio: " + AudioCodecinString( camcorderProfile.audioCodec) +"\n" + "Częstość próbkowania dźwięku: " + String.valueOf( camcorderProfile.audioSampleRate) +"\n" + "Czas trwania: " + String.valueOf( camcorderProfile.duration) +"\n" + "Format pliku: " + FileFormatinString( camcorderProfile.fileFormat) +"\n" + "Jakość: " + String.valueOf( camcorderProfile.quality) +"\n" + "Szybkość transmisji wideo: " + String.valueOf( camcorderProfile.videoBitRate) +"\n" + "Kodek wideo: " + VideoCodecinString( camcorderProfile.videoCodec) +"\n" + "Częstotliwość odświeżania klatek wideo: " + String.valueOf( camcorderProfile.videoFrameRate) +"\n" + "Szerokość ramki wideo: " + String.valueOf( camcorderProfile.videoFrameWidth) +"\n" + "Wysokość ramki wideo: " + String.valueOf( camcorderProfile.videoFrameHeight); textProfile.setText(profileInfo); } private String AudioCodecinString(int audioCodec){ switch(audioCodec){ case AudioEncoder.AAC: return "AAC"; case AudioEncoder.AAC_ELD: return "AAC_ELD"; case AudioEncoder.AMR_NB: return "AMR_NB"; case AudioEncoder.AMR_WB: return "AMR_WB"; case AudioEncoder.DEFAULT: return "DOMYŚLNY";
369
370
Rozdział 11. Przechwytywanie audio, wideo i obrazów case AudioEncoder.HE_AAC: return "HE_AAC"; default: return "nieznany"; } } private String FileFormatinString(int fileFormat){ switch(fileFormat){ case OutputFormat.AAC_ADTS: return "AAC_ADTS"; case OutputFormat.AMR_NB: return "AMR_NB"; case OutputFormat.AMR_WB: return "AMR_WB"; case OutputFormat.DEFAULT: return "DOMYŚLNY"; case OutputFormat.MPEG_4: return "MPEG_4"; case OutputFormat.THREE_GPP: return "TRZY_GPP"; default: return "nieznany"; } } private String VideoCodecinString(int videoCodec){ switch(videoCodec){ case VideoEncoder.H263: return "H263"; case VideoEncoder.H264: return "H264"; case VideoEncoder.MPEG_4_SP: return "MPEG_4_SP"; case VideoEncoder.DEFAULT: return "DOMYŚLNY"; default: return "nieznany"; } } }
Po uruchomieniu tej aplikacji na ekranie nie będzie nic widać, ale w pasku zadań wyświetlone zostaną elementy akcji Wysoka jakość i Niska jakość (patrz rysunek 11.5, lewy górny obrazek). Po kliknięciu elementu Wysoka jakość możesz uzyskać dostęp do ustawień profilu kamery odpowiadających najwyższej dostępnej rozdzielczości, tak jak pokazano na rysunku 11.5 (prawy górny obrazek). Analogicznie po kliknięciu elementu akcji Niska jakość wyświetlone zostaną ustawienia profilu kamery odpowiadające najniższej dostępnej rozdzielczości, tak jak pokazano na rysunku 11.5 (dolny obrazek).
Receptura: klasa CamcorderProfile
Rysunek 11.5. Dwa elementy akcji, Wysoka jakość i Niska jakość, wyświetlone po uruchomieniu aplikacji (lewy górny obrazek). Wyświetlone informacje o profilu kamery odpowiadające najwyższej rozdzielczości (prawy górny obrazek). Wyświetlone informacje o profilu kamery odpowiadające najniższej rozdzielczości (dolny obrazek)
371
372
Rozdział 11. Przechwytywanie audio, wideo i obrazów
Receptura: klasa MediaRecorder i jej metody Klasa MediaRecorder jest wykorzystywana do nagrywania audio i wideo. Metody tej klasy użyte do konfiguracji rejestratora audio i wideo zostały pokrótce opisane w tabeli 11.1. Tabela 11.1. Krótki opis metod klasy MediaRecorder wykorzystywanych do konfiguracji rejestratora
Metoda
Opis
void setAudioSource(int źródło_audio)/void setVideoSource(int źródło_wideo)
Ustawia źródło audio i wideo. Jeśli nie zastosujesz tej metody, ścieżka audio nie zostanie nagrana w pliku wyjściowym. Stałe używane dla parametru źródło_audio to CAMCORDER, DEFAULT, MIC, VOICE_CALL, VOICE_COMMUNICATION, VOICE_DOWNLINK, VOICE_RECOGNITION oraz VOICE_UPLINK. Stałe stosowane dla parametru źródło_wideo to CAMERA oraz DEFAULT.
void setOutputFormat (int format_wyjścia)
Ustawia format pliku wyjściowego tworzonego podczas nagrywania. Stałe stosowane dla parametru format_wyjścia to AAC_ADTS, AMR_NB, AMR_WB, DEFAULT, MPEG_4 oraz THREE_GPP.
void setAudioEncoder (int koder_audio) /setVideoEncoder (int koder_wideo)
Wywoływana po metodzie setOutputFormat(). Ustawia koder audio lub wideo, który ma być wykorzystany przy nagrywaniu. Jeśli nie zastosujesz tej metody, ścieżka audio lub wideo nie zostanie nagrana. Stałe używane dla parametru koder_wideo to DEFAULT, H263, H264 oraz MPEG_4_SP. Stałe stosowane dla parametru koder_audio to AAC, AAC_ELD, AMR_NB, AMR_WB, DEFAULT oraz HE_AAC.
void setOutputFile (String ścieżka)
Ustawia ścieżkę dla pliku wyjściowego.
void setPreviewDisplay (Surface widok_ powierzchni)
Ustawia powierzchnię, która ma pokazywać podgląd nagranego pliku wideo.
void prepare()
Przygotowuje rejestrator do rozpoczęcia przechwytywania i kodowania audio lub wideo. Metoda ta jest wywoływana po ustawieniu źródła, formatu wyjściowego, stylu kodowania oraz pliku wyjściowego.
void start()
Inicjuje zadanie przechwytywania i kodowania audio lub wideo oraz zapisuje audio lub wideo w pliku określonym za pomocą metody setOutputFile().
void stop()
Zatrzymuje sesję nagrywania.
void release()
Uwalnia zasoby powiązane z obiektem MediaRecorder.
Receptura: nagrywanie audio z wykorzystaniem kodu Java
Receptura: nagrywanie audio z wykorzystaniem kodu Java W tej recepturze nauczysz się pisać bezpośrednio kod wykorzystujący klasę MediaRecorder i wywołujący jej różne metody, które są wymagane do nagrywania audio w określonym formacie wyjściowym i stylu kodowania. Aby nagrać dźwięk za pomocą klasy MediaRecorder, postępuj według następujących instrukcji. 1. Utwórz obiekt klasy MediaRecorder. 2. Wywołaj metodę setAudioSource w celu ustawienia źródła audio. 3. Wywołaj metodę setOutputFormat w celu ustawienia formatu wyjściowego. 4. Wywołaj metodę setAudioEncoder w celu ustawienia kodera audio. 5. Wywołaj metodę setOutputFile w celu ustawienia pliku wynikowego. 6. Wywołaj metodę prepare w celu przygotowania rejestratora. 7. Wywołaj metodę start, aby rozpocząć nagrywanie. 8. Wywołaj metodę stop, aby zatrzymać nagrywanie. 9. Wywołaj metodę release, aby uwolnić zasoby przypisane do obiektu klasy MediaRecorder.
Utwórz projekt Android o nazwie AudioCaptureApp. W tej aplikacji będziesz używał dwóch elementów akcji o tytułach odpowiednio Rozpocznij nagrywanie i Zatrzymaj nagrywanie. Użytkownik może rozpocząć nagrywanie dźwięku, klikając element akcji Rozpocznij nagrywanie, a następnie zatrzymać nagrywanie dźwięku po kliknięciu Zatrzymaj nagrywanie. Nagrany dźwięk zostanie zapisany na karcie SD urządzenia i pojawi się na jego liście muzyki. Aby wskazać użytkownikowi, kiedy kliknąć elementy akcji Rozpocznij nagrywanie i Zatrzymaj nagrywanie, musisz wyświetlić na ekranie odpowiednie komunikaty tekstowe. Do ich wyświetlenia wykorzystasz kontrolkę TextView. Żeby zdefiniować kontrolkę TextView, wpisz w pliku układu aktywności activity_audio_capture_app.xml kod przedstawiony w listingu 11.11. Listing 11.11. Kod wpisany w pliku układu aktywności activity_audio_capture_app.xml
373
374
Rozdział 11. Przechwytywanie audio, wideo i obrazów android:textSize="@dimen/text_size" android:textStyle="bold" />
Dla celów identyfikacji i wykorzystania w kodzie Java kontrolce TextView przydzielono identyfikator textview. Kontrolka TextView jest inicjowana w celu wyświetlenia komunikatu Naciśnij przycisk 'Rozpocznij nagrywanie', aby rozpocząć nagrywanie dźwięku. Komunikat ten ma być prezentowany pogrubioną czcionką o rozmiarze określonym w zasobie wymiarów text_size. Zdefiniuj dwa elementy akcji w pliku menu activity_audio_capture_app.xml, który znajduje się w folderze res/menu. Aby zdefiniować elementy akcji o tytułach Rozpocznij nagrywanie i Zatrzymaj nagrywanie, wpisz w pliku menu activity_audio_capture_app.xml kod przedstawiony w listingu 11.12. Listing 11.12. Kod wpisany w pliku menu activity_audio_capture_app.xml
Dwa zdefiniowane elementy akcji będą wyświetlać tytuły odpowiednio Rozpocznij nagrywanie i Zatrzymaj nagrywanie. Ponadto dla celów uzyskiwania dostępu i identyfikacji tych elementów akcji przypisano im identyfikatory start_recording_button oraz stop_recording_button. Teraz musisz napisać kod Java, który będzie wykonywał następujące zadania. Uzyskanie dostępu do kontrolki TextView zdefiniowanej w pliku układu
aktywności i zmapowanie tej kontrolki na obiekt TextView. Wypełnienie lub scalenie menu zdefiniowanego w pliku menu
activity_audio_capture_app.xml. Zdefiniowanie metody onOptionsItemSelected() do obsługi zdarzeń kliknięcia
elementów akcji. Zdefiniowanie funkcji startRecording() i stopRecording(), które będą
wykonywać zadania związane z operacjami nagrywania dźwięku i zatrzymywania nagrywania dźwięku. Skonfigurowanie elementów akcji Rozpocznij nagrywanie i Zatrzymaj
nagrywanie, aby wywoływały funkcje startRecording() i stopRecording(). Zdefiniowanie nazwy pliku, pod którą nagrany dźwięk ma być zapisywany
na karcie SD.
Receptura: nagrywanie audio z wykorzystaniem kodu Java Zdefiniowanie obiektu klasy MediaRecorder i wywołanie jego metod do
zdefiniowania źródła audio, typu formatu audio, stylu kodowania oraz częstotliwości próbkowania. Wywołanie metod start() i stop() klasy MediaRecorder w celu rozpoczęcia
i zatrzymania nagrywania dźwięku, kiedy kliknięty zostanie odpowiedni element akcji.
Uwaga Jeśli urządzenie posiada kartę SD, jej zwyczajowa ścieżka to
android.os.Environment.getExternalStorageDirectory().getPath().
Aby wykonać wszystkie wymienione zadania, wpisz w pliku aktywności Java AudioCaptureAppActivity.java kod przedstawiony w listingu 11.13. Listing 11.13. Kod wpisany w pliku aktywności Java AudioCaptureAppActivity.java package com.androidtablet.audiocaptureapp; import import import import import import import import import import import import
java.io.IOException; android.os.Bundle; android.app.Activity; android.view.Menu; android.view.MenuItem; android.widget.TextView; android.media.MediaRecorder; android.media.MediaRecorder.AudioSource; android.media.MediaRecorder.OutputFormat; android.media.MediaRecorder.AudioEncoder; java.io.File; android.os.Environment;
public class AudioCaptureAppActivity extends Activity { private MediaRecorder mediaRecorder = null; private File audioFile = null; private TextView textView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_audio_capture_app); textView = (TextView)findViewById(R.id.textview); audioFile = new File(Environment. getExternalStorageDirectory(), "testaudio.3gp"); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.activity_audio_capture_app, menu); return true; }
#1
#2
375
376
Rozdział 11. Przechwytywanie audio, wideo i obrazów @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.start_recording_button: startRecording(audioFile); break; case R.id.stop_recording_button: stopRecording(); break; default: return super.onOptionsItemSelected(item); } return true; } private void startRecording(File file) { if (mediaRecorder != null) mediaRecorder.release(); mediaRecorder = new MediaRecorder(); mediaRecorder.setAudioSource(AudioSource.MIC); mediaRecorder.setOutputFormat(OutputFormat. THREE_GPP); mediaRecorder.setAudioEncoder(AudioEncoder. AMR_WB); mediaRecorder.setOutputFile(file. getAbsolutePath()); try { mediaRecorder.prepare(); mediaRecorder.start(); textView.setText("Rozpocząłeś nagrywanie. Naciśnij przycisk 'Zatrzymaj nagrywanie', aby zatrzymać."); } catch (IOException e) { e.printStackTrace(); } } private void stopRecording() { if (mediaRecorder != null) { mediaRecorder.stop(); mediaRecorder.release(); mediaRecorder = null; textView.setText("Nagrałeś dźwięk."); } }
#3 #4
#5 #6 #7 #8 #9 #10
#11 #12
}
Niektóre z instrukcji wykorzystanych w listingu 11.13 wymagają pewnego objaśnienia. Poniższa lista stanowi omówienie zastosowania i znaczenia tych instrukcji. Instrukcja #1 definiuje nazwę pliku, pod którą nagrany dźwięk ma zostać
zapisany na karcie SD. Nagrany dźwięk zostanie zapisany pod nazwą testaudio.3gp. Instrukcja #2 wypełnia plik menu zdefiniowany w folderze res/menu w celu
wyświetlenia elementów menu (elementów akcji), które są w nim zdefiniowane.
Receptura: nagrywanie audio z wykorzystaniem kodu Java
Chodzi o dwa elementy akcji, Rozpocznij nagrywanie i Zatrzymaj nagrywanie, zdefiniowane w pliku menu activity_audio_capture_app.xml, które będą wyświetlone w pasku akcji. Funkcja startRecording() jest wywoływana (instrukcja #3), kiedy użytkownik
wybierze element akcji Rozpocznij nagrywanie. Nazwa pliku wyjściowego, pod którą zostanie zapisany plik audio, jest przekazywana do tej funkcji jako parametr. Po wywołaniu funkcja startRecording() rozpocznie nagrywanie dźwięku. Funkcja stopRecording() jest wywoływana (instrukcja #4) w celu zatrzymania
nagrywania dźwięku, kiedy użytkownik wybierze element akcji Zatrzymaj nagrywanie. Źródło audio dla nagrywania jest ustawiane na AudioSource.MIC (instrukcja #5).
Oznacza to, że jako źródło nagrywania dźwięku wybrany jest mikrofon urządzenia. Instrukcje #6 i #7 definiują odpowiednio format wyjściowego pliku audio oraz
styl kodowania audio. Instrukcja #8 definiuje nazwę pliku audio, pod którą nagrany dźwięk zostanie
zapisany. Instrukcja #9 przygotowuje rejestrator dźwięku do nagrywania. Instrukcja #10 rozpoczyna nagrywanie dźwięku. Instrukcja #11 zatrzymuje nagrywanie dźwięku. Instrukcja #12 uwalnia zasoby alokowane dla obiektu MediaRecorder.
Aby aplikacja mogła nagrywać dźwięk i zapisywać utworzony plik audio na karcie SD, potrzebujesz określonych zezwoleń. Poniższe dwie instrukcje zostały dodane do pliku AndroidManifest.xml, aby zapewnić zezwolenia na nagrywanie dźwięku i zapisywanie nagranego pliku audio w przestrzeni do przechowywania danych urządzenia:
Jeśli te zezwolenia nie zostaną dodane do pliku manifestu, aplikacja zwróci wyjątek RuntimeException.
Po uruchomieniu tej aplikacji wyświetlony zostanie komunikat Naciśnij przycisk 'Rozpocznij nagrywanie', aby rozpocząć nagrywanie dźwięku. Ponadto w pasku akcji pojawią się dwa elementy akcji: Rozpocznij nagrywanie i Zatrzymaj nagrywanie (patrz rysunek 11.6, lewy górny obrazek). Kiedy klikniesz element akcji Rozpocznij nagrywanie, uruchomisz nagrywanie dźwięku. Komunikat zmieni się wtedy na Rozpocząłeś nagrywanie. Naciśnij przycisk 'Zatrzymaj nagrywanie', aby zatrzymać (patrz rysunek 11.6, prawy górny obrazek). Kiedy wybierzesz element akcji Zatrzymaj nagrywanie, nagrywanie zostanie przerwane. Komunikat tekstowy zmieni się na Nagrałeś dźwięk, co wskazuje zakończenie nagrywania audio, tak jak pokazano na rysunku 11.6 (dolny obrazek).
377
378
Rozdział 11. Przechwytywanie audio, wideo i obrazów
Rysunek 11.6. Wyświetlony po uruchomieniu aplikacji komunikat tekstowy informujący o zastosowaniu dwóch widocznych elementów akcji (lewy górny obrazek). Po wybraniu elementu akcji Rozpocznij nagrywanie zmienia się komunikat tekstowy i uruchamiane jest nagrywanie dźwięku (prawy górny obrazek). Nagrywanie dźwięku zostaje zatrzymane po wybraniu elementu akcji Zatrzymaj nagrywanie (dolny obrazek)
Receptura: nagrywanie wideo za pomocą wbudowanej intencji
Na playliście urządzenia pojawia się plik testaudio, co potwierdza udane nagranie dźwięku i zapisanie pliku na karcie SD (patrz rysunek 11.7, lewy obrazek). Wybranie pliku testaudio powoduje jego odtworzenie, tak jak pokazano na rysunku 11.7 (prawy obrazek).
Rysunek 11.7. Playlista urządzenia pokazuje nagrany plik audio (lewy obrazek). Odtwarzanie nagranego pliku audio (prawy obrazek)
Receptura: nagrywanie wideo za pomocą wbudowanej intencji Istnieją dwa sposoby nagrywania wideo w aplikacjach Android. W pierwszej metodzie nagrywania wideo wykorzystujesz klasę MediaRecorder
systemu Android i wywołujesz jej różne metody w celu uzyskania dostępu kamery urządzenia, nagrania wideo w pożądanym formacie i stylu kodowania oraz zapisania pliku w przestrzeni przechowywania danych urządzenia. W drugiej, łatwiejszej metodzie definiujesz i wywołujesz intencję wykorzystującą
akcję MediaStore.ACTION_VIDEO_CAPTURE, która automatycznie nagrywa, pauzuje, odtwarza i zapisuje wideo. W tej recepturze będziesz korzystał z drugiej metody nagrywania wideo (czyli wywołania intencji z określonym atrybutem MediaStore). W kolejnej recepturze nauczysz się nagrywać wideo za pomocą klasy MediaRecorder.
379
380
Rozdział 11. Przechwytywanie audio, wideo i obrazów
Utwórz projekt Android o nazwie VideoRecordApp. W tej aplikacji będziesz korzystał z kontrolek Button i VideoView. Kliknięcie kontrolki Button spowoduje wywołanie kamery urządzenia. Po zakończeniu nagrywania wideo zostanie odtworzone w kontrolce VideoView. Aby zdefiniować kontrolki Button i VideoView, wpisz w pliku układu aktywności activity_video_record_app.xml kod przedstawiony w listingu 11.14. Listing 11.14. Kod wpisany w pliku układu aktywności activity_video_record_app.xml
Dla celów identyfikacji i wykorzystania w kodzie Java kontrolkom Button i VideoView przypisano odpowiednio identyfikatory record_video_btn i videoview. Nagłówek wyświetlany dla kontrolki Button to Uruchom nagrywanie wideo. Teraz musisz napisać kod Java, który będzie wykonywał następujące zadania. Uzyskanie dostępu do kontrolek Button oraz VideoView i zmapowanie tych
kontrolek na odpowiednie obiekty. Powiązanie nasłuchiwacza setOnClickListener z kontrolką Button w celu
nasłuchiwania zdarzeń kliknięcia. Kiedy na kontrolce Button wystąpi zdarzenie kliknięcia, wywołana zostanie metoda wywołania zwrotnego onClick. Zdefiniowanie intencji o nazwie videoIntent, która odwołuje się do
wbudowanej aktywności MediaStore.ACTION_VIDEO_CAPTURE. Ta wbudowana aktywność odpowiada za uruchomienie kamery urządzenia i zainicjowanie nagrywania wideo. Uruchomienie danej aktywności po kliknięciu kontrolki Button. Umożliwi
to użytkownikowi nagranie pliku wideo. Analiza kodu wynikowego po zakończeniu uruchomionej aktywności, a to
pozwala sprawdzić, czy zadanie nagrywania zostało wykonane z powodzeniem. Jeśli nagrywanie wideo się powiodło, uzyskiwany jest dostęp do nagrania i jest
ono odtwarzane za pomocą kontrolki VideoView. Aby wykonać wymienione zadania, wpisz w pliku aktywności Java VideoRecordAppActivity.java kod przedstawiony w listingu 11.15.
Receptura: nagrywanie wideo za pomocą wbudowanej intencji Listing 11.15. Kod wpisany w pliku aktywności Java VideoRecordAppActivity.java package com.androidtablet.videorecordapp; import import import import import import import import import import
android.os.Bundle; android.app.Activity; android.widget.Button; android.view.View; android.provider.MediaStore; android.content.Intent; android.content.ActivityNotFoundException; android.widget.Toast; android.net.Uri; android.widget.VideoView;
public class VideoRecordAppActivity extends Activity { VideoView videoView; final int RECORD_VIDEO = 1; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_video_record_app); Button videoCaptureBtn = (Button) findViewById( R.id.record_video_btn); videoView = (VideoView) findViewById(R.id.videoview); videoCaptureBtn.setOnClickListener(new Button.OnClickListener() { public void onClick(View v) { try { Intent videoIntent = new Intent(MediaStore. ACTION_VIDEO_CAPTURE); startActivityForResult(videoIntent, RECORD_VIDEO); } catch(ActivityNotFoundException anfe){ Toast.makeText(VideoRecordAppActivity.this, "Wystąpił błąd podczas nagrywania wideo", Toast.LENGTH_SHORT).show(); } } }); } protected void onActivityResult(int requestCode, int resultCode, Intent intent) { if (resultCode == RESULT_OK) { if(requestCode == RECORD_VIDEO ){ Uri videoUri = intent.getData(); videoView.setVideoURI(videoUri); videoView.start(); } } } }
381
382
Rozdział 11. Przechwytywanie audio, wideo i obrazów
Jak możesz zauważyć, obiekt intencji o nazwie videoIntent jest definiowany wraz z akcją MediaStore.ACTION_VIDEO_CAPTURE. Wywołanie tej intencji powoduje aktywowanie kamery urządzenia. Następnie uruchamiana jest dana aktywność, a zwracany wynik jest analizowany z wykorzystaniem procedury obsługi onActivityResult. Przy uruchamianiu aktywności wykorzystywana jest zmienna RECORD_VIDEO w celu identyfikacji intencji podczas analizowania zwracanych przez nią wyników. Intencja zwraca identyfikator URI nagranego wideo, który jest następnie przypisywany do kontrolki VideoView w celu odtworzenia nagrania. System Android automatycznie dodaje nagrane wideo do katalogu głównego karty SD. Domyślna lokalizacja może różnić się w zależności od urządzenia. Po uruchomieniu tej aplikacji wyświetlona zostanie kontrolka Button z nagłówkiem Uruchom nagrywanie wideo (patrz rysunek 11.8, lewy górny obrazek). Wybranie tej kontrolki wywołuje kamerę wyświetlającą podgląd obrazu. Po prawej stronie podglądu wyświetlany jest przycisk Rozpocznij nagrywanie wideo (z czerwonym kółkiem), tak jak pokazano na rysunku 11.8 (prawy górny obrazek). Kiedy klikniesz ten przycisk, rozpocznie się nagrywanie wideo. W lewym dolnym rogu ekranu widoczny będzie zegar nagrywania. Po prawej stronie pojawi się przycisk Zatrzymaj nagrywanie wideo (z czerwonym kwadratem), tak jak pokazano na rysunku 11.8 (lewy dolny obrazek). Po wybraniu tego przycisku nagrane wideo zostanie zapisane w przestrzeni przechowywania danych urządzenia i odtworzone w kontrolce VideoView (patrz rysunek 11.8, prawy dolny obrazek), co potwierdza, że wideo zostało nagrane i z powodzeniem zapisane.
Receptura: nagrywanie wideo z użyciem kodu Java W tej recepturze nauczysz się pisać kod Java wykorzystujący klasę MediaRecorder i wywołujący różne jej metody, które są wymagane do nagrania wideo w żądanym formacie wyjściowym i stylu kodowania. Do obsługi wyświetlania zastosujesz klasę SurfaceView oraz interfejs SurfaceHolder.Callback. Widok SurfaceView to specjalny widok, który zapewnia powierzchnię rysowania w ramach hierarchii widoków. Optymalizuje on grafikę, wykorzystując niezależny wątek do rysowania i aktualizacji widoku. Interfejs SurfaceHolder zapewnia powierzchnię do pracy (czyli przechowuje obiekt SurfaceView, co pozwala kontrolować rozmiar, edycję i format powierzchni oraz ją monitorować). Aby nagrać wideo za pomocą klasy MediaRecorder, wykonaj następujące czynności. 1. Utwórz obiekt klasy MediaRecorder. 2. Wywołaj metodę setAudioSource w celu ustawienia źródła audio. 3. Wywołaj metodę setVideoSource w celu ustawienia źródła wideo. 4. Wywołaj metodę setOutputFormat w celu ustawienia wyjściowego formatu wideo. 5. Wywołaj metodę setVideoFrameRate w celu ustawienia liczby klatek na sekundę
dla nagrywanego wideo.
Receptura: nagrywanie wideo z użyciem kodu Java
Rysunek 11.8. Przycisk Uruchom nagrywanie wideo wyświetlony po uruchomieniu aplikacji (lewy górny obrazek). Wywołany rejestrator wideo urządzenia wyświetlający podgląd obrazu oraz przycisk Rozpocznij nagrywanie wideo widoczny po prawej stronie (prawy górny obrazek). Nagrywanie wideo rozpoczęte po wybraniu przycisku Rozpocznij nagrywanie wideo (lewy dolny obrazek). Nagrane wideo odtwarzane w kontrolce VideoView aplikacji (prawy dolny obrazek)
6. Wywołaj metodę setVideoEncoder w celu ustawienia kodera wideo. 7. Wywołaj metodę setAudioEncoder w celu ustawienia kodera audio. 8. Wywołaj metodę setOutputFile w celu ustawienia pliku wyjściowego. 9. Wywołaj metodę setMaxDuration w celu ustawienia w milisekundach
maksymalnego czasu trwania dla nagrywanego wideo. 10. Wywołaj metodę setMaxFileSize w celu ustawienia w bajtach maksymalnego
rozmiaru pliku dla nagrywanego wideo. 11. Wywołaj metodę setPreviewDisplay w celu ustawienia powierzchni
do wyświetlania podglądu nagrywanego wideo. 12. Wywołaj metodę prepare, aby przygotować rejestrator. 13. Wywołaj metodę start, aby rozpocząć nagrywanie wideo. 14. Wywołaj metodę stop, aby zatrzymać nagrywanie wideo. 15. Wywołaj metodę release, aby uwolnić zasoby powiązane z obiektem klasy MediaRecorder.
383
384
Rozdział 11. Przechwytywanie audio, wideo i obrazów
Utwórz projekt Android o nazwie VideoCaptureApp. Aplikacja ta będzie automatycznie rozpoczynać nagrywanie wideo po uruchomieniu. Nagrywanie będzie automatycznie zatrzymywane po upływie 10 sekund. Nagrane wideo zostanie zapisane na karcie SD urządzenia i będzie widoczne w jego galerii. Jak już wcześniej wspomniano, żeby wyświetlić podgląd nagrywania wideo, wykorzystasz widok SurfaceView. Aby zdefiniować ten widok, wpisz w pliku układu aktywności activity_video_capture_app.xml kod przedstawiony w listingu 11.16. Listing 11.16. Kod wpisany w pliku układu aktywności activity_video_capture_app.xml
Dla celów identyfikacji i wykorzystania w kodzie Java kontrolce SurfaceView przypisano identyfikator videoview. Teraz napisz kod Java wykonujący następujące zadania. Uzyskanie dostępu do kontrolki SurfaceView zdefiniowanej w pliku układu
aktywności i zmapowanie jej na obiekt SurfaceView. Za pomocą tej kontrolki będziesz wyświetlał podgląd z kamery, aby widzieć, co się nagrywa. Ustaw klasę aktywności, aby impelemtowała interfejs SurfaceHolder.Callback
oraz nasłuchiwacze OnInfoListener i OnErrorListener. Wywołanie zwrotne SurfaceHolder jest wykorzystywane do powiadomienia powiązanego widoku, kiedy bazowa powierzchnia jest tworzona, niszczona lub modyfikowana. OnInfoListener i OnErrorListener to nasłuchiwacze zdarzeń służące do nasłuchiwania aktualizacji obiektu MediaRecorder oraz błędów, jeśli takie pojawią się w trakcie nagrywania wideo. Zaimplementowanie trzech metod: surfaceCreated(SurfaceHolder),
surfaceDestroyed(SurfaceHolder) oraz surfaceChanged(SurfaceHolder holder, int format, int w, int h).
surfaceCreated(SurfaceHolder) informuje, kiedy powierzchnia jest
tworzona i staje się dostępna dla nagrywania wideo. surfaceDestroyed(SurfaceHolder) informuje, kiedy powierzchnia jest
niszczona. surfaceChanged(SurfaceHolder holder, int format, int w, int h)
informuje o zmianach w strukturze powierzchni, takich jak zmiana szerokości lub wysokości albo zmiana orientacji urządzenia.
Receptura: nagrywanie wideo z użyciem kodu Java Zdefiniowanie obiektu klasy MediaRecorder oraz powiązanie z nim
nasłuchiwaczy setOnInfoListener i setOnErrorListener w celu pozyskiwania aktualizacji MediaRecorder i nasłuchiwania błędów, aby podjąć odpowiednie akcje. Zdefiniowanie nazwy pliku, pod którą nagrane wideo zostanie zapisane na
karcie SD. Zdefiniowanie źródła audio, źródła wideo, formatu wyjściowego, liczby klatek
na sekundę dla wideo oraz atrybutów kodowania audio i wideo. Ustawienie czasu trwania nagrywanego wideo oraz maksymalnego rozmiaru
pliku wideo. Wywołanie metody start() klasy MediaRecorder w celu rozpoczęcia
nagrywania wideo po uruchomieniu aplikacji. Wywołanie metody stop() klasy MediaRecorder w celu zakończenia nagrywania
wideo po upływie 10 sekund. Uwolnienie i zresetowanie obiektu MediaRecorder po zakończeniu nagrywania
wideo. Sprawdzenie, czy wystąpiły błędy i, jeśli się pojawiły, wyświetlenie ich na ekranie.
Aby wykonać wymienione zadania, wpisz w pliku aktywności Java VideoCaptureAppActivity.java kod przedstawiony w listingu 11.17.
Uwaga Widok SurfaceView posiada w ramach hierarchii widoków dedykowaną powierzchnię rysowania. Rysuje i aktualizuje widoki, wykorzystując wątki działające w tle, a zatem odciąża wątek GUI i umożliwia mu wykonywanie innych zadań aplikacji. SurfaceView kontroluje format powierzchni, jej rozmiar oraz lokalizację na ekranie. Listing 11.17. Kod wpisany w pliku aktywności Java VideoCaptureAppActivity.java package com.androidtablet.videocaptureapp; import java.io.IOException; import import import import import import import import import import
android.app.Activity; android.media.CamcorderProfile; android.media.MediaRecorder; android.os.Bundle; android.view.SurfaceHolder; android.view.SurfaceView; android.widget.Toast; android.os.Environment; android.media.MediaRecorder.OnInfoListener; android.media.MediaRecorder.OnErrorListener;
public class VideoCaptureAppActivity extends Activity implements SurfaceHolder.Callback, OnInfoListener,
385
386
Rozdział 11. Przechwytywanie audio, wideo i obrazów OnErrorListener { private MediaRecorder mediaRecorder; private SurfaceHolder surfaceHolder; private SurfaceView surfaceView; private String outputFile; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mediaRecorder = new MediaRecorder(); mediaRecorder.setOnInfoListener(this); mediaRecorder.setOnErrorListener(this); initMediaRecorder(); setContentView(R.layout.activity_video_capture_app); surfaceView = (SurfaceView) findViewById( R.id.videoview); surfaceHolder = surfaceView.getHolder(); surfaceHolder.addCallback(this); } @Override protected void onPause() { super.onPause(); releaseMediaRecorder(); } @Override protected void onResume() { super.onResume(); } @Override protected void onDestroy() { super.onDestroy(); } private void releaseMediaRecorder(){ if (mediaRecorder != null) { mediaRecorder.reset(); mediaRecorder.release(); mediaRecorder = null; } } @Override public void onInfo(MediaRecorder mr, int what, int extra) { if(what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED) { try { mediaRecorder.stop(); } catch(IllegalStateException e) { } releaseMediaRecorder(); Toast.makeText(this, "Przekroczyłeś limit nagrywania. Następuje zatrzymanie procesu nagrywania.", Toast.LENGTH_SHORT).show(); finish();
Receptura: nagrywanie wideo z użyciem kodu Java } } @Override public void onError(MediaRecorder mr, int what, int extra) { try { mediaRecorder.stop(); } catch(IllegalStateException e) { } releaseMediaRecorder(); Toast.makeText(this, "Wystąpił błąd podczas nagrywania. Następuje zatrzymanie procesu nagrywania.", Toast.LENGTH_SHORT). show(); finish(); } private void initMediaRecorder(){ outputFile = Environment. getExternalStorageDirectory().getPath() + "/myvideo.mp4"; mediaRecorder.setAudioSource(MediaRecorder. AudioSource.CAMCORDER); mediaRecorder.setVideoSource(MediaRecorder. VideoSource.CAMERA); mediaRecorder.setOutputFormat(MediaRecorder. OutputFormat.MPEG_4); mediaRecorder.setVideoFrameRate(20); mediaRecorder.setVideoEncoder(MediaRecorder. VideoEncoder.MPEG_4_SP); mediaRecorder.setAudioEncoder(MediaRecorder. AudioEncoder.AMR_NB); mediaRecorder.setOutputFile(outputFile); mediaRecorder.setMaxDuration(10000); mediaRecorder.setMaxFileSize(5000000); } private void prepareMediaRecorder() { mediaRecorder.setPreviewDisplay(surfaceHolder. getSurface()); try { mediaRecorder.prepare(); } catch (IllegalStateException e) { e.printStackTrace(); finish(); } catch (IOException e) { e.printStackTrace(); finish(); } } @Override public void surfaceCreated(SurfaceHolder holder) { prepareMediaRecorder(); mediaRecorder.start(); } @Override public void surfaceChanged(SurfaceHolder holder, int
#1 #2 #3 #4
387
388
Rozdział 11. Przechwytywanie audio, wideo i obrazów format, int weight, int height) { } @Override public void surfaceDestroyed(SurfaceHolder holder) { } }
Możesz również użyć klasy CamcorderProfile do zdefiniowania różnych ustawień wideo. Możesz np. instrukcje #1, #2, #3 i #4 z listingu 11.17 zastąpić następującą instrukcją: mediaRecorder.setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_HIGH));
Aby aplikacja mogła uzyskiwać dostęp do kamery sprzętowej, nagrywać wideo i zapisywać nagrany plik na karcie SD, dodaj do pliku AndroidManifest.xml następujące instrukcje zezwoleń:
Teraz Twoja aplikacja jest gotowa do uruchomienia. Po jej uruchomieniu automatycznie rozpocznie się nagrywanie wideo (patrz rysunek 11.9, lewy obrazek). Możesz skierować kamerę urządzenia w dowolną stronę, aby nagrać wybrane widoki. Nagrywanie wideo zostanie zatrzymane automatycznie po upływie 10 sekund i zobaczysz komunikat Przekroczyłeś limit nagrywania. Następuje zatrzymanie procesu nagrywania, który informuje o zakończeniu nagrywania wideo. Nagrane wideo pojawi się w galerii urządzenia w albumie sdcard0, tak jak pokazano na rysunku 11.9 (prawy obrazek).
Rysunek 11.9. Nagrywanie wideo rozpoczyna się od razu po uruchomieniu aplikacji (lewy obrazek). Nagrane wideo wyświetlone w galerii urządzenia (prawy obrazek)
Podsumowanie
Podsumowanie W tym rozdziale nauczyłeś się przechwytywać obraz oraz nagrywać audio i wideo. Nauczyłeś się robić to ręcznie oraz przez wywoływanie wbudowanych intencji z predefiniowanymi akcjami. Poznałeś różne klasy i ich metody, które wymagane są w celu uzyskania dostępu do kamery i mikrofonu urządzenia. Mogłeś przeczytać o różnych rodzajach kodowania i formatach wyjściowych, które mogą być zastosowane do nagrywania audio i wideo. Dowiedziałeś się również, w jaki sposób uzyskać dostęp do informacji profilowych kamery. W następnym rozdziale skoncentrujemy się na łączeniu urządzeń przy użyciu sieci bezprzewodowej. Nauczysz się łączyć dwa urządzenia za pomocą technologii Bluetooth. Nauczysz się również z wykorzystaniem tej technologii przesyłać pliki pomiędzy dwoma urządzeniami. Na koniec dowiesz się, jak połączyć urządzenia i przesyłać dane za pomocą technologii Wi-Fi Direct.
389
390
Rozdział 11. Przechwytywanie audio, wideo i obrazów
Część IV Interfejs sieciowy i sprzętowy
12 Łączność bezprzewodowa B
luetooth i Wi-Fi to dwa główne standardy technologii bezprzewodowej. Bluetooth jest standardem stosowanym do wymiany danych na krótkich dystansach bez użycia kabli. Wykorzystuje się w nim transmisje radiowe o małej mocy do łączenia telefonów, komputerów i innych urządzeń sieciowych. Wi-Fi też jest technologią sieci bezprzewodowych, która jest łatwa w konfiguracji i dość niedroga. Nie wymaga żadnego fizycznego połączenia pomiędzy nadawcą i odbiorcą, ale wykorzystuje fale radiowe do utworzenia sieci bezprzewodowej, w tym zapewnienia szybkiego łącza internetowego. W tym rozdziale nauczysz się, w jaki sposób powiązać dwa urządzenia Bluetooth, ręcznie przesyłać pliki z jednego urządzenia na drugie za pomocą połączenia Bluetooth oraz łączyć urządzenie Bluetooth z komputerem na platformie Windows. Nauczysz się także włączać lokalne urządzenie Bluetooth, wyświetlać listę powiązanych urządzeń oraz poznasz klasy i metody wymagane do przesyłania plików za pomocą technologii Bluetooth. Ponadto zapoznasz się z technologią Wi-Fi i dowiesz się, jak włączać i wyłączać Wi-Fi.
Receptura: wiązanie ze sobą dwóch urządzeń Bluetooth Bluetooth jest protokołem przeznaczonym dla komunikacji peer-to-peer krótkiego zasięgu i o niskiej przepustowości. Android umożliwia zastosowanie technologii Bluetooth w swoich aplikacjach, oferując interfejsy API wykorzystywane do: zarządzania ustawieniami Bluetooth oraz ich konfiguracji i monitorowania, wykrywania aktywnych urządzeń Bluetooth znajdujących się w zasięgu, przesyłania danych pomiędzy powiązanymi urządzeniami.
Poniżej opisane zostały czynności wymagane do ustanowienia połączenia pomiędzy dwoma urządzeniami Bluetooth. 1. Wybierz przycisk Ustawienia na jednym z urządzeń Android. W sekcji Sieci
zwykłe i bezprzewodowe (patrz rysunek 12.1, lewy górny obrazek)
394
Rozdział 12. Łączność bezprzewodowa
Rysunek 12.1. Opcje ustawień wyświetlone po wybraniu na urządzeniu przycisku Ustawienia (lewy górny obrazek). Włączanie opcji Bluetooth w urządzeniu (prawy górny obrazek). Nazwa bieżącego urządzenia i pusta lista Dostępne urządzenia (lewy dolny obrazek). Nazwa drugiego urządzenia pojawia się na liście Dostępne urządzenia po włączeniu na nim opcji Bluetooth (prawy dolny obrazek)
wyświetlonych zostanie kilka opcji. Stuknij przycisk Włącz znajdujący się obok opcji Bluetooth, aby ją włączyć, tak jak pokazano na rysunku 12.1 (prawy górny obrazek). Po włączeniu opcji Bluetooth i jej kliknięciu wyświetlona zostanie nazwa samego urządzenia. Ponadto urządzenie wyszuka inne urządzenia Bluetooth znajdujące się w pobliżu (patrz rysunek 12.1, lewy dolny obrazek). Włącz opcję Bluetooth na drugim urządzeniu, a następnie wybierz na pierwszym urządzeniu Android przycisk Szukaj urządzeń. Na liście Dostępne urządzenia
Receptura: ręczne przesyłanie plików z jednego urządzenia na drugie przez Bluetooth
pojawi się nazwa drugiego urządzenia z włączoną opcją Bluetooth, tak jak pokazano na rysunku 12.1 (prawy dolny obrazek). Załóżmy, że nazwa tego drugiego urządzenia to Nokia 3720c. 2. Po kliknięciu wybranego urządzenia z listy bieżące urządzenie rozpocznie
procedurę łączenia w parę z drugim urządzeniem. Komunikat Parowanie zostanie wyświetlony pod nazwą urządzenia, z którym Twoje urządzanie próbuje się powiązać (patrz rysunek 12.2, lewy górny obrazek). Pojawi się okno dialogowe z prośbą o podanie PIN-u urządzenia, z którym bieżące urządzenie próbuje się powiązać, lub z prośbą o sprawdzenie, czy na drugim urządzeniu wyświetlony jest podany kod dostępu (patrz rysunek 12.2, prawy górny obrazek). PIN to zazwyczaj 0000 lub 1234. Wpisz 0000 jako PIN lub zweryfikuj podany kod dostępu na drugim urządzeniu, a następnie kliknij przycisk Powiąż. 3. Jeśli wpisałeś numer PIN (0000 lub 1234) na pierwszym urządzeniu, wpisz
taki sam na drugim, aby powiązać oba urządzenia. Nazwa tego drugiego urządzenia pojawi się na liście Powiązane urządzenia wyświetlonej na ekranie urządzenia bieżącego, co będzie potwierdzeniem, że powiązanie się powiodło (patrz rysunek 12.2, lewy dolny obrazek). Może zostać wyświetlone okno aplikacji Menedżer LiveWare z prośbą o wybranie aplikacji, którą chcesz uruchamiać automatycznie, kiedy nawiązane zostanie połączenie z danym urządzeniem. Powiadomienia aplikacji Menedżer LiveWare są opcjonalne i mogą nie pojawiać się na określonych urządzeniach — w naszym przypadku nie zostało wyświetlone okno powiadomienia aplikacji Menedżer LiveWare, tylko komunikat o pomyślnym dodaniu urządzenia (patrz rysunek 12.2, prawy dolny obrazek).
Uwaga W celu powiązania dwóch urządzeń musisz na obu wprowadzić prawidłowy numer PIN. 4. Wybierz jedną z dwóch opcji: Zmień nazwę lub Rozłącz parę (patrz
rysunek 12.3, lewy obrazek). Opcja Zmień nazwę pozwala zmienić nazwę urządzenia (parz rysunek 12.3, prawy obrazek). Jeśli klikniesz opcję Rozłącz parę, dwa urządzenia zostaną rozłączone. Nawet wtedy, kiedy połączenie Bluetooth zostanie wyłączone, urządzenia będą pamiętać się nawzajem po przywrócenia połączenia. Oznacza to, że nie musisz łączyć urządzeń ponownie w parę, kiedy włączysz Bluetooth następnym razem.
395
396
Rozdział 12. Łączność bezprzewodowa
Rysunek 12.2. Urządzenie Nokia 3720c wybrane w celu ustanowienia powiązania (lewy górny obrazek). Ekran z prośbą o upewnienie się, czy na drugim urządzeniu wyświetlony jest podany kod dostępu (prawy górny obrazek). Ustanowione powiązanie z urządzeniem Nokia 3720c, które pojawiło się na liście Powiązane urządzenia (lewy dolny obrazek). Informacja o pomyślnym dodaniu urządzenia Nokia 3720c (prawy dolny obrazek)
Receptura: ręczne przesyłanie plików z jednego urządzenia na drugie przez Bluetooth
Rysunek 12.3. Dwie opcje, Zmień nazwę i Rozłącz parę, wyświetlone po wybraniu powiązanego urządzenia (lewy obrazek) oraz okno dialogowe umożliwiające wpisanie nowej nazwy wybranego urządzenia powiązanego (prawy obrazek)
Nawet wtedy, kiedy połączenie Bluetooth zostanie wyłączone, urządzenia będą pamiętać się nawzajem po przywrócenia połączenia. Oznacza to, że nie musisz łączyć urządzeń ponownie w parę, kiedy włączysz Bluetooth następnym razem.
Receptura: ręczne przesyłanie plików z jednego urządzenia na drugie z wykorzystaniem technologii Bluetooth Aby przesłać pliki z jednego urządzenia na drugie, wykonaj następujące czynności. 1. Włącz Bluetooth na urządzeniach wysyłającym i odbierającym oraz upewnij się,
że są powiązane. 2. W urządzeniu wysyłającym otwórz aplikację zawierającą pliki, które chcesz
wysłać. Wybierz dowolny album ze zdjęciami oraz znajdujące się w nim zdjęcie, które ma zostać przesłane. 3. Dotknij i przytrzymaj zdjęcie lub wciśnij przycisk udostępniania widoczny
w pasku akcji, aby wyświetlić różne opcje udostępniania (patrz rysunek 12.4, lewy górny obrazek). 4. Z wyświetlonej listy udostępniania wybierz opcję Bluetooth
(patrz rysunek 12.4, prawy górny obrazek).
397
398
Rozdział 12. Łączność bezprzewodowa
Rysunek 12.4. Po wybraniu zdjęcia z albumu na górze wyświetlony zostanie pasek akcji z elementami akcji (lewy górny obrazek). Po wybraniu elementu udostępniania wyświetlona zostanie lista opcji (prawy górny obrazek). Po kliknięciu opcji Bluetooth z listy udostępniania otwarte zostaje okno dialogowe Wybór urządzenia Bluetooth, które wyświetla listę powiązanych urządzeń (lewy dolny obrazek). Wybrane zdjęcie jest wysyłane do wskazanego urządzenia powiązanego (prawy dolny obrazek)
5. Zakładamy, że powiązane urządzenie, do którego chcesz wysłać obraz,
to Nokia 3720c. Wybierz to urządzenie z listy Dostępne urządzenia (patrz rysunek 12.4, lewy dolny obrazek).
Receptura: łączenie w parę urządzenia Bluetooth z komputerem z Windows
6. Urządzenie wysyłające zacznie przesyłać wybrany obraz i wyświetli komunikat Udostępnianie Bluetooth: wysłano DSC_0311.jpg, tak jak pokazano na
rysunku 12.4 (prawy dolny obrazek). 7. Urządzenie odbierające zostanie poinformowane, że jakieś urządzenie próbuje
przesłać plik. Zaakceptuj przychodzący plik na urządzeniu odbierającym, aby zakończyć proces transferu pliku.
Receptura: łączenie w parę urządzenia Bluetooth z komputerem z systemem Windows Oto czynności, które należy wykonać, aby powiązać urządzenie Bluetooth z komputerem z systemem Windows. 1. Upewnij się, że w urządzeniu włączony jest moduł Bluetooth. W tym celu
wybierz na urządzeniu przycisk Ustawienia i włącz opcję Bluetooth widoczną w sekcji Sieci zwykłe i bezprzewodowe. 2. Zakładamy, że komputer z systemem Windows posiada wbudowany adapter
Bluetooth. Włącz ten adapter, wciskając przycisk Bluetooth na klawiaturze. Ikona Bluetooth pojawi się w obszarze powiadomień (prawa dolna strona pulpitu). 3. Wybierz na urządzeniu przycisk Szukaj urządzeń, aby możliwe było
odnalezienie komputera z systemem Windows. 4. W obszarze powiadomień komputera z systemem Windows wyświetlony
zostanie komunikat Urządzenie Bluetooth próbuje się połączyć (patrz rysunek 12.5, lewy obrazek). Kliknij ikonę Bluetooth i z wyświetlonej listy opcji wybierz Zezwalaj na połączenie urządzenia (patrz rysunek 12.5, prawy obrazek). 5. Wybierz ikonę Bluetooth z obszaru powiadomień i w wyświetlonej liście kliknij
opcję Dodaj urządzenie. 6. Pojawi się okno wyświetlające wszystkie urządzenia Bluetooth znajdujące się
w pobliżu (patrz rysunek 12.6, lewy górny obrazek). Wybierz urządzenie XPERIA P i kliknij przycisk Dalej. 7. W urządzeniu na liście dostępnych urządzeń pojawi się nazwa komputera
z systemem Windows. Kliknij tę nazwę w celu powiązania komputera z urządzeniem (patrz rysunek 12.6, prawy górny obrazek). 8. Urządzenie wyświetli komunikat Parowanie (patrz rysunek 12.6, lewy dolny
obrazek), a następnie pojawi się okno dialogowe z prośbą o podanie numeru PIN.
399
400
Rozdział 12. Łączność bezprzewodowa
Rysunek 12.5. Wyskakujący komunikat z informacją, że określone urządzenie Bluetooth próbuje nawiązać połączenie z tym komputerem (lewy obrazek). Po kliknięciu ikony Bluetooth wyświetlane jest okno z różnymi opcjami (prawy obrazek)
9. Na komputerze z systemem Windows pojawi się okno dialogowe z prośbą
o wprowadzenie kodu łączenia w parę. Porównanie kodów łączenia w parę komputera i urządzenia weryfikuje, czy połączenie zostało ustanowione pomiędzy właściwymi urządzeniami. Po przeprowadzeniu weryfikacji kodów nawiązywane jest połączenie, a nazwa komputera pojawia się na liście Powiązane urządzenia, co pokazano na rysunku 12.6 (prawy dolny obrazek). Zakładamy, że nazwa komputera, który został powiązany z urządzeniem w tej procedurze, to BINTU-KOMPUTER. Połączenie Bluetooth po ustanowieniu jest zapisywane. Oznacza to, że nawet jeśli komputer został wyłączony, to po jego ponownym włączeniu połączenie zostanie przywrócone. W zależności od ustawień zabezpieczeń lub zapory ogniowej, w prawym dolnym rogu na pasku zadań może pojawić się wyskakujące okienko z prośbą o pozwolenie na ponowne nawiązanie połączenia.
Receptura: włączanie lokalnego urządzenia Bluetooth Lokalne urządzenie Bluetooth jest kontrolowane za pomocą klasy BluetoothAdapter. Aby uzyskać dostęp do domyślnego urządzenia Bluetooth, wywołaj metodę getDefaultAdapter. Klasa BluetoothAdapter zapewnia metody umożliwiające uzyskanie dostępu do lokalnego urządzenia Bluetooth oraz jego konfigurację i kontrolę. Jednak metody działają tylko wtedy, kiedy urządzenie Bluetooth jest włączone. Niektóre z metod klasy BluetoothAdapter zostały przedstawione w tabeli 12.1.
Receptura: włączanie lokalnego urządzenia Bluetooth
Rysunek 12.6. Okno wyświetlające na komputerze listę urządzeń Bluetooth znajdujących się w pobliżu (lewy górny obrazek). W urządzeniu Android na liście Dostępne urządzenia pojawia się nazwa komputera z adapterem Bluetooth (prawy górny obrazek). Po wybraniu nazwy komputera do powiązania wyświetla się komunikat o łączeniu urządzeń w parę (lewy dolny obrazek). Po udanym nawiązaniu połączenia na liście Powiązane urządzenia pojawia się nazwa komputera z systemem Windows (prawy dolny obrazek)
401
402
Rozdział 12. Łączność bezprzewodowa Tabela 12.1. Krótki opis metod klasy BluetoothAdapter
Metoda
Opis
isEnabled
Informuje, czy urządzenie jest włączone. Metoda zwraca wartość logiczną true (prawda), jeśli urządzenie jest włączone.
getName
Zwraca nazwę adaptera Bluetooth, która jest wykorzystywana do identyfikacji urządzenia.
getAddress
Zwraca adres sprzętowy.
setName
Modyfikuje nazwę adaptera Bluetooth.
getState
Zwraca stan adaptera Bluetooth. Zwracana jest jedna z następujących stałych adaptera Bluetooth: STATE_TURNING_ON — wskazuje, że Bluetooth jest w trakcie włączania, STATE_ON — wskazuje, że Bluetooth jest włączony, STATE_TURNING_OFF — wskazuje, że Bluetooth jest w trakcie wyłączania, STATE_OFF — wskazuje, że Bluetooth jest wyłączony.
Wszystkie metody opisane w tabeli 12.1 będą zwracały wartość null, jeśli urządzenie Bluetooth jest wyłączone.
Uwaga Do modyfikacji nazwy adaptera Bluetooth wymagane jest zezwolenie BLUETOOTH_ADMIN.
W celu włączenia urządzenia Bluetooth aktywność preferencji systemowych uruchamiana jest z wykorzystaniem akcji BluetoothAdapter.ACTION_REQUEST_ENABLE. Aktywność ta prosi użytkownika o włączenie urządzenia Bluetooth. Jest ona zazwyczaj uruchamiana z użyciem metody startActivityForResult, możesz więc obserwować i analizować wynik. Jeśli wystąpi błąd, wtedy, w zależności od wyboru użytkownika, dana podaktywność się zamknie i powróci do aktywności wywołującej z urządzeniem Bluetooth włączonym lub wyłączonym. Utwórz nowy projekt Android o nazwie BlueToothApp. Aplikacja będzie sprawdzać, czy lokalne urządzenie Bluetooth jest dostępne. Jeśli urządzenie Bluetooth nie jest dostępne, aplikacja zostanie zakończona po wyświetleniu komunikatu o błędzie. Jeśli urządzenie Bluetooth jest dostępne, ale nie jest włączone, uruchomiona zostanie aktywność z określoną akcją w celu włączenia urządzenia Bluetooth. Aby sprawdzić dostępność lokalnego urządzenia Bluetooth i włączyć je, wpisz w pliku aktywności Java BlueToothAppActivity.java kod przedstawiony w listingu 12.1.
Receptura: włączanie lokalnego urządzenia Bluetooth Listing 12.1. Kod wpisany w pliku aktywności Java BlueToothAppActivity.java package com.androidtablet.bluetoothapp; import import import import import
android.os.Bundle; android.app.Activity; android.bluetooth.BluetoothAdapter; android.widget.Toast; android.content.Intent;
public class BlueToothAppActivity extends Activity { private static final int REQUEST_ENABLE_BT = 0; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_blue_tooth_app); BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); if (bluetoothAdapter == null) { Toast.makeText(this, "Usługa Bluetooth nie jest dostępna", Toast.LENGTH_LONG).show(); finish(); return; } if (bluetoothAdapter.isEnabled()) Toast.makeText(this, "Usługa Bluetooth jest gotowa", Toast.LENGTH_LONG).show(); else { Intent enableBtIntent = new Intent( BluetoothAdapter.ACTION_REQUEST_ENABLE); startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT); } }
#1
#2
#3 #4
protected void onActivityResult(int requestCode, int resultCode, Intent intent) { if (resultCode == RESULT_OK) { #5 if(requestCode == REQUEST_ENABLE_BT){ Toast.makeText(this, "Usługa Bluetooth jest włączona", Toast.LENGTH_LONG).show(); } else{ Toast.makeText(this, "Usługa Bluetooth nie może być włączona", Toast.LENGTH_LONG).show(); } } } }
Pewne instrukcje z listingu 12.1 wymagają wyjaśnienia. Instrukcja #1 uzyskuje dostęp do domyślnego urządzenia Bluetooth. Instrukcja #2 sprawdza, czy urządzenie Bluetooth jest włączone.
403
404
Rozdział 12. Łączność bezprzewodowa Instrukcja #3 definiuje intencję o nazwie enableBtIntent z akcją
BluetoothAdapter.ACTION_REQUEST_ENABLE. Instrukcja #4 uruchamia aktywność preferencji systemowych, wykorzystując
intencję enableBtIntent zdefiniowaną w instrukcji #3. Aktywność ta poprosi użytkownika o włączenie urządzenia Bluetooth. Aktywność jest uruchamiana z wykorzystaniem metody startActivityForResult, więc wynik działania aktywności może być analizowany za pomocą procedury obsługi onActivityResult. Instrukcja #5 sprawdza, czy aktywność włączania urządzenia Bluetooth została
wykonana z powodzeniem. Aby Twoja aplikacja mogła odczytywać właściwości lokalnego urządzenia Bluetooth, do pliku AndroidManifest.xml dodana została następująca instrukcja zezwolenia Bluetooth:
Po uruchomieniu tej aplikacji, jeśli lokalne urządzenie Bluetooth jest dostępne i zostało już włączone, wyświetlony zostanie komunikat Usługa Bluetooth jest gotowa, co pokazano na rysunku 12.7.
Rysunek 12.7. Komunikat Usługa Bluetooth jest gotowa pojawia się, jeśli w danym urządzeniu został już włączony moduł Bluetooth
Receptura: wyświetlanie listy powiązanych urządzeń
Jeśli lokalne urządzenie Bluetooth jest dostępne, ale nie jest włączone, uruchamiana jest aktywność z określoną akcją w celu włączenia urządzenia Bluetooth. Aktywność powoduje wyświetlenie okna dialogowego informującego, że aplikacja próbuje włączyć funkcję Bluetooth (patrz rysunek 12.8, lewy górny obrazek). Kliknij Tak w tym oknie dialogowym, aby włączyć urządzenie Bluetooth. Wyświetlony zostanie komunikat Włączanie modułu Bluetooth (patrz rysunek 12.8, prawy górny obrazek), a następnie pojawi się komunikat Usługa Bluetooth jest włączona (patrz rysunek 12.8, lewy dolny obrazek). Aby sprawdzić, czy urządzenie Bluetooth zostało rzeczywiście włączone, wybierz w urządzeniu przycisk Ustawienia. Na liście Ustawienia urządzenia Android zobaczysz, że funkcja Bluetooth jest uruchomiona (patrz rysunek 12.8, prawy dolny obrazek), co potwierdza, że dana aplikacja z powodzeniem włączyła urządzenie Bluetooth.
Receptura: wyświetlanie listy powiązanych urządzeń W tej recepturze nauczysz się wyświetlać informacje o urządzeniach powiązanych z Twoim urządzeniem Android. Aplikacja będzie wyświetlać informacje o adapterze Bluetooth oraz nazwę i adres Twojego urządzenia Android. Ponadto aplikacja będzie też wyświetlać nazwy i adresy urządzeń powiązanych z Twoim urządzeniem Android. Utwórz projekt Android o nazwie BlueToothPairedListApp. Informacje o powiązanych urządzeniach będą wyświetlane za pomocą kontrolki TextView. Aby zdefiniować tę kontrolkę, wpisz w pliku układu aktywności activity_blue_tooth_paired_list_app.xml kod przedstawiony w listingu 12.2. Listing 12.2. Kod wpisany w pliku układu aktywności activity_blue_tooth_paired_list_app.xml
Dla celów identyfikacji i uzyskiwania dostępu w kodzie Java kontrolce TextView przypisano identyfikator paired_list. Atrybuty zostały skonfigurowane w taki sposób, że tekst wyświetlany za pomocą kontrolki TextView będzie prezentowany pogrubioną czcionką o rozmiarze zdefiniowanym w zasobie wymiarów text_size.
405
406
Rozdział 12. Łączność bezprzewodowa
Rysunek 12.8. Okno dialogowe z zapytaniem o włączenie urządzenia Bluetooth (lewy górny obrazek). Komunikat informujący, że urządzenie Bluetooth jest włączane (prawy górny obrazek). Komunikat informujący, że urządzenie Bluetooth zostało włączone (lewy dolny obrazek). Na liście Ustawienia widać, że urządzenie Bluetooth jest włączone (prawy dolny obrazek)
Teraz musisz napisać kod Java, który będzie wykonywał następujące zadania. Sprawdzenie lokalnego urządzenia Bluetooth i włączenie go, jeśli jest wyłączone. Uzyskanie dostępu do domyślnego adaptera Bluetooth i wyświetlenie nazwy
oraz adresu danego urządzenia Android.
Receptura: wyświetlanie listy powiązanych urządzeń Wywołanie metody startDiscovery() na obiekcie BluetoothAdapter w celu
wyszukania powiązanych urządzeń znajdujących się w pobliżu. Wywołanie metody getBondedDevices() na obiekcie BluetoothAdapter w celu
uzyskania dostępu do zestawu powiązanych urządzeń. Uzyskanie dostępu do nazwy i adresu każdego z urządzeń z pobranego zestawu
i wyświetlenie tych informacji na ekranie. Aby wykonać wymienione zadania, wpisz w pliku aktywności Java BlueToothPairedListAppActivity.java kod przedstawiony w listingu 12.3. Listing 12.3. Kod wpisany w pliku aktywności Java BlueToothPairedListAppActivity.java package com.androidtablet.bluetoothpairedlistapp; import import import import import import import import
android.os.Bundle; android.app.Activity; android.bluetooth.BluetoothAdapter; android.widget.Toast; android.content.Intent; android.widget.TextView; android.bluetooth.BluetoothDevice; java.util.Set;
public class BlueToothPairedListAppActivity extends Activity { private BluetoothAdapter bluetoothAdapter = null; private static final int REQUEST_ENABLE_BT = 0; private TextView textView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_blue_tooth_paired_list_app); textView = (TextView) findViewById( R.id.paired_list); textView.setText(""); bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); if (bluetoothAdapter == null) { Toast.makeText(this, "Usługa Bluetooth nie jest dostępna", Toast.LENGTH_LONG).show(); finish(); return; } if (!bluetoothAdapter.isEnabled()) { Intent enableBtIntent = new Intent( BluetoothAdapter.ACTION_REQUEST_ENABLE); startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT); } else { Toast.makeText(this, "Usługa Bluetooth jest gotowa", Toast.LENGTH_LONG).show(); dispInfo(); } }
407
408
Rozdział 12. Łączność bezprzewodowa @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (resultCode == RESULT_OK) { if(requestCode == REQUEST_ENABLE_BT){ Toast.makeText(this, "Usługa Bluetooth jest włączona", Toast.LENGTH_LONG).show(); dispInfo(); } } else { Toast.makeText(this, "Błąd: usługa Bluetooth nie jest włączona", Toast.LENGTH_LONG).show(); finish(); return; } } protected void dispInfo() { textView.append("Adapter Bluetooth: " + bluetoothAdapter. toString() + "\nNazwa adaptera: " + bluetoothAdapter. getName() + "\nAdres MAC: " + bluetoothAdapter. getAddress()); bluetoothAdapter.startDiscovery(); textView.append("\n\nUrządzenia powiązane:"); Set devices = bluetoothAdapter. getBondedDevices(); for (BluetoothDevice device : devices) { textView.append("\n\nWykryto urządzenie: " + device.getName() + "\nAdres MAC: " + device.getAddress()); } } }
Aby Twoja aplikacja mogła odczytywać właściwości lokalnego urządzenia Bluetooth oraz wyszukiwać powiązane urządzenia, do pliku AndroidManifest.xml dodaj następujące instrukcje zezwolenia Bluetooth:
Zezwolenie BLUETOOTH_ADMIN jest wymagane do modyfikacji którejkolwiek z właściwości lokalnego urządzenia Bluetooth. Po uruchomieniu tej aplikacji, jeśli lokalne urządzenie Bluetooth jest dostępne i zostało już uruchomione, wyświetlony zostanie komunikat Usługa Bluetooth jest gotowa. Ponadto wyświetlone zostaną również informacje o adapterze Bluetooth, nazwa i adres urządzenia oraz nazwa i adres urządzenia powiązanego, co przedstawiono na rysunku 12.9 (lewy górny obrazek).
Receptura: wyświetlanie listy powiązanych urządzeń
Rysunek 12.9. Informacje o adapterze Bluetooth oraz nazwa i adres bieżącego, a także powiązanych urządzeń wyświetlone w oknie aplikacji (lewy górny obrazek). Okno dialogowe z zapytaniem o włączenie urządzenia Bluetooth (prawy górny obrazek). Informacje o adapterze Bluetooth oraz nazwa i adres bieżącego i powiązanych urządzeń wyświetlone po włączeniu urządzenia Bluetooth (dolny obrazek)
Jeśli lokalne urządzenie Bluetooth jest dostępne, ale nie jest włączone, uruchamiana jest aktywność z określoną akcją w celu włączenia tego urządzenia Bluetooth. Aktywność ta powoduje wyświetlenie okna dialogowego z informacją, że aplikacja próbuje włączyć urządzenie Bluetooth (patrz rysunek 12.9, prawy górny obrazek). Kliknięcie w tym oknie
409
410
Rozdział 12. Łączność bezprzewodowa
dialogowym przycisku Tak spowoduje włączenie urządzenia Bluetooth. Wyświetlane są komunikat Usługa Bluetooth jest włączona oraz nazwa i adres powiązanego urządzenia (patrz rysunek 12.9, dolny obrazek).
Receptura: przesyłanie plików za pomocą technologii Bluetooth W tej recepturze nauczysz się przesyłać pliki za pomocą technologii Bluetooth. Aplikacja będzie wyświetlać kontrolkę Button, która po naciśnięciu zainicjuje proces transferu plików. Oznacza to, że zostanie wyświetlona lista powiązanych urządzeń i użytkownik będzie proszony o wybranie tego, do którego ma być przesłany plik. Po wybraniu powiązanego urządzenia przesłany zostanie do niego określony plik z karty SD bieżącego urządzenia. Pamiętaj, że szybkość transferu plików w standardzie Bluetooth jest dość niska — około 50 kB/s. Utwórz projekt Android o nazwie BTFileTransferApp. Aby wyświetlić kontrolkę Button, wpisz w pliku układu aktywności activity_btfile_transfer_app.xml kod przedstawiony w listingu 12.4. Listing 12.4. Kod wpisany w pliku układu aktywności activity_btfile_transfer_app.xml
Jak możesz zauważyć, kontrolce Button przypisano nagłówek Prześlij plik. Nagłówek ten będzie wyświetlany czcionką o rozmiarze zdefiniowanym w zasobie wymiarów text_size. Ponadto dla celów identyfikacji kontrolki Button w kodzie Java przypisano jej unikatowy identyfikator transfer_file_button. Kontrolka Button ma być wyświetlana horyzontalnie na środku ekranu urządzenia. Teraz musisz napisać kod Java, który będzie wykonywał poniższe zadania. Uzyskanie dostępu do kontrolki Button zdefiniowanej w pliku układu
aktywności i zmapowanie tej kontrolki na obiekt Button. Powiązanie nasłuchiwacza setOnClickListener z kontrolką Button w celu
nasłuchiwania zdarzeń kliknięcia tej kontrolki i po ich wystąpieniu wywoływanie metody wywołania zwrotnego onClick.
Receptura: przesyłanie plików za pomocą technologii Bluetooth
Zdefiniowanie ścieżki pliku z karty SD, który chcesz przesłać. Zakładamy,
że plik o nazwie sampleimage.jpg istniej już na karcie SD bieżącego urządzenia. Zdefiniowanie intencji i ustawienie jej akcji jako Intent.ACTION_SEND. Określenie pliku do przesłania w obiekcie intencji. Wywołanie intencji intent w celu zainicjowania transferu pliku.
Aby wykonać wymienione zadania, wpisz w pliku aktywności Java BTFileTransferAppActivity.java kod przedstawiony w listingu 12.5. Listing 12.5. Kod wpisany w pliku aktywności Java BTFileTransferAppActivity.java package com.androidtablet.btfiletransferapp; import import import import import import import import import
android.os.Bundle; android.app.Activity; android.widget.Button; android.view.View; android.view.View.OnClickListener; android.os.Environment; java.io.File; android.content.Intent; android.net.Uri;
public class BTFileTransferAppActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_btfile_transfer_app); Button transferFileButton = (Button) findViewById( R.id.transfer_file_button); transferFileButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { String filePath = Environment. getExternalStorageDirectory().toString() + "/sampleimage.jpg"; File file = new File(filePath); Intent intent = new Intent(); intent.setAction(Intent.ACTION_SEND); intent.setType("text/plain"); intent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(file)); startActivity(intent); } }); } }
411
412
Rozdział 12. Łączność bezprzewodowa
Po uruchomieniu tej aplikacji horyzontalnie na środku ekranu znajdzie się kontrolka Button z nagłówkiem Prześlij plik (patrz rysunek 12.10, lewy górny obrazek). Po kliknięciu
tej kontrolki pojawi się okno dialogowe Wybór urządzenia Bluetooth, wyświetlające listę powiązanych urządzeń (patrz rysunek 12.10, prawy górny obrazek). Chcesz przesłać plik do powiązanego urządzenia o nazwie Nokia 3720c, zatem wybierz je z listy powiązanych urządzeń. Po wybraniu urządzenia rozpocznie się proces transferu pliku sampleimage.jpg z karty SD bieżącego urządzenia do urządzenia Nokia 3720c. Wyświetlony zostanie komunikat Wysyłanie pliku do urządzenia „Nokia 3720c”, który potwierdza, że plik jest przesyłany (patrz rysunek 12.10, lewy dolny obrazek). Urządzenie odbierające otrzyma wiadomość, że plik jest przesyłany. Jeśli użytkownik na urządzeniu odbierającym zaakceptuje przychodzący plik, proces transferu pliku zostanie ukończony z powodzeniem. Jeśli użytkownik na urządzeniu odbierającym nie zaakceptuje przychodzącego pliku, proces transferu zostanie przerwany i wyświetlony zostanie komunikat Udostępnianie Bluetooth: plik sampleimage.jpg nie został wysłany, tak jak pokazano na rysunku 12.10 (prawy dolny obrazek).
Receptura: standard Wi-Fi Wi-Fi to bezprzewodowa sieć lokalna, która jest wykorzystywana do wymiany danych bez zastosowania fizycznego połączenia pomiędzy urządzeniami elektronicznymi. Wi-Fi wykorzystuje do wymiany danych częstotliwości radiowe. System Android oferuje interfejs API, który odpowiada za: tworzenie i modyfikowanie ustawień konfiguracyjnych Wi-Fi, skanowanie w poszukiwaniu hotspotów, kontrolę i monitorowanie ustawień internetowych i łączności internetowej, transfer plików pomiędzy urządzeniami.
Dla celów konfigurowania, zarządzania i monitorowania połączeń sieci Wi-Fi oraz skanowania osiągalnych punktów dostępowych (ang. access points) system Android oferuje usługę łączności Wi-Fi o nazwie WifiManager. Aby skorzystać z usługi WifiManager, aplikacja musi posiadać następujące zezwolenia:
W celu uzyskania dostępu do usługi WifiManager wywołaj metodę getSystemService ze stałą Context.WIFI_SERVICE. Przykład: WifiManager wifiManager = (WifiManager) getBaseContext().getSystemService(Context.WIFI_SERVICE);
Receptura: standard Wi-Fi
Rysunek 12.10. Kontrolka Button z nagłówkiem Prześlij plik wyświetlona po uruchomieniu aplikacji (lewy górny obrazek). Lista powiązanych urządzeń wyświetlona w celu dokonania wyboru jednego z nich (prawy górny obrazek). Komunikat Toast informujący, że plik jest przesyłany (lewy dolny obrazek). Komunikat informujący, że plik sampleimage.jpg nie mógł zostać wysłany
413
414
Rozdział 12. Łączność bezprzewodowa
W tabeli 12.2 opisano w skrócie kilka metod klasy WifiManager. Tabela 12.2. Krótki opis metod klasy WifiManager
Metoda
Opis
setWifiEnabled
Wykorzystywana do włączenia lub wyłączenia sprzętowego modułu Wi-Fi. Przekazanie do tej metody wartości logicznej true (prawda) powoduje włączenie sprzętowego modułu Wi-Fi.
getWifiState
Zwraca bieżący stan Wi-Fi.
isWifiEnabled
Zwraca wartość true, jeśli sprzętowy moduł Wi-Fi jest włączony.
Usługa WifiManager rozgłasza intencje za każdym razem, kiedy następuje zmiana w statusie łączności sieci Wi-Fi. Intencje te są rozgłaszane za pomocą akcji reprezentowanych przez stałe (patrz tabela 12.3), które są zdefiniowane w klasie WifiManager. Tabela 12.3. Krótki opis stałych zdefiniowanych w klasie WifiManager
Stała
Opis
WIFI_STATE_CHANGED_ACTION
Wskazuje, że zmienił się status modułu sprzętowego Wi-Fi (oznacza to zmianę statusu na jeden z następujących stanów: włączanie, włączone, wyłączanie, wyłączone oraz stan nieznany).
EXTRA_WIFI_STATE
Klucz wyszukiwania, który wskazuje nowy stan Wi-Fi (czy Wi-Fi jest włączone, wyłączone, w trakcie włączania, w trakcie wyłączania lub ma stan nieznany).
EXTRA_PREVIOUS_STATE
Klucz wyszukiwania, który wskazuje poprzedni stan Wi-Fi.
SUPPLICANT_CONNECTION_ CHANGE_ACTION
Wskazuje, że połączenie z suplikantem (punktem dostępowym) zostało ustanowione lub utracone.
EXTRA_NEW_STATE
Klucz wyszukiwania, który opisuje nowy stan.
NETWORK_STATE_CHANGED_ ACTION
Wskazuje, że stan łączności Wi-Fi się zmienił.
RSSI_CHANGED_ACTION
Wskazuje, że zmienił się wskaźnik RSSI (siła sygnału).
EXTRA_NEW_RSSI
Klucz wyszukiwania, który wskazuje bieżącą siłę sygnału.
Receptura: włączanie i wyłączanie Wi-Fi W tej recepturze nauczysz sie włączać i wyłączać moduł Wi-Fi. W pasku akcji wyświetlane będą dwa elementy akcji o tytułach Włącz Wi-Fi i Wyłącz Wi-Fi. Kliknięcie elementu akcji Włącz Wi-Fi spowoduje włączenie sieci Wi-Fi, natomiast kliknięcie elementu akcji Wyłącz Wi-Fi spowoduje wyłączenie sieci Wi-Fi.
Receptura: włączanie i wyłączanie Wi-Fi
Utwórz nowy projekt o nazwie WiFiApp. Aby poinformować użytkownika, że sieć Wi-Fi jest włączona lub została przełączona do stanu wyłączonego, będziesz korzystał z kontrolki TextView. Po zdefiniowaniu tej kontrolki plik układu aktywności activity_wi_fi_app.xml będzie wyglądał tak, jak przedstawiono w listingu 12.6. Listing 12.6. Kod wpisany w pliku układu aktywności activity_wi_fi_app.xml
Aby zdefiniować dwa elementy akcji z tytułami odpowiednio Włącz Wi-Fi i Wyłącz Wi-Fi, wpisz w znajdującym się w folderze res/menu pliku menu activity_wi_fi_app.xml kod przedstawiony w listingu 12.7. Listing 12.7. Kod wpisany w pliku menu activity_wi_fi_app.xml
Dla celów uzyskiwania dostępu i identyfikacji w kodzie Java dwóm elementom akcji przypisano odpowiednio identyfikatory enable_wifi_btn oraz disable_wifi_btn. Teraz musisz napisać kod Java, który będzie wykonywał poniższe zadania. Uzyskanie dostępu do usługi WifiManager. Powiązanie obiektu BroadcastReceiver z daną aktywnością w celu
nasłuchiwania wszelkich zmian w statusie modułu sprzętowego Wi-Fi. Wyświetlenie elementów akcji zdefiniowanych w pliku menu. Powiązanie nasłuchiwaczy z elementami akcji w celu nasłuchiwania zdarzeń
kliknięcia tych elementów. Włączenie lub wyłączenie sieci Wi-Fi po kliknięciu odpowiedniego elementu akcji. Uzyskanie dostępu do nowego statusu modułu sprzętowego Wi-Fi w obiekcie
BroadcastReceiver i wyświetlenie tego statusu za pomocą kontrolki TextView.
415
416
Rozdział 12. Łączność bezprzewodowa
Aby wykonać wymienione zadania, wpisz w pliku aktywności Java WifiAppActivity.java kod przedstawiony w listingu 12.8. Listing 12.8. Kod wpisany w pliku aktywności Java WifiAppActivity.java package com.androidtablet.wifiapp; import import import import import import import import import import
android.os.Bundle; android.app.Activity; android.view.Menu; android.view.MenuItem; android.net.wifi.WifiManager; android.content.Context; android.content.BroadcastReceiver; android.content.Intent; android.widget.TextView; android.content.IntentFilter;
public class WiFiAppActivity extends Activity { private WifiManager wifiManager; TextView wifiStatus; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_wi_fi_app); wifiStatus = (TextView)findViewById( R.id.wifistatus); wifiManager = (WifiManager)getBaseContext(). getSystemService(Context.WIFI_SERVICE); this.registerReceiver(this.WifiStateChangedReceiver, new IntentFilter(WifiManager.WIFI_STATE_CHANGED_ACTION)); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.activity_wi_fi_app, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.enable_wifi_btn: wifiManager.setWifiEnabled(true); break; case R.id.disable_wifi_btn: wifiManager.setWifiEnabled(false); break; default: return super.onOptionsItemSelected(item); } return true; }
#1 #2
#3
#4 #5
Receptura: włączanie i wyłączanie Wi-Fi private BroadcastReceiver WifiStateChangedReceiver = new BroadcastReceiver(){ @Override public void onReceive(Context context, Intent intent) { int extraWifiState = intent.getIntExtra( WifiManager. EXTRA_WIFI_STATE, WifiManager. WIFI_STATE_UNKNOWN); switch(extraWifiState){ #6 case WifiManager.WIFI_STATE_DISABLED: wifiStatus.setText("Wi-Fi jest wyłączone"); break; case WifiManager.WIFI_STATE_DISABLING: wifiStatus.setText("Wi-Fi w trakcie wyłączania"); break; case WifiManager.WIFI_STATE_ENABLED: wifiStatus.setText("Wi-Fi jest włączone"); break; case WifiManager.WIFI_STATE_ENABLING: wifiStatus.setText("Wi-Fi w trakcie włączania"); break; case WifiManager.WIFI_STATE_UNKNOWN: wifiStatus.setText("Nieznany status połączenia Wi-Fi"); break; } } }; }
Niektóre z instrukcji wykorzystanych w listingu 12.8 wymagają wyjaśnienia. Poniższe zestawienie definiuje znaczenie i cel tych instrukcji. Instrukcja #1 uzyskuje dostęp do usługi WifiManager. Instrukcja #2 implementuje klasę BroadcastReceiver w celu rejestracji
z intencją WifiManager.WIFI_STATE_CHANGED_ACTION. Teraz za każdym razem, kiedy zmieni się status modułu sprzętowego Wi-Fi, obiekt BroadcastReceiver będzie informowany o nowym statusie. Instrukcja #3 wypełnia lub scala plik menu activity_wi_fi_app w celu
wyświetlenia zdefiniowanych w nim dwóch elementów akcji Włącz Wi-Fi i Wyłącz Wi-Fi. Instrukcja #4 włącza sieć Wi-Fi po kliknięciu elementu akcji Włącz Wi-Fi. Instrukcja #5 wyłącza sieć Wi-Fi po kliknięciu elementu akcji Wyłącz Wi-Fi. Instrukcja #6 analizuje nowy status modułu sprzętowego Wi-Fi za pomocą
wyszukiwania EXTRA_WIFI_STATE i wyświetla ten status na ekranie za pomocą kontrolki TextView.
417
418
Rozdział 12. Łączność bezprzewodowa
Aby korzystać z dostępu i zmieniać status modułu sprzętowego Wi-Fi, aplikacja musi posiadać odpowiednie uprawnienia. W celu umożliwienia aplikacji uzyskiwania dostępu do statusu modułu sprzętowego Wi-Fi i wykonywania zmian dodaj do pliku AndroidManifest.xml następujące instrukcje zezwoleń:
Po uruchomieniu tej aplikacji w pasku zadań wyświetlone zostaną dwa elementy akcji Włącz Wi-Fi oraz Wyłącz Wi-Fi (patrz rysunek 12.11, lewy górny obrazek). Po kliknięciu elementu akcji Włącz Wi-Fi status modułu sprzętowego Wi-Fi zmieni się na Wi-Fi w trakcie włączania, a następnie na Wi-Fi jest włączone (patrz rysunek 12.11, prawy górny obrazek). Analogicznie po kliknięciu elementu akcji Wyłącz Wi-Fi status zmieni się na Wi-Fi w trakcie wyłączania, a następnie na Wi-Fi jest wyłączone, tak jak pokazano na rysunku 12.11 (dolny obrazek).
Receptura: Wi-Fi Direct Wi-Fi Direct jest protokołem komunikacyjnym obsługiwanym przez system Android 4.0 (API poziomu 14.) i jego nowsze wersje. Został zaprojektowany dla komunikacji typu peer-to-peer. Jest szybki, niezawodny i umożliwia komunikację na dystansach znacznie większych niż typowe połączenie Bluetooth. Urządzenia wyposażone w odpowiedni moduł sprzętowy mogą łączyć się bezpośrednio za pomocą Wi-Fi, bez pośrednictwa punktu dostępowego. Technologia Wi-Fi Direct jest często wykorzystywana do operacji udostępniania multimediów. Abyś mógł skorzystać z Wi-Fi Direct, aplikacja wymaga zezwoleń ACCESS_WIFI_STATE, CHANGE_WIFI_STATE oraz INTERNET:
Połączenie Wi-Fi Direct jest zarządzane poprzez usługę systemową WifiP2pManager, do której dostęp uzyskiwany jest za pomocą metody getSystemService przekazującej stałą Context.WIFI_P2P_SERVICE: wifiP2pManager =(WifiP2pManager) getSystemService(Context.WIFI_P2P_SERVICE);
W celu umożliwienia interakcji z frameworkiem Wi-Fi Direct tworzony jest kanał z wykorzystaniem bieżącego kontekstu Context i obiektu klasy Looper, który nasłuchuje wystąpień odpowiednich zdarzeń. Aby na rzeczywistym przykładzie zrozumieć koncepcję Wi-Fi Direct, utwórz nowy projekt Android o nazwie WiFiDirectApp. Ustaw wartości atrybutów android:minSdkVersion i android:targetSdkVersion odpowiednio na 14 i 17.
Receptura: Wi-Fi Direct
Rysunek 12.11. Elementy akcji Włącz Wi-Fi i Wyłącz Wi-Fi wyświetlone po uruchomieniu aplikacji (lewy górny obrazek). Status modułu sprzętowego Wi-Fi zmieniony na włączony po kliknięciu elementu akcji Włącz Wi-Fi (prawy górny obrazek). Status modułu sprzętowego Wi-Fi zmieniony na wyłączony po kliknięciu elementu akcji Wyłącz Wi-Fi (dolny obrazek)
419
420
Rozdział 12. Łączność bezprzewodowa
W pliku aktywności Java WiFiDirectAppActivity.java wpisz kod przedstawiony w listingu 12.9. Listing 12.9. Kod wpisany w pliku aktywności Java WiFiDirectAppActivity.java package com.androidtablet.wifidirectapp; import import import import import import import import
android.os.Bundle; android.app.Activity; android.net.wifi.p2p.WifiP2pManager; android.net.wifi.p2p.WifiP2pManager.Channel; android.content.BroadcastReceiver; android.content.Context; android.content.IntentFilter; android.widget.Toast;
public class WiFiDirectAppActivity extends Activity { WifiP2pManager wifiP2pManager; Channel channel; BroadcastReceiver bcReceiver; IntentFilter intentFilter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_wi_fi_direct_app); wifiP2pManager = (WifiP2pManager) getSystemService( Context.WIFI_P2P_SERVICE); channel = (Channel) wifiP2pManager.initialize(this, getMainLooper(), null); bcReceiver = new WiFiBroadcastReceiver( wifiP2pManager, channel, this); intentFilter = new IntentFilter(); intentFilter.addAction(WifiP2pManager. WIFI_P2P_STATE_CHANGED_ACTION); wifiP2pManager.discoverPeers(channel, new WifiP2pManager.ActionListener() { public void onSuccess() { Toast.makeText(WiFiDirectAppActivity.this, "Wi-Fi Direct jest włączone", Toast.LENGTH_LONG).show(); } public void onFailure(int reasonCode) { String errorMessage = "Wi-Fi Direct jest wyłączone."; switch (reasonCode) { case WifiP2pManager.BUSY : errorMessage += " System jest zajęty."; break; case WifiP2pManager.ERROR : errorMessage += " Pojawił się błąd wewnętrzny."; break; case WifiP2pManager.P2P_UNSUPPORTED : errorMessage += " Brak wsparcia."; break; default: errorMessage += " Pojawił się nieznany błąd.";
#1 #2 #3
#4 #5
#6
Receptura: Wi-Fi Direct break; } Toast.makeText(WiFiDirectAppActivity.this, errorMessage, Toast.LENGTH_LONG).show(); } }); } @Override protected void onResume() { super.onResume(); registerReceiver(bcReceiver, intentFilter); } @Override protected void onPause() { super.onPause(); unregisterReceiver(bcReceiver); }
#7
#8
}
Pewne instrukcje z listingu 12.9 wymagają objaśnienia. Instrukcja #1 uzyskuje dostęp do usługi systemowej WifiP2pManager, która jest
wymagana do zarządzania połączeniem Wi-Fi Direct. Instrukcja #2 tworzy kanał, wykorzystując bieżący kontekst oraz klasę Looper.
Kanał ten jest wymagany do interakcji z Wi-Fi Direct. Ponadto na podstawie wydarzeń występujących w kanale możesz zdefiniować nasłuchiwacz ChannelListener, aby podejmować odpowiednie akcje. Instrukcja #3 definiuje odbiornik rozgłoszeniowy. Odbiornik rozgłoszeniowy
przyjmuje obiekt WifiP2pManager, kanał oraz bieżącą aktywność jako argumenty i wykorzystuje je do wykonania żądanych zadań, kiedy otrzymuje intencję. Instrukcja #4 dodaje akcję WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION
do filtra IntentFilter. Akcja ta będzie pobierana i porównywana w odbiorniku rozgłoszeniowym w celu podjęcia niezbędnego działania. Instrukcja #5 wywołuje metodę wykrywania urządzenia równorzędnego (ang.
peer) i powiązuje z nią nasłuchiwacz ActionListener w celu nasłuchiwania akcji wykonywanych przez usługę WifiP2pManager. Definiuje dwie metody onSuccess() i onFailure() dla informowania o wynikach podejmowanych prób nawiązania połączenia. Instrukcja #6 definiuje metodę onFailure() w celu wyświetlenia informacji
o przyczynach nieudanego połączenia Wi-Fi Direct. Instrukcja #7 rejestruje odbiornik rozgłoszeniowy. Odbiornik rozgłoszeniowy
będzie wykonywał żądaną akcję na podstawie otrzymanej intencji. Instrukcja #8 wyrejestrowuje odbiornik rozgłoszeniowy.
421
422
Rozdział 12. Łączność bezprzewodowa
Możesz sprawdzić więcej warunków, dodając do filtra IntentFilter poniższe akcje: intentFilter.addAction(WifiP2pManager. WIFI_P2P_PEERS_CHANGED_ACTION); intentFilter.addAction(WifiP2pManager. WIFI_P2P_CONNECTION_CHANGED_ACTION); intentFilter.addAction(WifiP2pManager. WIFI_P2P_THIS_DEVICE_CHANGED_ACTION);
Dla odbiornika rozgłoszeniowego do paczki projektu com.androidtablet.wifidirectapp dodaj nowy plik Java o nazwie WiFiBroadcastReceiver.java. W tym pliku wpisz kod przedstawiony w listingu 12.10. Listing 12.10. Kod wpisany w pliku Java WiFiBroadcastReceiver.java package com.androidtablet.wifidirectapp; import import import import import import
android.content.BroadcastReceiver; android.content.Context; android.content.Intent; android.net.wifi.p2p.WifiP2pManager; android.net.wifi.p2p.WifiP2pManager.Channel; android.widget.Toast;
public class WiFiBroadcastReceiver extends BroadcastReceiver { private WifiP2pManager manager; private Channel channel; private WiFiDirectAppActivity activity; public WiFiBroadcastReceiver(WifiP2pManager manager, Channel channel, WiFiDirectAppActivity activity) { super(); this.manager = manager; this.channel = channel; this.activity = activity; } @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION. equals(action)) { int state = intent.getIntExtra(WifiP2pManager.EXTRA_WIFI_STATE, -1); if (state == WifiP2pManager.WIFI_P2P_STATE_ENABLED) Toast.makeText(context, "Wi-Fi Direct jest włączone", Toast.LENGTH_LONG).show(); else Toast.makeText(context, "Wi-Fi Direct jest wyłączone", Toast.LENGTH_LONG).show(); } } }
Podsumowanie
Jak widzisz, w metodzie onReceive() odbiornika rozgłoszeniowego uzyskiwany jest dostęp do akcji z przekazanej intencji. Na podstawie odebranej akcji intencji wyświetlany jest odpowiedni komunikat. Aby skorzystać z Wi-Fi Direct, dodaj do pliku AndroidManifest.xml następujące instrukcje zezwoleń:
android:name= android:name= android:name= android:name= android:name=
"android.permission.ACCESS_WIFI_STATE" /> "android.permission.CHANGE_WIFI_STATE" /> "android.permission.CHANGE_NETWORK_STATE"/> "android.permission.INTERNET" /> "android.permission.ACCESS_NETWORK_STATE"/>
Po uruchomieniu aplikacji, jeśli urządzenie obsługuje Wi-Fi Direct, wyświetlony zostanie komunikat Wi-Fi Direct jest włączone, co pokazano na rysunku 12.12 (lewy obrazek). Jeśli urządzenie nie obsługuje Wi-Fi Direct, wyświetlony zostanie komunikat Wi-Fi Direct jest wyłączone. Brak wsparcia, co pokazano na rysunku 12.12 (prawy obrazek).
Rysunek 12.12. Komunikat Wi-Fi Direct jest włączone pojawia się, jeśli urządzenie obsługuje Wi-Fi Direct (lewy obrazek). Komunikat Wi-Fi Direct jest wyłączone. Brak wsparcia wyświetlany jest, jeśli urządzenie nie obsługuje Wi-Fi Direct (prawy obrazek)
Podsumowanie W tym rozdziale poznałeś sposób zastosowania technologii Bluetooth i Wi-Fi w aplikacjach Android. Nauczyłeś się włączać i wyłączać moduły Bluetooth i Wi-Fi. Dowiedziałeś się, w jaki sposób ręcznie i za pomocą kodu przesyłać pliki pomiędzy dwoma urządzeniami Bluetooth. Poznałeś procedurę łączenia w pary urządzeń Bluetooth i wyświetlania listy powiązanych urządzeń.
423
424
Rozdział 12. Łączność bezprzewodowa
W kolejnym rozdziale przeczytasz o obsłudze wielordzeniowych procesorów w systemie Android. Nauczysz się także korzystać z podstawowego wątkowania i porównasz wątkowanie z ładowarkami. Na koniec poznasz sposoby usprawnienia procesów odzyskiwania pamięci (ang. garbage collection).
13 Rdzenie i wątki A
by aplikacje Android działały szybko i wydajnie, opracowano i zaimplementowano w urządzeniach Android dwurdzeniowe i czterordzeniowe procesory. W celu wykorzystania zalet architektury wielordzeniowej w moduły Android ulepszono w określony sposób, m.in. wprowadzono algorytm szeregowania wątków (planistę wątków). Do zadań algorytmu szeregowania wątków należy dzielenie i dystrybucja czynności aplikacji pomiędzy różne rdzenie i zmaksymalizowanie ich wykorzystania. Dla sprawnego działania aplikacji dość istotne jest również zarządzanie pamięcią. Mechanizmy odzyskiwania pamięci (ang. garbage collector) odgrywają istotną rolę w oczyszczaniu obszaru pamięci. Kolejnym czynnikiem optymalizującym aplikację jest wykonywanie nieinteraktywnych zadań przez wątki działające w tle. Wątkowanie umożliwia zastosowanie funkcji wielozadaniowych. W tym rozdziale zapoznasz się z architekturą procesorów wielordzeniowych oraz dowiesz się, jaka jest rola mechanizmu odzyskiwania pamięci w zarządzaniu pamięcią. Nauczysz się także korzystać z wątków oraz klasy AsyncTask, aby wykonywać w tle zadania aplikacji, co poprawia jej wydajność.
Receptura: użyteczność architektury procesorów wielordzeniowych Wielozadaniowość jest oczywiście lepsza niż jednozadaniowość. Kiedy korzystasz z jednozadaniowości, wykonywane jest jedno zadanie, pozostałe są wstrzymywane do momentu zakończenia bieżącego zadania. Kiedy z kolei możesz użyć wielozadaniowości, jednocześnie wykonywane jest więcej niż jedno zadanie. Do wykonywania równocześnie wielu zadań wykorzystywana jest architektura procesorów wielordzeniowych. System Android 3.0 i jego nowsze wersje zostały zaprojektowane do obsługi zarówno architektury procesorów jednordzeniowych, jak i wielordzeniowych. Wykorzystanie wielu rdzeni zwiększa wydajność aplikacji poprzez rozdzielanie i uruchamianie obciążenia aplikacji jednocześnie na różnych rdzeniach. Wydajność procesorów wielordzeniowych zależy od zastosowanego algorytmu szeregowania
426
Rozdział 13. Rdzenie i wątki
wątków. Oznacza to, że przewaga wielordzeniowych procesorów mobilnych jest widoczna tylko wtedy, kiedy oprogramowanie może dystrybuować i przydzielać zadania do wszystkich rdzeni. Jeśli szeregowanie nie jest wykonywane w odpowiedni sposób, aplikacja będzie charakteryzowała się gorszą wydajnością podczas uruchomienia kilku rdzeni w urządzeniu. Wiele telefonów z systemem Android wyposażonych jest w procesory o kilku rdzeniach i szybkie zegary (czyli posiadają dwurdzeniowe chipsety i czterordzeniowe procesory). W celu zoptymalizowania systemu Android pod kątem dwu- i czterordzeniowych telefonów wprowadzono kilka zmian, a jego algorytm szeregowania wątków został usprawniony, aby jak najwydajniej wykorzystywać nowe dwu- i czterordzeniowe chipsety.
Receptura: użyteczność procesów odzyskiwania pamięci Odzyskiwanie pamięci (ang. garbage collection) jest procedurą zarządzania pamięcią, w której dynamicznie alokowana pamięć jest automatycznie przywracana. Oznacza to, że jeśli pamięć wykorzystywana przez pewien program nie jest już potrzebna, jest systematycznie odzyskiwana. Odzyskiwanie pamięci jest wykonywane przez mechanizm odzyskiwania pamięci (ang. garbage collector), który śledzi alokacje pamięci i przywraca ją do użycia przez inne uruchomione aplikacje. Mechanizm odzyskiwania pamięci jest wywoływany, kiedy osiągnięty zostaje określony poziom alokowania pamięci. Ma to na celu uwolnienie pamięci dla uruchomionych aplikacji. Mechanizm odzyskiwania pamięci uwalnia programistę z konieczności ręcznego zwalniania alokowanych bloków pamięci, co pozwala uniknąć problemów związanych w wyciekami pamięci i jej przedwczesnym zwalnianiem. W systemie Android zarządzanie pamięcią jest obsługiwane przez mechanizm odzyskiwania pamięci maszyny wirtualnej Dalvik, który wykorzystuje technikę zaznaczania i zamiatania (czyli stosuje bity zaznaczania na danych obiektach, aby wskazać, że są one używane i nie powinny być usuwane). Niezaznaczone obiekty są zamiatane w celu utworzenia wolnej pamięci, która może być wykorzystana przez inne aplikacje. Proces odzyskiwania pamięci zazwyczaj trwa od 100 do 200 ms i w jego trakcie spada wydajność aplikacji. Podczas wykonywania procesu zaznaczania i zamiatania uruchomione aplikacje są wstrzymywane i przestają odpowiadać. Ponadto aplikacje posiadają kilka niewielkich, krótko żyjących obiektów, co powoduje częste wywoływanie mechanizmu odzyskiwania pamięci. Aby umożliwić uniknięcie cyklicznego wywoływania mechanizmu odzyskiwania pamięci, pakiet SDK Android został wyposażony w użyteczne narzędzie o nazwie Allocation Tracker, które jest częścią usługi DDMS (ang. Dalvik Debug Monitor Service).
Uwaga Techniczne rzecz biorąc, w systemie Android nie ma mechanizmów odzyskiwania pamięci, ponieważ jest to system operacyjny oparty na języku C. Omawiane w tym rozdziale odzyskiwanie pamięci dostępne jest tylko w środowisku uruchomieniowym maszyny wirtualnej Dalvik.
Receptura: użyteczność procesów odzyskiwania pamięci
DDMS jest wszechstronnym narzędziem debugującym, które pobierane jest jako część pakietu Android SDK. Można je uruchomić, klikając ikonę DDMS widoczną w prawym górnym rogu Eclipse IDE lub wybierając z menu opcję Window/Open Perspective/DDMS. Kiedy uruchomisz DDMS, narzędzie automatycznie połączy się z podłączonym urządzeniem Android lub każdym działającym emulatorem. Okno narzędzia DDMS zostało przedstawione na rysunku 13.1.
Rysunek 13.1. Okno narzędzia DDMS
W lewym górnym panelu okna DDMS zakładka Devices (urządzenia) wyświetla listę podłączonych do danego komputera urządzeń Android, w tym również uruchomionych urządzeń AVD (jeśli jakieś działają). Wyświetlane są również maszyny wirtualne powiązane z każdym urządzeniem fizycznym lub AVD. Wybranie maszyny wirtualnej spowoduje wyświetlenie w prawym panelu informacji na jej temat. W zakładce Devices aktywne staną się następujące ikony, w kolejności od lewej do prawej. Debug (debuguj) — służy do debugowania wybranego procesu. Update Heap (aktualizuj stertę) — włącza informacje o stercie dla danego
procesu. Po kliknięciu tej ikony skorzystaj z zakładki Heap (sterta) w prawym panelu, aby uzyskać informacje o stercie. Dump HPROF file (zrzuć plik HPROF) — pokazuje plik HPROF, który może
być wykorzystany do wykrywania wycieków pamięci. Cause GC (uruchom odzyskiwanie pamięci) — wywołuje procedurę
odzyskiwania pamięci. Update Threads (aktualizuj wątki) — włącza pobieranie informacji o wątku dla
wybranego procesu. Po kliknięciu tej ikony musisz otworzyć zakładkę Threads (wątki) w prawym panelu, aby wyświetlić informacje o wątkach, które są tworzone i niszczone w danym procesie. Start Method Profiling (rozpocznij profilowanie metody) — stosowana do ustalenia
liczby wywołań różnych metod w aplikacji oraz czasu, jaki został spędzony w każdej z nich.
427
428
Rozdział 13. Rdzenie i wątki
Stop Process (zatrzymaj proces) — zatrzymuje wybrany proces. Screen Capture (przechwyć obraz) — przechwytuje obraz Twojego urządzenia
lub emulatora. Dump View Hierarchy for UI Automator (zrzuć hierarchię widoków dla
Automatora interfejsu użytkownika) — wyświetla obiekty widoków, które tworzą interfejs użytkownika aktywności uruchomionej na danym urządzeniu lub emulatorze. Możesz zobaczyć i porównać dowolne widoki w ramach kontekstu całego drzewa widoków. Ikona wyświetla również informacje związane z renderowaniem widoków. Capture System-Wide Trace Using Android systrace (przechwyć ślad całego
systemu za pomocą narzędzia systrace Androida) — po kliknięciu tej ikony należy określić plik dla przechwytywania śladu na poziomie systemu. Do tego pliku przechwycone zostaną zdarzenia śledzenia, takie jak zmiany częstotliwości CPU, zdarzenia bezczynności CPU, obciążenie CPU, zasoby We/Wy dysku itd. W prawym panelu znajdziesz następujące zakładki. Threads (wątki) — tu wyświetlane są informacje o wątkach w ramach każdego
procesu. Heap (sterta) — tu wyświetlane są informacje o stercie dla danego procesu
(jeśli kliknięta została ikona Update Heap z zakładki Devices). Wybierz przycisk Cause GC (patrz rysunek 13.2, górny obrazek), aby rozpocząć proces odzyskiwania pamięci. Wyświetlane są typy obiektów oraz rozmiar pamięci do nich przydzielonej. Kiedy wybierzesz jakiś typ obiektu, wyświetlony zostanie wykres słupkowy pokazujący liczbę obiektów, do których alokowano określony rozmiar pamięci w bajtach (patrz rysunek 13.2, dolny obrazek). Allocation Tracker (narzędzie do śledzenia alokacji) — śledzi obiekty
alokowane do aplikacji. Kliknij przycisk Start Tracking (rozpocznij śledzenie), a następnie spowoduj, aby aplikacja wykonała kod, który chcesz przeanalizować. Potem kliknij przycisk Get Allocations (pobierz alokacje), aby zobaczyć listę obiektów alokowanych do tej alokacji (patrz rysunek 13.3). Lista alokowanych obiektów znajdzie się w górnym panelu, a ślad stosu prowadzącego do wybranej alokacji będzie widoczny w dolnym panelu. Dostępne będą nie tylko informacje o typie alokowanego obiektu, ale również w którym wątku, klasie, pliku i wierszu jest on alokowany. Aby wyczyścić dane i ponownie uruchomić śledzenie, możesz kliknąć przycisk Stop Tracking (zatrzymaj śledzenie).
Receptura: wątki
Rysunek 13.2. Zakładka Heap wyświetlająca informacje o stercie dla uruchomionego procesu (górny obrazek) oraz okno dialogowe, które po kliknięciu przycisku Cause GC wyświetla informacje o obiektach i alokowanej do nich pamięci (dolny obrazek)
Receptura: wątki Wszystkie komponenty aplikacji Android, takie jak aktywności, usługi i odbiorniki rozgłoszeniowe, działają w głównym wątku aplikacji. Innymi słowy, główny wątek aplikacji wykonuje wszystkie zadania związane z interfejsem użytkownika. W celu zwiększenia wydajności aplikacji zadania długotrwałe lub asynchroniczne oraz zadania, które nie wchodzą w interakcję z użytkownikiem, są wykonywane w wątkach działających w tle.
429
430
Rozdział 13. Rdzenie i wątki
Rysunek 13.3. Zakładka Allocation Tracker wyświetlająca informacje o obiektach alokowanych do aplikacji
Ponadto, ponieważ system operacyjny oczekuje, że główny wątek będzie odpowiadał na komunikaty, każda operacja, która może blokować główny wątek, jest wykonywana w osobnym wątku. Główny wątek kontroluje zatem pozostałe wątki i aktualizuje elementy interfejsu użytkownika. Istnieją dwa sposoby przenoszenia zadań przetwarzania do wątków działających w tle: zastosowanie klasy Handler, zastosowanie klasy AsyncTask.
W tej recepturze nauczysz się korzystać z klasy Handler do wykonywania zadań przy użyciu wątków działających w tle. W celu przetwarzania zadań w tle będziesz implementował własne wątki i korzystał z klasy Handler do przeprowadzenia synchronizacji z nimi. Klasa Handler jest powiązana z wątkiem, z którego została utworzona. Oznacza to, że możesz wykonywać bloki kodu w wątku, z którego klasa Handler jest instancjonowana. Z klasą Handler możesz komunikować się na dwa sposoby: za pomocą komunikatów oraz obiektów oczekujących na uruchomienie. Obiekt utworzony z klasy Handler przetwarza komunikaty i oczekujące na uruchomienie obiekty, powiązane z kolejką komunikatów MessageQueue bieżącego wątku. W kolejce MessageQueue przechowywane są zadania oczekujące na wykonanie przez bieżący wątek. W celu przetwarzania komunikatów musisz nadpisać metodę handleMessage(). Do przetwarzania obiektów oczekujących na uruchomienie powinieneś użyć metody post(). Dany wątek może publikować komunikaty za pomocą metod sendMessage(Message msg) lub sendEmptyMessage(). Aby zrozumieć koncepcję stosowania klasy Handler, utwórz aplikację wyświetlającą kolejne liczby z zakresu od 1 do 10 z wykorzystaniem wątków. Nazwij ją ThreadApp. Ponieważ w tej aplikacji będziesz wyświetlał tylko kolejne liczby, musisz jedynie skorzystać z kontrolki TextView. W celu zdefiniowania kontrolki TextView wpisz w pliku activity_thread_app.xml kod przedstawiony w listingu 13.1.
Receptura: wątki Listing 13.1. Kod wpisany w pliku układu activity_thread_app.xml
Jak widzisz, kontrolce TextView przypisano identyfikator seqnums, który zostanie użyty w kodzie Java w celu identyfikacji i zastosowania tej kontrolki. Tekst wyświetlany za pomocą kontrolki TextView będzie prezentowany czcionką o rozmiarze zdefiniowanym w zasobie wymiarów text_size. Aby zdefiniować procedurę obsługi wątku i za jej pomocą wysyłać oraz odbierać komunikaty, wpisz w pliku aktywności Java ThreadAppActivity.java kod przedstawiony w listingu 13.2. Listing 13.2. Kod wpisany w pliku aktywności Java ThreadAppActivity.java package com.androidtablet.threadapp; import import import import import
android.app.Activity; android.os.Bundle; android.widget.TextView; android.os.Handler; android.os.Message;
public class ThreadAppActivity extends Activity { static TextView seqNums; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_thread_app); seqNums=(TextView)findViewById(R.id.seqnums); } static Handler handler = new Handler() { @Override public void handleMessage(Message msg) { seqNums.setText(msg.obj.toString()); } }; @Override protected void onStart() { super.onStart(); Thread thread=new Thread(new Runnable() { @Override public void run() {
431
432
Rozdział 13. Rdzenie i wątki for(int i=1;i<=10;i++){ try { Thread.sleep(1000); Message msg = new Message(); msg.obj=String.valueOf(i); handler.sendMessage(msg); } catch (InterruptedException e) { e.printStackTrace(); } } } }); thread.start(); } }
Jak widzisz, uzyskiwany jest dostęp do kontrolki TextView z pliku układu i kontrolka ta jest mapowana na obiekt TextView o nazwie seqNums. W metodzie onStart() wykorzystywana jest klasa Thread z obiektem oczekującym na uruchomienie. Metoda run() rozpoczyna wykonywanie danego wątku. Aby za pomocą kontrolki TextView wyświetlić sekwencję liczb od 1 do 10, w metodzie run() tworzony jest obiekt msg klasy Message i dodawane są do niego liczby tej sekwencji. Każda liczba od 1 do 10 dodawana jest do zmiennej obj obiektu msg klasy Message. Po dodaniu wartości sekwencji do zmiennej obj za pomocą obiektu klasy Handler wysyłany jest komunikat. Tworzony jest obiekt handler klasy Handler. W jego metodzie wywołania zwrotnego handlemessage() otrzymywany jest za pomocą parametru msg komunikat wysłany przez metodę run(). Uzyskiwany jest dostęp do zmiennej obj (liczba z sekwencji w parametrze msg) z parametru msg i zmienna ta jest wyświetlana za pomocą kontrolki TextView. Po uruchomieniu aplikacji wyświetlana jest sekwencja liczb, która rozpoczyna się od 1 (patrz rysunek 13.4, górny obrazek). Pomiędzy każdą z liczb sekwencji występuje opóźnienie 1 sekundy. Aplikacja zatrzymuje się na liczbie 10, co widać na rysunku 13.4 (dolny obrazek).
Receptura: używanie wielu wątków W tej recepturze nauczysz się korzystać z dwóch wątków. Zobaczysz, jak za pomocą tych wątków wykonywane są jednocześnie dwa zadania. Dla obu wątków utworzysz pojedynczą procedurę obsługi. Wątki będą wyświetlały sekwencje liczb od 1 do 10. Pierwszy wątek będzie wyświetlał każdą z liczb sekwencji po opóźnieniu 1 sekundy, podczas gdy drugi wątek będzie wyświetlał każdą z liczb sekwencji po opóźnieniu 2 sekund. Oba wątki rozpoczną swoje zadania jednocześnie, ale pierwszy z nich osiągnie swoją wartość końcową 10 w czasie o połowę krótszym niż drugi wątek.
Receptura: używanie wielu wątków
Rysunek 13.4. Aplikacja zaczyna liczby 1 z danej sekwencji (górny obrazek) i zatrzymuje się na liczbie 10 z tej sekwencji (dolny obrazek)
W procedurze obsługi nadpisana zostanie metoda handleMessage() w celu wyświetlenia sekwencji liczb. Liczby z sekwencji będą przekazywane do tej metody za pomocą parametru Message. Aby rozróżnić obiekty Message obu wątków, nazwa wątku wywołującego została zawarta w obiekcie Message. Utwórz nowy projekt Android o nazwie MultipleThreadsApp. W tej aplikacji będziesz korzystał z czterech kontrolek TextView. Dwie kontrolki TextView będą pełniły rolę nagłówków, wyświetlając odpowiednio tytuły Wątek 1 i Wątek 2. Pozostałe dwie kontrolki TextView zostaną wykorzystane do wyświetlenia sekwencji liczb obu wątków.
433
434
Rozdział 13. Rdzenie i wątki
Aby zdefiniować cztery kontrolki TextView, wpisz w pliku układu aktywności activity_multiple_threads_app.xml kod przedstawiony w listingu 13.3. Listing 13.3. Kod wpisany w pliku układu aktywności activity_multiple_threads_app.xml
Jak widzisz, dwie kontrolki TextView zostały skonfigurowane do wyświetlania odpowiednio tytułów Wątek 1 i Wątek 2. Pozostałym dwóm kontrolkom TextView, które posłużą do wyświetlania sekwencji liczb obu wątków, przypisano odpowiednio
Receptura: używanie wielu wątków
identyfikatory seqnums1 i seqnums2. Tekst wyświetlany za pomocą kontrolek TextView będzie prezentowany czcionką o rozmiarze zdefiniowanym w zasobie wymiarów text_size. Aby zdefiniować procedurę obsługi oraz wysyłać i odbierać komunikaty obu wątków, wpisz w pliku aktywności Java MultipleThreadsAppActivity.java kod przedstawiony w listingu 13.4. Listing 13.4. Kod wpisany w pliku aktywności Java MultipleThreadsAppActivity.java package com.androidtablet.multiplethreadsapp; import import import import import
android.os.Bundle; android.app.Activity; android.widget.TextView; android.os.Message; android.os.Handler;
public class MultipleThreadsAppActivity extends Activity { static private TextView seqNums1, seqNums2; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_multiple_threads_app); seqNums1 = (TextView)findViewById(R.id.seqnums1); seqNums2 = (TextView)findViewById(R.id.seqnums2); } static Handler handler1 = new Handler() { @Override public void handleMessage(Message msg) { String threadInvoked=msg.getData().getString( "threadName"); if (threadInvoked.equals("wątek 1")) seqNums1.setText(msg.obj.toString()); if (threadInvoked.equals("wątek 2")) seqNums2.setText(msg.obj.toString()); } }; @Override protected void onStart() { super.onStart(); Thread thread1 = new Thread(new Runnable() { @Override public void run() { try { for(int i=1;i<=10;i++){ Thread.sleep(1000); Message msg = new Message(); Bundle bundle = new Bundle(); bundle.putString("threadName", "wątek 1"); msg.setData(bundle); msg.obj=String.valueOf(i);
435
436
Rozdział 13. Rdzenie i wątki handler1.sendMessage(msg); } } catch (InterruptedException e) { e.printStackTrace(); } } }); Thread thread2 = new Thread(new Runnable() { @Override public void run() { try { for(int i=1;i<=10;i++){ Message msg = new Message(); Thread.sleep(2000); Bundle bundle = new Bundle(); bundle.putString("threadName", "wątek 2"); msg.setData(bundle); msg.obj=String.valueOf(i); handler2.sendMessage(msg); } } catch (InterruptedException e) { e.printStackTrace(); } } }); thread1.start(); thread2.start(); } }
Jak widzisz, uzyskiwany jest dostęp do kontrolek TextView z pliku układu i kontrolki te są mapowane na obiekty TextView o nazwach odpowiednio seqNums1 i seqNums2. W metodzie onStart() dla obu wątków wykorzystywana jest klasa Thread z obiektem Runnable. Metoda run() rozpoczyna wykonywanie obu wątków. W celu wyświetlenia sekwencji liczb od 1 do 10 w metodzie run() tworzony jest obiekt Message o nazwie msg i dodawane są do niego liczby z sekwencji. Nazwa uruchomionego wątku powinna być zawarta w obiekcie Message. Na koniec za pomocą obiektu Handler wysyłany jest komunikat w celu wyświetlenia przez kontrolki TextView sekwencji liczb na ekranie. Dla pierwszego wątku opóźnienie pomiędzy wyświetlanymi liczbami z sekwencji wynosi 1 sekundę, a dla drugiego wątku — 2 sekundy. Po uruchomieniu aplikacji oba wątki wyświetlą sekwencje liczb, rozpoczynające się od 1 (patrz rysunek 13.5, górny obrazek). Pierwszy wątek osiągnie wartość końcową 10 w czasie o połowę krótszym niż drugi wątek. Na rysunku 13.5 (dolny obrazek) przedstawiono pierwszy wątek wyświetlający wartość 10, podczas gdy drugi wątek wyświetla jeszcze wartość 5.
Receptura: korzystanie z klasy AsyncTask
Rysunek 13.5. Oba wątki są uruchamiane jednocześnie i wyświetlają początkową wartość 1 (górny obrazek). Wątek 1 kończy wyświetlać swoją sekwencję liczb przed wątkiem 2 (dolny obrazek)
Receptura: korzystanie z klasy AsyncTask W tej recepturze nauczysz się wykorzystywać klasę AsyncTask do tworzenia wątków i zarządzania nimi. Zasadniczo klasa AsyncTask tworzy asynchroniczne zadanie w celu wykonywania przetwarzania w tle. Klasa AsyncTask zapewnia kilka procedur obsługi zdarzeń, które synchronizują się z wątkiem, aby pokazywać postęp i zakończenie zadania.
437
438
Rozdział 13. Rdzenie i wątki
Klasa ta może wykonywać kilka zadań, m.in. działając w tle, pobierać dane z sieci i zapisywać dane. W klasie AsyncTask istnieją jednak pewne wady konstrukcyjne. Jest oparta na procedurach obsługi oraz puli i może obsługiwać jednocześnie tylko kilka wątków. Ponieważ klasa AsyncTask znajduje się w bibliotece zgodności, ten przykład może być również wykorzystany na starszych urządzeniach z systemem w wersji 2.2. Aby wykonać instancjonowanie klasy AsyncTask, musisz rozszerzyć tę klasę i zapewnić trzy parametry: Parametry wejściowe, Wartości progresu oraz Wartości wynikowe. Jeśli nie chcesz podawać któregoś z tych parametrów, po prostu wstaw w jego miejsce wartość Void. Dana podklasa powinna również nadpisywać następujące procedury obsługi zdarzeń. doInBackground — metoda jest wykonywana w wątku działającym w tle.
W procedurze obsługi umieszczany jest kod, który nie wchodzi w interakcję z użytkownikiem. Jako dane wejściowe do tej metody przekazywany jest parametr Parametry wejściowe. Z poziomu tej metody wywoływana jest metoda publishProgress(), a metoda onProgressUpdate() jest wykonywana w głównym wątku. Wykorzystując metody publishProgress() i onProgressUpdate(), wątek działający w tle komunikuje się z wątkiem głównym w celu aktualizacji elementów interfejsu użytkownika, aby wskazać postęp pracy. Pamiętaj, że kod w tej metodzie uruchamiany jest w osobnym wątku działającym w tle. onProgressUpdate — nadpisz tę procedurę obsługi, aby zaktualizować interfejs
użytkownika w celu wskazania postępu w wykonywaniu zadania. Ta procedura obsługi otrzymuje zestaw parametrów przekazanych do metody publishProgress(). Podczas wykonywania procedura jest synchronizowana z wątkiem, możesz więc bezpiecznie modyfikować elementy interfejsu użytkownika. onPostExecute — jak wskazuje nazwa, metoda ta jest wywoływana po
zakończeniu metody doInBackground(). Parametr Wartości wynikowe zwracany przez metodę jest przekazywany do tej procedury obsługi zdarzeń. Może ona informować, kiedy asynchroniczne zadanie zostanie zakończone. Klasa AsyncTask zapewnia również poniższe dwie pomocne metody wywołania zwrotnego. onPreExecute — jak wskazuje nazwa, metoda jest wywoływana przed wywołaniem
metody doInBackground(). Jest uruchamiana w głównym wątku i wykonuje konfigurację lub inne tego typu zadania. onCancelled — zarządza unieważnianiem wątku. Metoda przerywa
wykonywanie wątku i zapobiega wykonaniu metody onPostExecute().
Uwaga Nadpisywanie metod onPostExecute, onPreExecute, onProgressUpdate oraz onCancelled jest opcjonalne.
Receptura: korzystanie z klasy AsyncTask
Utwórz aplikację, która wyświetli sekwencję liczb od 1 do 10, wykorzystując wątek działający w tle. Nadaj jej nazwę AsyncTasksApp. Do wyświetlenia sekwencji liczb od 1 do 10 potrzebujesz jedynie kontrolki TextView. Po jej zdefiniowaniu plik układu aktywności activity_async_tasks_app.xml będzie wyglądał tak, jak przedstawiono w listingu 13.5. Listing 13.5. Kod wpisany w pliku układu aktywności activity_async_tasks_app.xml
Jak widzisz, kontrolce TextView przypisano identyfikator seqnums, który umożliwia jej identyfikację i użycie w kodzie Java. Aby rozszerzyć klasę AsyncTask i wyświetlić za jej pomocą sekwencję liczb od 1 do 10, zmodyfikuj plik aktywności Java AsyncTasksAppActivity.java w sposób przedstawiony w listingu 13.6. Listing 13.6. Kod wpisany w pliku aktywności Java AsyncTasksAppActivity.java package com.androidtablet.asynctasksapp; import import import import
android.app.Activity; android.os.Bundle; android.widget.TextView; android.os.AsyncTask;
public class AsyncTasksAppActivity extends Activity { TextView seqNums; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_async_tasks_app); seqNums=(TextView) findViewById(R.id.seqnums); new PrintSequenceTask().execute(1); } private class PrintSequenceTask extends AsyncTask { @Override protected void onPreExecute() { seqNums.setText("Sekwencja liczb rozpoczęta"); } @Override protected Void doInBackground(Integer... args) {
439
440
Rozdział 13. Rdzenie i wątki for (int i = args[0]; i <= 10; i++) { publishProgress(i); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } return null; } @Override protected void onProgressUpdate(Integer... args) { seqNums.setText(args[0].toString()); } @Override protected void onPostExecute(Void result) { seqNums.setText("Sekwencja liczb zakończona"); } } }
Jak widać w kodzie, klasa PrintSequenceTask rozszerza klasę AsyncTask. Ponieważ chcesz wyświetlić sekwencję liczb, rozpoczynając od 1, wartość numeryczna 1 jest przekazywana jako Parametr wejściowy podczas instancjonowania klasy PrintSequence. Przed wykonaniem metody doInBackground() wykonywana jest metoda onPreExecute(), która za pomocą kontrolki TextView wyświetla tekst Sekwencja liczb rozpoczęta. Po wykonaniu metody onPreExecute() wykonywana jest metoda doInBackground(), a wartość parametru Parametr wejściowy jest przekazywana do tej metody i przypisywana do tablicy args. Wewnątrz metody doInBackground() wykonywana jest pętla for, rozpoczynająca się od pierwszego elementu w tablicy args (wartość 1) i działająca aż do wartości 10. W pętli for wykonywana jest metoda publishProgress() i każda wartość pętli for jest przekazywana do tej metody jako parametr. Oznacza to, że liczby z sekwencji od 1 do 10 zostaną przypisane do tej metody jako parametr. Kiedy wywoływana jest metoda publishProgress(), metoda onProgressUpdate() jest wykonywana w głównym wątku z tą samą wartością parametru, co wartość przypisana do metody publishProgress(). Oznacza to również, że liczby z sekwencji od 1 do 10 zostaną przypisane do parametru args metody onProgressUpdate(). W metodzie onProgressUpdate() wyświetlane są za pomocą kontrolki TextView liczby sekwencji przypisane do parametru args. Pomiędzy każdą liczbą z sekwencji wprowadzane jest opóźnienie 1 sekundy. Na koniec, kiedy zakończona zostaje metoda doInBackground() i wszystkie liczby wyświetlone, wykonywana jest metoda onPostExecute(), która za pomocą kontrolki TextView wyświetla komunikat Sekwencja liczb zakończona. Po uruchomieniu aplikacji pierwszym komunikatem wyświetlanym za pomocą kontrolki TextView jest tekst Sekwencja liczb rozpoczęta (patrz rysunek 13.6, pierwszy obrazek). Po tym komunikacie wyświetlana jest sekwencja liczb od 1 (patrz rysunek 13.6,
Receptura: korzystanie z klasy AsyncTask
drugi obrazek) do 10 (patrz rysunek 13.6, trzeci obrazek). Każda liczba z sekwencji zostanie wyświetlona po opóźnieniu trwającym 1 sekundę. Na koniec w kontrolce TextView pojawia się komunikat Sekwencja liczb zakończona (patrz rysunek 13.6, czwarty obrazek).
Rysunek 13.6. Komunikat Sekwencja liczb rozpoczęta wyświetlony po uruchomieniu aplikacji (pierwszy obrazek). Sekwencja liczb od 1 do 10 wyświetlana na ekranie (drugi i trzeci obrazek). Komunikat Sekwencja liczb zakończona pojawia się po wyświetleniu ostatniej liczby z sekwencji (czwarty obrazek)
441
442
Rozdział 13. Rdzenie i wątki
Podsumowanie W tym rozdziale poznałeś różne czynniki, które zwiększają wydajność aplikacji. Dowiedziałeś się, jaka jest użyteczność procesorów dwu- i czterordzeniowych dla poprawy działania aplikacji. Zobaczyłeś, jak wywoływany jest mechanizm odzyskiwania pamięci i jak jest czyszczona pamięć dla nowych aplikacji. Poznałeś także procedurę uruchamiania zadań aplikacji w wątkach działających w tle. Na koniec dowiedziałeś się, w jaki sposób klasa AsyncTask tworzy asynchroniczne zadania i zarządza nimi w celu wykonania przetwarzania zadań w tle. Kolejny rozdział poświęcony jest standardowym sensorom. Nauczysz się, jak w aplikacjach Android wykorzystywać różne sensory, takie jak akcelerometr, czujnik zbliżeniowy oraz żyroskop.
14 Klawiatury i sensory J
eśli chcesz wprowadzać dane różnych typów i różnych językach, musisz wybrać i skonfigurować swoją klawiaturę w taki sposób, aby odpowiadała żądanemu rodzajowi danych wejściowych. Żeby obserwować zmiany, które mogą występować w otoczeniu Twojego środowiska i podejmować niezbędne działania, musisz rozumieć działanie różnych czujników obsługiwanych przez urządzenia z systemem Android. W tym rozdziale nauczysz się konfigurować swoją klawiaturę tak, aby sprostała wymaganiom wprowadzanych danych. Zapoznasz się z sensorami, ich nasłuchiwaczami zdarzeń oraz metodą wymaganą w celu uzyskiwania dostępu do danych generowanych przez sensory urządzenia. Będziesz wyświetlał listę sensorów obsługiwanych przez fizyczne urządzenie oraz nauczysz się procedury określania bieżącego przyspieszania urządzenia wzdłuż trzech osi z wykorzystaniem czujnika przyspieszenia. Wykorzystasz czujnik zbliżeniowy do ustalania odległości pomiędzy obiektem a urządzeniem oraz zastosujesz żyroskop do wyznaczenia orientacji urządzenia wokół danej osi.
Receptura: zmiana klawiatury i metody wprowadzania danych w systemie Android Aby wprowadzać dane w różnych językach lub dane, które wymagają znaków specjalnych, musisz skonfigurować ustawienia klawiatury. Kiedy chcesz zmienić klawiaturę lub metodę wprowadzania danych, wykonaj poniższe czynności. 1. W ustawieniach urządzenia wybierz opcje Język i wprowadzanie
(patrz rysunek 14.1, lewy górny obrazek). 2. W ekranie Język i wprowadzanie wskaż język, włącz lub wyłącz sprawdzanie
pisowni podczas pisania, włącz opcję Pisanie głosowe Google i wybierz typ klawiatury (patrz rysunek 14.1, prawy górny obrazek). Na tym ekranie możesz również włączyć wyszukiwanie głosowe, przetwarzanie tekstu na mowę oraz dostosować szybkość wskaźnika (patrz rysunek 14.1, lewy dolny obrazek).
444
Rozdział 14. Klawiatury i sensory
Rysunek 14.1. Ekran ustawień wyświetlający różne opcje (lewy górny obrazek). Ekran Język i wprowadzanie pokazujący opcje, które umożliwiają ustawienie języka klawiatury i metod wprowadzania (prawy górny obrazek). Opcje włączania wyszukiwania głosowego, przetwarzania tekstu na mowę itd. (lewy dolny obrazek). Klawiatura ze zdefiniowanymi ustawieniami wyświetlona po kliknięciu pola tekstowego (prawy dolny obrazek)
Receptura: sensory
3. Ustaw klawiaturę tak, aby obsługiwała funkcje sugerowania wyrazów,
rozpoznawania gestów itd. Po prostu wybierz opcje, które najbardziej Ci odpowiadają. 4. Aby sprawdzić zdefiniowane ustawienia, otwórz jakąś aplikację, przejdź
do dowolnego pola tekstowego i kliknij je. Wyświetlona zostanie klawiatura z określonymi ustawieniami i będziesz mógł rozpocząć wprowadzanie danych (patrz rysunek 14.1, prawy dolny obrazek).
Receptura: sensory Sensory to urządzenia pełniące funkcję czujników. Oznacza to, że wyczuwają wszelkie zmiany temperatury, ciśnienia, szybkości, ilości i oświetlenia. Po zmierzeniu stopnia zmiany konwertują ją na sygnał, który może być dalej przetwarzany w celu podjęcia niezbędnych działań. Choć może się to różnić w zależności od urządzenia, większość urządzeń Android posiada wbudowane sensory sprzętowe, które mierzą ruch, temperaturę, siłę ciężkości, orientację i inne zmiany występujące w otoczeniu urządzenia. System Android obsługuje różne typy sensorów, w tym akcelerometr, czujnik pola magnetycznego, czujnik orientacji oraz czujnik zbliżeniowy. Dostęp do sensorów urządzenia uzyskiwany jest za pomocą klasy SensorManager. Klasa SensorManager to uruchamiana w Androidzie usługa systemowa, która nasłuchuje danych dostarczanych przez sensory. Instancję tej usługi możesz utworzyć, wywołując metodę Context.getSystemService() z argumentem SENSOR_SERVICE, tak jak pokazano w poniższej instrukcji: SensorManager sensorManager= (SensorManager)getSystemService(SENSOR_SERVICE);
Aby uzyskać dostęp do któregoś z sensorów urządzenia, wywołaj metodę getDefaultSensor() klasy SensorManager, przekazując do niej stałą, która reprezentuje
typ żądanego sensora. Każdy typ sensora jest identyfikowany przez unikatową stałą zdefiniowaną w klasie Sensor. Przykładowo poniższa instrukcja uzyskuje dostęp do akcelerometru urządzenia: sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
Różne typy sensorów wraz z odpowiadającymi im stałymi opisano w tabeli 14.1.
445
446
Rozdział 14. Klawiatury i sensory Tabela 14.1. Krótki opis różnych typów sensorów
Stała sensora
Opis
Sensor.TYPE_AMBIENT_TEMPERATURE
Zwraca temperaturę otoczenia w stopniach Celsjusza.
Sensor.TYPE_ACCELEROMETER
Trzyosiowy akcelerometr, który zwraca bieżące przyspieszenie wzdłuż trzech osi w jednostkach m/s2 (metr na sekundę do kwadratu).
Sensor.TYPE_GRAVITY
Trzyosiowy czujnik grawitacji, który zwraca bieżące kierunek i wielkość siły ciężkości w jednostkach m/s2.
Sensor.TYPE_LINEAR_ACCELERATION
Trzyosiowy czujnik przyspieszenia liniowego, który zwraca przyspieszenie wzdłuż trzech osi w jednostkach m/s2 bez uwzględnienia grawitacji.
Sensor.TYPE_GYROSCOPE
Trzyosiowy żyroskop, który zwraca stopień obrotu urządzenia wzdłuż trzech osi w jednostkach rad/s (radiany na sekundę).
Sensor.TYPE_ROTATION_VECTOR
Zwraca orientację urządzenia jako kombinację kąta wokół osi.
Sensor.TYPE_MAGNETIC_FIELD
Magnetometr, który odczytuje bieżącą wartość pola magnetycznego w mikroteslach (μT) wzdłuż trzech osi.
Sensor.TYPE_PRESSURE
Czujnik ciśnienia atmosferycznego, który zwraca bieżące ciśnienie atmosferyczne w milibarach.
Sensor.TYPE_RELATIVE_HUMIDITY
Zwraca bieżącą wilgotność względną w procentach.
Sensor.TYPE_PROXIMITY
Wskazuje odległość pomiędzy urządzeniem a obiektem docelowym w centymetrach. Zazwyczaj wykorzystywany do wykrywania, czy urządzenie zostało przyłożone do ucha użytkownika, co pozwala zarządzać jasnością ekranu i ustawieniami głosu.
Sensor.TYPE_LIGHT
Zwraca stopień oświetlenia otoczenia i zazwyczaj jest wykorzystywany do dynamicznej kontroli jasności ekranu.
Uwaga Urządzenia Android mogą posiadać wszystkie z wymienionych sensorów, niektóre z nich albo nie posiadać żadnego. Emulator Androida w ograniczonym stopniu obsługuje symulację czujników sprzętowych, modułu Wi-Fi, modułu Bluetooth oraz baterii urządzenia. Kod omówiony w tym rozdziale działa tylko na fizycznych urządzeniach Android.
Receptura: sensory
Po uzyskaniu dostępu do konkretnego sensora w urządzeniu wykorzystujesz metodę SensorManager.registerListener() do nasłuchiwania zdarzeń, które mogą wystąpić w sensorze. Metoda akceptuje interfejs wywołania zwrotnego SensorEventListener, który klasa SensorManager wykorzystuje do informowania o wystąpieniu zdarzeń sensora. Informacja o wystąpieniu tych zdarzeń jest przekazywana za pomocą metod onSensorChanged() i onAccuracyChanged(). abstract void onAccuracyChanged(Sensor sensor, int dokładność) —
metoda jest wywoływana, kiedy ma miejsce zmiana dokładności czujnika lub stopnia błędu. Pierwszy parametr sensor w tej metodzie jest zarejestrowanym sensorem, a drugi parametr jest wartością dokładności. Parametr dokładność może przyjmować następujące wartości: SensorManager.SENSOR_STATUS_ACCURACY_HIGH — reprezentuje dużą dokładność, SensorManager.SENSOR_STATUS_ACCURACY_MEDIUM — reprezentuje średnią
dokładność, SensorManager.SENSOR_STATUS_ACCURACY_LOW — reprezentuje małą dokładność, SensorManager.SENSOR_STATUS_UNRELIABLE — wskazuje, że dokładność jest
zawodna. abstract void onSensorChanged(SensorEvent zdarzenie) — metoda jest
wywoływana, kiedy występuje zmiana w wartościach sensora. Wartości sensora w tej metodzie reprezentuje parametr SensorEvent. Zasadniczo SensorEvent jest tablicą wartości zmiennoprzecinkowych, które zawierają wartości sensora. Wielkość i zawartość tej tablicy zależą od typu stosowanego sensora. Sensor urządzenia zwraca dane, kiedy zostanie dla niego zarejestrowany nasłuchiwacz. Szybkość zwracania danych jest określana przez argument przekazywany podczas rejestrowania nasłuchiwacza. Argument ten może przyjmować jedną z następujących wartości: SENSOR_DELAY_NORMAL — dane są przesyłane w normalnym tempie, SENSOR_DELAY_UI — dane są przesyłane z szybkością wymaganą dla interakcji
w interfejsie użytkownika, SENSOR_DELAY_GAME — dane są przesyłane z szybkością wymaganą w grach, SENSOR_DELAY_FASTEST — dane są przesyłane z największą możliwą szybkością.
Przy użyciu poniższej instrukcji zarejestrujesz nasłuchiwacz dla akcelerometru urządzenia, a urządzenie zostanie skonfigurowane do przesyłania danych w normalnym tempie: sensorManager.registerListener(this, sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER), SensorManager.SENSOR_DELAY_NORMAL);
447
448
Rozdział 14. Klawiatury i sensory
Nie zapomnij wyrejestrować nasłuchiwacza zdarzeń i wyłączyć sensor, kiedy ten nie jest używany. Inaczej nasłuchiwacz zdarzeń będzie nadal pochłaniał zasoby CPU i baterii. Nasłuchiwacz zdarzeń wyrejestrowuje się za pomocą metody unregisterListener(), tak jak pokazano w kolejnej instrukcji: sensorManager.unregisterListener(event_listener);
Zalecane jest rejestrowanie nasłuchiwacza do pobierania powiadomień w metodzie onResume() i wyrejestrowanie go w metodzie onPause().
Receptura: lista sensorów obsługiwanych przez urządzenie Nie wszystkie sensory dostępne są w każdym urządzeniu. Przykładowo urządzenie, z którego korzystam, posiada akcelerometr i czujnik zbliżeniowy, ale nie ma czujnika temperatury, ciśnienia, pola magnetycznego ani żyroskopu. Ponieważ urządzenie może nie być wyposażone w określony sensor lub może on w nim istnieć, warto najpierw to sprawdzić. Możesz sprawdzić obecność określonego sensora (lub sensorów) za pomocą metod SensorManager.getDefaultSensor() lub ServiceManager.getSensorList(). Korzystając z metody getSensorList() na usłudze SensorManager, przekazujesz do niej jako parametr stałą Sensor.TYPE_ALL: List sensorsList = sensorManager.getSensorList(Sensor.TYPE_ALL);
Aby otrzymać listę wszystkich dostępnych sensorów określonego typu, wywołaj metodę getSensorList(), przekazując do niej jako parametr typ sensora, o który chodzi. Poniższa instrukcja zwraca listę dostępnych akcelerometrów: List accelerometersList = sensorManager.getSensorList(Sensor.TYPE_ACCELEROMETER);
Utwórz nowy projekt Android, który będzie wyświetlał listę sensorów dostępnych w urządzeniu. Nazwij ten projekt SensorsListApp. Do wyświetlenia listy dostępnych sensorów wykorzystasz kontrolkę ListView. Po zdefiniowaniu tej kontrolki plik układu aktywności activity_sensors_list_app.xml będzie wyglądał tak, jak przedstawiono w listingu 14.1. Listing 14.1. Kod wpisany w pliku układu aktywności activity_sensors_list_app.xml
Receptura: lista sensorów obsługiwanych przez urządzenie
Dla celów uzyskania dostępu i identyfikacji w kodzie Java kontrolce ListView przypisano identyfikator sensors_list. Wszystkie dostępne w urządzeniu sensory zostaną wyświetlone w kontrolce ListView w postaci elementów listy. Domyślny rozmiar elementów listy wyświetlanych w kontrolce ListView jest odpowiedni dla telefonów, ale za mały dla tabletów. Aby zmienić rozmiar elementów listy kontrolki ListView w zależności od wielkości ekranu urządzenia, dodaj do folderu res/layout kolejny plik XML o nazwie list_item.xml. Wpisz w tym pliku następujący kod:
Powyższy kod spowoduje wypełnienie elementów listy kontrolki ListView spacjami o szerokości 6 dp. Elementy listy będą wyświetlane pogrubioną czcionką o rozmiarze zdefiniowanym w zasobie wymiarów text_size. Teraz musisz napisać kod Java, który będzie wykonywał poniższe zadania. Uzyskanie instancji usługi systemowej SensorManager. Wywołanie metody getSensorList usługi SensorManager ze stałą Sensor.TYPE_ALL
w celu pobrania listy sensorów dostępnych w urządzeniu. Zdefiniowanie tablicy ArrayList i skopiowanie do niej wszystkich pobranych
sensorów urządzenia. Zdefiniowanie adaptera ArrayAdapter za pomocą tablicy ArrayList. Przypisanie adaptera ArrayAdapter do kontrolki ListView w celu wyświetlenia
za jej pomocą pobranych sensorów urządzenia. W pliku aktywności Java SensorsListAppActivity.java wpisz kod przedstawiony w listingu 14.2. Listing 14.2. Kod wpisany w pliku aktywności Java SensorsListAppActivity.java package com.androidtablet.sensorslistapp; import import import import import import import import import
android.os.Bundle; android.app.Activity; android.hardware.SensorManager; android.content.Context; java.util.ArrayList; java.util.List; android.widget.ArrayAdapter; android.hardware.Sensor; android.widget.ListView;
public class SensorsListAppActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) {
449
450
Rozdział 14. Klawiatury i sensory super.onCreate(savedInstanceState); setContentView(R.layout.activity_sensors_list_app); ListView sensorsList = (ListView)findViewById( R.id.sensors_list); SensorManager sensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE); List listOfSensors = sensorManager. getSensorList(Sensor.TYPE_ALL); ArrayList arrayListSensors = new ArrayList(); for(int i=0; i arrayAdpt= new ArrayAdapter (this, R.layout.list_item, arrayListSensors); sensorsList.setAdapter(arrayAdpt); } }
Po uruchomieniu aplikacji wyświetlona zostanie lista dostępnych sensorów. Na rysunku 14.2 (górny obrazek) widać listę sensorów urządzenia wirtualnego Android (AVD), podczas gdy rysunek 14.2 (dolny obrazek) przedstawia listę sensorów dostępnych w urządzeniu fizycznym.
Receptura: korzystanie z akcelerometru Akcelerometr stosowany jest do określania bieżącego przyspieszenia urządzenia wzdłuż trzech osi. Przyspieszenie to mierzone jest w jednostkach m/s2 (metr na sekundę do kwadratu). Akcelerometr pozwala wyczuć ruchy dłoni użytkownika i jest dość często implementowany w grach. Jak zwykle, aby użyć akcelerometru, musisz uzyskać instancję klasy SensorManager — usługi systemowej, która nasłuchuje danych z sensorów urządzenia. Następnie, aby pobrać akcelerometr urządzenia, wywołujesz metodę getDefaultSensor() klasy SensorManager, przekazując do niej stałą Sensor.TYPE_ACCELEROMETER. Jest to zdefiniowana w klasie Sensor stała, która reprezentuje akcelerometr. Przy użyciu poniższej instrukcji pobierzesz akcelerometr urządzenia: SensorManager sensorManager=(SensorManager) getSystemService(SENSOR_SERVICE); sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
Następny przykładowy kod rejestruje nasłuchiwacz dla akcelerometru. Szybkość zwracania danych przez sensor będzie normalna: sensorManager.registerListener(this, sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER), SensorManager.SENSOR_DELAY_NORMAL);
Receptura: korzystanie z akcelerometru
Rysunek 14 2. Lista sensorów wyświetlona w urządzeniu AVD (górny obrazek) oraz lista sensorów dostępnych w fizycznym urządzeniu Android (dolny obrazek)
Utwórz nowy projekt Android o nazwie SensorAccApp. Aplikacja ta pokaże przyspieszenie urządzenia wzdłuż osi X, Y i Z. Przy braku ruchu aplikacja pokaże wartości dla każdej osi, jeśli urządzenie będzie zorientowane w dowolnym kierunku. Żeby wyświetlić wartości trzech osi, musisz w pliku układu activity_sensor_acc_app.xml zdefiniować trzy kontrolki TextView. Po zdefiniowaniu tych kontrolek plik układu będzie wyglądał tak, jak przedstawiono w listingu 14.3.
451
452
Rozdział 14. Klawiatury i sensory Listing 14.3. Kod wpisany w pliku układu activity_sensor_acc_app.xml
Dla celów uzyskiwania dostępu i identyfikacji w kodzie Java trzem kontrolkom TextView przypisano identyfikatory xaxisview, yaxisview i zaxisview. Kontrolki te zostały skonfigurowane do wyświetlania odpowiednio tekstów Oś X, Oś Y i Oś Z. Tekst wyświetlany za pomocą kontrolek TextView będzie prezentowany czcionką o rozmiarze zdefiniowanym w zasobie wymiarów text_size.
Po zdefiniowaniu pliku układu musisz napisać kod Java, który będzie wykonywał następujące zadania. Uzyskanie dostępu do akcelerometru urządzenia. Zarejestrowanie dla niego nasłuchiwacza zdarzeń. Uzyskanie dostępu do danych zwracanych przez akcelerometr. Uzyskanie dostępu do trzech kontrolek TextView zdefiniowanych w pliku
układu i zmapowanie tych kontrolek na obiekty TextView. Uzyskanie dostępu do danych zwracanych przez akcelerometr i wyświetlenie
ich za pomocą trzech kontrolek TextView. Aby wykonać wymienione zadania, wpisz w pliku aktywności Java SensorAccAppActivity.java kod przedstawiony w listingu 14.4.
Receptura: korzystanie z akcelerometru Listing 14.4. Kod wpisany w pliku aktywności Java SensorAccAppActivity.java package com.androidtablet.sensoraccapp; import import import import import import import
android.app.Activity; android.hardware.Sensor; android.hardware.SensorEvent; android.hardware.SensorEventListener; android.hardware.SensorManager; android.os.Bundle; android.widget.TextView;
public class SensorAccAppActivity extends Activity implements SensorEventListener { TextView xAxisView,yAxisView, zAxisView; SensorManager sensorManager; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_accelerometer_app); xAxisView=(TextView)findViewById(R.id.xaxisview); yAxisView=(TextView)findViewById(R.id.yaxisview); zAxisView=(TextView)findViewById(R.id.zaxisview); sensorManager=(SensorManager)getSystemService( SENSOR_SERVICE); } @Override protected void onResume() { super.onResume(); sensorManager.registerListener(this,sensorManager. getDefaultSensor(Sensor.TYPE_ACCELEROMETER), SensorManager.SENSOR_DELAY_NORMAL); } @Override protected void onStop() { sensorManager.unregisterListener(this); super.onStop(); } public void onAccuracyChanged(Sensor sensor, int accuracy) { } public void onSensorChanged(SensorEvent event) { if(event.sensor.getType()==Sensor.TYPE_ACCELEROMETER) { float x=event.values[0]; float y=event.values[1]; float z=event.values[2]; xAxisView.setText("X: "+x); yAxisView.setText("Y: "+y); zAxisView.setText("Z: "+z); } } }
453
454
Rozdział 14. Klawiatury i sensory
Jak już wiesz, metoda onResume() jest uruchamiana za każdym razem, kiedy aktywność rozpoczyna swój cykl życia, więc w tej metodzie rejestrowany jest nasłuchiwacz dla akcelerometru. Sensor będzie wysyłał dane z normalną szybkością. Analogicznie metoda onStop() jest wywoływana, kiedy aplikacja przestaje być widoczna. Nasłuchiwacz jest wyrejestrowywany w tej metodzie, bo sensor nie jest wymagany w tym stanie. Ponieważ klasa aktywności implementuje interfejs SensorEventListener, istotne jest zdefiniowanie dla tej aktywności następujących metod: onAccuracyChanged() — metoda wykonywana, kiedy zmienia się dokładność
informacji zwracanych przez sensor, onSensorChanged() — metoda wykonywana za każdym razem, kiedy zmieniają
się wartości z sensora. Ponieważ nie musimy przejmować się dokładnością danych zwracanych przez sensor urządzenia, metoda onAccuracyChanged() w poprzednim kodzie pozostaje pusta. W metodzie onSensorChanged() analizowane są wartości zwracane przez sensor. Wartości są zwracane w postaci tablicy będącej członkiem obiektu event. Uzyskiwany jest dostęp do wartości z tablicy i wartości te są wyświetlane na ekranie za pomocą trzech kontrolek TextView. Teraz uruchom tę aplikację. Przy założeniu, że urządzenie jest w trybie pionowym, wartości przyspieszenia wzdłuż osi X, Y i Z mogą wyglądać tak, jak pokazano na rysunku 14.3 (lewy obrazek). W trybie poziomym wartości przyspieszenia wzdłuż osi X, Y i Z mogą wyglądać tak, jak pokazano na rysunku 14.3 (prawy obrazek).
Rysunek 14.3. Wartości przyspieszenia wzdłuż osi X, Y i Z zwrócone przez akcelerometr (lewy obrazek) oraz wartości przyspieszenia wzdłuż osi X, Y i Z zwrócone przez akcelerometr, kiedy urządzenie ma orientację poziomą (prawy obrazek)
Receptura: korzystanie z czujnika zbliżeniowego
Receptura: korzystanie z czujnika zbliżeniowego Zazwyczaj czujnik zbliżeniowy jest wykorzystywany w aplikacjach do mierzenia odległości słuchawki telefonu od głowy użytkownika. Koncepcja jest taka, że jeśli urządzenie znajduje się blisko ucha lub głowy użytkownika, ekran może zostać wygaszony w celu oszczędzania baterii lub ekran dotykowy może zostać wyłączony, aby podczas rozmowy nie został przypadkowo wciśnięty żaden klawisz. Oznacza to, że zdarzenia przypadkowego dotknięcia ekranu, które mogą wystąpić, kiedy użytkownik dotyka uchem ekranu podczas rozmowy, są po prostu ignorowane dzięki zastosowaniu czujnika zbliżeniowego. Czujniki zbliżeniowe różnią się od innych sensorów. Pozostałe sensory pracują w trybie próbkowania, podczas gdy czujnik zbliżeniowy działa w trybie przerwania. Pozostałe sensory są próbkowane w regularnych interwałach, a czujnik zbliżeniowy pracuje, kiedy zmienia się odległość (kiedy urządzenie zbliża się lub oddala od użytkownika). To czujnik oświetlenia w urządzeniu pomaga wykrywać zmiany w odległości danego obiektu od urządzenia. Czujnik zbliżeniowy zwraca odległość (w centymetrach) pomiędzy obiektem a urządzeniem. Niektóre czujniki zbliżeniowe dostarczają wartości z zakresu od 0.0 do maksimum w określonych przyrostach, podczas gdy inne zwracają jedynie wartość 0.0 lub wartość maksymalną. W większości urządzeń Android czujnik zbliżeniowy zwraca wartości 0.0 lub 1.0, gdzie wartość 0.0 wskazuje, że obiekt znajduje się bliżej urządzenia, a 1.0 wskazuje, że obiekt jest daleko od urządzenia. Mógłbyś zapytać, czemu tylko dwie wartości? Odpowiedź na to pytanie jest taka, że czujnik zbliżeniowy posiada wartość progową. Wartość LUX zwracana przez czujnik oświetlania w urządzeniu jest porównywana z określoną wartością progową. Jeśli wartość LUX jest większa niż wartość progowa, urządzenie znajduje się daleko od użytkownika, a czujnik zbliżeniowy zwraca wartość 1.0. Analogicznie, jeśli wartość LUX jest mniejsza niż wartość progowa, urządzenie znajduje się blisko użytkownika, a czujnik zbliżeniowy zwraca wartość 0.0. Aby na działającym przykładzie zrozumieć koncepcję funkcjonowania czujnika zbliżeniowego, utwórz nowy projekt Android o nazwie SensorProximityApp. Aplikacja będzie wyświetlać odległość obiektu od urządzenia. Do wyświetlania odległości będziesz wykorzystywał kontrolkę TextView. Po zdefiniowaniu tej kontrolki plik układu aktywności activity_sensor_proximity_app.xml będzie wyglądał tak, jak przedstawiono w listingu 14.5. Listing 14.5. Kod wpisany w pliku układu activity_sensor_proximity_app.xml
455
456
Rozdział 14. Klawiatury i sensory
Dla celów uzyskiwania dostępu i identyfikacji w kodzie Java kontrolce TextView przypisano identyfikator distance. Kontrolka będzie wyświetlała tekst pogrubioną czcionką o rozmiarze zdefiniowanym w zasobie wymiarów text_size. Kontrolka jest inicjowana w celu wyświetlenia tekstu Dystans wynosi:. Teraz napisz kod Java, który będzie wykonywał następujące zadania. Uzyskanie dostępu do kontrolki TextView zdefiniowanej w pliku układu
aktywności i zmapowanie tej kontrolki na obiekt TextView. Pobranie czujnika zbliżeniowego urządzenia. Zarejestrowanie dla tego czujnika nasłuchiwacza zdarzeń. Pobranie odczytu czujnika (czyli odległości obiektu od urządzenia)
i wyświetlenie wartości na ekranie za pomocą kontrolki TextView. Aby wykonać wszystkie te zadania, wpisz w pliku aktywności Java SensorProximityAppActivity.java kod przedstawiony w listingu 14.6. Listing 14.6. Kod wpisany w pliku aktywności Java SensorProximityAppActivity.java package com.androidtablet.sensorproximityapp; import import import import import import import
android.app.Activity; android.hardware.Sensor; android.hardware.SensorEvent; android.hardware.SensorEventListener; android.hardware.SensorManager; android.os.Bundle; android.widget.TextView;
public class SensorProximityAppActivity extends Activity implements SensorEventListener { TextView distanceView; SensorManager sensorManager; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_sensor_proximity_app); distanceView=(TextView)findViewById(R.id.distance); sensorManager=(SensorManager)getSystemService( SENSOR_SERVICE); } @Override protected void onResume() { super.onResume(); sensorManager.registerListener(this,sensorManager. getDefaultSensor(Sensor.TYPE_PROXIMITY), SensorManager.SENSOR_DELAY_NORMAL); } @Override protected void onStop() {
Receptura: korzystanie z czujnika zbliżeniowego sensorManager.unregisterListener(this); super.onStop(); } public void onAccuracyChanged(Sensor sensor, int accuracy){ } public void onSensorChanged(SensorEvent event) { if(event.sensor.getType()==Sensor.TYPE_PROXIMITY) { float d=event.values[0]; distanceView.setText("Dystans wynosi: "+d); } } }
Jeśli po uruchomieniu tej aplikacji żaden obiekt nie znajduje się w pobliżu urządzenia, czujnik zbliżeniowy wyświetli swoją maksymalną wartość, czyli 1.0 cm (patrz rysunek 14.4, lewy obrazek). Kiedy przysuniesz jakiś obiekt do urządzenia, odległość zmniejszy się do 0.0 cm (patrz rysunek 14.4, prawy obrazek).
Rysunek 14.4. Czujnik zbliżeniowy wyświetla wartość 1.0, kiedy żaden obiekt nie znajduje się w pobliżu urządzenia (lewy obrazek). Czujnik zbliżeniowy wyświetla wartość 0.0, kiedy obiekt znajduje się bardzo blisko urządzenia (prawy obrazek)
Chociaż aplikacja będzie działać prawidłowo, warto dodać do pliku AndroidManifest.xml następującą instrukcję:
457
458
Rozdział 14. Klawiatury i sensory
Po dodaniu do pliku manifestu instrukcja ta wskazuje, że czujnik zbliżeniowy jest w urządzeniu niezbędny, aby poprawnie obsłużyć tę aplikację. Jeśli więc Twoja aplikacja jest dostępna w serwisie Google Play, zostanie zainstalowana jedynie na urządzeniach, które posiadają czujnik zbliżeniowy, co zapewni właściwe jej działanie.
Receptura: korzystanie z żyroskopu Żyroskop zwraca prędkość kątową wokół trzech osi podaną w radianach na sekundę. Mierzy stopień obrotu wokół osi X, Y i Z. Rotacja jest dodatnia w kierunku przeciwnym do ruchu wskazówek zegara. Ponieważ żyroskop mierzy prędkość, a nie kierunek, jego wskazania są integrowane w czasie w celu określenia bieżącej orientacji. Obliczone wyniki będą wtedy reprezentować zmianę orientacji wokół danej osi. Żyroskop mierzy prędkość kątową wokół trzech osi. Za każdym razem, kiedy sensor wyczuwa zmianę w prędkości kątowej urządzenia, odpowiednia informacja jest zapisywana w tablicy event.values. Oznacza to, że tablica event.values zwraca prędkość kątową wokół trzech osi: event.values[0] — reprezentuje prędkość kątową wokół osi X, event.values[1] — reprezentuje prędkość kątową wokół osi Y, event.values[2] — reprezentuje prędkość kątową wokół osi Z.
Aby obliczyć kąt obrotu w radianach, pomnóż zwróconą prędkość kątową przez różnicę czasu pomiędzy bieżącą i poprzednią próbką. Ponieważ sensor zwraca próbki danych razem ze znacznikami czasu mierzonego w nanosekundach, żeby obliczyć kąt obrotu w radianach wykonaj następujące trzy czynności. 1. Oblicz różnicę czasu pomiędzy dwoma odczytami sensora (czyli pomiędzy
bieżącą i poprzednią próbką). 2. Przelicz różnicę czasu z nanosekund na sekundy. 3. Pomnóż prędkość kątową (kąt na sekundę) przez różnicę czasu wyrażoną
w sekundach. Wartość wynikowa będzie wskazywała, jak bardzo żyroskop obrócił się od momentu pobrania poprzedniej próbki do momentu pobrania bieżącej próbki. Musisz nauczyć się wykonywać te czynności na działającym przykładzie. Utwórz więc projekt Android o nazwie SensorGyroscopeApp. Aby wyświetlić prędkość kątową wokół trzech osi, musisz zdefiniować trzy kontrolki TextView. Po zdefiniowaniu tych kontrolek plik układu aktywności activity_sensor_gyroscope_app.xml będzie wyglądał tak, jak przedstawiono w listingu 14.7. Listing 14.7. Kod wpisany w pliku układu aktywności activity_sensor_gyroscope_app.xml
Receptura: korzystanie z żyroskopu android:layout_height="match_parent" android:orientation="vertical" >
Jak widzisz, trzy kontrolki TextView są inicjowane w celu wyświetlenia odpowiednio tekstów Oś X, Oś Y i Oś Z. Tekst będzie prezentowany pogrubioną czcionką o rozmiarze zdefiniowanym w zasobie wymiarów text_size. Dla celów uzyskania dostępu i identyfikacji w kodzie Java kontrolkom TextView przypisano odpowiednio identyfikatory xaxisview, yaxisview i zaxisview. W pliku aktywności Java SensorGyroscopeAppActivity.java wpisz kod przedstawiony w listingu 14.8. Listing 14.8. Kod wpisany w pliku aktywności Java SensorGyroscopeAppActivity.java package com.androidtablet.sensorgyroscopeapp; import import import import import import import
android.os.Bundle; android.app.Activity; android.widget.TextView; android.hardware.SensorManager; android.hardware.SensorEventListener; android.hardware.Sensor; android.hardware.SensorEvent;
public class SensorGyroscopeAppActivity extends Activity implements SensorEventListener { TextView xAxisView,yAxisView, zAxisView; SensorManager sensorManager; float angleX, angleY, angleZ; private long previousTime =0 ; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_sensor_gyroscope_app);
459
460
Rozdział 14. Klawiatury i sensory
}
xAxisView=(TextView)findViewById(R.id.xaxisview); yAxisView=(TextView)findViewById(R.id.yaxisview); zAxisView=(TextView)findViewById(R.id.zaxisview); sensorManager=(SensorManager)getSystemService( SENSOR_SERVICE);
@Override protected void onResume() { super.onResume(); sensorManager.registerListener(this,sensorManager. getDefaultSensor(Sensor.TYPE_GYROSCOPE), SensorManager.SENSOR_DELAY_NORMAL); } @Override protected void onStop() { sensorManager.unregisterListener(this); super.onStop(); } @Override public void onAccuracyChanged(Sensor arg0, int arg1) { } public void onSensorChanged(SensorEvent event) { if (previousTime != 0) { final float timeDiff = (event.timestamp – previousTime) * 1.0f / 1000000000.0f; angleX += event.values[0] * timeDiff; angleY += event.values[1] * timeDiff; angleZ += event.values[2] * timeDiff; xAxisView.setText("Orientacja osi X : "+angleX); yAxisView.setText("Orientacja osi Y : "+angleY); zAxisView.setText("Orientacja osi Z : "+angleZ); } previousTime = event.timestamp; } }
Kod z listingu 14.8 wykonuje następujące zadania. Uzyskanie dostępu do kontrolek TextView zdefiniowanych w pliku układu
aktywności i zmapowanie ich na odpowiednie obiekty TextView. Te trzy kontrolki TextView będą wyświetlać kąt obrotu wokół trzech osi. Pobranie żyroskopu urządzenia. Zarejestrowanie nasłuchiwacza zdarzeń dla tego sensora. Pobranie odczytu sensora. Przeliczenie prędkości kątowej na kąt obrotu wyrażony w radianach i określenie,
jak bardzo żyroskop obrócił się od momentu pobrania poprzedniej próbki. Wyświetlenie kąta obrotu wokół trzech osi.
Podsumowanie
Wskazówka Ze względu na szum i offset żyroskopu zwracane przez niego wartości orientacji mogą być niedokładne. Dla kompensacji i uzyskania prawidłowych wartości orientacji żyroskopy są często stosowane w połączeniu z innymi sensorami, takimi jak czujnik siły ciężkości lub akcelerometr.
Podsumowanie W tym rozdziale nauczyłeś się wybierać i konfigurować klawiaturę w taki sposób, aby odpowiadała Twoim wymaganiom dotyczącym wprowadzania danych. Poznałeś klasy, metody i interfejs, wymagane do pobrania sensorów urządzenia i odczytu ich wskazań. Dowiedziałeś się, jak wyświetlić listę sensorów obsługiwanych przez dane urządzenie. Poznałeś sposób zastosowania akcelerometru do wyświetlania aktualnego przyspieszenia urządzenia wzdłuż trzech osi. Nauczyłeś się określać odległość pomiędzy danym obiektem i urządzeniem, wykorzystując czujnik zbliżeniowy. Na koniec poznałeś technikę określania orientacji urządzenia względem danej osi z wykorzystaniem żyroskopu. Kolejny rozdział poświęcony jest formatowi JSON. Nauczysz się korzystać z klas JsonReader i JsonWriter oraz poznasz zastosowanie formatu JSON w usługach sieciowych.
461
462
Rozdział 14. Klawiatury i sensory
Część V Eksploracja sieci WWW
15 JSON T
ransmisja danych jest powszechnie stosowana w aplikacjach. Czy to w celu zapisania w bazie danych, czy też przesłania przez Internet do innej aplikacji, dane muszą być przekazywane dwukierunkowo, czyli do aplikacji i z aplikacji. JSON (ang. JavaScript Object Notation) jest jednym z formatów wymiany danych, powszechnie stosowanym w aplikacjach Android. System Android zawiera biblioteki json.org, które umożliwiają pracę z plikami JSON. Format JSON jest szybki, łatwy w użyciu i zarządzaniu. W tym rozdziale dowiesz się, w jaki sposób tworzyć i wstawiać informacje do obiektów JSONObject oraz uzyskiwać dostęp do informacji z tych obiektów. Nauczysz się również, jak zagnieżdżać jeden obiekt JSONObject w drugim obiekcie JSONObject. Ponadto zobaczysz, jak utrzymywać więcej niż jeden obiekt JSONObject w tablicy JSONArray. Dowiesz się, jak informacje są odczytywane i zapisywane za pomocą klas JsonReader oraz JsonWriter. Na koniec poznasz procedurę wykorzystywania usług sieciowych JSON w aplikacjach Android.
Receptura: JSON JSON jest protokołem komunikacji i wymiany danych. Uznawany za alternatywę dla języka XML, JSON jest lekkim, tekstowym formatem danych, szeroko stosowanym do wymiany danych w aplikacjach Java i Android. Podstawową jednostką danych w formacie JSON jest obiekt JSONObject, który zawiera dane w postaci par klucz-wartość. Z uwagi na stosowanie tych par obiekt JSONObject jest łatwy w użyciu i zarządzaniu. Oto przykładowy obiekt JSONObject, który zawiera informacje o produkcie: { "id":"A101", "productname":"Smartfon", "price":19.99 }
Jak widzisz, id (identyfikator), productname (nazwa produktu) i price (cena) to klucze, a A101, Smartfon i 19.99 to wartości odpowiednich kluczy. Obiekt JSONObject
466
Rozdział 15. JSON
może być również zagnieżdżany w innym obiekcie JSONObject. W kolejnym przykładzie obiekt JSONObject z kluczem details został zagnieżdżony w innym obiekcie JSONObject: { "id":"A101", "productname":"Smartfon", "price":19.99, "details": { "Packed On": "Lipiec 2013", "Manufacturing Date": "Sierpień 2013", "Expiry Date": "Grudzień 2015" } }
Uwaga Format JSON umożliwia zagnieżdżanie do kilku poziomów. Jeden zagnieżdżony obiekt JSONObject może zawierać kolejny itd.
W tabeli 15.1 przedstawiono dwa konstruktory używane do tworzenia obiektów JSONObject. Tabela 15.1. Krótki opis konstruktorów wykorzystywanych do tworzenia obiektów JSONObject
Konstruktor
Opis
public JSONObject()
Tworzy pusty obiekt JSONObject. Przykład: JSONObject jObject = new JSONObject();
public JSONObject (String jsondata)
Tworzy obiekt JSONObject z dostarczonego parametru String. Parametr String zawiera dane w postaci par klucz-wartość umieszczonych pomiędzy nawiasami klamrowymi otwierającym i zamykającym {}. Przykład: String productdata = "{\"id\": \"A101\",\"productname\": \"Smartphone\",\"price\": 19.99}"; JSONObject jObject = new JSONObject(productdata);
Wartości w obiekcie JSONObject mogą być kilku typów, w tym int, double, string, boolean, array oraz null.
W tabeli 15.2 przedstawiono kilka metod często wykorzystywanych przy pracy z obiektami JSONObject.
Receptura: JSON Tabela 15.2. Krótki opis metod stosowanych z obiektem JSONObject
Metoda
Opis
JSONObject put (klucz, wartość)
Tworzy wewnątrz obiektu JSONObject nową wartość powiązaną z określonym parametrem klucz. Parametr klucz musi być ciągiem znaków umieszczonym w podwójnym cudzysłowie, a parametr wartość może być dowolnym typem podstawowym, takim jak int, long, boolean, double, a także innym obiektem JSONObject. Przykłady: JSONObject product = new JSONObject(); product.put("productname", "Smartphone"); product.put("price", Double.valueOf(19.99));
Jeśli dany klucz istnieje już w obiekcie JSONObject, metoda put zastąpi poprzednią wartość. get+typ_danych (klucz)
Pozyskuje dane z obiektu JSONObject. typ_danych odnosi się do typu danych wartości wartość, przechowywanej pod określonym kluczem klucz. Jeśli
przykładowo typem danych wartości jest ciąg znaków, zastosować należy metodę getString(klucz). Analogicznie, jeśli typem danych wartości jest liczba całkowita, użyta będzie metoda getInt(klucz). Poniższe instrukcje można zastosować w celu uzyskania dostępu do wartości przechowywanych w obiekcie JSONObject, który wykorzystuje klucze id, productname i price: String productID=product.getString ("id"); String productName=product.getString ("productname"); double price=product.getDouble("price");
Object remove (klucz)
Usuwa z obiektu JSONObject wartość z określonym kluczem klucz. Jeśli ta metoda zostanie wykonana z powodzeniem, zwraca usuniętą wartość. W przeciwnym razie zwraca null.
boolean has (klucz)
Zwraca wartość logiczną, która wskazuje, czy dany obiekt JSONObject zawiera wartość z określonym kluczem klucz.
boolean isNull (klucz)
Zwraca wartość logiczną, która wskazuje, czy klucz istnieje w obiekcie JSONObject. Metoda ta sprawdza również, czy wartość powiązana z kluczem klucz jest wartością pustą.
int length()
Zwraca liczbę par klucz-wartość istniejących w obiekcie JSONObject. Długość potomnych obiektów JSONObject (jeśli takie istnieją) nie zostanie podana.
467
468
Rozdział 15. JSON Tabela 15.2. Krótki opis metod stosowanych z obiektem JSONObject (ciąg dalszy)
Metoda
Opis
Iterator keys()
Zwraca iterator, który może być użyty do iteracji w obiekcie JSONObject.
JSONArray names()
Zwraca tablicę nazw (kluczy) JSONArray obiektu JSONObject. Metody keys() i names() różnią się typem zwracanych przez nie danych. Metoda keys() zwraca Iterator, podczas gdy metoda names() zwraca tablicę JSONArray. Tablica JSONArray została omówiona w kolejnej recepturze.
Obiekt JSONObject może być serializowany do typu String w celu zapisania do pliku lub przesłania przez Internet. Możliwe jest też działanie odwrotne (ciąg znaków może być przekonwertowany na obiekt JSONObject). Przykład: Poniższe instrukcje definiują obiekt JSONObject o nazwie jObject, umieszczają w nim pewne informacje, konwertują go na ciąg znaków o nazwie JsonString i tworzą z tego ciągu znaków kolejny obiekt JSONObject o nazwie jObject2: JSONObject jObject = new JSONObject(); jObject.put("id", "A101"); jObject.put("productname", "Smartfon"); jObject.put("price", Double.valueOf(19.99)); String JsonString = jObject.toString(); JSONObject jObject2 = new JSONObject(JsonString);
W kolejnych instrukcjach pokazano, w jaki sposób można uzyskać dostęp do wszystkich kluczy obiektu JSONObject: String productInfo=""; Iterator iterator = jObject.keys(); while (iterator.hasNext()) productInfo+=iterator.next();
Uzyskiwany jest dostęp do wszystkich kluczy obiektu JSONObject o nazwie jObject i klucze te są zapisywane w ciągu znaków productInfo.
Receptura: wykorzystywanie obiektu JSONObject do przechowywania informacji W tej recepturze utworzysz obiekt JSONObject składający się z trzech pól, w których będziesz wpisywał identyfikator, nazwę i cenę produktu. Receptura ta koncentruje się na objaśnieniu, w jaki sposób dane są wprowadzane do obiektu JSONObject i jak można uzyskać dostęp do danych z tego obiektu.
Receptura: wykorzystywanie obiektu JSONObject do przechowywania informacji
Utwórz nowy projekt Android o nazwie JSONApp. Do wyświetlenia identyfikatora, nazwy i ceny produktu wykorzystasz kontrolkę TextView. Po zdefiniowaniu tej kontrolki plik układu aktywności activity_jsonapp.xml będzie wyglądał tak, jak przedstawiono w listingu 15.1. Listing 15.1. Kod wpisany w pliku układu aktywności activity_jsonapp.xml
Jak widzisz, kontrolce TextView przypisano identyfikator jsondata, który umożliwia jej identyfikację w kodzie Java. Ponadto tekst wyświetlany za pomocą kontrolki TextView będzie prezentowany pogrubioną czcionką o rozmiarze zdefiniowanym w zasobie wymiarów text_size. Następnie musisz napisać kod Java, który będzie wykonywał poniższe zadania. Uzyskanie dostępu do kontrolki TextView zdefiniowanej w pliku układu
i zmapowanie tej kontrolki na obiekt TextView. Zdefiniowanie obiektu JSONObject. Umieszczenie w obiekcie JSONObject identyfikatora produktu, nazwy produktu
i jego ceny odpowiednio pod kluczami id, productname i price. Uzyskanie za pomocą odpowiednich kluczy dostępu do identyfikatora, nazwy
i ceny produktu z obiektu JSONObject i wyświetlenie ich w kontrolce TextView. Aby wykonać wszystkie wymienione zadania, wpisz w pliku aktywności Java JSONAppActivity.java kod przedstawiony w listingu 15.2. Listing 15.2. Kod wpisany w pliku aktywności Java JSONAppActivity.java package com.androidtablet.jsonapp; import import import import import
android.os.Bundle; android.app.Activity; org.json.JSONObject; org.json.JSONException; android.widget.TextView;
public class JSONAppActivity extends Activity { private JSONObject jObject; private TextView jsonData; String productInfo;
469
470
Rozdział 15. JSON @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_jsonapp); jsonData = (TextView)findViewById(R.id.jsondata); writeJSON(); readJSON(); } public void writeJSON() { jObject = new JSONObject(); try { jObject.put("id", "A101"); jObject.put("productname", "Smartfon"); jObject.put("price", Double.valueOf(19.99)); } catch (JSONException e) { e.printStackTrace(); } } private void readJSON() { try{ productInfo="Identyfikator produktu: "+jObject.getString("id") +"\n" +"Nazwa produktu: " +jObject.getString ("productname")+ "\n" +"Cena: " + jObject.getString("price")+ "\n"; jsonData.setText(productInfo); } catch (Exception e) { e.printStackTrace(); } } }
Po uruchomieniu tej aplikacji uzyskiwany jest dostęp do identyfikatora, nazwy i ceny produktu z obiektu JSONObject i dane te są wyświetlane na ekranie, tak jak pokazano na rysunku 15.1.
Rysunek 15.1. Wyświetlone na ekranie informacje o produkcie pobrane z obiektu JSONObject
Receptura: zagnieżdżanie obiektów JSONObject
Receptura: zagnieżdżanie obiektów JSONObject W poprzedniej recepturze dowiedziałeś się, jak utworzyć prosty obiekt JSONObject. W tej recepturze nauczysz się zagnieżdżać jeden obiekt JSONObject w drugim. W obiekcie JSONObject o nazwie jObject, który zdefiniowałeś w aplikacji JSONApp w poprzedniej recepturze, zagnieżdżony zostanie kolejny obiekt JSONObject o nazwie jsubObject. Innymi słowy, zdefiniujesz obiekt JSONObject o nazwie jsubObject i wprowadzisz w nim pod odpowiednimi kluczami informacje o dacie spakowania, produkcji i ważności. Następnie zagnieździsz ten obiekt w istniejącym obiekcie JSONObject o nazwie jObject. Otwórz aplikację Android JSONApp, którą utworzyłeś w poprzedniej recepturze. Aby zagnieździć obiekt JSONObject o nazwie jsubObject w obiekcie JSONObject o nazwie jObject, zmodyfikuj plik aktywności Java JSONAppActivity.java w sposób przedstawiony w listingu 15.3. Zmodyfikowane zostały jedynie fragmenty kodu zaznaczone pogrubioną czcionką. Reszta kodu pozostaje taka sama jak w listingu 15.2. Listing 15.3. Kod wpisany w pliku aktywności Java JSONAppActivity.java package com.androidtablet.jsonapp; import import import import import
android.os.Bundle; android.app.Activity; org.json.JSONObject; org.json.JSONException; android.widget.TextView;
public class JSONAppActivity extends Activity { private JSONObject jObject, jsubObject; private TextView jsonData; String productInfo; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_jsonapp); jsonData = (TextView)findViewById(R.id.jsondata); writeJSON(); readJSON(); } public void writeJSON() { jObject = new JSONObject(); jsubObject = new JSONObject(); try { jsubObject.put("packedon", "Sierpień 2013"); jsubObject.put("manufacturingdate", "Lipiec 2013"); jsubObject.put("expirydate", "Grudzień 2015"); jObject.put("id", "A101"); jObject.put("productname", "Smartfon"); jObject.put("price", Double.valueOf(19.99)); jObject.put("details", jsubObject);
471
472
Rozdział 15. JSON } catch (JSONException e) { e.printStackTrace(); } } private void readJSON() { try{ productInfo="Identyfikator produktu: "+jObject.getString("id") +"\n" +"Nazwa produktu: " +jObject.getString ("productname")+ "\n" +"Cena: " + jObject.getString("price")+ "\n"; JSONObject prodDetails=jObject.getJSONObject( "details"); productInfo+="Spakowano: "+prodDetails.getString( "packedon")+"\n" +"Data produkcji: " + prodDetails.getString("manufacturingdate")+"\n"+ "Data ważności: " + prodDetails.getString( "expirydate")+ "\n"; jsonData.setText(productInfo); } catch (Exception e) { e.printStackTrace(); } } }
Jak widzisz, zdefiniowany został kolejny obiekt JSONObject o nazwie jsubObject. Odpowiednio pod kluczami packedon, manufacturingdate oraz expirydate umieszczone zostały w nim informacje o datach spakowania, wyprodukowania i ważności. Obiekt jsubObject został następnie zagnieżdżony pod kluczem details w poprzednim obiekcie JSONObject o nazwie jObject. W funkcji readJSON() możesz zobaczyć, że za pomocą kluczy id, productname oraz price uzyskiwany jest dostęp do identyfikatora, nazwy i ceny produktu z obiektu jObject. Dostęp do obiektu JSONObject uzyskiwany jest pod kluczem details i jest przypisywany do obiektu JSONObject o nazwie prodDetails. Następnie uzyskiwany jest również dostęp do przechowywanych pod kluczami packedon, manufacturingdate oraz expirydate informacji o dacie spakowania, wyprodukowania i dacie ważności, które są wyświetlane na ekranie za pomocą kontrolki TextView. Po uruchomieniu aplikacji uzyskiwany jest dostęp do informacji przechowywanych w kluczach głównego obiektu JSONObject o nazwie jObject oraz w kluczach zagnieżdżonego obiektu JSONObject o nazwie jsubObject. Informacje te są wyświetlane na ekranie, co pokazano na rysunku 15.2.
Uwaga Obiekty JSONObject mogą być zagnieżdżane do dowolnej liczby poziomów.
Receptura: korzystanie z tablicy JSONArray
Rysunek 15.2. Pobrane i wyświetlone na ekranie informacje o produkcie z obiektu JSONObject oraz z zagnieżdżonego w nim kolejnego obiektu JSONObject
Receptura: korzystanie z tablicy JSONArray Tablica JSONArray jest zasadniczo tablicą Java, która przechowuje dane dowolnego typu akceptowanego przez format JSON. Tablica JSONArray może jednocześnie zawierać dane różnych typów. Oto konstruktory służące do budowania tablicy JSONArray. JSONArray() — tworzy tablicę JSONArray bez żadnych elementów. JSONArray(String json) — przyjmuje w tablicy listę elementów umieszczonych
w nawiasach kwadratowych ([ oraz]) i rozdzielonych przecinkiem (,) lub średnikiem (;). Przykłady: Za pomocą poniższych instrukcji utworzysz pustą tablicę JSONArray. String data = "[]"; JSONArray jsArray = new JSONArray(data);
Przy użyciu kolejnych instrukcji utworzysz tablicę JSONArray z dwoma elementami. String data = "[Smartfon, 19.99]"; JSONArray jsArray = new JSONArray(data);
Z wykorzystaniem następnych instrukcji utworzysz tablicę JSONArray z dwoma elementami i jednym elementem pustym. String data = "[Smartfon, 19.99,,]"; JSONArray jsArray = new JSONArray(data);
W powyższym przykładzie wszelkie dane określone po prawym nawiasie kwadratowym (]) zostaną zignorowane. JSONArray (Collection zbiór) — tworzy tablicę JSONArray z określonego zbioru.
473
474
Rozdział 15. JSON
W tabeli 15.3 przedstawiono listę metod, które są często wykorzystywane podczas pracy z tablicą JSONArray. Tabela 15.3. Krótki opis metod używanych z tablicą JSONArray
Metoda
Opis
JSONArray put(wartość)
Używana do wprowadzenia nowej wartości w tablicy. Podana wartość zostanie dodana na końcu tablicy. Wartość może być typem boolean, int, long, double, string lub object.
JSONArray put(indeks, wartość)
Używana do wprowadzenia nowej wartości w lokalizacji określonej przez indeks. Ponownie wartość może być typem boolean, int, long, double, string lub object. Typ object odnosi się tutaj do tablicy JSONArray i obiektu JSONObject. Każda wartość w lokalizacji określonej indeksem zostanie nadpisana. Indeksy liczone są od 0 i nie mogą być liczbą ujemną.
get+typ_danych(indeks)
Używana do zwrócenia wartości z lokalizacji określonej przez indeks. Parametr typ_danych musi odpowiadać typowi danych wartości, do której uzyskiwany jest dostęp. Przykładowo metoda getString(i) uzyskuje dostęp do wartości będącej ciągiem znaków i przechowywanej w indeksie i. Analogicznie, metody getBoolean(i), getInt(i), getLong(i), getDouble(i), get(i), getJSONArray(i) oraz getJSONObject(i) uzyskują odpowiednio dostęp do wartości typu boolean, int, long, double, object, JSONArray oraz JSONObjec z indeksu i tablicy.
String toString()
Wykorzystywana do przekonwertowania tablicy JSONArray na ciąg znaków.
boolean isNull(indeks)
Zwraca wartość logiczną true (prawda), jeśli wartość z lokalizacji określonej przez indeks jest pusta (null).
int length()
Zwraca długość tablicy.
boolean equals(Object)
Sprawdza, czy dwie tablice JSONArray są równoważne (czyli, czy obie są instancjami tego samego obiektu).
String join(String separator)
Konwertuje wszystkie elementy tablicy JSONArray na ciąg znaków z określonym separatorem separator rozdzielającym te elementy.
Uwaga Tablica JSONArray jest również obiektem JSONobject.
Receptura: korzystanie z tablicy JSONArray
Aby na działającym przykładzie zapoznać się z koncepcją tablicy JSONArray, utwórz nowy projekt Android o nazwie JSONArrayApp. Receptura ta poświęcona jest objaśnieniu, w jaki sposób tworzy się tablicę JSONArray, jak wprowadzane są do niej informacje oraz jak uzyskuje się dostęp do informacji z tablicy. W tej aplikacji utworzysz dwa obiekty JSONObject, z których każdy będzie zawierał informacje o indywidualnym produkcie. Informacje o produkcie będą obejmowały identyfikator produktu, nazwę produktu, cenę, datę spakowania, datę wyprodukowania oraz datę ważności. Utworzone obiekty JSONObject zostaną następnie dodane do tablicy JSONArray. Uzyskiwany będzie po kolei dostęp do wszystkich informacji o produkcie i będą one wyświetlane na ekranie za pomocą kontrolki TextView. Pierwszym krokiem jest zdefiniowanie kontrolki TextView w pliku układu aktywności. Po jej zdefiniowaniu plik układu aktywności activity_jsonarray_app.xml będzie wyglądał tak, jak w listingu 15.4. Listing 15.4. Kod wpisany w pliku układu aktywności activity_jsonarray_app.xml
Do celów uzyskiwania dostępu i identyfikacji w kodzie Java kontrolce TextView przypisano identyfikator jsondata. Ponadto tekst wyświetlany za pomocą tej kontrolki będzie prezentowany pogrubioną czcionką o rozmiarze zdefiniowanym w zasobie wymiarów text_size. Po przygotowaniu pliku układu musisz napisać kod Java, który będzie wykonywał następujące zadania. Uzyskanie dostępu do kontrolki TextView z pliku układu i zmapowanie tej
kontrolki na obiekt TextView. Zdefiniowanie dwóch obiektów JSONObject i zapisanie w nich informacji
o dwóch produktach. Informacje o produktach obejmują identyfikator produktu, nazwę produktu, cenę produktu, datę spakowania, datę wyprodukowania oraz datę ważności. Zdefiniowanie tablicy JSONArray i wstawienie do niej dwóch obiektów
JSONObject. Pobranie każdego z elementów tablicy JSONArray do tymczasowego obiektu
JSONObject.
475
476
Rozdział 15. JSON
Uzyskanie dostępu do każdego pola (pary klucz-wartość) z tymczasowego
obiektu JSONObject, w tym również zagnieżdżonego obiektu JSONObject, oraz wyświetlenie na ekranie informacji o tych dwóch produktach za pomocą kontrolki TextView. Aby wykonać wszystkie wymienione zadania, wpisz w pliku aktywności Java JSONArrayAppActivity.java kod przedstawiony w listingu 15.5. Listing 15.5. Kod wpisany w pliku aktywności Java JSONArrayAppActivity.java package com.androidtablet.jsonarrayapp; import import import import import import
android.os.Bundle; android.app.Activity; org.json.JSONObject; org.json.JSONException; android.widget.TextView; org.json.JSONArray;
public class JSONArrayAppActivity extends Activity { private JSONObject jObject1, jObject2, jsubObject; private TextView jsonData; String productInfo=""; JSONArray productsArray; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_jsonarray_app); jsonData = (TextView)findViewById(R.id.jsondata); writeJSON(); readJSON(); } public void writeJSON() { jObject1 = new JSONObject(); jsubObject = new JSONObject(); String jString = "{\"id\":\"A102\", \"productname\":\"Laptop\", \"price\":49.99, \"details\": {\"packedon\":\"Oct 2013\", \"manufacturingdate\":\"Sep 2013\", \"expirydate\":\"Mar 2017\"}}"; try { jsubObject.put("packedon", "Sierpień 2013"); jsubObject.put("manufacturingdate", "Lipiec 2013"); jsubObject.put("expirydate", "Dec 2015"); jObject1.put("id", "A101"); jObject1.put("productname", "Smartfon"); jObject1.put("price", Double.valueOf(19.99)); jObject1.put("details", jsubObject); jObject2 = new JSONObject(jString); } catch (JSONException e) { e.printStackTrace(); } productsArray = new JSONArray();
Receptura: korzystanie z tablicy JSONArray productsArray.put(jObject1); productsArray.put(jObject2); } private void readJSON() { try{ for (int i =0 ; i
Aby receptura była bardziej pouczająca, każdy z dwóch obiektów JSONObject wymaganych do przechowywania informacji o produktach, został zbudowany w inny sposób. Pierwszy obiekt JSONObject o nazwie jObject1 powstał pusty, a następnie dodane zostały do niego informacje o produkcie. Daty spakowania, wyprodukowania i ważności zebrano w osobnym obiekcie JSONObject o nazwie jsubObject, który później został zagnieżdżony pod kluczem details w głównym obiekcie JSONObject o nazwie jObject1. Drugi obiekt JSONObject o nazwie jObject2 utworzono z ciągu znaków. Oznacza to, że definiowany jest ciąg znaków o nazwie jString i umieszczane są w nim informacje o drugim produkcie. Następnie drugi obiekt JSONObject o nazwie jObject2 jest tworzony na podstawie informacji przechowywanych w ciągu znaków o nazwie jString. Obiekty JSONObject o nazwach jObject1 i jObject2 są w metodzie writeJSON() wstawiane do tablicy JSONArray o nazwie productsArray. W metodzie readJSON() uzyskiwany jest dostęp do dwóch obiektów JSONObject z tablicy JSONArray oraz do wartości różnych kluczy z tych obiektów, a następnie pobrane informacje są wyświetlane za pomocą kontrolki TextView.
477
478
Rozdział 15. JSON
Po uruchomieniu tej aplikacji pobierane są informacje o produktach z tablicy JSONArray; informacje są wyświetlane na ekranie za pomocą kontrolki TextView,
tak jak pokazano na rysunku 15.3.
Rysunek 15.3. Informacje o produktach pobrane z tablicy JSONArray i wyświetlone na ekranie
Receptura: korzystanie z klas JsonReader oraz JsonWriter Klasa JsonWriter zapisuje zakodowane w formacie JSON wartości do strumienia danych. Kodowanie JSON ogólnie oznacza dane w postaci par klucz-wartość. Klasa JsonReader odczytuje z dostarczonego strumienia wartości zakodowane w formacie JSON. Aby skorzystać z klasy JsonReader, musisz zdefiniować instancję tej klasy, wywołując jej konstruktor i przekazując do niej obiekt Reader, który ma posłużyć do odczytu danych. Przykład: JsonReader jsonReader = new JsonReader(new StringReader(data));
W tym przykładzie parametr data odnosi się do obiektu JSONObject lub tablicy, które zawierają informacje w postaci par klucz-wartość. Instancja JsonReader iteruje przez dostarczone obiekt lub tablicę i uzyskuje dostęp do wartości powiązanych z każdym kluczem. W tabeli 15.4 przedstawione zostały metody stosowane podczas korzystania z klasy JsonReader.
Receptura: korzystanie z klas JsonReader oraz JsonWriter Tabela 15.4. Krótki przegląd metod klasy JsonReader
Metoda
Opis
JsonReader.beginObject()
Wskazuje początek tablicy lub obiektu. Jest bezpośrednio wywoływana w celu przekazania tej informacji do obiektu JsonReader.
JsonReader.hasNext()
Sprawdza, czy obiekt lub tablica zawierają więcej danych. Metoda zwraca wartość logiczną true (prawda), jeśli obiekt lub tablica posiadają kolejny element.
JsonReader.nextName()
Zwraca kolejną nazwę (klucz) z obiektu lub tablicy, które zostały dostarczone.
JsonReader.peek()
Analizuje typ wartości. Używana do sprawdzenia, czy wartość jest pusta, czyli null.
JsonReader.skipValue()
Pomija klucz i wszystkie jego elementy potomne. Używana do ignorowania kluczy, których nie chcesz parsować.
JsonReader.nextString()
Uzyskuje dostęp do kolejnych danych z tablicy lub obiektów i zwraca te dane w postaci ciągu znaków. Analogicznie, metody nextInt(), nextLong(), nextDouble() oraz nextBoolean() zwracają kolejne dane z tablicy lub obiektów w postaci typów int, long, double oraz boolean.
Aby skorzystać z klasy JsonWriter, musisz utworzyć instancję tej klasy, wywołując jej konstruktor. Przedtem musisz jednak utworzyć obiekt Writer, w którym umieszczane (kodowane) będą informacje w postaci par klucz-wartość. Obiekt Writer jest następnie przekazywany jako parametr do konstruktora JsonWriter, tak jak pokazano w poniższych instrukcjach: StringWriter stringWriter = new StringWriter(); JsonWriter jsonWriter = new JsonWriter(stringWriter);
Instrukcje te definiują obiekt jsonWriter klasy JsonWriter. Wszystkie informacje zapisane przez klasę JsonWriter zostaną umieszczone lub zakodowane w obiekcie stringWriter. W tabeli 15.5 opisano kilka metod klasy JsonWriter. Tabela 15.5. Krótki przegląd metod klasy JsonWriter
Metoda
Opis
JsonWriter.beginObject()
Wskazuje początek procedury kodowania (zapisu) w nowym obiekcie.
JsonWriter.endObject()
Wskazuje koniec kodowania w bieżącym obiekcie.
JsonWriter.name(String nazwa)
Koduje lub zapisuje nazwę (klucz) w bieżącym obiekcie.
JsonWriter.value(wartość)
Koduje lub zapisuje dostarczoną wartość w bieżącym obiekcie. Wartością może być string, int, double lub inny typ danych.
479
480
Rozdział 15. JSON
Utwórz nowy projekt Android o nazwie JSONReaderWriterApp. W tej aplikacji będziesz korzystał z klasy JsonWriter do zapisania informacji o produkcie (identyfikator, nazwa i cena produktu) w obiekcie StringWriter. Informacje o produkcie z obiektu StringWriter są przypisywane do obiektu StringReader z wykorzystaniem obiektu typu string. Obiekt StringReader korzysta z klasy JsonReader w celu uzyskania dostępu do każdego pola (identyfikator, nazwa i cena produktu) i wyświetlenia informacji z tych pól na ekranie za pomocą kontrolki TextView. Po zdefiniowaniu kontrolki TextView plik układu aktywności activity_jsonreader_writer_app.xml powinien wyglądać tak, jak przedstawiono w listingu 15.6. Listing 15.6. Kod wpisany w pliku układu aktywności activity_jsonreader_writer_app.xml
Do celów uzyskania dostępu i identyfikacji w kodzie Java kontrolce TextView przypisano identyfikator jsondata. Tekst wyświetlany za pomocą tej kontrolki będzie prezentowany pogrubioną czcionką o rozmiarze zdefiniowanym w zasobie wymiarów text_size. Teraz musisz napisać kod Java, który będzie wykonywał następujące zadania. Uzyskanie dostępu do kontrolki TextView z pliku układu i zmapowanie tej
kontrolki na obiekt TextView. Zdefiniowanie obiektów StringWriter i JsonWriter. Przekazanie do obiektu
JsonWriter obiektu StringWriter jako parametru. Przypomnijmy, że obiekt Writer jest wymagany dla klasy JsonWriter w celu zapisania (zakodowania) par
klucz-wartość. Użycie obiektu JsonWriter do wprowadzenia informacji o produkcie
(identyfikator, nazwa i cena produktu) pod kluczami id, productname oraz price. Wprowadzone informacje zostaną zapisane w obiekcie StringWriter. Zdefiniowanie obiektu StringReader. Dostarczenie do obiektu StringReader
za pomocą obiektu typu string informacji zapisanych w obiekcie StringWriter. Zdefiniowanie obiektu JsonReader i przekazanie do niego jako parametru
obiektu StringReader. Obiekt JsonReader odczytuje informacje z dostarczonego obiektu Reader.
Receptura: korzystanie z klas JsonReader oraz JsonWriter Uzyskanie dostępu do informacji o produkcie z obiektu StringReader przy
wykorzystaniu różnych metod klasy JsonReader oraz wyświetlenie tych informacji na ekranie za pomocą kontrolki TextView. Aby wykonać wszystkie wymienione zadania, wpisz w pliku aktywności Java JSONReaderWriterAppActivity.java kod przedstawiony w listingu 15.7. Listing 15.7. Kod wpisany w pliku aktywności Java JSONReaderWriterAppActivity.java package com.androidtablet.jsonreaderwriterapp; import import import import import import import import import
android.os.Bundle; android.app.Activity; java.io.StringWriter; android.widget.TextView; android.util.JsonWriter; java.io.IOException; java.io.StringReader; android.util.JsonReader; android.util.JsonToken;
public class JSONReaderWriterAppActivity extends Activity { private TextView jsonData; String id,productname, productData, productInfo; double price; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_jsonreader_writer_app); jsonData = (TextView)findViewById(R.id.jsondata); writeJSON(); readJSON(); } public void writeJSON() { StringWriter stringWriter = new StringWriter(); JsonWriter jsonWriter = new JsonWriter(stringWriter); try { jsonWriter.beginObject(); jsonWriter.name("id").value("A101"); jsonWriter.name("productname").value("Smartfon"); jsonWriter.name("price").value(19.99); jsonWriter.endObject(); } catch (IOException e) { e.printStackTrace(); } productData = stringWriter.toString(); } private void readJSON() { JsonReader jsonReader = new JsonReader(new StringReader(productData)); try{ jsonReader.beginObject(); while (jsonReader.hasNext()) { String name = jsonReader.nextName();
481
482
Rozdział 15. JSON if (jsonReader.peek() == JsonToken.NULL) jsonReader.skipValue(); if (name.equals("id")) { id = jsonReader.nextString(); } else if (name.equals("productname")) { productname = jsonReader.nextString(); } else if (name.equals("price")) { price = jsonReader.nextDouble(); } else { jsonReader.skipValue(); }
} jsonReader.endObject();
}
}
} catch (Exception e) { e.printStackTrace(); } try{ productInfo="Identyfikator produktu: "+id+"\n" +"Nazwa produktu: " +productname+ "\n" +"Cena: " + String.valueOf(price)+ "\n"; jsonData.setText(productInfo); } catch (Exception e) { e.printStackTrace(); }
Po uruchomieniu aplikacji uzyskiwany jest za pomocą obiektu JsonReader dostęp do informacji zapisanych przy wykorzystaniu obiektu JsonWriter. Następnie pobrane informacje są wyświetlane przy użyciu kontrolki TextView, tak jak pokazano na rysunku 15.4.
Rysunek 15.4. Wyświetlone na ekranie informacje o produkcie pobrane przez obiekt JsonReader
Receptura: wykorzystywanie usług sieciowych JSON w aplikacjach Android
Receptura: wykorzystywanie usług sieciowych JSON w aplikacjach Android W tej recepturze nauczysz się wykorzystywać usługę sieciową JSON w celu poznania nazwy stolicy danego województwa i jej długości oraz szerokości geograficznej. Aplikacja wyświetli zapytanie o nazwę województwa, a następnie przy wykorzystaniu usługi sieciowej pobrane informacje dotyczące tego województwa zostaną wyświetlone na ekranie. Pierwszym krokiem jest utworzenie usługi sieciowej, która przechowuje nazwę województwa, nazwę stolicy województwa oraz wartości długości i szerokości geograficznej stolicy. Usługa sieciowa powinna być zakodowana w taki sposób, aby zwracała dane w formacie JSON. W tym celu utwórz plik SampleJSON.php z kodem przedstawionym w listingu 15.8. Listing 15.8. Kod wpisany w pliku SampleJSON.php 'mazowieckie', 'stolica' => 'Warszawa', 'szerokość geograficzna' => '52.232222', 'długość geograficzna' => '21.008333' ), array( 'województwo' => 'śląskie', 'stolica' => 'Katowice', 'szerokość geograficzna' => '50.25', 'długość geograficzna' => '19' ), array( 'województwo' => 'zachodniopomorskie', 'stolica' => 'Szczecin', 'szerokość geograficzna' => '53.438056', 'długość geograficzna' => '14.542222' )); echo json_encode($stateDetails); ?>
Jak widzisz, usługa sieciowa zawiera wielowymiarową tablicę, służącą do przechowywania informacji o trzech województwach: mazowieckim, śląskim i zachodniopomorskim. W tym pliku możesz dodawać informacje o dowolnej liczbie województw. Kolejnym krokiem jest załadowanie pliku SampleJSON.php na serwer w celu udostępnienia go aplikacjom Android. W tym przykładzie plik SampleJSON.php został załadowany na domenę http://helion.pl. Aby skorzystać z usługi sieciowej JSON (czyli z informacji umieszczonych w pliku SampleJSON.php), utwórz aplikację Android o nazwie ConsumeJSONWebserviceApp. Jak wcześniej wspomniano, aplikacja będzie wyświetlała zapytanie z prośbą o wprowadzenie przez użytkownika nazwy województwa. Podana nazwa województwa będzie wyszukiwana w danych JSON pobranych z usługi sieciowej. Jeśli nazwa ta zostanie odnaleziona
483
484
Rozdział 15. JSON
w dostarczonych danych JSON, nazwa stolicy województwa oraz długość i szerokość geograficzna tej stolicy zostaną pobrane i wyświetlone na ekranie. W tej aplikacji będziesz potrzebował trzech kontrolek: EditText, Button oraz TextView. Kontrolka EditText pozwoli użytkownikowi wprowadzić nazwę wybranego województwa. Kontrolka Button będzie inicjować proces pobierania danych JSON z usługi sieciowej oraz inicjować proces przeszukiwania tych danych pod kątem wprowadzonej nazwy województwa. Kontrolka TextView będzie wykorzystywana do wyświetlenia informacji pozyskanych z usługi sieciowej. W celu zdefiniowania tych trzech kontrolek — EditText, Button oraz TextView — wpisz w pliku układu aktywności activity_consume_jsonwebservice_app.xml kod przedstawiony w listingu 15.9. Listing 15.9. Kod wpisany w pliku układu aktywności activity_consume_jsonwebservice_app.xml
W listingu 15.9 zdefiniowane zostały kontrolki EditText, Button oraz TextView, którym przypisano odpowiednio identyfikatory state_name, submit_btn oraz response. Aby uzyskać dostęp do danych JSON z usługi sieciowej oraz wyświetlić nazwę stolicy, szerokość i długość geograficzną tej stolicy dla wprowadzonego województwa, wpisz w pliku aktywności Java ConsumeJSONWebserviceAppActivity kod przedstawiony w listingu 15.10. Listing 15.10. Kod wpisany w pliku aktywności Java ConsumeJSONWebserviceAppActivity package com.androidtablet.consumejsonwebserviceapp; import import import import import
android.os.Bundle; android.app.Activity; android.widget.Button; android.widget.EditText; android.widget.TextView;
Receptura: wykorzystywanie usług sieciowych JSON w aplikacjach Android import import import import import import import import import import import import import import
android.view.View; org.apache.http.client.methods.HttpPost; org.json.JSONException; org.apache.http.impl.client.DefaultHttpClient; org.apache.http.client.HttpClient; org.apache.http.HttpResponse; org.apache.http.HttpEntity; java.io.InputStream; java.io.BufferedReader; java.io.InputStreamReader; android.os.AsyncTask; org.json.JSONObject; org.apache.http.StatusLine; org.json.JSONArray;
public class ConsumeJSONWebserviceAppActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_consume_jsonwebservice_app); Button submitButton = (Button)this.findViewById(R.id.submit_btn); submitButton.setOnClickListener(new Button.OnClickListener(){ public void onClick(View v) { new ReadJSONFeed().execute(" http://helion.pl/pliki/antare/SampleJSON.php"); } }); } private class ReadJSONFeed extends AsyncTask { protected void onPreExecute() {} @Override protected String doInBackground(String... urls) { HttpClient httpclient = new DefaultHttpClient(); StringBuilder builder = new StringBuilder(); HttpPost httppost = new HttpPost(urls[0]); try { HttpResponse response = httpclient.execute(httppost); StatusLine statusLine = response.getStatusLine(); int statusCode = statusLine.getStatusCode(); if (statusCode == 200) { HttpEntity entity = response.getEntity(); InputStream content = entity.getContent(); BufferedReader reader = new BufferedReader(new InputStreamReader(content)); String line; while ((line = reader.readLine()) != null) { builder.append(line); } } } catch (Exception e) { e.printStackTrace(); } return builder.toString(); } protected void onPostExecute(String result) { String state="";
485
486
Rozdział 15. JSON String stateInfo=""; EditText stateName = (EditText) findViewById(R.id.state_name); String searchState=stateName.getText().toString(); try{ JSONArray countriesArray = new JSONArray(result); for (int i =0 ; i0 ) resp.setText(stateInfo); else resp.setText("Niestety, nie znaleziono dopasowania"); } } }
Jak widzisz, dostęp do usługi sieciowej nie jest uzyskiwany za pomocą głównego wątku interfejsu użytkownika. Zamiast tego dostęp do danych JSON z usługi sieciowej jest uzyskiwany przez proces działający w tle. Pobrana zawartość jest odbierana w postaci tablicy JSONArray. Wprowadzana nazwa województwa jest wyszukiwana w każdym obiekcie JSONObject znalezionym w danej tablicy JSONArray. Jeśli nazwa województwa zostanie znaleziona, na ekranie wyświetlane są nazwa stolicy województwa oraz jej długość i szerokość geograficzna. Jeżeli wprowadzona nazwa nie istnieje w danych usługi sieciowej, na ekranie wyświetlany jest komunikat Niestety, nie znaleziono dopasowania. Aby uzyskać dostęp do usługi sieciowej załadowanej na serwer, musisz dodać do tego projektu Android zezwolenie INTERNET. W tym celu w pliku AndroidManifest.xml w elemencie zagnieźdź następującą instrukcję:
Po uruchomieniu aplikacji pojawia się ekran z prośbą o wprowadzenie przez użytkownika nazwy województwa, tak jak pokazano na rysunku 15.5 (górny obrazek). Kiedy wpisana zostanie nazwa województwa, powiązane z nią informacje — czyli nazwa stolicy oraz jej długość i szerokość geograficzna — zostaną pobrane z usługi sieciowej i wyświetlone na ekranie (patrz rysunek 15.5 — środkowy obrazek). Jeśli informacje na temat wpisanego województwa nie istnieją w danej usłudze sieciowej, na ekranie wyświetlany jest komunikat Niestety, nie znaleziono dopasowania, co pokazano na rysunku 15.5 (dolny obrazek).
Receptura: wykorzystywanie usług sieciowych JSON w aplikacjach Android
Rysunek 15.5. Ekran wyświetlany po uruchomieniu aplikacji (górny obrazek). Wyświetlone informacje na temat wpisanego województwa (środkowy obrazek). Komunikat: Niestety, nie znaleziono dopasowania, wyświetlany w przypadku, jeśli wpisana nazwa województwa nie istnieje w danych usługi sieciowej
487
488
Rozdział 15. JSON
Podsumowanie Ten rozdział został poświęcony objaśnieniu sposobu formatowania danych za pomocą formatu JSON. Poznałeś różne klasy i metody wymagane do definiowania obiektów JSONObject oraz zapisywania w tych obiektach informacji i pobierania z nich informacji. Zobaczyłeś, jak tworzy się tablicę JSONArray i poznałeś różne metody wymagane do zapisywania informacji w elementach tej tablicy oraz uzyskiwania do nich dostępu. Nauczyłeś się korzystać z klas JsonWriter i JsonReader w celu zapisywania i odczytywania informacji. Na koniec poznałeś procedurę uzyskiwania dostępu do usługi sieciowej w aplikacjach Android. W kolejnym rozdziale omówione zostaną sposoby wyświetlania dokumentów WWW za pomocą klas WebView, WebViewClient oraz WebViewFragment.
16 Klasa WebView U
zyskiwanie dostępu do Internetu i wyświetlanie stron WWW to obecnie chyba najbardziej wymagane zastosowania. Każdy chce korzystać z Internetu do wyszukiwania, przeglądania, korzystania np. z usług bankowych i zaspokajania innych potrzeb. System Android zapewnia kilka klas i fragmentów służących do obsługi przeglądania zawartości sieci WWW w aplikacjach Android. W tym rozdziale dowiesz się, jakie jest zastosowanie widżetu WebView do wyświetlania zawartości sieci WWW. Ponadto poznasz metody wymagane do włączenia określonych funkcji przeglądania. Dowiesz się, w jaki sposób klasa WebViewClient może ładować i wyświetlać dany adres URL w określonym widoku. Na koniec zobaczysz, jaka jest rola klasy WebViewFragment w wyświetlaniu zawartości sieci WWW.
Receptura: klasa WebView i jej metody Kontrolka WebView jest widżetem powszechnie stosowanym do wyświetlania aplikacji lub stron WWW. Wyświetla strony WWW jako część układu aktywności. Posiada standardowe funkcje przeglądarki, takie jak historia, zoom oraz obsługa języka JavaScript. Jak inne kontrolki, kontrolka WebView może być definiowana w pliku układu aktywności w następujący sposób:
Aby wykorzystać kontrolkę WebView w aplikacji, musisz uzyskać obiekt WebView w jeden z poniższych sposobów, takich jak: instancjonowanie z konstruktora: WebView webview = new WebView(this);
uzyskanie dostępu z pliku układu aktywności: WebView webView = (WebView) findViewById(R.id.webview);
490
Rozdział 16. Klasa WebView
Po uzyskaniu obiektu WebView możesz skorzystać z jego metody loadURL() do załadowania żądanej strony WWW. Przykładowo przy użyciu poniższej instrukcji załadujesz stronę google.pl: webView.loadUrl("http://www.google.pl/");
Możesz również dostosować kontrolkę WebView do własnych potrzeb za pomocą klasy WebSettings. Instancję klasy WebSettings możesz uzyskać, wywołując metodę getSettings() na klasie WebView. Następnie w celu dostosowania kontrolki WebView możesz wywołać jedną z poniższych metod. setJavaScriptEnabled() — przekaż do tej metody wartość logiczną true
(prawda), aby włączyć obsługę języka JavaScript. supportMultipleWindows() — przekaż do tej metody wartość logiczną true,
aby włączyć w kontrolce WebView obsługę wielu okien. setSupportZoom() — przekaż do tej metody wartość logiczną true, aby włączyć
obsługę funkcji zoom dla przeglądanej zawartości. setDisplayZoomControls() — przekaż do tej metody wartość logiczną true,
aby wyświetlić na ekranie kontrolki funkcji zoom. setTextZoom() — tu ustawisz w procentach zoom dla tekstu zawartości WWW.
Domyślna wartość wynosi 100. setSavePassword() — przekaż do tej metody wartość logiczną true,
aby kontrolka WebView mogła zapisywać hasła. Instrukcje z poniższego przykładu włączają obsługę języka JavaScript: WebSettings webSettings = webView.getSettings(); webSettings.setJavaScriptEnabled(true);
Aby uzyskać dostęp do zawartości online, musisz w aplikacji wykorzystującej kontrolkę WebView włączyć obsługę przeszukiwania Internetu. W tym celu w pliku manifestu aplikacji dodaj zezwolenie INTERNET:
W tabeli 16.1 przedstawiono w zarysie różne metody klasy WebView. Tabela 16.1. Krótki opis metod klasy WebView
Metoda
Opis
reload()
Odświeża i przeładowuje aktualnie przeglądaną stronę WWW.
goBack()
Cofa o jeden krok w historii przeglądarki.
canGoBack()
Sprawdza, czy istnieje historia, w której można się cofnąć.
goForward()
Przechodzi do przodu o jeden krok w historii przeglądarki.
canGoForward()
Sprawdza, czy istnieje historia, w której można przejść do przodu.
Receptura: wyświetlanie stron WWW za pomocą kontrolki WebView Tabela 16.1. Krótki opis metod klasy WebView (ciąg dalszy)
Metoda
Opis
goBackOrForward()
Cofa lub przechodzi do przodu w historii przeglądarki, w zależności od tego, czy liczba dostarczona do tej metody jako argument jest ujemna albo dodatnia. Wartość ujemna reprezentuje liczbę kroków, o które należy się cofnąć w historii, a wartość dodatnia reprezentuje liczbę kroków, o które należy przejść do przodu w historii przeglądarki.
canGoBackOrForward()
Sprawdza, czy przeglądarka może się cofnąć lub przejść do przodu określoną liczbę kroków w historii przeglądarki. Liczba dostarczana do tej metody jako argument zależy od tego, czy chcesz się cofnąć, czy przejść do przodu.
clearCache()
Czyści pamięć podręczną przeglądarki.
clearHistory()
Czyści historię przeglądarki.
Receptura: wyświetlanie stron WWW za pomocą kontrolki WebView Utwórz niewielką aplikację przeglądarkową, która prosi użytkownika o wprowadzenie adresu URL strony WWW, a następnie ładuje i wyświetla tę stronę za pomocą kontrolki WebView. Uruchom Eclipse IDE i utwórz nową aplikację Android o nazwie WebViewApp. W tej aplikacji będziesz korzystał z kontrolek TextView, EditText, Button oraz WebView. Kontrolka TextView będzie wyświetlać tekst Adres internetowy:, aby wskazać użytkownikowi, że w znajdującej się poniżej kontrolce EditText należy wpisać adres URL żądanej strony WWW. Kontrolka EditText wyświetla puste pole tekstowe, w którym użytkownik może wprowadzić adres URL strony do otwarcia. Kiedy użytkownik kliknie kontrolkę Button, strona WWW o adresie URL wpisanym w kontrolce EditText zostanie załadowana i wyświetlona za pomocą kontrolki WebView. Aby zdefiniować te cztery kontrolki, wpisz w pliku układu activity_web_view_app.xml kod przedstawiony w listingu 16.1. Listing 16.1. Kod wpisany w pliku układu activity_web_view_app.xml
491
492
Rozdział 16. Klasa WebView
Jak widzisz, kontrolka TextView została skonfigurowana do wyświetlania tekstu Adres internetowy:. Identyfikator przypisany do kontrolki EditText to url, a tekst podpowiedzi to http://. Tekst podpowiedzi w kontrolce EditText ma jaśniejszy kolor i wskazuje użytkownikowi, jaki typ danych należy wpisać. Do kontrolki Button przypisano identyfikator go_button. Będzie ona wyświetlana z nagłówkiem Przejdź. Ostatniej kontrolce, WebView, przypisano identyfikator web_view. Tekst wyświetlany przez kontrolkę TextView, nagłówek kontrolki Button oraz tekst wprowadzany w kontrolce EditText będą wyświetlane czcionką o rozmiarze zdefiniowanym w zasobie wymiarów text_size. Szerokość kontrolki EditText będzie zdefiniowana w zasobie wymiarów box_width. Zakładamy, że plik wymiarów dimens.xml istnieje już w folderze res/values. Dodaj do tego pliku wymiary, które zmienią wielkość widoków w zależności od wielkości ekranu urządzenia, na którym uruchamiana będzie aplikacja. Wpisz w pliku dimens.xml następujący kod: 14sp 200dp
Zasoby wymiarów text_size i box_width definiują odpowiednio rozmiar czcionki tekstu oraz szerokość kontrolki EditText. Powyższe zasoby wymiarów przeznaczone są dla urządzeń o normalnych ekranach (telefonów).
Receptura: wyświetlanie stron WWW za pomocą kontrolki WebView
Aby zdefiniować zasoby wymiarów dla 7-calowych tabletów, otwórz znajdujący się w folderze res/values-sw600dp plik dimens.xml i wpisz w nim następujący kod: 24sp 400dp
Na koniec, aby zdefiniować wymiary dla urządzeń o ekstradużych ekranach (10-calowych tabletów), otwórz znajdujący się w folderze values-sw720dp plik dimens.xml i wpisz w nim następujący kod: 32sp 600dp
Porównując zasoby wymiarów dla telefonów, 7-calowych tabletów oraz 10-calowych tabletów, możesz zauważyć, że wielkość widoków w aplikacji zmienia się w zależności od rozmiaru ekranu urządzenia. Aby za pomocą kontrolki WebView wyświetlić stronę WWW, wpisz w pliku aktywności Java WebViewAppActivity.java kod przedstawiony w listingu 16.2. Listing 16.2. Kod wpisany w pliku aktywności Java WebViewAppActivity.java package com.androidtablet.webviewapp; import import import import import import import import import import
android.app.Activity; android.os.Bundle; android.widget.Button; android.widget.EditText; android.view.View; android.view.View.OnClickListener; android.webkit.WebView; android.view.KeyEvent; android.widget.TextView; android.widget.TextView.OnEditorActionListener;
public class WebViewAppActivity extends Activity implements OnClickListener { EditText url; WebView webView; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_web_view_app); url = (EditText)this.findViewById(R.id. url); webView = (WebView) findViewById(R.id.web_view); webView.getSettings().setJavaScriptEnabled(true); url.setOnEditorActionListener(new OnEditorActionListener() { @Override public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
#1
#2
493
494
Rozdział 16. Klasa WebView if(event!=null && event.getAction()== KeyEvent.ACTION_DOWN){ webView.loadUrl(url.getText().toString()); return true; } return false; } }); Button b = (Button)this.findViewById(R.id.go_button); b.setOnClickListener(this);
#3 #4 #5 #6
#7
} @Override public void onClick(View v) { webView.loadUrl(url.getText().toString()); } @Override public boolean onKeyUp(int keyCode, KeyEvent event) { if ((keyCode == KeyEvent.KEYCODE_BACK) && webView.canGoBack()) { webView.goBack(); return true; } return super.onKeyUp(keyCode, event); }
#8 #9
}
Kontrolki EditText i WebView z pliku układu są przechwytywane i mapowane na obiekty EditText i WebView o nazwach odpowiednio url i webView. Niektóre instrukcje z listingu 16.2 wymagają objaśnienia. Instrukcja #1 włącza obsługę języka JavaScript, która jest domyślnie wyłączona
w kontrolce WebView. Może się zdarzyć, że strona WWW ładowana przez użytkownika za pomocą kontrolki WebView zawiera skrypty JavaScript. Aby wyświetlić taką stronę poprawnie, musisz włączyć obsługę języka JavaScript dla kontrolki WebView. Obsługa języka skryptowego JavaScript jest włączana za pomocą klasy WebSettings dołączonej do klasy WebView. Dlatego dostęp do klasy WebSettings uzyskiwany jest przez wywołanie metody getSettings(). Następnie obsługa języka JavaScript jest włączana przez przekazanie wartości logicznej true (prawda) do metody setJavaScriptEnabled() klasy WebSettings. Instrukcja #2 sprawdza występowanie zdarzeń klawiatury w kontrolce EditText.
Zasadniczo nasłuchiwacz setOnEditActionListener jest kojarzony z kontrolką EditText, aby wywoływać metodę wywołania zwrotnego onEditorAction() za każdym razem, kiedy jakaś akcja (zdarzenie) ma miejsce w kontrolce EditText. Instrukcja #3 sprawdza, czy w kontrolce EditText wystąpiło jakiekolwiek
zdarzenie. Sprawdza również, czy w tej kontrolce został wciśnięty jakiś klawisz. Każde wciśnięcie klawisza składa się z kilku zdarzeń klawiatury. Każde zdarzenie klawiatury ma dołączony kod klawisza, który pomaga zidentyfikować wciśnięty klawisz.
Receptura: wyświetlanie stron WWW za pomocą kontrolki WebView Instrukcja #4 ładuje do kontrolki WebView stronę WWW, której adres URL
został wprowadzony przez użytkownika w kontrolce EditText. Instrukcja #5 kończy działanie procedury obsługi setOnEditActionListener,
zwracając wartość logiczną true (prawda) w metodzie wywołania zwrotnego onEditorAction(). Oznacza to, że kiedy użytkownik wciśnie klawisz Enter, procedura obsługi setOnEditActionListener jest przerywana, a dana strona WWW jest ładowana do kontrolki WebView. Instrukcja #6 zwraca wartość logiczną false (fałsz) z metody wywołania
zwrotnego onEditorAction(), aby nasłuchiwacz setOnEditActionListener nasłuchiwał kolejnych zdarzeń w kontrolce EditText. Instrukcja #7 kojarzy nasłuchiwacz setOnClickListener z kontrolką Button,
aby metoda wywołania zwrotnego onClick() była wywoływana, kiedy na tej kontrolce wystąpi zdarzenie kliknięcia. W metodzie wywołania zwrotnego onClick() do kontrolki WebView ładowana jest strona WWW, której adres URL został wprowadzony przez użytkownika w kontrolce EditText. Instrukcja #8 sprawdza, czy użytkownik wcisnął przycisk Back (wstecz). Sprawdza
również historię strony WWW dla nawigacji wstecz. Metoda canGoBack() zwraca wartość true (prawda) tylko wtedy, kiedy istnieje dla danej strony jakaś historia, do której można wrócić. Instrukcja #9 nawiguje do poprzedniej strony w historii stron WWW otwartych
w kontrolce WebView. Jeśli do ładowania stron WWW wykorzystywany jest widżet WebView, przechowuje on historię odwiedzanych stron. Historia stron WWW może być zastosowana do nawigacji wstecz i do przodu. Aby wyświetlić poprzednią lub następną stronę, należy skorzystać z metod goBack() i goForward(). Pamiętaj, że po dojściu do końca historii metody te nie wykonają żadnej czynności.
Uwaga Metoda wywołania zwrotnego onKey() jest definiowana do zwracania wartości logicznej true (prawda), jeśli nie chcesz nasłuchiwać kolejnych wciśnięć klawiszy i chcesz zakończyć działanie procedury obsługi zdarzeń oraz wyjść z tej metody w celu dalszego przetwarzania. Metoda ta jest definiowana do zwracania wartości logicznej false (fałsz), jeśli nie chcesz przerywać działania procedury obsługi zdarzeń i chcesz nasłuchiwać kolejnych wciśnięć klawiszy.
Twoja aplikacja musi mieć dostęp do Internetu w celu ładowania stron WWW. Musisz więc zażądać zezwolenia INTERNET, dodając w pliku AndroidManifest.xml następującą instrukcję:
495
496
Rozdział 16. Klasa WebView
Teraz Twoja aplikacja jest kompletna i gotowa do uruchomienia. Pierwszym wyświetlonym po uruchomieniu ekranem jest ekran przedstawiony na rysunku 16.1 (górny obrazek). Pusty biały obszar pod przyciskiem Przejdź reprezentuje kontrolkę WebView, która początkowo nie wyświetla niczego. W kontrolce EditText możesz wpisać adres URL strony WWW, którą chcesz otworzyć, a następnie kliknąć Enter lub przycisk Przejdź, aby załadować i wyświetlić daną stronę w kontrolce WebView. Po wpisaniu adresu URL mojej strony WWW http://bmharwani.com i kliknięciu przycisku Przejdź strona ta zostanie załadowana i wyświetlona tak, jak pokazano na rysunku 16.1 (środkowy obrazek). Po wybraniu któregoś z odnośników kontrolka WebView zostanie zaktualizowana w celu wyświetlenia informacji na temat powiązanej strony WWW, tak jak pokazano na rysunku 16.1 (dolny obrazek). Jeśli klikniesz przycisk Back (wstecz), cofniesz się o jeden krok w historii przeglądania, a w kontrolce WebView ponownie pojawi się strona przedstawiona na rysunku 16.1 (środkowy obrazek).
Uwaga Zazwyczaj, kiedy wciskasz przycisk Enter w kontrolce EditText, kursor przechodzi do kolejnego wiersza. Ponieważ jednak powiązałeś z kontrolką EditText nasłuchiwacz setOnEditActionListener, po wpisaniu w tej kontrolce adresu URL i wciśnięciu Enter kursor nie przejdzie do kolejnego wiersza, ale do wprowadzonego adresu URL.
Obserwując działanie tej aplikacji, możesz zauważyć, że po kliknięciu na stronie odnośnika ładowana jest wskazana strona WWW, która pokrywa cały widok, przez co kontrolki TextView, EditText i Button stają się niewidoczne. Jest to spowodowane tym, że kliknięty link jest ładowany przez domyślną przeglądarkę. Zamiast skorzystać z kontrolki WebView aplikacji, system Android wywołuje domyślną przeglądarkę do otwarcia i załadowania powiązanej strony WWW. Aby poradzić sobie z tym problemem, należy skorzystać z klasy WebViewClient.
Receptura: korzystanie z klasy WebViewClient Do otwarcia i załadowania powiązanej strony WWW system Android wywołuje domyślną przeglądarkę internetową. Aby otwierać linki w kontrolce WebView, musisz zastosować klasę WebViewClient oraz jej metodę shouldOverrideUrlLoading(). Otwórz aplikację Android WebViewApp, którą utworzyłeś w poprzedniej recepturze, i zmodyfikuj jej plik aktywności Java w taki sposób, aby wykonywał zadania, takie jak: zastosowanie klasy WebViewClient, użycie metody shouldOverrideUrlLoading() do załadowania klikniętych linków
w kontrolce WebView. Po zmodyfikowaniu kod aktywności Java WebViewAppActivity.java powinien wyglądać tak, jak przedstawiono w listingu 16.3. Zmodyfikowane zostały jedynie fragmenty kodu zaznaczone pogrubioną czcionką. Reszta kodu pozostaje taka sama jak w listingu 16.2.
Receptura: korzystanie z klasy WebViewClient
Rysunek 16.1. Aplikacja wyświetlająca kontrolkę EditText z tekstem podpowiedzi dla wpisania adresu URL strony WWW (górny obrazek). Strona WWW załadowana i wyświetlona w kontrolce WebView (środkowy obrazek). Powiązana strona WWW otwarta przez domyślną przeglądarkę (dolny obrazek)
497
498
Rozdział 16. Klasa WebView Listing 16.3. Kod wpisany w pliku aktywności Java WebViewAppActivity.java package com.androidtablet.webviewapp; import import import import import import import import import import import
android.app.Activity; android.os.Bundle; android.widget.Button; android.widget.EditText; android.view.View; android.view.View.OnClickListener; android.webkit.WebView; android.view.KeyEvent; android.webkit.WebViewClient; android.widget.TextView; android.widget.TextView.OnEditorActionListener;
public class WebViewAppActivity extends Activity implements OnClickListener { EditText url; WebView webView; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_web_view_app); url = (EditText)this.findViewById(R.id.url); webView = (WebView) findViewById(R.id.web_view); webView.getSettings().setJavaScriptEnabled(true); webView.setWebViewClient(new WebViewClient(){ public boolean shouldOverrideUrlLoading(WebView view, String url) { view.loadUrl(url); return true; } }); url.setOnEditorActionListener(new OnEditorActionListener() { @Override public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { if(event!=null && event.getAction()== KeyEvent.ACTION_DOWN){ webView.loadUrl(url.getText().toString()); return true; } return false; } }); Button b = (Button)this.findViewById(R.id.go_button); b.setOnClickListener(this); } @Override public void onClick(View v) { webView.loadUrl(url.getText().toString()); } @Override
Receptura: korzystanie z klasy WebViewFragment public boolean onKeyUp(int keyCode, KeyEvent event) { if ((keyCode == KeyEvent.KEYCODE_BACK) && webView.canGoBack()) { webView.goBack(); return true; } return super.onKeyUp(keyCode, event); } }
W kodzie zostały użyte dwie funkcje, które opisano poniżej. setWebViewClient() — funkcja zastępuje bieżącą procedurę obsługi klasą
WebViewClient, co pozwala wykorzystać metody tej klasy do wymuszenia otwierania klikniętych linków w kontrolce WebView. public boolean shouldOverrideUrlLoading (WebView widok, String
adres_url) — użyte w metodzie shouldOverrideUrlLoading() parametry widok i adres_url odnoszą się odpowiednio do kontrolki WebView inicjującej wywołanie zwrotne oraz adresu URL klikniętego odnośnika. Domyślnie kontrolka WebView
prosi zarządcę aktywności (ang. activity manager) o wybranie odpowiedniej procedury obsługi do otwarcia klikniętego na stronie WWW odnośnika. Zarządca aktywności wywołuje z kolei domyślną przeglądarkę użytkownika w celu załadowania adresu URL powiązanej strony. Korzystając z tej funkcji, możesz zdecydować, czy powiązany adres URL ma być załadowany przez aplikację hosta, czy przez kontrolkę WebView. Jeśli funkcja zwraca wartość true (prawda), oznacza to, że dany adres URL ma zostać obsłużony przez aplikację hosta. Jeśli funkcja zwraca wartość false (fałsz), oznacza to, że nie chcesz korzystać z aplikacji hosta i wolisz, aby dany adres URL został obsłużony przez kontrolkę WebView. (Innymi słowy, adres URL ma być załadowany w Twojej kontrolce WebView). Kiedy uruchomisz aplikację po wprowadzeniu wspomnianych zmian w pliku aktywności, wyświetlony zostanie ekran z polem do wprowadzenia adresu URL. Strona WWW, której adres URL wprowadzisz w kontrolce EditText, zostanie załadowana i otwarta w kontrolce WebView, tak jak pokazano na rysunku 16.2 (górny obrazek). Po kliknięciu dowolnego odnośnika powiązana strona WWW również zostanie otwarta w kontrolce WebView (patrz rysunek 16.2, dolny obrazek). Powiązane adresy URL nie będą już obsługiwane przez domyślną przeglądarkę internetową.
Receptura: korzystanie z klasy WebViewFragment Klasa WebViewFragment jest fragmentem zawierającym kontrolkę WebView. Żądane strony WWW mogą być wyświetlane w kontrolce WebView zawartej we fragmencie WebViewFragment.
499
500
Rozdział 16. Klasa WebView
Rysunek 16 2. Ładowanie strony WWW w kontrolce WebView (górny obrazek). Powiązana strona WWW również zostaje otwarta w kontrolce WebView (dolny obrazek)
Aby zapoznać się z koncepcją działania fragmentu WebViewFragment, utworzysz aplikację proszącą użytkownika o wpisanie adresu URL strony WWW, która będzie wyświetlana we fragmencie WebViewFragment. W tej recepturze wykorzystasz w sumie cztery następujące kontrolki. TextView — posłuży do wyświetlenia komunikatu Adres internetowy,
aby wskazać użytkownikowi, że w wyświetlonej obok kontrolce EditText należy wpisać adres strony WWW. EditText — umożliwi użytkownikowi wpisanie adresu URL strony WWW.
Kontrolka ta będzie również wyświetlać tekst podpowiedzi http://. Button — zostanie wykorzystana do zainicjowania procesu wyświetlania
za pomocą fragmentu WebViewFragment strony WWW, której adres URL zostanie wprowadzony w kontrolce EditText. Fragment — posłuży do wyświetlenia danej strony WWW za pomocą
fragmentu WebViewFragment.
Receptura: korzystanie z klasy WebViewFragment
Utwórz nowy projekt Android o nazwie WebViewFragApp. Aby zdefiniować cztery wspomniane kontrolki, wpisz w pliku układu aktywności activity_web_view_frag_app.xml kod przedstawiony w listingu 16.4. Listing 16.4. Kod wpisany w pliku układu aktywności activity_web_view_frag_app.xml
Uwaga Fragment wykorzystany w powyższym układzie aktywności rezerwuje część ekranu aktywności dla kontrolki WebView fragmentu WebViewFragment.
Jak widzisz, tekst Adres internetowy: wyświetlany za pomocą kontrolki TextView będzie prezentowany pogrubioną czcionką o rozmiarze zdefiniowanym w zasobie
501
502
Rozdział 16. Klasa WebView
wymiarów text_size. Dla celów uzyskiwania dostępu oraz identyfikacji w kodzie Java kontrolce EditText przypisano identyfikator url. Kontrolka będzie dodatkowo wyświetlać tekst podpowiedzi http://. Nagłówek przypisany kontrolce Button to Przejdź, a jej identyfikatorem jest go_button. Aby wyświetlić nagłówek Przejdź w centralnym obszarze kontrolki Button, obszar ten wypełniono ze wszystkich czterech stron pustymi polami. Znacznik oraz identyfikator przypisane do fragmentu to odpowiednio fragment oraz web_fragment. Fragment ten został skonfigurowany do wywoływania klasy WebViewFragmentActivity, która z kolei będzie korzystać z klasy WebViewFragment w celu uzyskania dostępu do strony WWW w danym fragmencie i wyświetlenia jej. Aby zmieniać rozmiary widoków aplikacji w zależności od wielkości ekranu urządzenia, na którym aplikacja będzie uruchamiana, musisz zdefiniować odpowiednie zasoby wymiarów. Otwórz znajdujący się w folderze res/values plik dimens.xml i zdefiniuj zasoby wymiarów, wpisując w nim następujący kod: 14sp 200dp
Zasoby wymiarów text_size i box_width definiują odpowiednio rozmiar czcionki tekstu oraz szerokość kontrolki EditText. Zdefiniowane powyżej zasoby wymiarów przeznaczone są dla urządzeń o normalnych ekranach (telefonów). Aby zdefiniować zasoby wymiarów dla 7-calowych tabletów, otwórz znajdujący się w folderze res/values-sw600dp plik dimens.xml i wpisz w nim następujący kod: 24sp 400dp
Na koniec, aby zdefiniować zasoby wymiarów dla urządzeń o ekstradużych ekranach (10-calowych tabletów), otwórz znajdujący się w folderze values-sw720dp plik dimens.xml i wpisz w nim następujący kod: 32sp 600dp
Porównując zasoby wymiarów dla telefonów, 7-calowych tabletów i 10-calowych tabletów, możesz zauważyć, że rozmiary widoków aplikacji zmieniają się w zależności od wielkości ekranu urządzenia. Kolejny krok to dodanie do paczki com.androidtablet.webviewfragapp pliku klasy Java o nazwie WebViewFragmentActivity.java. Wpisz w tym pliku kod przedstawiony w listingu 16.5.
Receptura: korzystanie z klasy WebViewFragment Listing 16.5. Kod wpisany w pliku Java WebViewFragmentActivity.java package com.androidtablet.webviewfragapp; import import import import
android.webkit.WebViewFragment; android.os.Bundle; android.webkit.WebView; android.webkit.WebViewClient;
public class WebViewFragmentActivity extends WebViewFragment{ String webURL; @Override public void onCreate(Bundle state) { super.onCreate(state); if (null == state) state = getArguments(); if (null != state){ webURL=state.getString("url"); } setRetainInstance(true); } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); WebView webView = getWebView(); if (webView != null) { webView.getSettings().setJavaScriptEnabled(true); webView.setWebViewClient(new WebViewClient()); webView.loadUrl(webURL); } } }
Jak widzisz w powyższym kodzie, klasa WebViewFragmentActivity wykonuje następujące zadania. Uzyskanie dostępu do adresu URL wpisanego przez użytkownika w kontrolce
EditText i przekazanie do tej klasy głównego pliku aktywności Java aplikacji. Zdefiniowanie instancji WebView przez wywołanie metody getWebView()
klasy WebViewFragment. Włączenie obsługi języka skryptowego JavaScript w kontrolce WebView.
Przypomnijmy, że obsługa języka JavaScript jest domyślnie wyłączona. Wywołanie metody setWebViewClient() na instancji WebView w celu ustawienia
klasy WebViewClient jako procedury obsługi. Oznacza to, że linki klikane na danej stronie WWW nie będą otwierane i ładowane w domyślnej przeglądarce, ale w kontrolce WebView. Załadowanie w kontrolce WebView strony WWW, której adres URL został
przekazany do tej klasy.
503
504
Rozdział 16. Klasa WebView
Następnie musisz w pliku aktywności Java napisać kod, który będzie wykonywał poniższe zadania. Uzyskanie dostępu do kontrolek EditText i Button z pliku układu
i zmapowanie ich na odpowiednie obiekty. Powiązanie nasłuchiwacza setOnEditActionListener z kontrolką EditText
w celu nasłuchiwania wystąpienia na niej zdarzeń. Kiedy w kontrolce EditText wystąpi zdarzenie, wywołana zostanie metoda onEditorAction(). Powiązanie nasłuchiwacza setOnClickListener z kontrolką Button w celu
nasłuchiwania wystąpienia na niej zdarzeń kliknięcia. Po kliknięciu kontrolki Button wywołana zostanie metoda onClick. Zdefiniowanie instancji klas FragmentManager i FragmentTransaction. Zdefiniowanie instancji klasy Java WebViewFragmentActivity, którą
zdefiniowałeś wcześniej. Zdefiniowanie obiektu Bundle. Umieszczenie w obiekcie Bundle adresu URL wpisanego przez użytkownika
w kontrolce EditText i przekazanie tego obiektu do instancji klasy WebViewFragmentActivity. Dostęp do danego adresu URL może być uzyskany w klasie WebViewFragmentActivity, a odpowiadająca mu strona WWW może być załadowana w kontrolce WebView. Wykorzystanie obiektu klasy FragmentTransaction do takiego skonfigurowania,
aby zawartość (WebView) wyświetlana za pomocą klasy WebViewFragmentActivity pojawiała się we fragmencie zdefiniowanym w pliku układu aktywności. Aby wykonać wszystkie wymienione zadania, wpisz w pliku aktywności Java WebViewFragAppActivity.java kod przedstawiony w listingu 16.6. Listing 16.6. Kod wpisany w pliku aktywności Java WebViewFragAppActivity.java package com.androidtablet.webviewfragapp; import import import import import import import import import import
android.os.Bundle; android.app.Activity; android.view.View; android.view.View.OnClickListener; android.widget.EditText; android.widget.Button; android.view.KeyEvent; android.app.FragmentManager; android.widget.TextView; android.widget.TextView.OnEditorActionListener;
public class WebViewFragAppActivity extends Activity implements OnClickListener { EditText url; FragmentManager fragmentManager;
Receptura: korzystanie z klasy WebViewFragment Bundle args; WebViewFragmentActivity webviewFragment; android.app.FragmentTransaction fragmentTransaction; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_web_view_frag_app); fragmentManager = getFragmentManager(); webviewFragment = new WebViewFragmentActivity(); fragmentTransaction = fragmentManager.beginTransaction(); args = new Bundle(); url = (EditText)this.findViewById(R.id.url); url.setOnEditorActionListener(new OnEditorActionListener() { @Override public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { if(event!=null && event.getAction()== KeyEvent.ACTION_DOWN){ args.putString("url", url.getText().toString()); webviewFragment.setArguments(args); fragmentTransaction.replace(R.id.fragment, webviewFragment); fragmentTransaction.commit(); return true; } return false; } }); Button b = (Button)this.findViewById(R.id.go_button); b.setOnClickListener(this); } @Override public void onClick(View v) { args.putString("url", url.getText().toString()); webviewFragment.setArguments(args); fragmentTransaction.replace(R.id.fragment, webviewFragment); fragmentTransaction.commit(); } }
Ponownie, w celu uzyskania w tej aplikacji dostępu do Internetu dodaj do pliku AndroidManifest.xml poniższą instrukcję zezwolenia:
Po uruchomieniu aplikacji otrzymasz ekran z prośbą o wpisanie adresu URL. Wprowadź go w kontrolce EditText i kliknij przycisk Przejdź. Odpowiednia strona zostanie otwarta w kontrolce WebView zawartej we fragmencie WebViewFragment.
505
506
Rozdział 16. Klasa WebView
Kontrolka WebView pojawi się w lokalizacji fragmentu zdefiniowanego w pliku układu aktywności, co zostało pokazane na rysunku 16.3 (górny obrazek). Po wybraniu dowolnego odnośnika na wyświetlonej stronie strona powiązana z odnośnikiem również zostanie otwarta w kontrolce WebView fragmentu WebViewFragment (patrz rysunek 16.3, dolny obrazek).
Rysunek 16.3. Ładowanie strony WWW za pomocą fragmentu WebViewFragment (górny obrazek) oraz powiązana strona WWW otwarta w kontrolce WebView należącej do fragmentu WebViewFragment
Uwaga Każdy odnośnik kliknięty na stronie WWW wyświetlonej za pomocą kontrolki WebView będzie obsługiwany przez domyślną przeglądarkę.
Podsumowanie
Podsumowanie W tym rozdziale nauczyłeś się wyświetlać zawartość sieci WWW w kontrolce WebView i dowiedziałeś się, jak włączyć obsługę języka JavaScript oraz inne funkcje. Zobaczyłeś, jak korzystać z klasy WebViewClient w celu wyświetlenia klikniętych odnośników w kontrolce WebView, a nie w domyślnej przeglądarce. Nauczyłeś się także uzyskiwać dostęp do kontrolki WebView we fragmencie WebViewFragment i wykorzystywać ją do wyświetlania stron WWW. W kolejnym rozdziale dowiesz się, jak dodać obsługę dla małych ekranów. Poznasz alternatywne układy oraz pakiet Android Compatibility Package. Przeczytasz także o wykorzystaniu zasobów oraz o technikach obsługi starszych wersji pakietu SDK (ang. Software Development Kit).
507
508
Rozdział 16. Klasa WebView
Część VI Zaawansowane techniki systemu Android
17 Obsługa małych ekranów U
rządzenia Android są dostępne z ekranami o różnej wielkości i gęstości. Poza obsługą odmiennych fizycznie urządzeń, dostępne jest również wsparcie różnic związanych z wersjami systemu Android. Dla programistów istotne jest tworzenie aplikacji działających na urządzeniach o różnych rozmiarach i gęstościach ekranów oraz obsługujących odmienne wersje systemu. Aplikacje muszą być zaprojektowane w taki sposób, aby widoki i bitmapy były automatycznie rozmieszczane w celu dopasowania do rozmiaru i gęstości ekranu danego urządzenia. Interfejs użytkownika i bitmapy nie powinny być zbytnio rozciągane lub zmniejszane, aby nie stawały się rozmyte. Aplikacje nie powinny zawierać jedynie funkcji oferowanych przez najnowsze wersje systemu Android, lecz warto zadbać o to, by były kompatybilne również ze starszymi wersjami. W tym rozdziale poznasz czynniki, które należy wziąć pod uwagę, aby zapewnić obsługę różnych ekranów i gęstości. Nauczysz się włączać w aplikacji obsługę różnych wersji platformy. Dowiesz się, w jaki sposób wykorzystywać pakiet Android Support Library dla obsługi starszych wersji systemu Android. Na koniec nauczysz się obsługiwać orientację ekranu przez kotwiczenie kontrolek oraz zobaczysz, jak korzystać z alternatywnych układów w celu zapewnienia obsługi ekranów o różnych wielkościach.
Receptura: czynniki decydujące o obsłudze różnych ekranów i gęstości Aplikacje tworzone przez programistów mogą mieć wymóg uruchamiania na szeregu urządzeń o różnych rozmiarach i gęstościach ekranu. Dlatego też niezbędna jest optymalizacja projektu interfejsu użytkownika, aby aplikacje Android były kompatybilne z odmiennymi rozmiarami i gęstościami ekranów. W celu zapewnienia obsługi różnorodnych ekranów należy wziąć pod uwagę następujące czynniki. Rozmiar ekranu — określa fizyczny obszar aplikacji. W systemie Android
faktyczne rozmiary ekranów zostały podzielone na cztery grupy: małe, normalne, duże i ekstraduże ekrany.
512
Rozdział 17. Obsługa małych ekranów
Gęstość ekranu — reprezentuje liczbę pikseli w danym obszarze ekranu.
Gęstość jest mierzona w jednostkach dpi, oznaczających liczbę punktów na cal (ang. dots per inch). Istnieją cztery kategorie gęstości ekranu: ldpi (niska, ang. low), mdpi (średnia, ang. medium), hdpi (wysoka, ang. high) oraz xhdpi (ekstraduża, ang. extra-high). Orientacja — urządzenie może być zorientowane poziomo lub pionowo.
Oznacza to, że ekran może być albo szeroki, albo wysoki. Rozdzielczość — reprezentuje całkowitą liczbę pikseli na ekranie.
Rozdzielczości małych, średnich, dużych i ekstradużych ekranów są następujące: mały ekran — 426 dp×320 dp, normalny ekran — 470 dp×320 dp, duży ekran — 640 dp×480 dp, ekstraduży ekran — 960 dp×720 dp.
Dwa główne czynniki, które należy wziąć pod uwagę podczas tworzenia aplikacji, to rozmiar i gęstość ekranu. Oto sposoby optymalizacji interfejsu użytkownika w aplikacji pod kątem różnych rozmiarów i gęstości ekranu. Pomiar w jednostkach dp — aby aplikacja była kompatybilna z ekranami
o różnej gęstości, interfejs użytkownika powinien być mierzony w jednostkach dp, oznaczających piksele niezależne od gęstości (ang. density-independent pixel), które są wirtualną jednostką miary. Piksel niezależny od gęstości jest równoważny jednemu pikselowi na ekranie o gęstości 160 dpi. Gęstość 160 dpi przyjmuje się dla ekranów o średniej gęstości. Aplikacja automatycznie skaluje jednostki dp na podstawie faktycznej gęstości ekranu. Formuła konwertująca jednostki dp na piksele ekranu jest następująca: px = dp×dpi/160 Zastosowanie fragmentów — fragmenty umożliwiają fragmentowanie
lub dzielenie aktywności na zhermetyzowane, nadające się do wielokrotnego użytku moduły. Każdy z nich ma własny interfejs użytkownika, co sprawia, że aplikacja nadaje się do różnych rozmiarów ekranów. Oznacza to, że w zależności od dostępnego rozmiaru ekranu możesz dodawać lub usuwać fragmenty w Twojej aplikacji. Unikanie stosowania bezwzględnych szerokości i wysokości — definiując
szerokości i wysokości elementów interfejsu użytkownika, należy stosować szerokości i wysokości kontenerów. Oznacza to używanie wartości wrap_content i match_parent podczas definiowania szerokości i wysokości interfejsu użytkownika, ponieważ powoduje to automatyczną zmianę rozmiarów elementów interfejsu użytkownika na podstawie rozmiarów ich kontenerów.
Receptura: zapewnianie obsługi dla różnych wersji platformy
Zapewnienie alternatywnych zasobów — musisz dostarczyć alternatywne
układy dla różnych rozmiarów ekranu i alternatywne bitmapy obrazków dla różnych gęstości ekranu. Na podstawie rozmiaru i gęstości ekranu urządzenia system Android wybierze najbardziej odpowiednie zasoby aplikacji. Jeśli zasoby odpowiadające różnym rozmiarom ekranu nie zostaną dostarczone, elementy interfejsu użytkownika mogą na siebie zachodzić lub być zbyt szeroko rozstawione. Zapewnienie alternatywnych układów — podczas testowania aplikacji
na małym ekranie możesz odkryć, że niektóre widoki nie mieszczą się w szerokości ekranu i zostają ukryte. Z kolei na urządzeniach o ekstradużych ekranach pewne widoki mogą niepotrzebnie zostać rozciągnięte w celu wypełnienia dodatkowej przestrzeni. Aby użytkownik miał poczucie, że dana aplikacja została zaprojektowana specjalnie pod kątem jego urządzenia, zaleca się zdefiniowanie alternatywnych układów (czyli osobnych układów odpowiednio dla mniejszych i ekstradużych ekranów). Mówiąc w skrócie, powinieneś zapewnić alternatywne układy, aby aplikacja była kompatybilna zarówno z małymi, jak i dużymi ekranami, a także zoptymalizowana pod kątem obu orientacji, czyli poziomej i pionowej. Zapewnienie alternatywnych bitmap — jeśli alternatywne bitmapy nie są
dostarczone, wtedy w zależności od gęstości ekranu urządzenia mogą zostać przeskalowane, przez co mogą wyglądać na rozmyte i nieostre. Zaleca się więc dostarczenie alternatywnych bitmap dla różnych gęstości urządzenia. Oznacza to, że musisz zapewnić bitmapy dla pięciu różnych gęstości (ldpi, mdpi, hdpi, xhdpi oraz xxhdpi), aby zapobiec rozmywaniu się bitmap podczas zmiany rozmiarów. Preferowane rozmiary elementów rysowalnych (ang. drawables) dla różnych gęstości ekranu są następujące: 36×36 dla niskiej gęstości, 48×48 dla średniej gęstości, 72×72 dla wysokiej gęstości, 96×96 dla ekstradużej gęstości, 144×144 dla naprawdę dużych gęstości.
Receptura: zapewnianie obsługi dla różnych wersji platformy Aplikacje tworzone przez programistów powinny być przeznaczone docelowo dla najnowszej wersji interfejsu API. Jednocześnie aplikacje te powinny kontynuować obsługę starszych wersji systemu Android. Pamiętaj, że udział systemu Android 2.x stanowi nadal ponad 50% rynku. Idea jest taka, że aplikacje powinny działać na maksymalnie dużej liczbie urządzeń, a jest to możliwe tylko wtedy, kiedy będą obsługiwać wersje systemu Android o największej bazie użytkowników.
513
514
Rozdział 17. Obsługa małych ekranów
Plik AndroidManifest.xml, poza opisywaniem innych szczegółów Twojej aplikacji, identyfikuje obsługiwane wersje systemu Android. Dokładniej mówiąc, atrybuty minSdkVersion i targetSdkVersion elementu identyfikują najniższy poziom API, z którym kompatybilna jest aplikacja, oraz poziom API, na jakim aplikacja została zaprojektowana, skompilowana i przetestowana. Poniżej zamieszczono przykładowe atrybuty minSdkVersion i targetSdkVersion sugerujące poziomy API wymagane przez aplikację do jej uruchomienia: ...
Atrybut minSdkVersion z powyższego kodu wskazuje, że do uruchomienia aplikacji wymagany jest interfejs API poziomu 4. Oznacza to również, że aplikacja będzie działać na interfejsach API poziomu 4. i wyższych, ale nie uruchomi się na API poziomu 3. lub niższym. Atrybut targetSdkVersion wskazuje, że preferowanym poziomem API, na którym aplikacja została zaprojektowana do uruchamiania, jest poziom 17. Zazwyczaj wartość atrybutu targetSdkVersion jest ustawiana tak, aby odpowiadała najnowszej dostępnej wersji systemu Android. Dla każdej wersji platformy system Android oferuje unikatowy kod wykorzystujący stałe klasy Build. Możesz zastosować te kody, aby upewnić się, że fragmenty kodu zależne od wyższych poziomów API będą wykonywane tylko wtedy, kiedy urządzenie będzie obsługiwać dany poziom API. Przykładowo poniższy kod wywołuje pasek akcji tylko w przypadku, kiedy urządzenie obsługuje interfejs API poziomu 11. (Honeycomb) lub wyższy: private void useActionBar() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { ActionBar actionBar = getActionBar(); . .... } }
Parsując zasoby XML, Android ignoruje atrybuty XML, których nie obsługuje bieżące urządzenie. Możesz więc bezpiecznie używać atrybutów XML obsługiwanych jedynie przez nowsze wersje, bez konieczności przejmowania się starszymi wersjami, ponieważ one po prostu zignorują ten kod. Urządzenia z obsługą interfejsu API poziomu 10. lub niższego zignorują np. poniższy kod służący do wyświetlania elementów akcji w pasku akcji:
Wpisanie takiego kodu w pliku XML przeznaczonym dla różnych wersji jest całkiem bezpieczne, ponieważ starsze wersje systemu Android po prostu zignorują atrybut showAsAction.
Receptura: zapewnianie obsługi dla różnych wersji platformy
W tej recepturze nauczysz się korzystać ze stałych klasy Build w celu uruchamiania jedynie kodu obsługiwanego przez interfejs API poziomu dostępnego w danym urządzeniu. Mówiąc ściślej, receptura będzie służyła identyfikacji poziomu interfejsu API, który jest obsługiwany przez urządzenie. Jeśli urządzenie obsługuje API poziomu 11. lub wyższe, wyświetlone zostaną pasek akcji z kilkoma elementami akcji lub menu. Zanim zaczęto stosować pasek akcji, do obsługi aplikacji wykorzystywano menu z elementami menu. Obecnie korzystniejsze jest stosowanie paska akcji zamiast menu, ponieważ większość dostępnych urządzeń nie posiada przycisku Menu. Utwórz nowy projekt Android o nazwie ActionBarOnOlderApp. Jeśli poziom API w urządzeniu to poziom 10. lub niższy, pasek akcji nie zostanie wyświetlony. Musisz wyświetlić komunikat tekstowy, aby poinformować użytkownika o konieczności wciśnięcia przycisku Menu na urządzeniu w celu wywołania menu. Aby wyświetlić taki komunikat, musisz zdefiniować kontrolkę TextView w pliku układu aktywności. Po jej zdefiniowaniu plik układu aktywności activity_action_bar_on_older_app.xml powinien wyglądać tak, jak plik przedstawiony w listingu 17.1. Listing 17.1. Kod wpisany w pliku układu activity_action_bar_on_older_app.xml
Dla celów uzyskania dostępu oraz identyfikacji w kodzie Java kontrolce TextView przypisano identyfikator textview. Ponadto tekst wyświetlany za pomocą kontrolki TextView będzie prezentowany pogrubioną czcionką o rozmiarze zdefiniowanym w zasobie wymiarów text_size. W tej aplikacji będziesz chciał wyświetlić pasek akcji z dwoma elementami akcji Utwórz i Zaktualizuj. W tym celu w znajdującym się w folderze res/menu pliku activity_action_bar_on_older_app.xml wpisz kod przedstawiony w listingu 17.2. Listing 17.2. Kod wpisany w pliku menu activity_action_bar_on_older_app.xml
515
516
Rozdział 17. Obsługa małych ekranów
android:icon="@drawable/ic_launcher" android:showAsAction="always" />
Elementom akcji Utwórz i Zaktualizuj przypisano odpowiednio identyfikatory create i update. Ponadto dostarczany domyślnie plik obrazka ic_launcher.png został ustawiony jako ikona obu elementów akcji. Element akcji Zaktualizuj ma wyświetlać swoją ikonę zawsze, podczas gdy ikona elementu akcji Utwórz będzie wyświetlana tylko wtedy, jeśli w pasku akcji będzie wystarczająco dużo wolnego miejsca. Teraz musisz napisać kod Java, który będzie wykonywał poniższe zadania. Sprawdzeniu poziomu API obsługiwanego przez urządzenie. Wyświetlenie paska akcji z dwoma elementami akcji, jeśli urządzenie obsługuje
API poziomu 11. (Honeycomb) lub wyższego. Wyświetlenie za pomocą kontrolki TextView tekstu Naciśnij przycisk 'Menu',
aby wyświetlić menu, jeśli urządzenie wyposażono w API poziomu 10.
lub niższego. Zadaniem komunikatu jest poinformowanie użytkownika o konieczności wciśnięcia przycisku Menu na urządzeniu w celu wyświetlenia elementów menu. Aby wykonać te zadania, wpisz w pliku aktywności Java ActionBarOnOlderAppActivity.java kod przedstawiony w listingu 17.3. Listing 17.3. Kod wpisany w pliku aktywności Java ActionBarOnOlderAppActivity.java package com.androidtablet.actionbaronolderapp; import import import import import import
android.os.Bundle; android.app.Activity; android.view.Menu; android.widget.TextView; android.os.Build; android.app.ActionBar;
public class ActionBarOnOlderAppActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_action_bar_on_older_app); TextView textView=(TextView)findViewById(R.id.textview); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { ActionBar actionBar = getActionBar(); actionBar.setDisplayHomeAsUpEnabled(true); } else textView.setText("Naciśnij przycisk 'Menu', aby wyświetlić menu."); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.activity_action_bar_on_older_app, menu);
Receptura: zapewnianie obsługi dla różnych wersji platformy return true; } }
Aby przetestować aplikację i zobaczyć rezultat jej działania, ustaw poziomy API, które są wymagane do jej uruchomienia. Najpierw sprawdź aplikację, kiedy minimalny poziom API wymagamy do jej uruchomienia wynosi 11, a docelowy poziom API to 17. W tym celu zmodyfikuj atrybuty minSdkVersion i targetSdkVersion elementu w pliku AndroidManifest.xml w następujący sposób:
Ponieważ w interfejsach API poziomu 11. i wyższego pasek akcji jest dostępny, po uruchomieniu aplikacji zostanie wyświetlony z dwoma elementami akcji Utwórz i Zaktualizuj (patrz rysunek 17.1, lewy obrazek). Jeśli zmniejszysz wartość minimalnego poziomu API wymaganego przez aplikację (czyli zmienisz wartość atrybutu minSdkVersion na 10 lub mniej), pasek akcji nie zostanie wyświetlony. Zamiast niego, za pomocą kontrolki TextView wyświetlony zostanie komunikat Naciśnij przycisk 'Menu', aby wyświetlić menu. Po wciśnięciu przycisku Menu na urządzeniu wyświetlone zostaną dwa elementy menu Utwórz i Zaktualizuj, tak jak pokazano na rysunku 17.1 (prawy obrazek).
Rysunek 17.1. Pasek akcji wyświetlony z dwoma elementami akcji (lewy obrazek) oraz menu z dwoma elementami menu wyświetlone po wciśnięciu przycisku Menu na urządzeniu (prawy obrazek)
517
518
Rozdział 17. Obsługa małych ekranów
Receptura: wykorzystanie pakietu Android Support Library do zapewnienia obsługi starszych wersji systemu Aby zapewnić funkcjonalność dla kilku wersji systemu Android, należy zastosować w aplikacji pakiet Android Support Library, który umożliwia wykorzystanie interfejsów API najnowszych platform na ich starszych wersjach. Pakiet ten zawiera biblioteki, które mogą być dodane do aplikacji Android, aby można było skorzystać z interfejsów API niedostępnych na starszych wersjach platformy. Pakiet Android Support Library składa się z kilku bibliotek, a każda z nich wymaga określonego minimalnego poziomu API. Jedna biblioteka może wymagać np. API poziomu 4. lub wyższego, podczas gdy inna wymaga API poziomu 13. lub wyższego. Poniższa biblioteka wymaga co najmniej API poziomu 4.: import android.support.v4.app.Fragment;
Parametr v4 wskazuje, że biblioteka wymaga co najmniej API poziomu 4. Analogicznie, biblioteka zawierająca parametr v13 będzie wymagała co najmniej API poziomu 13. Pakiet Android Support Library umożliwia programistom wykorzystanie nowych funkcji systemu Android nawet wtedy, kiedy aplikacje uruchamiane są na starszych wersjach platformy. Nie możesz np. użyć fragmentów w aplikacji, która jest oparta na interfejsie API poziomu 10. lub niższego, ponieważ fragmenty są dostępne dla API poziomu 11. i wyższego. Aby w takiej aplikacji zastosować fragmenty i mieć pewność, że aplikacja ta nadal będzie obsługiwać API poziomu 10., musisz użyć pakietu Android Support Library. Tej procedury nauczysz się na przykładzie. Utwórz projekt Android o nazwie FragmentOnOlderApp. W aplikacji będziesz początkowo ustawiał wymagania dla minimalnego API aplikacji na poziomie 11. i wykorzystywał fragmenty. Po uruchomieniu fragmentów zmniejsz wymagania dla minimalnego API aplikacji do poziomu 10. Ponieważ fragmenty wymagają do uruchomienia interfejsu API poziomu 11. lub wyższego, aplikacja nie zostanie uruchomiona i wyświetlą się komunikaty o błędach. Dodasz zatem do aplikacji pakiet Android Support Library i sprawisz, że fragmenty będą uruchamiane również na API poziomu 10. Aby wyświetlić fragment, w pliku układu aktywności zdefiniujesz układ FrameLayout, który będzie pełnił funkcję kontenera fragmentu. Po zagnieżdżeniu kontenera FrameLayout wewnątrz kontenera LinearLayout plik układu aktywności activity_fragment_on_older_app.xml powinien wyglądać tak, jak przedstawiono w listingu 17.4. Listing 17.4. Kod wpisany w pliku układu aktywności activity_fragment_on_older_app
Receptura: wykorzystanie pakietu Android Support Library android:layout_height="match_parent" android:orientation="vertical" >
Dla celów uzyskania dostępu w kodzie Java kontenerowi FrameLayout przydzielono identyfikator fragment_container. Aby zdefiniować układ fragmentu, dodaj do folderu res/layout plik XML o nazwie fragment.xml. Żeby nieco uprościć przykład, za pomocą tego fragmentu wyświetlany będzie jedynie komunikat tekstowy. W tym celu w pliku układu fragment.xml zdefiniuj kontrolkę TextView w sposób przedstawiony w listingu 17.5. Listing 17.5. Kod wpisany w pliku układu fragment.xml
Jak widzisz, kontrolce TextView przypisano identyfikator textview. Kontrolka jest inicjowana w celu wyświetlenia komunikatu tekstowego To jest fragment. Tekst wyświetlany za pomocą kontrolki TextView ma być prezentowany pogrubioną czcionką o rozmiarze zdefiniowanym w zasobie wymiarów text_size. Aby załadować widok (lub widoki) fragmentu zdefiniowanego w pliku układu fragment.xml, potrzebujesz pliku klasy Java. Dodaj plik klasy Java o nazwie MyFragmentActivity.java do paczki com.androidtablet.fragmentonolderapp aplikacji. Aby załadować i wyświetlić kontrolkę TextView, którą zdefiniowałeś w pliku układu fragment.xml, wpisz w pliku klasy Java MyFragmentActivity.java kod przedstawiony w listingu 17.6. Listing 17.6. Kod wpisany w pliku klasy Java MyFragmentActivity.java package com.androidtablet.fragmentonolderapp; import import import import import
android.view.LayoutInflater; android.view.ViewGroup; android.view.View; android.os.Bundle; android.app.Fragment;
519
520
Rozdział 17. Obsługa małych ekranów public class MyFragmentActivity extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment, container, false); } }
Następnie musisz napisać kod Java, który będzie wykonywał poniższe zadania. Uzyskanie dostępu do kontenera fragmentu (czyli FrameLayout) zdefiniowanego
w pliku układu aktywności activity_fragment_on_older_app.xml. Zdefiniowanie fragmentu przez instancjonowanie pliku klasy Java
MyFragmentActivity. Wyświetlenie danego fragmentu przez dodanie go do kontenera fragmentu.
Aby wykonać wymienione zadania, wpisz w pliku aktywności Java FragmentOnOlderAppActivity.java kod przedstawiony w listingu 17.7. Listing 17.7. Kod wpisany w pliku aktywności Java FragmentOnOlderAppActivity.java package com.androidtablet.fragmentonolderapp; import import import import
android.os.Bundle; android.app.Activity; android.app.FragmentTransaction; android.app.FragmentManager;
public class FragmentOnOlderAppActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_fragment_on_older_app); FragmentManager fragmentManager = getFragmentManager(); FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); if(null==fragmentManager.findFragmentById(( R.id.fragment_container))){ MyFragmentActivity fragment = new MyFragmentActivity(); fragmentTransaction.add(R.id.fragment_container, fragment); } fragmentTransaction.commit(); } }
Nie zapomnij ustawić minimalnego poziomu API aplikacji na 11 oraz docelowego poziomu API na 17. Upewnij się, że atrybuty minSdkVersion i targetSdkVersion elementu w pliku AndroidManifest.xml zostały zmodyfikowane w następujący sposób:
Receptura: wykorzystanie pakietu Android Support Library
Ponieważ fragmenty działają na API poziomu 11. i wyższego, aplikacja będzie działać bez zarzutu. Fragment pokaże komunikat tekstowy To jest fragment, wyświetlony za pomocą kontrolki TextView zdefiniowanej w układzie fragmentu (patrz rysunek 17.2, górny obrazek). Teraz zmniejsz minimalny poziom API do wartości 10. W tym celu zmodyfikuj wartość atrybutu minSdkVersion elementu w pliku AndroidManifest.xml w następujący sposób:
Ponieważ fragmenty działają tylko na API poziomu 11. lub wyższego, aplikacja nie zostanie uruchomiona i pojawią się błędy w instrukcjach, które wywołują i wykorzystują fragmenty. Na rysunku 17.2 (dolny obrazek) przedstawiono okienko edytora wyświetlające błędy z komunikatem Call requires API level 11 (current min is 10) (Wywołanie wymaga interfejsu API poziomu 11 (bieżący poziom minimalny jest ustawiony na 10)). Aby fragmenty były uruchamiane w interfejsach API poziomu niższego niż 11., musisz dodać do aplikacji pakiet Android Support Library. W celu dodania tego pakietu do istniejącego kodu wykonaj następujące czynności. 1. Uruchom środowisko Eclipse i wywołaj okno Android SDK Manager
(menedżer pakietów Android SDK). Przejdź w tym oknie do pozycji Extras (dodatki), zaznacz pakiet Android Support Library i zainstaluj go. Pakiet wraz ze źródłami i przykładami zostanie zainstalowany na Twojej maszynie. 2. Aby dodać pakiet Android Support Library do projektu, kliknij w Eclipse
prawym przyciskiem myszy wybrany projekt i wybierz z wyświetlonej listy opcję Android Tools (narzędzia Android)/Add Support Library (dodaj pakiet Support Library). Pojawi się okno dialogowe Choose Packages to Install (wybierz pakiety do zainstalowania), które pokazano na rysunku 17.3. Zaznacz opcję Accept (akceptuję), a następnie kliknij przycisk Install (instaluj), aby dodać pakiet Android Support Library do Twojej aplikacji. Po dodaniu pakietu Android Support Library do aplikacji musisz do pliku klasy Java MyFragmentActivity.java oraz do pliku aktywności Java FragmentOnOlderAppActivity.java zaimportować klasy pakietu wymagane do obsługi starszych poziomów API. Zmodyfikuj plik klasy Java MyFragmentActivity.java w sposób przedstawiony w listingu 17.8. Zmienione zostały jedynie fragmenty kodu zaznaczone pogrubioną czcionką. Reszta kodu pozostaje taka sama jak w listingu 17.6.
521
522
Rozdział 17. Obsługa małych ekranów
Rysunek 17.2. Fragment wykonany, kiedy wartość atrybutu minSdkVersion wynosi 11 (górny obrazek). Błędy wyświetlone, kiedy wartość atrybutu minSdkVersion wskazuje interfejs API poziomu 10. (dolny obrazek) Listing 17.8. Kod wpisany w pliku klasy Java MyFragmentActivity.java package com.androidtablet.fragmentonolderapp; import import import import import
android.view.LayoutInflater; android.view.ViewGroup; android.view.View; android.os.Bundle; android.support.v4.app.Fragment;
Receptura: wykorzystanie pakietu Android Support Library
Rysunek 17.3. Okno dialogowe, na którym przed zainstalowaniem wybranego pakietu wyświetlają się warunki licencyjne i inne informacje public class MyFragmentActivity extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment, container, false); } }
Jak widzisz, w celu zapewnienia obsługi starszych wersji systemu Android do powyższego pliku klasy Java zaimportowana została klasa android.support.v4.app.Fragment z pakietu Android Support Library. Zmodyfikować należy także plik aktywności Java FragmentOnOlderAppActivity.java w sposób przedstawiony w listingu 17.9. Zmienione zostały jedynie fragmenty kodu zaznaczone pogrubioną czcionką. Reszta kodu pozostaje taka sama jak w listingu 17.7. Listing 17.9. Kod wpisany w pliku aktywności Java FragmentOnOlderAppActivity.java package com.androidtablet.fragmentonolderapp; import import import import
android.os.Bundle; android.support.v4.app.FragmentManager; android.support.v4.app.FragmentTransaction; android.support.v4.app.FragmentActivity;
public class FragmentOnOlderAppActivity extends FragmentActivity{ @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_fragment_on_older_app); FragmentManager fragmentManager = getSupportFragmentManager(); FragmentTransaction fragmentTransaction =
523
524
Rozdział 17. Obsługa małych ekranów fragmentManager.beginTransaction(); if(null==fragmentManager.findFragmentById(( R.id.fragment_container))){ MyFragmentActivity fragment = new MyFragmentActivity(); fragmentTransaction.add(R.id.fragment_container, fragment); } fragmentTransaction.commit(); } }
Jak widzisz, klasa w powyższym kodzie rozszerza zamiast aktywności klasę FragmentActivity, a do aplikacji zaimportowano kilka klas obsługi, w tym android.support.v4.app.FragmentActivity. Ponieważ klasa FragmentManager nie jest
kompatybilna z interfejsem API poziomu 10. i niższych, pakiet Android Support Library zawiera klasę pośredniczącą SupportFragmentManager, która zapewnia zgodność wsteczną. Każda aktywność FragmentActivity posiada obiekt SupportFragmentManager, a metoda getSupportFragmentManager() jest wywoływana w powyższym kodzie w celu zastosowania klasy SupportFragmentManager zamiast FragmentManager. Teraz Twoja aplikacja jest gotowa do uruchomienia bez błędów w interfejsie API poziomu 10. lub niższego. Po jej uruchomieniu wyświetlony zostanie fragment. Na ekranie pojawi się komunikat tekstowy, taki jak na rysunku 17.2 (górny obrazek), co potwierdza prawidłowe działanie fragmentu.
Uwaga Każda aktywność wykorzystująca fragmenty musi dziedziczyć po klasie android.support.v4.app.FragmentActivity. Klasa ta jest częścią pakietu Support Library i umożliwia uruchamianie fragmentów bez względu na wersję systemu Android. R
Receptura: dostosowywanie aplikacji do orientacji ekranu za pomocą kotwiczenia kontrolek System Android, tak jak niemal wszystkie smartfony, obsługuje dwie orientacje ekranu — pionową i poziomą. Kiedy zmienia się orientacja urządzenia Android, bieżąca wyświetlana aktywność jest niszczona i odtwarzana automatycznie w celu ponownego jej narysowania w nowej orientacji. Tryb pionowy ma większą wysokość i mniejszą szerokość, podczas gdy tryb poziomy ma większą szerokość, ale mniejszą wysokość. Z uwagi na swoją szerokość orientacja pozioma ma więcej wolnej przestrzeni po prawej stronie ekranu. Jednocześnie niektóre kontrolki znikają z powodu mniejszej wysokości. Dlatego kontrolki interfejsu użytkownika muszą być za każdym razem ułożone inaczej w celu dostosowania ich do obu orientacji ekranu, z uwagi na różnice w wysokości i szerokości tych orientacji.
Receptura: dostosowywanie aplikacji do orientacji ekranu
Istnieją dwa sposoby obsługi zmiany orientacji ekranu. Kotwiczenie kontrolek — polega na ustalaniu położenia kontrolek względem
czterech brzegów ekranu. Kiedy zmienia się orientacja ekranu, kontrolki nie znikają, lecz są ponownie rozmieszczane względem czterech brzegów. Zdefiniowanie układu dla każdej orientacji — dla każdej orientacji ekranu
definiowany jest osobny plik układu. W jednym układzie kontrolki rozmieszczone są odpowiednio dla orientacji pionowej, a w drugim — dla poziomej. W tej recepturze nauczysz się obsługiwać orientację ekranu za pomocą kotwiczenia kontrolek. Aby zakotwiczyć kontrolki względem czterech brzegów ekranu, musisz skorzystać z kontenera RelativeLayout. Sprawdzisz tę metodę, tworząc nowy projekt Android o nazwie HandleOrientationApp. Aby rozmieścić kontrolki względem czterech brzegów ekranu, wpisz w pliku układu activity_handle_orientation_app.xml kod przedstawiony w listingu 17.10. Listing 17.10. Plik układu activity_handle_orientation_app.xml służący do rozmieszczenia kontrolek względem czterech brzegów ekranu
525
526
Rozdział 17. Obsługa małych ekranów
Kod z listingu 17.10 pokazuje rozmieszczenie pięciu kontrolek Button w kontenerze RelativeLayout. Kontrolki zostały wyrównane względem brzegów kontenera lub względem siebie. Plik aktywności HandleOrientationAppActivity.java pozostaje bez zmian z domyślnym kodem, tak jak pokazano w listingu 17.11. Listing 17.11. Domyślny kod w pliku aktywności Java HandleOrientationAppActivity.java package com.androidtablet.handleorientationapp; import android.app.Activity; import android.os.Bundle; public class HandleOrientationAppActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_handle_ orientation_app); } }
Kiedy aplikacja zostanie uruchomiona w domyślnej orientacji poziomej, kontrolki będą rozmieszczone tak, jak przedstawiono na rysunku 17.4 (górny obrazek). Ponieważ wszystkie kontrolki zostały rozmieszczone względem czterech brzegów ekranu oraz względem siebie, żadna z nich nie zniknie, kiedy ekran zostanie obrócony do orientacji pionowej, co pokazano na rysunku 17.4 (dolny obrazek). Aby przełączyć się pomiędzy trybem pionowym i poziomym w emulatorze urządzenia, wciśnij kombinację klawiszy CTRL+F11. Czytając rozdział 1., „Przegląd aplikacji na tablety z systemem Android”, dowiedziałeś się, że dla tabletów domyślna jest orientacja pozioma.
Receptura: dostosowywanie aplikacji do orientacji ekranu
Rysunek 17.4. Kontrolki rozmieszczone w trybie poziomym (górny obrazek) oraz kontrolki rozmieszczone w trybie pionowym (dolny obrazek)
Skoro zapoznałeś się już z koncepcją dostosowywania aplikacji do orientacji ekranu za pomocą kotwiczenia kontrolek, zajmijmy się kolejną metodą, czyli definiowaniem alternatywnych układów.
527
528
Rozdział 17. Obsługa małych ekranów
Receptura: obsługa orientacji ekranu przy użyciu alternatywnych układów W tej metodzie definiowane są dwa układy. Jeden rozmieszcza kontrolki interfejsu użytkownika odpowiednio dla orientacji pionowej urządzenia, a drugi — dla orientacji poziomej. Aby zrozumieć działanie tej metody, utwórz nową aplikację Android o nazwie AlternateLayoutApp. W folderze res aplikacji utwórz dwa foldery o nazwach layout-sw600dp i layout-sw720dp. Foldery te będą przechowywać zasoby układów orientacji poziomej dla 7-calowych i 10-calowych tabletów. Plik układu aktywności activity_alternate_layout_app.xml z folderu res/layout skopiuj do folderów layout-sw600dp i layout-sw720dp. Aby zorganizować kontrolki interfejsu użytkownika dla orientacji poziomej, wpisz w pliku układu aktywności activity_alternate_layout_app.xml (znajdującym się w folderach layout-sw600dp i layout-sw720dp) kod przedstawiony w listingu 17.12. Listing 17.12. Plik układu activity_alternate_layout_app.xml służący do rozmieszczenia kontrolek w orientacji poziomej
Receptura: obsługa orientacji ekranu przy użyciu alternatywnych układów android:text="Smartfon" android:textSize="@dimen/text_size" android:layout_width="400dip" android:layout_height="wrap_content" android:padding="40dip" android:layout_marginTop="40dip" android:layout_marginLeft="150dip" android:layout_below="@id/camera" android:layout_toRightOf="@id/watch" />
Jak widzisz, w powyższym kodzie pięć kontrolek Button zostało rozmieszczonych rzędami w kontenerze RelativeLayout. Ten poziomy układ powoduje, że po przełączeniu ekranu do orientacji pionowej kilka kontrolek zostaje częściowo ukrytych. Jeśli uruchomisz aplikację bez zdefiniowania układu dla orientacji pionowej, zastosowane zostanie rozmieszczenie kontrolek z orientacji poziomej, co pokazano na rysunku 17.5 (górny obrazek). Jeśli jednak przełączysz ekran do orientacji pionowej, zobaczysz, że kilka kontrolek Button po prawej stronie ekranu jest częściowo ukrytych, tak jak pokazano na rysunku 17.5 (dolny obrazek). Jest to spowodowane tym, że w trybie pionowym ekran staje się wyższy, ale zmniejsza się jego szerokość. W celu zdefiniowania układu orientacji pionowej dla tabletów w folderze res aplikacji utwórz dwa foldery o nazwach layout-sw600dp-port oraz layout-sw720dp-port. Skopiuj plik układu aktywności activity_alternate_layout_app.xml z folderu res/layout do nowo utworzonych folderów layout-sw600dp-port i layout-sw720dp-port. Zrozumiałe jest, że plik układu z folderów layout-sw600dp-port i layout-sw720dp zostanie użyty do rozmieszczenia kontrolek interfejsu użytkownika dla orientacji pionowej 7- i 10-calowych tabletów.
Uwaga Przygotowując aplikacje na telefony, dla rozmieszczenia kontrolek interfejsu użytkownika w orientacji pionowej telefonu wykorzystujesz plik układu aktywności znajdujący się w folderze res/layout. Dla rozmieszczenia kontrolek w orientacji poziomej stosujesz plik układu z folderu res/layout-land.
Aby wykorzystać dodatkową przestrzeń u dołu ekranu, kiedy tablet zostaje przełączony do orientacji pionowej, w plikach układu activity_alternate_layout_app.xml znajdujących się w folderach res/layout-sw600dp-port oraz res/layout-sw720dp-port wpisz kod przedstawiony w listingu 17.13.
529
530
Rozdział 17. Obsługa małych ekranów
Rysunek 17.5. Układ kontrolek, kiedy urządzenie znajduje się w orientacji poziomej (górny obrazek). Niektóre kontrolki zostają częściowo ukryte, kiedy urządzenie zostaje przełączone do orientacji pionowej (dolny obrazek) Listing 17.13. Plik układu activity_alternate_layout_app.xml w folderach res/layout-sw600dp-port i res/layout-sw720dp-port
Receptura: obsługa orientacji ekranu przy użyciu alternatywnych układów android:text="Aparat" android:textSize="@dimen/text_size" android:layout_width="400dp" android:layout_height="wrap_content" android:padding="40dip" android:layout_marginTop="40dip" android:layout_marginLeft="50dip" />
W tym kodzie, w celu wypełnienia pustej przestrzeni u dołu ekranu wszystkie kontrolki Button zostały zorganizowane pionowo, jedna pod drugą. Orientację ekranu możesz wykryć, stosując kod Java. Zmodyfikuj plik aktywności AlternateLayoutAppActivity.java w taki sposób, aby wyświetlał komunikat, kiedy urządzenie przełącza się pomiędzy orientacjami poziomą i pionową. Kod, który należy wpisać w pliku aktywności Java AlternateLayoutAppActivity.java, został zamieszczony w listingu 17.14.
531
532
Rozdział 17. Obsługa małych ekranów Listing 17.14. Kod wpisany w pliku aktywności Java package com.androidtablet.alternatelayoutapp; import android.app.Activity; import android.os.Bundle; import android.widget.Toast; public class AlternateLayoutAppActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_alternate_ layout_app); if(getResources().getDisplayMetrics().widthPixels > getResources().getDisplayMetrics().heightPixels) { Toast.makeText(this,"Ekran został przełączony do orientacji poziomej",Toast.LENGTH_SHORT).show(); } else Toast.makeText(this,"Ekran został przełączony do orientacji pionowej",Toast.LENGTH_SHORT).show(); } }
Kiedy teraz uruchomisz aplikację, kontrolki Button w orientacji poziomej będą rozmieszczone tak, jak pokazano na rysunku 17.6 (górny obrazek), a w orientacji pionowej tak, jak pokazano na rysunku 17.6 (dolny obrazek). Jak widać, żadna z kontrolek Button w trybie pionowym nie została ukryta.
Podsumowanie W tym rozdziale poznałeś czynniki wpływające na sposób implementacji w aplikacjach Android obsługi różnych rozmiarów i gęstości ekranów. Nauczyłeś się stosować procedurę obsługi w aplikacjach różnych wersji systemu Android. Zapoznałeś się także z pakietem Android Support Library i dowiedziałeś się, w jaki sposób wykorzystywać go do obsługi starszych wersji Androida. Ponadto zobaczyłeś, jak orientacja ekranu jest obsługiwana za pomocą kotwiczenia kontrolek. Na koniec nauczyłeś się wykorzystywać alternatywne układy do obsługi różnych rozmiarów ekranów. W kolejnym rozdziale nauczysz się tworzyć i wykorzystywać widżety ekranu głównego. Ponadto dowiesz się, jak monitorować wykorzystanie zasobów. Na koniec poznasz klasę RemoteViewsService i zobaczysz, w jaki sposób widoki zdalne są kontrolowane i wywoływane przez menedżery usług i alarmów.
Podsumowanie
Rysunek 17.6. Kontrolki w orientacji poziomej (górny obrazek). Wszystkie kontrolki w pełni widoczne w orientacji pionowej (dolny obrazek)
533
534
Rozdział 17. Obsługa małych ekranów
18 Widżety ekranu głównego R
ozdział został poświęcony omówieniu roli widżetów aplikacji oraz rodzajów wyświetlanych przez nie danych i sposobów ich aktualizacji. Widżety aplikacji (ang. app widgets) to poręcznie, niewielkie aplikacje wykorzystywane przez użytkowników do wyświetlania informacji. Dowiesz się, w jaki sposób widżety są osadzane w aplikacjach ekranu głównego, stając się w ten sposób widżetami ekranu głównego. Zapoznasz się z metodami cyklu życia widżetów aplikacji oraz z klasą BroadcastReceiver, która implementuje te metody. Zobaczysz również, jak widżety ekranu głównego są aktualizowane za pomocą plików konfiguracyjnych XML, nasłuchiwaczy zdarzeń oraz usługi AlarmManager.
Receptura: widżety aplikacji oraz widżety ekranu głównego Widżety aplikacji to niewielkie komponenty wizualne, które mogą być osadzane w innych aplikacjach. Aplikacje z osadzonymi lub umieszczonymi w nich widżetami zwane są hostami widżetów aplikacji (ang. app widget hosts). Najbardziej typowym przykładem hosta widżetu aplikacji jest aplikacja ekranu głównego. Widżety aplikacji umieszczane na ekranie głównym urządzenia Android w celu uzyskania szybkiego dostępu zwane są widżetami ekranu głównego (ang. home screen widgets). Niektóre z cech widżetów ekranu głównego lub widżetów aplikacji zostały opisane poniżej.
Uwaga Pojęcia widżety aplikacji i widżety ekranu głównego są stosowane zamiennie, ponieważ w tym rozdziale oba odnoszą się do tego samego komponentu. Widżety ekranu głównego są niewielkimi komponentami wizualnymi lub
elementami, które są wyświetlane na ekranie głównym urządzenia. Widżety ekranu głównego są wykorzystywane do wyświetlania informacji,
które użytkownik chce mieć na wyciągnięcie ręki. Za pomocą tych widżetów wyświetlane są np. informacje z rozkładem lotów, repertuarem kin, pogodą itd.
536
Rozdział 18. Widżety ekranu głównego
Widżety ekranu głównego wyświetlają zazwyczaj informacje, które nie
wymagają zbyt częstej aktualizacji. Częsta aktualizacja widżetów może skutkować wysokim zużyciem energii baterii. Widżety ekranu głównego korzystają z klasy RemoteViews do wyświetlania
informacji oraz przy interakcji z użytkownikiem. Jak wskazuje nazwa, RemoteViews to widoki zarządzane przez zdalne procesy inne niż dana aplikacja. Proces zdalny reprezentuje tutaj hosta widżetu aplikacji (aplikację ekranu głównego), który zarządza osadzonymi widżetami ekranu głównego. Zawartość widżetów ekranu głównego (RemoteViews) to odłączone widoki,
które są aktualizowane periodycznie przez procesy działające w tle. Dwie preferowane metody aktualizacji widżetów ekranu głównego to wykorzystanie pliku konfiguracyjnego XML oraz usługi AlarmManager systemu Android. Klasa BroadcastReceiver definiuje interfejs użytkownika dla widżetów ekranu
głównego. Odbiornik ten rozszerza klasę AppWidgetProvider w celu zapewnienia obsługi cyklu życia widżetów aplikacji. Aby zaktualizować informacje wyświetlane za pomocą widoków RemoteViews w widżecie ekranu głównego, widżet rozgłasza komunikaty do obiektu BroadcastReceiver, dzięki czemu mogą być podjęte niezbędne akcje. Zadaniem obiektu BroadcastReceiver jest inicjowanie, aktualizowanie, a nawet usuwanie widżetów ekranu głównego. Aby zobaczyć, jakie widżety aplikacji są dostępne w Twoim urządzeniu Android, wykonaj następujące czynności. 1. W urządzeniu z systemem Android w wersji 3.x lub niższej, przyciśnij
i przytrzymaj dowolne puste miejsce na ekranie głównym, aby wyświetlić menu (patrz rysunek 18.1, lewy obrazek). Lista dostępnych widżetów aplikacji zostanie wyświetlona po wybraniu z menu pozycji Widżety (patrz rysunek 18.1, prawy obrazek). 2. W urządzeniach z systemem Ice Cream Sandwich (Android 4.0 lub wyższe
wersje) widżety aplikacji wyświetlane są jako osobna kategoria. Otwórz listę aplikacji dostępnych w urządzeniu. U góry ekranu widoczne będą dwie zakładki: Aplikacje i Widżety. Po wybraniu zakładki Widżety wyświetlona zostanie lista dostępnych widżetów aplikacji, co pokazano na rysunku 18.2. 3. Po wybraniu z listy konkretny widżet aplikacji zostanie umieszczony na
ekranie głównym. Widżet na ekranie głównym jest właściwie instancją widżetu aplikacji. Aby usunąć instancję widżetu aplikacji, przyciśnij go na ekranie głównym i rozpocznij operację przeciągania. Po rozpoczęciu przeciągania instancji widżetu aplikacji u góry ekranu głównego pojawi się kosz. Przeciągnij do kosza instancję widżetu aplikacji, aby usunąć go z ekranu głównego.
Receptura: widżety aplikacji oraz widżety ekranu głównego
Rysunek 18.1. Menu wyświetlone po przyciśnięciu i przytrzymaniu dowolnego pustego miejsca na ekranie głównym urządzenia (lewy obrazek). Lista widżetów wyświetlona po wybraniu z menu opcji Widżety (prawy obrazek)
Rysunek 18.2. Lista widżetów wyświetlona po wybraniu zakładki Widżety widocznej u góry ekranu
537
538
Rozdział 18. Widżety ekranu głównego
Receptura: metody cyklu życia widżetu aplikacji Klasa BroadcastReceiver odgrywa ważną rolę w zarządzaniu widżetami aplikacji. Nie tylko definiuje interfejs użytkownika dla widżetu ekranu głównego, ale również nasłuchuje komunikatów rozgłaszanych w celu zaktualizowania tych widżetów. Klasa BroadcastReceiver rozszerza klasę AppWidgetProvider, by zapewnić obsługę cyklu życia widżetów aplikacji. Metody cyklu życia widżetu aplikacji implementowane przez klasę BroadcastReceiver zostały w skrócie opisane w tabeli 18.1. Tabela 18.1. Krótki opis metod cyklu życia widżetu aplikacji
Metoda
Opis
onEnabled() metoda wywołania zwrotnego
Wywoływana, kiedy tworzona jest instancja pierwszego widżetu. Metoda potwierdza, że utworzona została co najmniej jedna instancja widżetu. W tej metodzie zapisywane są instrukcje inicjowania widżetu.
onDeleted() metoda wywołania zwrotnego
Wywoływana za każdym razem, kiedy widżet jest usuwany (czyli za każdym razem, kiedy instancja widżetu jest przeciągana do kosza). Jeśli stan instancji widżetu jest zapisywany w aplikacji, może być usunięty w tej metodzie.
onDisabled() metoda wywołania zwrotnego
Wywoływana po tym, jak ostatnia instancja widżetu zostanie usunięta z ekranu głównego. Ma to miejsce, kiedy użytkownik przeciągnie ostatnią instancję widżetu do kosza. Metoda może być użyta do zatrzymania odbierania rozgłaszanych komunikatów, zamykania baz danych lub przeprowadzania działań czyszczących.
onUpdate() metoda wywołania zwrotnego
Wywoływana za każdym razem po upływie określonego czasu oraz kiedy instancja widżetu jest tworzona po raz pierwszy, jeśli nie ma żadnej aktywności konfiguracyjnej. Metoda aktualizuje zawartość widżetu. Jeśli istnieje aktywność konfiguracyjna, metoda nie jest wywoływana przy tworzeniu instancji widżetu, ale jest wywoływana po upływie określonego czasu. Metoda zawiera identyfikatory tablicy appWidgetIds, dla których wymagana jest aktualizacja. Jeśli więcej niż jeden widżet został dodany do ekranu głównego, domyślnie aktualizowany jest tylko ostatni z nich.
onReceive()
Obsługuje akcje obiektu BroadcastReceiver i rozdziela żądania do powyższych metod. Metoda jest wywoływana dla każdego rozgłaszania.
Receptura: tworzenie widżetów ekranu głównego
Receptura: tworzenie widżetów ekranu głównego W tej recepturze utworzysz widżet ekranu głównego, który będzie wyświetlał bieżącą datę i aktualny czas. Poniżej opisano procedurę tworzenia widżetu ekranu głównego. 1. Zdefiniuj plik zasobów układu zawierający definicje widoków, które mają być
wyświetlane w widżecie ekranu głównego. Widżet ekranu głównego wymaga dwóch plików XML — jednego do zdefiniowania układu widoków, które będą wyświetlane w widżecie ekranu głównego, a drugiego do skonfigurowania tego widżetu. 2. Zdefiniuj plik XML do skonfigurowania widżetu. Może on być nazywany
plikiem konfiguracyjnym widżetu lub definicji widżetu. Plik informuje o minimalnym obszarze wymaganym przez widżet na ekranie głównym, interwale czasowym, w jakim widżet powinien być aktualizowany itd. 3. Utwórz odbiornik BroadcastReceiver w celu zbudowania interfejsu
użytkownika dla widżetu. Odbiornik rozszerza klasę AppWidgetProvider i jest odpowiedzialny za inicjowanie widoków widżetu, kiedy jego instancja jest przeciągana na ekran główny. Ta klasa Java również aktualizuje widżet zgodnie z interwałem czasowym ustalonym w pliku konfiguracyjnym XML lub kiedy pojawia się określone zdarzenie. Klasa ta nawet usuwa widżet, kiedy jego instancja zostaje przeciągnięta z ekranu głównego do kosza. W rzeczywistości ta klasa Java zarządza całym cyklem życia widżetu aplikacji. 4. Zarejestruj klasę BroadcastReceiver w pliku AndroidManifest.xml. Pamiętaj,
że klasa BroadcastReceiver inicjuje, aktualizuje, a nawet usuwa widżet.
Uwaga Do ekranu głównego możesz dodawać po kilka instancji danego widżetu głównego.
Aby na działającym przykładzie zrozumieć działanie opisanej procedury, utwórz nowy projekt Android o nazwie TimeHomeWidgetApp. Pierwszym krokiem będzie zdefiniowanie widoków widżetu ekranu głównego. Ponieważ w tym widżecie chcesz wyświetlać bieżącą datę i aktualny czas, potrzebna będzie jedynie kontrolka TextView. Po jej zdefiniowaniu plik aktywności activity_time_home_widget_app.xml będzie wyglądał tak, jak przedstawiono w listingu 18.1. Listing 18.1. Kod wpisany w pliku układu aktywności activity_time_home_widget_app.xml
539
540
Rozdział 18. Widżety ekranu głównego
Dla celów uzyskania dostępu oraz identyfikacji w kodzie Java kontrolce TextView przypisano identyfikator time_textview. Tekst wyświetlany za pomocą kontrolki TextView będzie prezentowany pogrubioną białą czcionką o rozmiarze zdefiniowanym w zasobie wymiarów text_size i umieszczony w centralnym obszarze widżetu. Kontrolka TextView jest inicjowana do wyświetlania tekstu 00:00:00. Aby zdefiniować plik konfiguracyjny XML dla widżetu, w folderze res utwórz folder o nazwie xml. Do folderu res/xml dodaj plik XML o nazwie timewidgetproviderinfo.xml. Wpisz w nim definicję widżetu. Definicja widżetu składa się z określonych atrybutów zawartych w pojedynczym elemencie . W definicji widżetu określane są następujące rzeczy. Minimalna szerokość i wysokość widżetu (czyli minimalny obszar, którego
widżet będzie wymagał na ekranie głównym). Rozmiar widżetu jest definiowany w jednostkach dp i zazwyczaj jest wielokrotnością rozmiaru jednej komórki na pulpicie (czyli 74×74 dp). Zalecany rozmiar widżetu jest określany następującą formułą: ((liczba kolumn lub wierszy)×74)–2 W tym przypadku –2 oznacza pozostawienie pewnej przestrzeni na marginesy. Przykładowo minimalna szerokość widżetu ekranu głównego, który jest szeroki na dwie komórki, będzie wynosić (2×74)–2, czyli 146 dp. Analogicznie minimalna wysokość widżetu, który jest wysoki na trzy komórki, będzie wynosić (3×74)–2, czyli 220 dp. Jednak w systemie Android 4.0 i w nowszych wersjach marginesy są ustawiane automatycznie. Dlatego formułę na obliczanie minimalnego rozmiaru widżetu należy zmodyfikować w następujący sposób: ((liczba kolumn lub wierszy)×70)–30 Według nowej formuły minimalna szerokość widżetu, który jest szeroki na dwie komórki, będzie wynosić (2×70)–30, czyli 110 dp. Analogicznie minimalna wysokość widżetu, który jest wysoki na trzy komórki, będzie wynosić (3×70)–30, czyli 180 dp.
Receptura: tworzenie widżetów ekranu głównego
Interwał czasowy, według którego widżet ma być aktualizowany. Początkowy plik zasobów układu. Plik zasobów układu zawiera widoki,
które mają być wyświetlane w widżecie ekranu głównego. W pliku konfiguracyjnym XML timewidgetproviderinfo.xml wpisz kod przedstawiony w listingu 18.2. Listing 18.2. Kod wpisany w pliku timewidgetproviderinfo.xml
Jak widzisz, minimalna szerokość i wysokość widżetu ekranu głównego zostały skonfigurowane odpowiednio na 2 i 3 komórki. W powyższym kodzie atrybut android:updatePeriodMillis określa interwał aktualizacji widżetu. Odbiornik BroadcastReceiver, który utworzysz jako następny, będzie automatycznie wywoływany po tym interwale czasowym w celu zaktualizowania widżetu. Najdłuższy interwał aktualizacji wynosi 180000 milisekund (30 minut) i pomaga oszczędzać baterię. Aby aktualizować widżet szybciej, niż określa to interwał czasowy określony za pomocą android:updatePeriodMillis w pliku konfiguracyjnym XML, możesz wykorzystać menedżera alarmów (ang. alarm manager) lub zaimplementować nasłuchiwacze zdarzeń. Widżet może mieć elastyczny rozmiar (użytkownik może przeciągnąć jego uchwyty, by zmienić rozmiar). Aby widżet mógł zmieniać rozmiar, zastosowano atrybut android:resizeMode="horizontal|vertical". Po kliknięciu na krawędziach widżetu pojawią się uchwyty do zmiany rozmiaru. Widżet może być rozciągany w pionie i w poziomie. Do widżetu ekranu głównego możesz również przypisać określoną ikonę, dodając w powyższym pliku konfiguracyjnym atrybut android:previewImage. Przykładowo poniższa instrukcja spowoduje, że ikoną widżetu ekranu głównego będzie plik home_widget_icon.png: android:previewImage="@drawable/home_widget_icon"
Twój widżet ekranu głównego będzie reprezentowany przez określoną ikonę na liście widżetów urządzenia. Zanim dodasz powyższą instrukcję do pliku konfiguracyjnego XML, upewnij się, że plik obrazka home_widget_icon.png został skopiowany do folderów drawable aplikacji. Jeśli nie zastosowałeś atrybutu android:previewImage, domyślny plik obrazka ic_launcher.png zostanie automatycznie przypisany jako ikona widżetu.
541
542
Rozdział 18. Widżety ekranu głównego
Uwaga Android oferuje na emulatorze aplikację Widget Preview, która pomaga tworzyć obrazy ikon widżetów.
Opcjonalnie możesz również utworzyć aktywność konfiguracyjną, która jest wywoływana, kiedy nowa instancja widżetu zostanie dodana do ekranu głównego urządzenia Android. Innymi słowy, gdy z listy widżetów urządzenia wybrany zostaje jakiś widżet, odpowiednia aktywność konfiguracyjna jest wywoływana w celu skonfigurowania instancji widżetu. Jeśli na ekranie głównym tworzone są dwie instancje widżetu, aktywność konfiguracyjna jest wywoływana dwukrotnie, jeden raz dla każdej instancji. Aktywność konfiguracyjna śledzi instancje widżetu i przypisuje im unikatowe identyfikatory. Aktywność zapisuje również w pamięci trwałej informacje o instancjach widżetu. Aby zdefiniować aktywność konfiguracyjną, możesz zastosować atrybut android:configure w pliku konfiguracyjnym XML widżetu. Przykładowo poniższa instrukcja deklaruje TimeHomeWidgetAppActivity jako aktywność konfiguracyjną widżetu: android:configure="com.androidtablet.timehomewidgetapp.TimeHomeWidgetAppActivity"
Aby zdefiniować odbiornik BroadcastReceiver dla widżetu, dodaj do paczki com.androidtablet.timehomewidgetapp plik klasy Java o nazwie TimeWidgetProvider.java. Klasa TimeWidgetProvider rozszerzy klasę AppWidgetProvider, która z kolei rozszerzy klasę BroadcastReceiver.
Uwaga Odbiornik BroadcastReceiver z akcją android.appwidget.action.APPWIDGET_UPDATE jest używany do tworzenia i aktualizacji widżetu aplikacji.
Klasa ta nie tylko wypełni widżet ekranu głównego widokami zdefiniowanymi w pliku układu aktywności, ale również będzie aktualizować te widoki. Zasadniczo klasa ta odbiera rozgłaszane komunikaty z prośbą o aktualizację widżetu. Po otrzymaniu rozgłaszanych komunikatów klasa podejmuje odpowiednie akcje. Klasa ta implementuje pięć metod cyklu życia widżetu aplikacji w celu obsłużenia różnych żądań akcji. Pięcioma zaimplementowanymi w tej klasie (TimeWidgetProvider.java) metodami są onEnabled, onDisabled, onUpdate, onDeleted oraz onReceive (patrz listing 18.3).
Uwaga Pojawianie się sygnału do aktualizacji widżetu zależy od częstotliwości interwału czasowego określonego w pliku konfiguracyjnym XML. Sygnał dostarczany jest w postaci komunikatów rozgłaszania.
Receptura: tworzenie widżetów ekranu głównego Listing 18.3. Kod wpisany w pliku Java TimeWidgetProvider.java package com.androidtablet.timehomewidgetapp; import import import import import import import
java.util.Date; android.appwidget.AppWidgetManager; android.appwidget.AppWidgetProvider; android.content.Context; android.widget.RemoteViews; android.widget.Toast; java.text.SimpleDateFormat;
public class TimeWidgetProvider extends AppWidgetProvider { private static SimpleDateFormat formatter = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss"); @Override public void onDeleted(Context context, int[] appWidgetIds) { super.onDeleted(context, appWidgetIds); Toast.makeText(context, "Widżet usunięty", Toast.LENGTH_LONG).show(); } @Override public void onDisabled(Context context) { super.onDisabled(context); Toast.makeText(context, "Widżet wyłączony", Toast.LENGTH_LONG).show(); } @Override public void onEnabled(Context context) { super.onEnabled(context); Toast.makeText(context, "Widżet włączony", Toast.LENGTH_LONG).show(); } @Override public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { super.onUpdate(context, appWidgetManager, appWidgetIds); String currentTime = formatter.format(new Date()); final int N = appWidgetIds.length; for (int i=0; i
543
544
Rozdział 18. Widżety ekranu głównego
W powyższym kodzie appWidgetIds jest tablicą identyfikatorów wszystkich instancji aktywnych widżetów. Aby aktualizować wszystkie instancje widżetu jednocześnie, w metodzie onUpdate() zastosowano pętlę do iteracji przez każdy wpis (identyfikator) aktywnych instancji widżetów w tablicy appWidgetIds. Pozwala to zaktualizować każdy wymagany widżet. Tworzona jest instancja klasy RemoteViews o nazwie remoteViews. Pamiętasz, że RemoteViews to widoki widżetu ekranu głównego, które są aktualizowane przez proces inny niż dana aplikacja. W konstruktorze klasy RemoteViews dostarczane są nazwa paczki (getPackageName()) oraz układ, który definiuje widoki widżetu ekranu głównego. Tworzona jest referencja do klasy AppWidgetManager. Za pomocą tej klasy wywoływana jest metoda updateAppWidget w celu narysowania i zaktualizowania widoków RemoteViews w widżecie ekranu głównego. Kiedy usuwasz instancję widżetu, we wspomnianej klasie wywoływana jest metoda onDeleted(). Ponadto, jeśli usuwana instancja widżetu jest ostatnią instancją, wywoływana jest metoda onDisabled(). Możesz użyć tych dwóch metod do uwalniania zasobów przez wykonywanie takich czynności jak usuwanie stanów instancji widżetu. Następnym krokiem jest zarejestrowanie klasy BroadcastReceiver (TimeWidgetProvider) w pliku AndroidManifest.xml. Wiesz już, że klasa BroadcastReceiver wywołuje metody cyklu życia widżetu aplikacji, w tym metodę onUpdate(), która jest wymagana do aktualizacji zawartości widżetu periodycznie lub w przypadku wystąpienia określonego zdarzenia. Zaznaczone pogrubieniem fragmenty kodu z listingu 18.4 zostały dopisane do pliku AndroidManifest.xml w celu zarejestrowania klasy BroadcastReceiver TimeWidgetProvider. Listing 18.4. Kod wpisany w pliku AndroidManifest.xml
Receptura: tworzenie widżetów ekranu głównego
Kod w listingu 18.4 wskazuje, że klasą BroadcastReceiver jest TimeWidgetProvider. Akcją obsługiwaną przez ten odbiornik jest akcja APPWIDGET_UPDATE, która jest rozgłaszana, kiedy zawartość widżetu wymaga aktualizacji. Atrybut android:name="android.appwidget.provider" definiuje metadane dla widżetu. Nazwa i lokalizacja pliku konfiguracyjnego widżetu są dostarczane przez te metadane. Plik konfiguracji widżetu zawiera takie informacje jak minimalny obszar wymagany przez widżet, czasowy interwał aktualizacji widżetu oraz lokalizacja pliku zasobów układu. Kiedy określisz definicję widżetu i jego klasę BroadcastReceiver, będzie on dostępny na liście widżetów urządzenia i użytkownik będzie mógł go przeciągnąć i upuścić na ekranie głównym. Twoja aplikacja jest gotowa do uruchomienia. Po jej uruchomieniu utworzony zostanie widżet ekranu głównego, który będzie widoczny na liście Widżety. Dalej podano procedurę wyświetlenia nowo utworzonego widżetu na ekranie głównym. Ekran główny urządzenia może początkowo wyglądać tak, jak pokazano na rysunku 18.3 (górny obrazek). 1. Otwórz listę aplikacji w urządzeniu. Wybierz zakładkę Widżety widoczną
u góry ekranu. 2. Pojawi się ekran wyświetlający listę dostępnych widżetów w formie widoku
siatki, co pokazano na rysunku 18.3 (środkowy obrazek). 3. Zlokalizuj nowo utworzony widżet TimeHomeWidgetApp na liście Widżety. 4. Kliknij i przeciągnij widżet TimeHomeWidgetApp. Kiedy będziesz przeciągał
widżet, pojawi się ekran główny. Zobaczysz również okienko upuszczania oraz wskaźnik myszy pokazujący lokalizację, w której widżet może być upuszczony na ekranie głównym. 5. Zwolnij wskaźnik myszy w wybranej lokalizacji. Na ekranie głównym pojawi się
widżet wyświetlający bieżącą datę i aktualny czas (patrz rysunek 18.3, dolny obrazek).
Uwaga Widok tworzony na ekranie głównym jest instancją widżetu. Możesz uruchomić jednocześnie więcej niż jedną instancję widżetu w aplikacji.
Aby usunąć instancję widżetu, przyciśnij ją na ekranie głównym i zacznij przeciągać. Po rozpoczęciu przeciągania u góry ekranu pojawi się kosz. Przeciągnięcie instancji widżetu do kosza spowoduje jej usunięcie z ekranu głównego. Widżet ekranu głównego utworzony w tej recepturze będzie aktualizowany co 30 minut. W kolejnej recepturze nauczysz się, jak aktualizować widżet, kiedy tego potrzebujesz.
545
546
Rozdział 18. Widżety ekranu głównego
Rysunek 18.3. Standardowy ekran główny urządzenia (górny obrazek). Twój widżet TimeHomeWidgetApp widoczny na liście widżetów urządzenia (środkowy obrazek). Bieżąca data i aktualny czas wyświetlone w tym widżecie (dolny obrazek)
Receptura: aktualizowanie widżetu ekranu głównego za pomocą kontrolki Button
Receptura: aktualizowanie widżetu ekranu głównego za pomocą kontrolki Button W tej recepturze zmodyfikujesz aplikację TimeHomeWidgetApp, którą utworzyłeś w poprzedniej recepturze. Dodasz do widżetu kontrolkę Button. Po kliknięciu tej kontrolki przeprowadzona zostanie aktualizacja daty i czasu wyświetlanych za pomocą kontrolki TextView. Aby dodać kontrolkę Button, zmodyfikuj plik układu activity_time_home_widget_app.xml w sposób przedstawiony w listingu 18.5. Dodany został tylko fragment kodu zaznaczony pogrubioną czcionką. Reszta kodu pozostaje taka sama jak w listingu 18.1. Listing 18.5. Kod wpisany w pliku układu aktywności activity_time_home_widget_app.xml
Jak widzisz, do pliku układu widżetu dodana została kontrolka Button z identyfikatorem update_button. Kontrolce Button przypisano nagłówek Zaktualizuj czas, który będzie wyświetlany czcionką o rozmiarze zdefiniowanym w zasobie wymiarów text_size. W rzeczywistości do aktualizacji widoku RemoteViews widżetu będziesz wykorzystywał pewną usługę. Usługa będzie powiązana za pomocą intencji oczekującej PendingIntent ze zdarzeniem kliknięcia kontrolki Button. Oznacza to, że kiedy kliknięty zostanie przycisk w widżecie, intencja PendingIntent uruchomi usługę aktualizacji widżetu. Aby tak się stało, musisz zdefiniować wewnętrzną klasę usługi o nazwie UpdateService w klasie TimeWidgetProvider odbiornika BroadcastReceiver. Zmodyfikuj plik TimeWidgetProvider.java w sposób przedstawiony w listingu 18.6. Dodano jedynie
547
548
Rozdział 18. Widżety ekranu głównego
fragmenty kodu zaznaczone pogrubioną czcionką. Reszta kodu pozostaje taka sama jak w listingu 18.3.
Uwaga Ponieważ widoki RemoteViews to widoki odłączone, preferowanym sposobem komunikacji z nimi jest zastosowanie intencji oczekującej PendingIntent. Listing 18.6. Kod wpisany w pliku Java TimeWidgetProvider.java package com.androidtablet.timehomewidgetapp; import import import import import import import import import import import
java.util.Date; android.appwidget.AppWidgetManager; android.appwidget.AppWidgetProvider; android.content.Context; android.widget.RemoteViews; android.widget.Toast; java.text.SimpleDateFormat; android.content.Intent; android.app.PendingIntent; android.app.Service; android.os.IBinder;
public class TimeWidgetProvider extends AppWidgetProvider { private static SimpleDateFormat formatter = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss"); @Override public void onDeleted(Context context, int[] appWidgetIds) { super.onDeleted(context, appWidgetIds); Toast.makeText(context, "Widżet usunięty", Toast.LENGTH_LONG).show(); } @Override public void onDisabled(Context context) { super.onDisabled(context); Toast.makeText(context, "Widżet wyłączony", Toast.LENGTH_LONG).show(); } @Override public void onEnabled(Context context) { super.onEnabled(context); Toast.makeText(context, "Widżet włączony", Toast.LENGTH_LONG).show(); } @Override public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { super.onUpdate(context, appWidgetManager, appWidgetIds); final int N = appWidgetIds.length; for (int i=0; i
Receptura: aktualizowanie widżetu ekranu głównego za pomocą kontrolki Button int widgetId = appWidgetIds[i]; Intent intent = new Intent(context, UpdateService.class); intent.setAction(AppWidgetManager. ACTION_APPWIDGET_UPDATE); intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId); PendingIntent pendingIntent = PendingIntent.getService(context, 0, intent,PendingIntent.FLAG_UPDATE_CURRENT); RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.activity_time_home_widget_app); remoteViews.setOnClickPendingIntent( R.id.update_button, pendingIntent); appWidgetManager.updateAppWidget(widgetId, remoteViews); } } public static class UpdateService extends Service { String currentTime = formatter.format(new Date()); @Override public int onStartCommand(Intent intent, int flags, int startId) { super.onStartCommand(intent, flags, startId); RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.activity_time_home_widget_app); remoteViews.setTextViewText( R.id.time_textview, currentTime); AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(this); int appWidgetId=intent.getIntExtra( AppWidgetManager.EXTRA_APPWIDGET_ID, 0); appWidgetManager.updateAppWidget(appWidgetId, remoteViews); stopSelf(startId); return 0; } @Override public IBinder onBind(Intent intent) { return null; } } }
W powyższej klasie wykonywane są następujące zadania. Metoda onUpdate jest nadpisywana w celu aktualizacji danych w widżecie. Tworzona jest intencja dla Twojej usługi UpdateService. Dla intencji ustawiana jest akcja ACTION_APPWIDGET_UPDATE w celu aktualizacji
widżetu.
549
550
Rozdział 18. Widżety ekranu głównego
Przed wywołaniem usługi do aktualizacji widżetu identyfikator instancji
widżetu jest zapisywany w intencji jako dodatkowe dane. Jest to spowodowane tym, że klasa AppWidgetProvider jest bezstanowa i nie pamięta ani nie śledzi wywołanych instancji widżetu ekranu głównego. Intencja jest następnie hermetyzowana w obiekcie PendingIntent, aby mogła
zostać wywołana później. Stała FLAG_UPDATE_CURRENT jest stosowana do potwierdzenia, że obiekt PendingIntent już istnieje. Obiekt jest zachowywany, a jego dodatkowe dane są zastępowane nową intencją. Tworzona jest instancja klasy RemoteViews o nazwie remoteViews, kiedy widoki
widżetu są wyświetlane za pomocą tej klasy. Metoda setOnClickPendingIntent jest wykorzystywana do powiązania procedury
obsługi zdarzeń kliknięcia z kontrolką Button. W rzeczywistości obiekt PendingIntent jest ustawiany jako procedura obsługi zdarzeń kliknięcia dla kontrolki Button, aby usługa była uruchamiana po każdym kliknięciu tej kontrolki. W usłudze UpdateService implementowana jest metoda onStartCommand. Wywoływana jest metoda setTextViewText klasy RemoteViews w celu
przypisania bieżącej daty i aktualnego czasu do kontrolki TextView widżetu. Tworzona jest referencja do klasy AppWidgetManager. Za pomocą klasy AppWidgetManager wywoływana jest metoda updateAppWidget
w celu aktualizacji widżetu. Aby zarejestrować wewnętrzną klasę usługi o nazwie UpdateService w klasie TimeWidgetProvider, do pliku AndroidManifest.xml dodaj poniższą instrukcję (poza kodem, który dodałeś w listingu 18.4):
W powyższej instrukcji atrybut android:name w elemencie wskazuje, że klasą odbiornika jest TimeWidgetProvider. Klasa ta wykorzystuje wewnętrzną klasę usługi UpdateService do aktualizacji widżetu.
Uwaga Przy podawaniu nazwy klasy w języku Java znak $ oznacza wewnętrzną klasę.
Po uruchomieniu aplikacji zaktualizowany zostanie Twój widżet ekranu głównego TimeHomeWidgetApp na liście widżetów. Przeciągnij i upuść do kosza starą instancję widżetu ekranu głównego, którą utworzyłeś w poprzedniej recepturze (patrz rysunek 18.3, dolny obrazek). Przeciągnij nową instancję widżetu z listy Widżety urządzenia i upuść ją na ekranie głównym. Kontrolka TextView widżetu wyświetli początkowy tekst 00:00:00, tak jak pokazano na rysunku 18.4 (górny obrazek). Kiedy klikniesz przycisk Zaktualizuj czas, bieżące data i czas zostaną wyświetlone za pomocą kontrolki TextView, co pokazano na rysunku 18.4 (dolny obrazek). Od tej chwili każde kliknięcie przycisku Zaktualizuj czas będzie powodować aktualizację czasu wyświetlanego za pomocą kontrolki TextView.
Receptura: zastosowanie klasy AlarmManager do aktualizacji widżetu ekranu
Rysunek 18.4. Kontrolka TextView widżetu wyświetlająca początkowy tekst 00:00:00 oraz kontrolka Button (górny obrazek). Bieżąca data i aktualny czas wyświetlane za pomocą kontrolki TextView po kliknięciu przycisku Zaktualizuj czas (dolny obrazek)
Receptura: zastosowanie klasy AlarmManager do częstej aktualizacji widżetu ekranu głównego Jeśli chcesz często aktualizować widżet, zalecana jest klasa AlarmManager z uwagi na oszczędność zasobów. W tej recepturze sprawisz, że Twój widżet będzie aktualizowany automatycznie co 1000 milisekund. Data i czas wyświetlane w widżecie będą aktualizowane automatycznie bez konieczności wciskania kontrolki Button. Otwórz aplikację Android TimeHomeWidgetApp, którą utworzyłeś w poprzedniej recepturze. Zmodyfikuj plik klasy
551
552
Rozdział 18. Widżety ekranu głównego
Java TimeWidgetProvider.java w sposób przedstawiony w listingu 18.7. Zmodyfikowane zostały jedynie fragmenty kodu zaznaczone pogrubioną czcionką. Reszta kodu pozostaje taka sama jak w listingu 18.6. Listing 18.7. Kod wpisany w pliku Java TimeWidgetProvider.java package com.androidtablet.timehomewidgetapp; import import import import import import import import import import import import import
java.util.Date; android.appwidget.AppWidgetManager; android.appwidget.AppWidgetProvider; android.content.Context; android.widget.RemoteViews; android.widget.Toast; java.text.SimpleDateFormat; android.content.Intent; android.app.PendingIntent; android.app.Service; android.os.IBinder; android.app.AlarmManager; android.os.SystemClock;
public class TimeWidgetProvider extends AppWidgetProvider { private static SimpleDateFormat formatter = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss"); @Override public void onDeleted(Context context, int[] appWidgetIds) { super.onDeleted(context, appWidgetIds); Toast.makeText(context, "Widżet usunięty", Toast.LENGTH_LONG).show(); } @Override public void onDisabled(Context context) { super.onDisabled(context); Toast.makeText(context, "Widżet wyłączony", Toast.LENGTH_LONG).show(); } @Override public void onEnabled(Context context) { super.onEnabled(context); Toast.makeText(context, "Widżet włączony", Toast.LENGTH_LONG).show(); } @Override public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { super.onUpdate(context, appWidgetManager, appWidgetIds); final int N = appWidgetIds.length; for (int i=0; i
Receptura: zastosowanie klasy AlarmManager do aktualizacji widżetu ekranu intent.setAction(AppWidgetManager. ACTION_APPWIDGET_UPDATE); intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId); PendingIntent pendingIntent = PendingIntent. getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); AlarmManager alarm = (AlarmManager) context. getSystemService(Context.ALARM_SERVICE); alarm.setRepeating(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime(), 1000, pendingIntent); } } public static class UpdateService extends Service { String currentTime = formatter.format(new Date()); @Override public int onStartCommand(Intent intent, int flags, int startId) { super.onStartCommand(intent, flags, startId); RemoteViews remoteViews = new RemoteViews( getPackageName(), R.layout.activity_time_home_widget_app); remoteViews.setTextViewText( R.id.time_textview, currentTime); AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(this); int appWidgetId=intent.getIntExtra( AppWidgetManager.EXTRA_APPWIDGET_ID, 0); appWidgetManager.updateAppWidget( appWidgetId, remoteViews); stopSelf(startId); return 0; } @Override public IBinder onBind(Intent intent) { return null; } } }
Jak widzisz, w powyższym kodzie tworzona jest instancja alarm klasy AlarmManager. Alarm ma być uruchamiany co 1000 milisekund. Przy każdym włączeniu alarmu wywoływana będzie intencja PendingIntent w celu uruchomienia powiązanej z nią usługi UpdateService. Usługa będzie następnie aktualizować widżet, wyświetlając bieżący czas. Kiedy uruchomisz aplikację, bieżący czas pojawi się w kontrolce TextView widżetu, tak jak pokazano na rysunku 18.5. Zwróć uwagę, że czas wyświetlany w widżecie będzie automatycznie aktualizowany co 1000 milisekund.
553
554
Rozdział 18. Widżety ekranu głównego
Rysunek 18.5. Kontrolka TextView widżetu wyświetla bieżącą datę i czas. Widżet jest aktualizowany co 1000 milisekund
Podsumowanie W tym rozdziale prześledziłeś procedurę tworzenia i wyświetlania widżetu ekranu głównego na urządzeniu. Poznałeś metody cyklu życia widżetu aplikacji. Zobaczyłeś, w jaki sposób odbiornik BroadcastReceiver odbiera rozgłaszane komunikaty przeznaczone do aktualizacji widżetu, a następnie aktualizuje ten widżet. Ponadto nauczyłeś się określać czas aktualizacji widżetu za pomocą pliku konfiguracyjnego XML. Na koniec dowiedziałeś się, jak widżety ekranu głównego są aktualizowane za pomocą nasłuchiwaczy zdarzeń oraz usługi AlarmManager. W kolejnym rozdziale dowiesz się, jak uzyskać dostęp do urządzenia sprzętowego NFC. Poznasz także procedurę odczytywania znaczników NFC i zapisywania w nich danych. Na koniec zapoznasz się z funkcją Android Beam i dowiesz się, w jaki sposób jest ona wykorzystywana do bezprzewodowego przesyłania danych.
19 Android Beam C
zęstotliwość radiowa odgrywa ważną rolę w komunikacji bezprzewodowej. W rozdziale 12., „Łączność bezprzewodowa”, poznałeś zastosowanie technologii Bluetooth w przesyłaniu danych. Ten rozdział został poświęcony zastosowaniu standardu NFC oraz funkcji Android Beam do udostępniania danych za pomocą częstotliwości radiowej. Dowiesz się, czym jest NFC i jaki ma związek z techniką RFID. Poznasz struktury wykorzystywane do wymiany danych za pomocą znaczników NFC. Ponadto nauczysz się zapisywać dane do znaczników NFC oraz odczytywać dane ze znaczników NFC, wykorzystując w tym celu procedury krok po kroku. Na koniec zapoznasz się z funkcją Android Beam i dowiesz się, w jaki sposób za jej pomocą przesyłane są dane pomiędzy urządzeniami Android.
Receptura: standard NFC NFC (ang. near field communication — komunikacja małego zasięgu) to krótkozasięgowa technologia komunikacji bezprzewodowej wysokich częstotliwości, która umożliwia przesyłanie danych pomiędzy telefonami, znacznikami i kartami elektronicznymi. Standard NFC jest oparty na technice RFID (ang. radio frequency identification), która umożliwia urządzeniom Android komunikowanie się za pomocą fal radiowych. Mówiąc bardziej szczegółowo, w technice RFID pole elektromagnetyczne jest generowane przez fale radiowe, które umożliwiają transfer danych pomiędzy urządzeniami obsługującymi RFID. NFC działa w trzech trybach. Odczyt i zapis znaczników — znaczniki to małe obiekty, które mogą być
wbudowane w przedmioty, takie jak breloczki czy naklejki. Znaczniki nie wymagają zasilania baterią i wykorzystują pole RF generowane przez urządzenia Android w celu zapewnienia sobie zasilania. Urządzenie Android działa jako czytnik (lub aplikacja do zapisu), który generuje pole RF i wykonuje skan w poszukiwaniu znajdujących się w pobliżu urządzeń NFC. Kiedy znacznik znajduje się w obrębie pola RF, jest wykrywany przez czytnik, który odczytuje z niego informacje. Niektóre znaczniki można ponownie zapisywać, więc urządzenia Android mogą aktualizować ich zawartość.
556
Rozdział 19. Android Beam
Tryb emulacji kart — w tym trybie urządzenie Android działa jako czytnik
kart elektronicznych (ang. smart card). Czytnik przeprowadza transakcje z urządzeniem Android, tak jakby było kartą kredytową. Działające w tym trybie urządzenie Android może być stosowane we wszystkich miejscach, w których można używać kart kredytowych. Komunikacja typu peer-to-peer — w tym trybie oba urządzenia generują
własne pola RF i wymieniają dane w obie strony. NFC ma trzy cechy. Komunikacja pomiędzy urządzeniami odbywa się po prostu przez ich
zetknięcie. Jest prostą technologią, która obsługuje transmisję krótkiego zasięgu. Standard
NFC jest zbliżony do technologii Bluetooth, z tym że NFC działa na dystansie około 4 cm, podczas gdy Bluetooth może łączyć urządzenia w pary na odległość do 50 m. Komunikacja NFC jest wydajna i energooszczędna, jest więc preferowana
w przypadku płatności mobilnych.
Ostrzeżenie Przy płatnościach mobilnych urządzenie musi posiadać czytnik NFC, aby połączyć się z kasą fiskalną. Ponadto wymagana jest bezpieczna aplikacja, która może obsłużyć karty kredytowe i inne formalności związane z dokonywaniem zakupu.
Receptura: znaczniki NFC Znaczniki NFC (ang. NFC tags) to niewielkie urządzenia elektroniczne w postaci niedużych plastikowych kółeczek lub breloczków (patrz rysunek 19.1). Nie mają na wyposażeniu baterii, lecz są zasilane przez otaczające je pole RF. Znaczniki NFC mają niewielką pamięć. Kiedy czytnik NFC wykrywa w zasięgu swojego pola znacznik NFC, uzyskuje dostęp do informacji zawartych w tym znaczniku. Znaczniki NFC, które można przyklejać, określane są mianem naklejek NFC. Naklejki NFC przenoszące informacje o produkcie są do niego przytwierdzone.
Rysunek 19.1. Znaczniki NFC
Receptura: struktura wykorzystywana do wymiany informacji za pomocą NFC
Znaczniki NFC mogą wyświetlać informacje o przedmiotach, produktach, miejscach itd. Przykładowo w domu towarowym znaczniki NFC mogą być umieszczane na produktach. Kiedy użytkownik przesunie telefonem w pobliżu znacznika, wyświetlona zostanie strona WWW zawierająca multimedialne informacje o danym produkcie. Takie informacje mogą zawierać pliki wideo ze szczegółami dotyczącymi produktu. W podobny sposób znaczniki NFC mogą być wykorzystywane do pobierania informacji o najbliższym odjeździe pociągu, autobusu czy odlocie samolotu. Znaczniki NFC mogą również służyć jako klucz otwierający wyposażone w moduł NFC drzwi w domu, hotelu, samochodzie czy biurze.
Uwaga Możesz programować znaczniki NFC w celu dodania informacji tekstowych, identyfikatorów URI oraz metadanych. R
Receptura: struktura wykorzystywana do wymiany informacji za pomocą znaczników NFC Do wymiany informacji za pomocą znaczników NFC używany jest format NDEF (ang. NFC data exchange format). Mówiąc bardziej precyzyjnie, do wymiany danych za pomocą znaczników NFC stosowane są komunikaty NdefMessage. Komunikat NdefMessage zawiera jeden lub kilka rekordów NdefRecord, a te z kolei zawierają dane, które chcesz udostępnić za pomocą znaczników NFC. Rekord NdefRecord składa się z następujących pól. TNF (Type Name Format) — 3-bitowe pole, które reprezentuje typ TNF
(typ zawartego ładunku). Type — reprezentuje typ MIME (format rekordu). ID — reprezentuje unikatowy identyfikator dla danego rekordu. Payload — reprezentuje ładunek danych.
Oto najbardziej powszechne typy TNF. TNF_EMPTY — wskazuje, że dany rekord jest pusty. TNF_MIME_MEDIA — wskazuje, że pole type zawiera typ media. TNF_ABSOLUTE_URI — wskazuje, że pole type zawiera bezwzględny identyfikator URI. TNF_UNKNOWN — wskazuje, że typ ładunku jest nieznany. TNF_WELL_KNOWN — wskazuje, że pole type zawiera „dobrze znany” typ RTD
(ang. Record Type Definition). Aby utworzyć nowy komunikat NDEF, musisz najpierw napisać co najmniej jeden rekord NdefRecord. W tym rekordzie przechowywany jest ładunek danych, który chcesz zapisać w znaczniku. Ładunek zapisywany w rekordzie NdefRecord może być bezwzględnym
557
558
Rozdział 19. Android Beam
adresem URI, charakterystycznym dla aplikacji typem MIME, a nawet zwykłym tekstem. Teraz zajmijmy się kwestią, w jaki sposób zdefiniować wartości dla tych czterech pól (typ TNF, typ rekordu, identyfikator oraz ładunek rekordu NdefRecord), by umożliwić przechowywanie adresu URI, charakterystycznego dla aplikacji typu MIME oraz zwykłego tekstu. Ułatwieniem może być tabela 19.1; w niej pokazano przykładowy kod, za pomocą którego utworzony został rekord NdefRecord przechowujący wspomniane trzy typy danych. Tabela 19.1. Przykładowe kody dla tworzenia rekordu NdefRecord z różnymi typami ładunku
Typ danych
Przykładowy kod
Aby umieścić bezwzględny adres URI w rekordzie
String url = "http://bmharwani.com";
NdefRecord
byte[] payload = url.getBytes(); byte[] tagId = new byte[0]; NdefRecord ndefRecord = new NdefRecord(NdefRecord.TNF_ABSOLUTE_URI, NdefRecord.RTD_URI, tagId, payload);
Powyższy przykład tworzy rekord NdefRecord przechowujący adres URI. Aby umieścić zwykły tekst w rekordzie NdefRecord
String text = "Tekst do zapisania w module NFC"; byte[] langBytes = Locale.ENGLISH.getLanguage().getBytes(Charset. forName("US-ASCII")); byte[] textBytes = text.getBytes(Charset.forName("UTF-8")); byte[] tagId = new byte[0]; byte[] payload = new byte[1 + langBytes.length + textBytes.length]; payload[0] = (byte) langBytes.length; System.arraycopy(langBytes, 0, payload, 1, langBytes.length); System.arraycopy(textBytes, 0, payload, 1 + langBytes.length, textBytes.length); NdefRecord record = new NdefRecord(NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_TEXT, tagId, payload);
Powyższy przykład tworzy rekord NdefRecord przechowujący zwykły tekst. Odpowiednie kodowanie odbywa się przed umieszczeniem tekstu w rekordzie NdefRecord.
Receptura: struktura wykorzystywana do wymiany informacji za pomocą znaczników NFC 559 Tabela 19.1. Przykładowe kody dla tworzenia rekordu NdefRecord z różnymi typami ładunku
Typ danych
Przykładowy kod
Aby umieścić chcarakterystyczny dla aplikacji typ MIME w rekordzie
byte[] mimeType =
NdefRecord
"application/com.androidtablet.nfcapp". getBytes(Charset.forName("US-ASCII")); byte[] tagId = new byte[0]; byte[] payload = "Tekst do zapisania w module NFC".getBytes(Charset.forName("US-ASCII")); NdefRecord ndefRecord = new NdefRecord(NdefRecord.TNF_MIME_MEDIA,mimeType,tagId,payload);
Rekord NdefRecord może być również załączony w postaci rekordu AAR (ang. Android application record), ponieważ zapewnia on, że Twoja aplikacja zostanie uruchomiona na urządzeniu docelowym. W takim przypadku Twoja aplikacja nie jest instalowana na urządzeniu docelowym. Wywołany zostanie sklep Google Play, zachęcający użytkownika do jej zainstalowania. Aby utworzyć rekord AAR NdefRecord, należy wywołać metodę createApplicationRecord() na klasie NdefRecord, określając nazwę paczki Twojej aplikacji, tak jak pokazano w poniższych instrukcjach: byte[] payload = "Tekst do zapisania w module NFC".getBytes(Charset.forName("US-ASCII")); byte[] tagId = new byte[0]; String mimeType = "application/com.androidtablet.nfcapp"; byte[] mimeBytes = mimeType.getBytes(Charset.forName("USASCII")); NdefMessage nfcMessage = new NdefMessage(new NdefRecord[] {new NdefRecord (NdefRecord.TNF_MIME_MEDIA,mimeBytes,tagId,payload), NdefRecord.createApplicationRecord("com.androidtablet. androidbeamapp") });
Ponieważ znacznik z rekordem AAR określa nazwę paczki aplikacji, po jego odebraniu urządzenie Android będzie szukać lokalnie aplikacji o danej nazwie paczki i uruchomi ją, jeśli zostanie znaleziona. Jeśli Android nie odnajdzie aplikacji, uruchomiony zostanie sklep Google Play w celu pobrania aplikacji, aby możliwe było przetworzenie znacznika.
560
Rozdział 19. Android Beam
Receptura: odczytywanie danych ze znaczników NFC Aby skorzystać z funkcjonalności NFC w aplikacji, musisz uzyskać dostęp do chipa NFC urządzenia, dodając w pliku manifestu następujące zezwolenie:
Aby ograniczyć instalowanie aplikacji jedynie do urządzeń z obsługą NFC, dodaj do pliku manifestu poniższą instrukcję:
Uwaga Aby przygotować i przetestować aplikację do odczytu znaczników NFC, potrzebujesz urządzenia z obsługą tego standardu.
Kiedy skanujesz znacznik NFC, obiekt znacznika jest umieszczany w intencji jako EXTRA_TAG. Następnie intencja jest wysyłana do aplikacji, która filtruje intencje. Intencja
uruchamia aktywność przeznaczoną do obsługi znacznika. Jeśli dostępna jest więcej niż jedna aplikacja, która może obsłużyć intencję, podejmowane jest wyszukiwanie odpowiedniej aktywności, do jakiej może być wysłana intencja, aby zawarty w niej znacznik został obsłużony prawidłowo. Wystąpić mogą następujące sytuacje. Jeśli istnieje aktywność na pierwszym planie (aktywność, która wywołała
metodę enableForegroundDispatch()), intencja jest przekazywana do niej. Sprawdzany jest pierwszy rekord NdefRecord komunikatu NdefMessage
znacznika danych. Jeśli rekord jest adresem URI lub typem MIME, wyszukiwana jest aktywność zarejestrowana dla intencji ACTION_NDEF_DISCOVERED. Intencja jest wysyłana do pasującej aktywności. Wyszukiwana jest aktywność, która jest zarejestrowana dla intencji
ACTION_TECH_DISCOVERED i odpowiada określonemu zestawowi technologii
dla znacznika. Intencja jest wysyłana do pasującej aktywności. Jeśli żadna aktywność nie odpowiada trzem powyższym sytuacjom, intencja
jest przekazywana jako akcja ACTION_TAG_DISCOVERED. W tej recepturze nauczysz się, jak urządzenia Android odczytują znaczniki i uruchamiają właściwą aktywność. Aby Twoja aplikacja była w stanie skanować znaczniki zawierające wymagane dane, w pliku AndroidManifest.xml musisz wpisać filtry intencji, które przechwytują różne intencje. Kiedy urządzenie zostaje zbliżone do znacznika NFC, w urządzeniu uruchamiana jest odpowiednia intencja powiadamiająca aplikację, że wykryty został znacznik NFC. Poprzez zadeklarowanie właściwego filtra intencji w pliku AndroidManifest.xml lub wykorzystanie techniki rozsyłania na pierwszym planie Twoja aplikacja może wysyłać żądanie obsługi danej intencji.
Receptura: odczytywanie danych ze znaczników NFC
Element jest umieszczany w Twojej aktywności, ponieważ właśnie filtry intencji pomagają „systemowi rozsyłania znaczników” określić aplikacje, które mogą obsłużyć dane znaczniki. Na podstawie danych zawartych w znaczniku NFC „system rozsyłania znaczników” określa i wyszukuje aplikacje, które są w stanie obsłużyć zeskanowany znacznik NFC. Aplikacje deklarują filtry intencji, aby poinformować, że będą obsługiwać zeskanowany znacznik i jego dane. Poniżej zamieszczono filtry intencji dla różnych typów danych znacznika. Filtr intencji dla znacznika NFC z typem MIME:
Powyższy typ intencji jest ustawiony dla zwykłego tekstu. Aby ustawić go dla wszystkich typów tekstowych, zmień wartość atrybutu android:mimeType na text/*. Filtr intencji dla znacznika NFC z identyfikatorem URI:
Twoja aktywność zostanie uruchomiona, jeśli wykryty zostanie znacznik z identyfikatorem URI http. Aby wyszukiwać znaczniki NFC wykorzystujące konkretną technologię, zastosuj następujący filtr intencji:
Aby przefiltrować pod kątem określonej technologii, należy zastosować znacznik , który odwołuje się do kolejnego pliku umieszczonego w folderze /res/xml Twojej aplikacji. Plik tech_filter.xml będzie zawierał listę wszystkich technologii, których szuka Twoja aktywność. Filtr intencji dla znacznika NFC z typem "unknown":
Twoja aktywność zostanie uruchomiona, jeśli znacznik zawiera dane inne niż typ MIME, identyfikator URI lub wybrane technologie. Powyższe filtry intencji pomogą Ci wyszukiwać znaczniki z określoną zawartością.
561
562
Rozdział 19. Android Beam
Możesz upewnić się, że tylko Twoja aktywność będzie obsługiwała znacznik NFC, stosując system rozsyłania działający na pierwszym planie. Przeczytasz o tym dalej w tej recepturze. Utwórz projekt Android o nazwie NFCReadTagApp. Budując tę aplikację, ustaw wartości atrybutów android:minSdkVersion i android:targetSdkVersion odpowiednio na 11 i 17. W pliku AndroidManifest.xml dodaj zezwolenia i filtry intencji, takie jak w listingu 19.1. Dodane zostały jedynie fragmenty kodu zaznaczone pogrubioną czcionką. Reszta to tworzony automatycznie kod domyślny. Listing 19.1. Kod wpisany w pliku AndroidManifest.xml
Jak widzisz, do pliku manifestu dodane zostały zezwolenia umożliwiające uzyskanie dostępu do chipa NFC urządzenia oraz ograniczenia instalowania aplikacji tylko
Receptura: odczytywanie danych ze znaczników NFC
na urządzeniach z obsługą NFC. Ponieważ chcesz, aby Twoja aktywność skanowała znacznik wtedy, gdy zawiera zwykły tekst lub dane typu "unknown", do powyższego pliku manifestu dodaj odpowiednie filtry intencji. Musisz napisać kod Java służący do odczytu danych z wykrytego znacznika. Najpierw jednak zapoznaj się z krótkim przeglądem całej procedury, zamieszczonym poniżej. Proces odczytu znaczników NFC rozpoczyna się od uzyskania referencji do adaptera NFC: nfcAdapter = NfcAdapter.getDefaultAdapter(this);
Następnie możesz wywołać metodę enableForegroundDispatch() na adapterze NfcAdapter w celu przełączenia aktywności na pierwszy plan. W konsekwencji Twoja aktywność będzie skanowała i obsługiwała znacznik, zanim zrobi to jakakolwiek inna aplikacja: PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, new Intent(this, getClass()).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), 0); IntentFilter intentFilter = new IntentFilter(NfcAdapter.ACTION_TAG_DISCOVERED); IntentFilter[] readTagFilters = new IntentFilter[] { intentFilter }; nfcAdapter.enableForegroundDispatch(this, pendingIntent, readTagFilters, null);
W tym kodzie ustawiasz, aby intencja PendingIntent była uruchamiana, kiedy skanowany będzie znacznik. Dlatego wtedy będzie włączana bieżąca aktywność. Kiedy system roześle akcję ACTION_TAG_DISCOVERED, zostanie uruchomiona dana aktywność i wywołana metoda onNewIntent() z informacją o znaczniku: public void onNewIntent(Intent intent) { Tag tagDetected = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG); readFromTag(tagDetected); }
Uwaga Kiedy rozgłaszana jest intencja z danymi znacznika, obiekt Tag jest zawsze pakowany do dodatkowej wiązki intencji pod kluczem EXTRA_TAG. Jeśli znacznik zawiera dane NDEF, kolejna wartość EXTRA jest ustawiana pod kluczem EXTRA_NDEF_MESSAGES. Nawet identyfikator znacznika może być opcjonalnie dodany do intencji pod kluczem EXTRA_ID.
W metodzie onNewIntent() uzyskiwany jest dostęp do znacznika, który jest przekazywany do funkcji readFromTag() w celu odczytania z niego danych. Aby odczytać dane ze znacznika, wpisz w pliku aktywności Java NFCReadTagAppActivity.java kod przedstawiony w listingu 19.2. Listing 19.2. Kod wpisany w pliku aktywności Java NFCReadTagAppActivity.java package com.androidtablet.nfcreadtagapp; import android.os.Bundle; import android.app.Activity; import android.nfc.NdefMessage;
563
564
Rozdział 19. Android Beam import import import import import import import import import
android.content.Intent; android.nfc.NfcAdapter; android.os.Parcelable; android.nfc.NdefRecord; android.widget.Toast; android.nfc.Tag; android.app.PendingIntent; android.content.IntentFilter; android.nfc.tech.Ndef;
public class NFCReadTagAppActivity extends Activity { Tag tagDetected; NfcAdapter nfcAdapter; PendingIntent pendingIntent; IntentFilter[] readTagFilters; IntentFilter intentFilter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_nfcread_tag_app); nfcAdapter = NfcAdapter.getDefaultAdapter(this); if(nfcAdapter == null) Toast.makeText(this, "Moduł NFC nie jest dostępny w tym urządzeniu.", Toast.LENGTH_LONG).show(); else Toast.makeText(this, "Moduł NFC jest dostępny w tym urządzeniu.", Toast.LENGTH_LONG).show(); pendingIntent = PendingIntent.getActivity( getApplicationContext(), 0, new Intent(this,getClass()). addFlags( Intent.FLAG_ACTIVITY_SINGLE_TOP), 0); IntentFilter intentFilter = new IntentFilter(NfcAdapter.ACTION_TAG_DISCOVERED); readTagFilters = new IntentFilter[]{intentFilter}; } @Override protected void onResume() { super.onResume(); nfcAdapter.enableForegroundDispatch(this, pendingIntent, readTagFilters, null); } @Override protected void onPause() { super.onPause(); if(nfcAdapter != null) nfcAdapter.disableForegroundDispatch(this); } protected void onNewIntent(Intent intent) { if(intent.getAction().equals(NfcAdapter. ACTION_TAG_DISCOVERED)){ tagDetected = intent.getParcelableExtra( NfcAdapter.EXTRA_TAG); readFromTag(intent);
#1
#2 #3 #4
#5
#6
#7 #8
Receptura: odczytywanie danych ze znaczników NFC } } public void readFromTag(Intent intent){ Ndef ndef = Ndef.get(tagDetected); try{ ndef.connect(); Parcelable[] messages = intent. getParcelableArrayExtra(NfcAdapter. EXTRA_NDEF_MESSAGES); if (messages != null) { NdefMessage[] ndefMessages = new NdefMessage[messages.length]; for (int i = 0; i < messages.length; i++) { ndefMessages[i] = (NdefMessage) messages[i]; } NdefRecord record = ndefMessages[0].getRecords()[0]; byte[] payload = record.getPayload(); String text = new String(payload); Toast.makeText(this, text, Toast.LENGTH_LONG).show(); ndef.close(); } } catch (Exception e) { Toast.makeText(getApplicationContext(), "Nie można odczytać zawartości znacznika.", Toast.LENGTH_LONG).show(); } }
#9 #10 #11
#12
#13 #14
#15
}
Oto objaśnienie instrukcji użytych w listingu 19.2. Instrukcja #1 pobiera adapter NFC urządzenia. Jest on reprezentowany
przez klasę NfcAdapter i w celu jego pobrania wywoływana jest metoda getDefaultAdapter(kontekst). Klasa NfcAdapter nie tylko pomaga odczytywać znaczniki i zapisywać w nich informacje, ale również kieruje wykryte znaczniki do żądanej aktywności w urządzeniu. Instrukcja #2 tworzy intencję oczekującą PendingIntent. Uruchomi ona bieżącą
aktywność, kiedy znacznik będzie skanowany. Instrukcja #3 ustawia obiekt intentFilter klasy IntentFilter w celu
filtrowania pod kątem intencji NfcAdapter.ACTION_TAG_DISCOVERED. Instrukcja #4 umieszcza obiekt intentFilter w tablicy readTagFilters. Jeśli
w aktywności użyto więcej niż jednego filtra, wszystkie filtry są gromadzone w tablicy. Instrukcja #5 ustawia Twoją aktywność na pierwszym planie (jest ona pierwsza
w kolejności do odebrania znacznika). Pamiętaj, że wykryty znacznik jest najpierw rozsyłany do aktywności działającej na pierwszym planie.
565
566
Rozdział 19. Android Beam Instrukcja #6 wywołuje metodę disableForegroundDispatch(), kiedy aktywność
zostaje wstrzymana (kiedy aktywność jest w stanie wstrzymania, nie musi znajdować się na pierwszym planie). Instrukcja #7 obsługuje intencję TAG_DISCOVERED. Instrukcja #8 pobiera wykryty znacznik. Kiedy znacznik jest skanowany,
rozgłaszana jest intencja z określonym ładunkiem stanowiącym powiązane dane. Obiekt Tag (i opcjonalnie Tag Id), poza ładunkiem, jest zawierany w intencji jako EXTRA_TAG. Instrukcja #9 określa, czy znacznik został już sformatowany jako NDEF. Instrukcja #10 ustanawia połączenie ze znacznikiem w celu przeprowadzenia
operacji wejścia/wyjścia (I/O). Instrukcja #11 odbiera komunikaty z wykrytego znacznika. Komunikaty te są
pakowane jako tablica obiektów Parcelable pod kluczem NfcAdapter.EXTRA_NDEF_MESSAGES. Instrukcja #12 iteruje przez tablicę komunikatów uzyskaną z intencji, która
zawiera ładunek NDEF. Instrukcja #13 pobiera rekord NdefRecord. Pożądanym rekordem jest pierwszy
rekord NdefRecord w pierwszym komunikacie NdefMessage. Instrukcja #14 pobiera ładunek danych. Instrukcja #15 zamyka połączenie ze znacznikiem.
Receptura: zapisywanie danych do znacznika NFC W tej recepturze nauczysz się zapisywać zwykły tekst do znacznika NFC. W tym celu utwórz nowy projekt Android o nazwie NFCApp. Wartości atrybutów android:minSdkVersion i android:targetSdkVersion ustaw odpowiednio na 11 i 17. Aby dodać zezwolenia na użycie i pobranie chipa NFC oraz określić typ znacznika NFC, który chcesz wykrywać, do pliku AndroidManifest.xml dodaj kod zaznaczony w listingu 19.3 pogrubioną czcionką. Listing 19.3. Kod wpisany w pliku AndroidManifest.xml
Receptura: zapisywanie danych do znacznika NFC
Jak widzisz, do pliku manifestu dodane zostały filtry intencji, które określają, że skanowany jest tylko znacznik zawierający zwykły tekst lub dane typu "unknown". Aby zapisać tekst w wykrytym znaczniku NFC, wpisz w pliku aktywności Java NFCAppActivity.java kod przedstawiony w listingu 19.4. Listing 19.4. Kod wpisany w pliku aktywności Java NFCAppActivity.java package com.androidtablet.nfcapp; import import import import import import import import import import import import import import
android.os.Bundle; android.app.Activity; android.widget.Toast; android.nfc.NfcAdapter; android.content.Intent; android.app.PendingIntent; android.content.IntentFilter.MalformedMimeTypeException; android.content.IntentFilter; android.nfc.tech.Ndef; android.nfc.NdefMessage; android.nfc.NdefRecord; java.nio.charset.Charset; java.util.Locale; android.nfc.Tag;
567
568
Rozdział 19. Android Beam public class NFCAppActivity extends Activity { private NfcAdapter nfcAdapter; private PendingIntent pendingIntent; private IntentFilter[] writeFilters; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); nfcAdapter = NfcAdapter.getDefaultAdapter(this); if(nfcAdapter == null) Toast.makeText(this, "Moduł NFC nie jest dostępny w tym urządzeniu.", Toast.LENGTH_LONG).show(); else Toast.makeText(this, "Moduł NFC jest dostępny w tym urządzeniu.", Toast.LENGTH_LONG).show(); pendingIntent = PendingIntent.getActivity(this, 0, new Intent(this, getClass()).addFlags( Intent.FLAG_ACTIVITY_SINGLE_TOP), 0); IntentFilter intentFilter = new IntentFilter( NfcAdapter.ACTION_NDEF_DISCOVERED); try { intentFilter.addDataType("text/plain"); } catch (MalformedMimeTypeException e) {} writeFilters = new IntentFilter[] { intentFilter }; } @Override protected void onResume() { super.onResume(); nfcAdapter.enableForegroundDispatch(this, pendingIntent, writeFilters, null); } public void onPause() { super.onPause(); nfcAdapter.disableForegroundDispatch(this); } public void onNewIntent(Intent intent) { Tag tagDetected = intent.getParcelableExtra( NfcAdapter.EXTRA_TAG); writeTag(tagDetected); } private boolean writeTag(Tag tagDetected) { byte[] langBytes = Locale.ENGLISH.getLanguage(). getBytes(Charset.forName("US-ASCII")); String text = "Tekst do zapisania w znaczniku NFC"; byte[] textBytes = text.getBytes(Charset.forName ("UTF-8")); byte[] tagId = new byte[0]; byte[] payload = new byte[1 + langBytes.length + textBytes.length]; payload[0] = (byte) langBytes.length; System.arraycopy(langBytes, 0, payload, 1, langBytes.length); System.arraycopy(textBytes, 0, payload, 1 + langBytes.length, textBytes.length);
#1
#2
#3 #4
#5
#6
#7 #8
#9
Receptura: zapisywanie danych do znacznika NFC NdefRecord record = new NdefRecord( NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_TEXT, tagId, payload); #10 NdefMessage message = new NdefMessage(new NdefRecord[] { record }); #11 try { Ndef ndef = Ndef.get(tagDetected); if (ndef != null) { ndef.connect(); #12 if (!ndef.isWritable()) { #13 Toast.makeText(this, "Ten znacznik NFC jest tylko do odczytu.", Toast.LENGTH_LONG).show(); return false; } int size = message.toByteArray().length; if (ndef.getMaxSize() < size) { Toast.makeText(this, "W znaczniku NFC nie ma wystarczająco wolnego miejsca.", Toast.LENGTH_LONG).show(); return false; } ndef.writeNdefMessage(message); #14 Toast.makeText(this, "Tekst został zapisany w znaczniku NFC z powodzeniem.", Toast.LENGTH_LONG).show(); ndef.close(); #15 return true; } } catch (Exception e) { Toast.makeText(this, "Zapis do znacznika NFC zakończył się niepowodzeniem.", Toast.LENGTH_LONG).show(); } return false; } }
Oto objaśnienie instrukcji użytych w listingu 19.4. Instrukcja #1 pobiera adapter NFC urządzenia. Instrukcja #2 tworzy intencję oczekującą PendingIntent, która uruchomi
bieżącą aktywność, kiedy znacznik będzie skanowany. Instrukcja #3 ustawia obiekt intentFilter klasy IntentFilter w celu filtrowania
pod kątem intencji NfcAdapter.ACTION_TAG_DISCOVERED. Ponadto do obiektu intentFilter dodano typ danych "text/plain", który ma filtrować typ MIME. Instrukcja #4 umieszcza obiekt intentFilter w tablicy writeFilters. Instrukcja #5 ustawia Twoją aktywność na pierwszym planie, ponieważ chcesz,
aby właśnie do niej rozsyłany był znacznik. Pamiętaj, że wykryty znacznik jest najpierw rozsyłany do aktywności działającej na pierwszym planie. Instrukcja #6 usuwa aktywność z pierwszego planu, gdy zostaje wstrzymana. Instrukcja #7 pobiera wykryty znacznik z danych EXTRA intencji. Instrukcja #8 wywołuje funkcję writeTag(), przekazując do niej wykryty
znacznik jako parametr w celu zapisania w tym znaczniku żądanego tekstu.
569
570
Rozdział 19. Android Beam Instrukcja #9 koduje tekst do postaci ładunku. Tekst, który ma być zapisany
w znaczniku, składa się z liter alfabetu polskiego lub znaków ASCII. Instrukcja #10 tworzy rekord NdefRecord zawierający ładunek z tekstem. Ponieważ
ten tekst ma być zapisany w znaczniku NFC, typ TNF rekordu NdefRecord jest ustawiany na TNF_WELL_KNOWN. Pamiętaj, że typ TNF jest ustawiany dla reprezentowania typu ładunku zapisywanego w rekordzie NdefRecord. Instrukcja #11 tworzy komunikat NdefMessage, w którym umieszczany jest
rekord NdefRecord. Komunikat NdefMessage zawiera jeden rekord NdefRecord lub więcej takich rekordów. Instrukcja #12 ustanawia połączenie ze znacznikiem w celu przeprowadzenia
operacji wejścia/wyjścia (I/O). Instrukcja #13 sprawdza, czy znacznik jest zapisywalny. Instrukcja #14 wywołuje metodę writeNdefMessage() w celu zapisania
komunikatu NdefMessage w znaczniku NFC. Instrukcja #15 zamyka połączenie ze znacznikiem NFC.
Receptura: korzystanie z funkcji Android Beam Funkcja Android Beam wprowadzona w systemie Android 4.0 (API poziomu 14.) wykorzystuje standard NFC do udostępniania danych pomiędzy urządzeniami Android po prostu przez zetknięcie urządzeń obudowami. Oznacza to, że możesz zetknąć ze sobą dwa urządzenia Android z obsługą NFC i przesłać między nimi w kilka sekund treści, takie jak aplikacje, strony WWW, kontakty, mapy, filmy z serwisu YouTube itd. Aby uzyskać szybszy transfer danych, Android Beam wykorzystuje również technologię Bluetooth. (Połączenie nawiązywane jest za pomocą standardu NFC, który stosowany jest również do przesyłania niewielkich plików. Reszta plików przesyłana jest przy użyciu technologii Bluetooth). Aby przesłać strumień danych z jednego urządzenia Android na drugie, upewnij się, że oba urządzenia są odblokowane i mają włączoną obsługę standardu NFC i funkcji Android Beam. W tym celu przejdź do opcji Ustawienia/Więcej w urządzeniu Android i sprawdź, czy włączone są opcje NFC i Android Beam. Zrób to na obu urządzeniach. Włącz na nich również moduł Bluetooth. Aby skorzystać z funkcji Android Beam, wykonaj następujące czynności. 1. Otwórz wybraną aplikację i przejdź do zawartości, którą chcesz udostępnić
(czyli do strumienia danych, ang. beam). Jak już wspomniano, przesyłać można takie strumienie danych jak strony WWW, kontakty, mapy, filmy z serwisu YouTube itd. 2. Zetknij ze sobą oba urządzenia tyłami obudów. Nie muszą one całkowicie
do siebie przylegać, ale muszą zostać przysunięte na tyle blisko, abyś poczuł
Receptura: przesyłanie danych za pomocą funkcji Android Beam
wibrację. Jeśli nie poczujesz wibracji, odsuń urządzenia od siebie i zetknij je ponownie. Kiedy będziesz chciał przesłać dane z telefonu na tablet, przesuwaj telefonem po powierzchni tylnej obudowy tabletu, aż zlokalizujesz chip NFC. 3. Po wystąpieniu wibracji zawartość, którą chcesz przesłać, przyjmie postać
obiektu przypominającego kartę. 4. Kliknij tę kartę, a jej zawartość pojawi się na ekranie urządzenia odbiorczego.
Użytkownik urządzenia odbiorczego może kliknąć OK, aby zaakceptować przesyłaną zawartość. Na ekranie urządzenia odbiorczego wyświetlone mogą zostać także instrukcje wskazujące, w jaki sposób zapisać daną treść.
Uwaga Przesyłanie strumienia danych nie zadziała, jeśli ekran urządzenia docelowego będzie zablokowany.
Receptura: przesyłanie danych za pomocą funkcji Android Beam W tej recepturze nauczysz się przesyłać dane z wykorzystaniem funkcji Android Beam. Funkcja ta pomaga udostępniać dane pomiędzy urządzeniami, które są fizycznie zetknięte obudowami. Dane są udostępniane w postaci komunikatów NDEF. Aby zdefiniować komunikat NDEF, musisz do swojej aplikacji dodać zezwolenie NFC, załączając w pliku AndroidManifest.xml następującą instrukcję:
Instrukcja ta pozwoli Twojej aplikacji uzyskać dostęp do chipa NFC w urządzeniu Android. Ponadto możesz dodać poniższą instrukcję, która spowoduje, że Twoja aplikacja w sklepie z aplikacjami będzie widoczna tylko dla urządzeń ze sprzętową obsługą standardu NFC:
Aby otrzymywać komunikaty NDEF za pomocą komunikacji NFC typu peer-to-peer, Twoja aktywność musi nasłuchiwać, czy w pliku AndroidManifest.xml zdefiniowana jest następująca intencja:
571
572
Rozdział 19. Android Beam
Dzięki zastosowaniu powyższego filtra Twoja aktywność będzie uruchamiana na urządzeniu odbiorczym, kiedy wywołana zostanie funkcja Android Beam. Jeśli Twoja aplikacja nie jest zainstalowana, uruchomiony zostanie sklep Google Play, aby użytkownik mógł pobrać aplikację. Przesyłane dane zostaną dostarczone do Twojej aktywności za pomocą intencji z akcją NfcAdapter.ACTION_NDEF_DISCOVERED. Atrybut android:mimeType odczytuje lub zapisuje dane w żądanym formacie w znaczniku NFC. Zapewnia, że dostęp do znacznika ma odpowiednia aplikacja. Utwórz projekt Android o nazwie AndroidBeamApp. Ustaw wartości atrybutów android:minSdkVersion i android:targetSdkVersion odpowiednio na 14 i 17. W pliku AndroidManifest.xml dodaj kod, który w listingu 19.5 został zaznaczony pogrubioną czcionką. Listing 19.5. Kod wpisany w pliku AndroidManifest.xml
Uwaga Funkcja Android Beam wymaga interfejsu API poziomu 14. lub wyższego.
Receptura: przesyłanie danych za pomocą funkcji Android Beam
Aby udostępniać dane pomiędzy urządzeniami z obsługą NFC wykorzystującymi funkcję Android Beam, wpisz w pliku aktywności Java kod przedstawiony w listingu 19.6. Listing 19.6. Kod wpisany w pliku aktywności Java AndroidBeamAppActivity.java package com.androidtablet.androidbeamapp; import import import import import import import import import import import import import import
android.os.Bundle; android.app.Activity; java.nio.charset.Charset; android.nfc.NdefRecord; android.nfc.NdefMessage; android.nfc.NfcAdapter; android.nfc.NfcEvent; android.os.Parcelable; android.content.Intent; android.widget.Toast; android.util.Log; android.nfc.NfcAdapter.CreateNdefMessageCallback; android.app.PendingIntent; android.content.IntentFilter;
public class AndroidBeamAppActivity extends Activity { NdefMessage nfcMessage; NfcAdapter nfcAdapter; PendingIntent pendingIntent; IntentFilter[] intentFiltersArray; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_android_beam_app); nfcAdapter = NfcAdapter.getDefaultAdapter(this); if(nfcAdapter == null) Toast.makeText(this, "Moduł NFC nie jest dostępny w tym urządzeniu.", Toast.LENGTH_LONG).show(); else Toast.makeText(this, "Moduł NFC jest dostępny w tym urządzeniu.", Toast.LENGTH_LONG).show(); nfcAdapter.setNdefPushMessage(nfcMessage, this); nfcAdapter.setNdefPushMessageCallback(new CreateNdefMessageCallback() { public NdefMessage createNdefMessage(NfcEvent event) { String payload = "Tekst dla transmisji beam"; String mimeType = "com.androidtablet.androidbeamapp"; byte[] mimeBytes = mimeType.getBytes( Charset.forName("US-ASCII")); NdefRecord record = new NdefRecord(NdefRecord. TNF_MIME_MEDIA,mimeBytes,new byte[0], payload.getBytes()); nfcMessage = new NdefMessage(new NdefRecord[] { record, NdefRecord.createApplicationRecord( "com.androidtablet.androidbeamapp") }); return nfcMessage; }
573
574
Rozdział 19. Android Beam }, this); } @Override public void onResume() { super.onResume(); pendingIntent = PendingIntent.getActivity( getApplicationContext(), 0, new Intent( this,getClass()).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), 0); IntentFilter intentFilter = new IntentFilter(NfcAdapter. ACTION_NDEF_DISCOVERED); intentFiltersArray = new IntentFilter[]{intentFilter}; nfcAdapter.enableForegroundDispatch(this, pendingIntent, intentFiltersArray, null); Intent intent = getIntent(); if (NfcAdapter.ACTION_NDEF_DISCOVERED. equals(intent.getAction())) { try { Parcelable[] messages = intent. getParcelableArrayExtra(NfcAdapter. EXTRA_NDEF_MESSAGES); NdefMessage ndefMessage = (NdefMessage) messages[0]; NdefRecord ndefRecord = ndefMessage. getRecords()[0]; String textReceived = new String(ndefRecord. getPayload()); Toast.makeText(this, textReceived, Toast.LENGTH_LONG).show(); } catch (Exception e) { Log.e("Błąd:", "Błąd podczas odbierania komunikatu beam.", e); } } } @Override public void onNewIntent(Intent intent) { setIntent(intent); } public void onPause() { super.onPause(); nfcAdapter.disableForegroundDispatch(this); } }
Jak widzisz, w metodzie onCreate pobierany jest adapter NFC urządzenia. Następnie komunikat setNdefPushMessage jest kojarzony z adapterem urządzenia i metoda setNdefPushMessageCallback jest wywoływana w celu zapisania w znaczniku NFC danych za pomocą komunikatu NdefMessage. Tekst, który ma być zapisany w znaczniku, jest odpowiednio zakodowany, umieszczony w rekordzie NdefRecord i ostatecznie skompilowany do komunikatu NdefMessage. W metodzie onResume odczytywane są dane ze znacznika NFC. Intencja PendingIntent jest tworzona w celu uruchomienia bieżącej aktywności, kiedy skanowany jest znacznik.
Podsumowanie
Ponadto definiowany jest obiekt klasy IntentFilter, aby filtrować pod kątem intencji nfcAdapter.ACTION_NDEF_DISCOVERED. Twoja aktywność jest ustawiana na pierwszym planie, by znacznik został do niej wysłany. Odbierane są komunikaty NdefMessage ze znacznika. Z pierwszego komunikatu odbierany jest pierwszy rekord NdefRecord. Na koniec z pierwszego rekordu NdefRecord odbierany jest ładunek (dane ze znacznika).
Podsumowanie W tym rozdziale poznałeś standard NFC i jego funkcje. Dowiedziałeś się o korzyściach stosowania znaczników NFC i poznałeś informacje, jakie mogą one wyświetlać. Przeczytałeś o różnych operacjach związanych ze znacznikami NFC. Poznałeś różne filtry intencji stosowane do wykorzystywania typów znaczników i nauczyłeś się tworzyć aplikacje czytającą znaczniki NFC. Prześledziłeś również procedurę zapisu danych w znacznikach NFC. Dowiedziałeś się, jak stosować funkcję Android Beam do udostępniania danych pomiędzy urządzeniami Android z obsługą NFC. W kolejnym rozdziale poznasz różne narzędzia do analizy i śledzenia. Dowiesz się, jak analizować aplikacje Android i wyświetlać raporty.
575
576
Rozdział 19. Android Beam
20 Analityka i śledzenie aplikacji A
byś po przygotowaniu aplikacji mógł sprawić, by stała się bardziej przyjazna dla użytkownika, musisz wiedzieć, które strony aplikacji są najczęściej używane, jakie działania podejmuje użytkownik na różnych kontrolkach oraz jak wielu użytkowników korzysta z aplikacji. Analiza informacji o aplikacji lub ich śledzenie pomogą dowiedzieć się, które sekcje są popularne wśród użytkowników, a które nie są zbyt często używane. Możesz wykorzystać tę wiedzę, by Twoja aplikacja stała się bardziej wydajna i użyteczna, co na pewno przyciągnie dodatkowych użytkowników. W tym rozdziale poznasz sposoby analizowania i śledzenia aplikacji. Dodasz do swojej aplikacji pakiet Google Analytics Software Development Kit (SDK). Będziesz również obserwował aplikacje Android za pomocą biblioteki EasyTracker. Na koniec nauczysz się korzystać z narzędzia GoogleAnalytics, by efektywnie śledzić aplikacje.
Receptura: analizowanie i śledzenie aplikacji Będąc programistą, możesz potrzebować przeanalizowania swojej aplikacji pod kątem takich kwestii jak: liczba osób aktualnie korzystających z aplikacji, poziom urządzenia, z jakiego uzyskiwany jest dostęp do aplikacji, natężenie ruchu na określonych stronach (aktywnościach) w aplikacji, działania podejmowane na różnych przyciskach lub innych kontrolkach
w aplikacji. Mówiąc w skrócie, aby dowiedzieć się, ilu użytkowników korzysta z Twojej aplikacji, musisz śledzić ją za pomocą narzędzi do analizy stron WWW. Jednym ze sposobów na śledzenie aplikacji jest zintegrowanie w niej pakietu Google Analytics SDK. Pakiet ten można wykorzystać na dwa sposoby.
578
Rozdział 20. Analityka i śledzenie aplikacji
Zastosowanie biblioteki EasyTracker — biblioteka EasyTracker jest prosta do
zintegrowania. Obsługuje dodatkowo zarządzanie sesją, automatycznie śledzi widoki aktywności oraz znacznie ułatwia śledzenie samej aplikacji. Zastosowanie narzędzia GoogleAnalytics — narzędzie GoogleAnalytics
pomaga zarządzać wieloma trackerami w aplikacji. Ponadto pomaga bardziej precyzyjnie skonfigurować opcje śledzenia. Pamiętaj, że narzędzie GoogleAnalytics jest biblioteką pośredniczącą i nie sprawdza się we wszystkich aplikacjach. Bez względu na to, którą z metod zastosujesz, informacje pochodzące ze śledzenia lub statystyki Twojej aplikacji będą periodycznie wysyłane na określone konto Google Analytics. Google Analytics jest usługą dostarczaną przez firmę Google. Usługa ta ułatwia śledzenie działań użytkowników w Twoich aplikacjach i generuje użyteczne statystyki. Możesz zalogować się na stronie WWW usługi Google Analytics i przeglądać zagregowane statystyki, generując różne typy raportów oraz analizując wykresy i tabele. Usługa Google Analytics ma kilka ograniczeń, takich jak brak danych, które mogą identyfikować unikatowego użytkownika, oraz ograniczona liczba zdarzeń w miesiącu.
Receptura: wykorzystanie biblioteki EasyTracker do śledzenia aplikacji Android Aby skorzystać z biblioteki EasyTracker, musisz najpierw dodać do swojej aplikacji pakiet Google Analytics SDK. Zakładamy, że posiadasz aplikację Android, dla której chcesz uruchomić zbieranie danych statystycznych. Dodanie pakietu Google Analytics do tej aplikacji wymaga wykonania poniższych czynności. 1. Utwórz konto dla usługi Google Analytics na stronie http://www.google.com/
analytics. Zaloguj się na to konto i kliknij zakładkę Administrator. Wybierz nazwę konta, do którego chcesz dodać nową właściwość aplikacji. Kliknij przycisk Aplikacja mobilna jako typ właściwości do śledzenia (patrz rysunek 20.1). Do śledzenia aplikacji i witryn wymagane są osobne właściwości. Innymi słowy, nie możesz śledzić jednocześnie aplikacji i stron WWW, korzystając z tej samej właściwości. Wpisz nazwę aplikacji, którą chcesz śledzić. Zostaniesz poproszony o wybranie kategorii branży oraz strefy czasowej raportowania. Na koniec kliknij przycisk Pobierz identyfikator śledzenia. 2. Usługa Google Analytics wygeneruje identyfikator śledzenia, który będzie
wykorzystywany do śledzenia Twojej aplikacji. Poza identyfikatorem śledzenia, wyświetlone zostaną odnośniki do pobrania pakietu Google Analytics dla systemów Android oraz iOS. Możesz również opcjonalnie pobrać pakiet Google Analytics zintegrowany z platformą reklamową AdMob (patrz rysunek 20.2).
Receptura: wykorzystanie biblioteki EasyTracker do śledzenia aplikacji Android
Rysunek 20.1. Okno dialogowe służące do wprowadzenia informacji o aplikacji Android, która ma być śledzona
Identyfikator śledzenia wyświetlany jest w formacie UA-xxxxxxx-yy, gdzie x i y reprezentują różne cyfry. Wygenerowany identyfikator śledzenia jest wykorzystywany w Twojej aplikacji do przesyłania statystyk na określone konto usługi Google Analytics. 3. Pobierz pakiet Google Analytics SDK (jeśli nie zrobiłeś tego wcześniej).
Następnie rozpakuj plik ZIP. W tej recepturze wykorzystasz projekt Android o nazwie ActionBarTabApp, który utworzyłeś w rozdziale 3., „Paski akcji w działaniu”. Pewnie pamiętasz, że ActionBarTabApp to aplikacja składająca się z dwóch fragmentów — Utwórz i Zaktualizuj. Po uruchomieniu aplikacja wyświetla dwie zakładki, Utwórz i Zaktualizuj. Po wybraniu którejś z zakładek wywoływany jest odpowiedni fragment, a jego zawartość wyświetlana na ekranie.
579
580
Rozdział 20. Analityka i śledzenie aplikacji
Rysunek 20.2. Okno dialogowe wyświetlające wygenerowany identyfikator śledzenia
4. Uruchom środowisko Eclipse IDE i otwórz aplikację ActionBarTabApp.
Z rozpakowanego pliku ZIP pakietu Google Analytics SDK skopiuj plik libGoogleAnalyticsV2.jar do podkatalogu /libs projektu Android ActionBarTabApp. 5. Aby dodać pakiet Google Analytics SDK do projektu Android, otwórz okno
dialogowe właściwości projektu, klikając prawym przyciskiem myszy projekt widoczny w oknie Package Explorer i wybierając z listy opcję Properties (właściwości). W oknie dialogowym Properties w ustawieniach Java Build Path wybierz zakładkę Libraries (biblioteki) i kliknij przycisk Add JARs (dodaj pliki jar). Wybierz plik libGoogleAnalyticsV2.jar z katalogu libs Twojego projektu. Kliknij przycisk OK, aby zamknąć okno dialogowe Properties. Wybrany plik jar pakietu Google Analytics SDK zostanie dodany do Twojego projektu, tak jak pokazano na rysunku 20.3. Wykonując powyższe czynności, dodałeś do swojej aplikacji pakiet Google Analytics. Teraz dowiesz się, w jaki sposób korzystać z biblioteki EasyTracker do śledzenia aplikacji i wysyłania danych na Twoje konto w usłudze Google Analytics. Musisz ustawić zezwolenia dla projektu. Ponieważ usługa Google Analytics przesyła statystyki przez Internet, do pliku AndroidManifest.xml należy dodać dwa następujące zezwolenia:
Receptura: wykorzystanie biblioteki EasyTracker do śledzenia aplikacji Android
Rysunek 20.3. Okno dialogowe właściwości projektu umożliwiające dodanie pliku jar pakietu Google Analytics
W tej recepturze będziesz śledził charakterystykę działania aplikacji na poziomie stron oraz liczbę zdarzeń kliknięcia. Aby generować zdarzenia kliknięcia, dodasz do swojego projektu Android ActionBarTabApp kontrolki Button. Mówiąc ściślej, dodasz do aplikacji dwa przyciski z nagłówkami Przycisk start i Przycisk stop. Po dodaniu tych dwóch kontrolek Button plik układu głównej aktywności activity_action_bar_tab_app.xml będzie wyglądał tak, jak przedstawiono w listingu 20.1. Listing 20.1. Kod wpisany w pliku układu aktywności activity_action_bar_tab_app.xml
581
582
Rozdział 20. Analityka i śledzenie aplikacji
Jak widzisz, w pliku układu kontener LinearLayout i dwie kontrolki Button są zagnieżdżone w nadrzędnym kontenerze LinearLayout. Zagnieżdżony kontener LinearLayout pełni rolę kontenera fragmentu i zostanie wykorzystany do wyświetlenia zawartości fragmentu, którego zakładka zostanie kliknięta. Kontrolkom Button przypisano odpowiednio nagłówki Przycisk start i Przycisk stop. Nagłówki będą wyświetlane czcionką o rozmiarze zdefiniowanym w zasobie wymiarów text_size. W celu uzyskania dostępu do kontrolek Button w kodzie Java przypisano im odpowiednio identyfikatory start_button i stop_button. Aby skonfigurować i zainicjować opcje śledzenia dla biblioteki EasyTracker, potrzebujesz pliku XML. Do folderu res/values dodaj plik XML o nazwie analytics.xml. W tym pliku wpisz kod przedstawiony w listingu 20.2. Listing 20.2. Kod wpisany w pliku analytics.xml UA-xxxxxxxx-y true true true
#1 #2 #3 #4
Oto znaczenie parametrów biblioteki EasyTracker wykorzystanych w powyższym pliku XML. Instrukcja #1 definiuje kod śledzenia. Wpisz w tej instrukcji swój identyfikator
śledzenia. Instrukcja #2 automatycznie śledzi aktywności. Instrukcja #3 śledzi wyjątki, które nie zostały wyłapane. Instrukcja #4 włącza debugowanie. Informacje z debugowania będą zapisywane
w dzienniku. Poza przedstawionymi powyżej parametrami, możesz zastosować dodatkowo następujące parametry biblioteki EasyTracker. ga_appName — wykorzystywany do zdefiniowania nazwy aplikacji, która ma być
śledzona. ga_dispatchPeriod — ustawia w sekundach interwał czasowy dla wysyłania
statystyk aplikacji (zdarzeń śledzenia) na konto usługi Google Analytics. ga_sessionTimeout — ustawia czas sesji w sekundach. Domyślna wartość tego
atrybutu wynosi 30 sekund. Zastosowanie wartości ujemnej powoduje wyłączenie funkcji zarządzania sesją w bibliotece EasyTracker. Aby zaimplementować śledzenie, zmodyfikuj plik aktywności Java ActionBarTabAppActivity w sposób przedstawiony w listingu 20.3.
Receptura: wykorzystanie biblioteki EasyTracker do śledzenia aplikacji Android
Dodane zostały jedynie fragmenty kodu zaznaczone pogrubioną czcionką. Reszta kodu pozostaje taka sama jak w listingu 3.16 z rozdziału 3. Listing 20.3. Kod wpisany w pliku aktywności Java ActionBarTabAppActivity package com.androidtablet.actionbartabapp; import import import import import import import import import import import import
android.os.Bundle; android.app.Activity; android.app.ActionBar; android.app.ActionBar.Tab; android.app.FragmentTransaction; android.util.Log; android.app.FragmentManager; android.app.Fragment; com.google.analytics.tracking.android.EasyTracker; android.widget.Button; android.view.View; android.view.View.OnClickListener;
public class ActionBarTabAppActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_action_bar_tab_app); Fragment createFragment = new CreateActivity(); Fragment updateFragment = new UpdateActivity(); ActionBar actionBar = getActionBar(); actionBar.setNavigationMode( ActionBar.NAVIGATION_MODE_TABS); actionBar.setDisplayShowTitleEnabled(true); ActionBar.Tab CreateTab = actionBar.newTab(). setText("Utwórz"); ActionBar.Tab UpdateTab = actionBar.newTab(). setText("Zaktualizuj"); CreateTab.setTabListener(new MyTabsListener( createFragment)); UpdateTab.setTabListener(new MyTabsListener( updateFragment)); actionBar.addTab(CreateTab); actionBar.addTab(UpdateTab); Button startButton = (Button) findViewById( R.id.start_button); startButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View view) { EasyTracker.getTracker().trackEvent("Przyciski", "Kliknięcia", "Przycisk start", 0L); } }); Button stopButton = (Button) findViewById( R.id.stop_button); stopButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View view) {
583
584
Rozdział 20. Analityka i śledzenie aplikacji EasyTracker.getTracker().trackEvent("Przyciski", "Kliknięcia", "Przycisk stop", 0L); } }); } @Override public void onStart() { super.onStart(); EasyTracker.getInstance().activityStart(this); EasyTracker.getTracker().trackView("/CreateActivity"); EasyTracker.getTracker().trackView("/UpdateActivity"); } @Override public void onStop() { super.onStop(); EasyTracker.getInstance().activityStop(this); } protected class MyTabsListener implements ActionBar.TabListener { Fragment fragment; public MyTabsListener(Fragment fragment){ this.fragment = fragment; } public void onTabSelected(Tab tab, FragmentTransaction ft) { ft.replace(R.id.fragment_container, fragment, null); } public void onTabUnselected(Tab tab, FragmentTransaction ft) { ft.remove(fragment); getFragmentManager().popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE); } public void onTabReselected(Tab tab, FragmentTransaction ft) { Log.d("Zakładka", String.valueOf(tab.getPosition()) + " wybrana ponownie"); } } }
Jak widzisz, w metodzie onStart() pobierana jest instancja biblioteki EasyTracker, która jest kierowana do rozpoczęcia śledzenia aplikacji. Biblioteka EasyTracker może śledzić, poza całą aplikacją, różne jej sekcje. W celu śledzenia odsłon stron aplikacji zastosowano metodę trackView(). Metoda ta jest wywoływana do śledzenia odsłon stron dwóch fragmentów — CreateActivity i UpdateActivity.
Receptura: wykorzystanie biblioteki EasyTracker do śledzenia aplikacji Android
W metodzie onStop() biblioteka EasyTracker jest instruowana, aby zatrzymać śledzenie aplikacji. Aby dowiedzieć się, które akcje (przyciski lub opcje) są wykorzystywane w Twojej aplikacji, musisz zaimplementować w niej śledzenie zdarzeń. W tej aplikacji śledzenie zdarzeń zostało zaimplementowane na dwóch kontrolkach Button. Do śledzenia zdarzeń dwóch kontrolek Button wywoływana jest metoda trackEvent() w metodzie wywołania zwrotnego onClick() nasłuchiwacza zdarzeń setOnClickListener, który jest powiązany z tymi kontrolkami. Składnia metody trackEvent() jest następująca: trackEvent(kategoria, akcja, etykieta, wartość) kategoria — reprezentuje grupę obiektów do śledzenia, np. grupę przycisków
lub pól zaznaczania. akcja — reprezentuje akcję, którą chcesz śledzić w danej grupie obiektów, np.
kliknięcia lub upływ czasu. W rzeczywistości akcja jest połączona z parametrem kategoria w celu identyfikacji śledzonych obiektów. etykieta — reprezentuje nazwę lub nagłówek śledzonego obiektu. Jeśli nagłówkiem
kontrolki Button jest np. Przycisk start, można w tej metodzie użyć nagłówka jako etykiety. wartość — reprezentuje wartość liczbową dotyczącą zdarzenia. Parametr
wykorzystywany jest do obliczenia średniej liczby akcji. Wartość tego parametru to zazwyczaj 0. Poniższa instrukcja śledzi zdarzenia na kontrolce Przycisk start danej aplikacji: EasyTracker.getTracker().trackEvent("Przyciski", "Kliknięcia", "Przycisk start", 0L);
Kiedy zastosujesz metodę trackEvent(), usługa Google Analytics będzie śledzić zdarzenia na kontrolce Button i pokazywać całkowitą liczbę wystąpień zdarzenia (kliknięcia), całkowitą liczbę użytkowników, którzy spowodowali to zdarzenie, oraz średnią liczbę wystąpień zdarzenia przypadającą na pojedynczego użytkownika. Jesteś już gotowy do śledzenia swojej aplikacji. Po jej uruchomieniu zobaczysz w pasku akcji dwie zakładki (Utwórz i Zaktualizuj) oraz dwie kontrolki Button z nagłówkami odpowiednio Przycisk start i Przycisk stop. Po kliknięciu zakładki Utwórz wyświetlony zostanie komunikat To jest fragment tworzenia, co wskazuje, że wywołany został fragment tworzenia (patrz rysunek 20.4, górny obrazek). Analogicznie, kiedy klikniesz zakładkę Zaktualizuj, wywołany zostanie fragment aktualizacji, a komunikat na ekranie zmieni się na To jest fragment aktualizacji, co pokazano na rysunku 20.4 (środkowy obrazek). Po kliknięciu tych dwóch zakładek oraz dwóch kontrolek Button (Przycisk start i Przycisk stop) informacje dotyczące śledzenia zostaną wyświetlone za pomocą komunikatów dziennika (patrz rysunek 20.4, dolny obrazek).
585
586
Rozdział 20. Analityka i śledzenie aplikacji
Rysunek 20.4. Tekst wyświetlany po kliknięciu zakładki Utwórz (górny obrazek). Tekst wyświetlany po kliknięciu zakładki Zaktualizuj (środkowy obrazek). Komunikaty dziennika wyświetlane po kliknięciu zakładek i przycisków (dolny obrazek)
Receptura: wykorzystanie narzędzia GoogleAnalytics do śledzenia aplikacji Android
Receptura: wykorzystanie narzędzia GoogleAnalytics do śledzenia aplikacji Android GoogleAnalytics to narzędzie, które obsługuje globalny stan usługi Google Analytics
w aplikacji, zarządza wieloma trackerami, konfiguruje ustawienia, takie jak tryb debugowania, oraz przydziela interwały w celu przesłania danych na konto Google Analytics. Aby zrozumieć sposób wykorzystania narzędzia GoogleAnalytics do śledzenia aplikacji Android, posłużymy się aplikacją ActionBarTabApp zastosowaną w poprzedniej recepturze. W tej recepturze nie będziesz potrzebował pliku analytics.xml, który utworzyłeś w folderze res/values. Zmodyfikuj plik ActionBarTabAppActivity.java, aby wyglądał tak, jak w listingu 20.4. Zmienione zostały jedynie fragmenty kodu zaznaczone pogrubioną czcionką. Reszta pozostaje taka sama jak w listingu 20.3. Listing 20.4. Kod wpisany w pliku aktywności Java ActionBarTabAppActivity package com.androidtablet.actionbartabapp; import import import import import import import import import import import import import import
android.os.Bundle; android.app.Activity; android.app.ActionBar; android.app.ActionBar.Tab; android.app.FragmentTransaction; android.util.Log; android.app.FragmentManager; android.app.Fragment; android.widget.Button; android.view.View; android.view.View.OnClickListener; android.content.Context; com.google.analytics.tracking.android.Tracker; com.google.analytics.tracking.android.GoogleAnalytics;
public class ActionBarTabAppActivity extends Activity { Tracker tracker; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_action_bar_tab_app); Fragment createFragment = new CreateActivity(); Fragment updateFragment = new UpdateActivity(); ActionBar actionBar = getActionBar(); actionBar.setNavigationMode(ActionBar. NAVIGATION_MODE_TABS); actionBar.setDisplayShowTitleEnabled(true); ActionBar.Tab CreateTab = actionBar.newTab(). setText("Utwórz"); ActionBar.Tab UpdateTab = actionBar.newTab(). setText("Zaktualizuj"); CreateTab.setTabListener(new MyTabsListener( createFragment)); UpdateTab.setTabListener(new MyTabsListener( updateFragment)); actionBar.addTab(CreateTab);
587
588
Rozdział 20. Analityka i śledzenie aplikacji actionBar.addTab(UpdateTab); Button startButton = (Button) findViewById( R.id.start_button); startButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View view) { tracker.trackEvent( "Przyciski", "Kliknięcia", "Przycisk start", 0L); } }); Button stopButton = (Button) findViewById( R.id.stop_button); stopButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View view) { tracker.trackEvent( "Przyciski", "Kliknięcia", "Przycisk stop", 0L); } }); } @Override public void onStart() { super.onStart(); Context context = this; GoogleAnalytics analyticsInstance = GoogleAnalytics. getInstance(context.getApplicationContext()); analyticsInstance.setDebug(true); tracker = analyticsInstance.getTracker("UA-xxxxxxxx-y"); analyticsInstance.setDefaultTracker(tracker); tracker.trackView("/CreateActivity"); tracker.trackView("/UpdateActivity"); } @Override public void onStop() { super.onStop(); } protected class MyTabsListener implements ActionBar.TabListener { Fragment fragment; public MyTabsListener(Fragment fragment){ this.fragment = fragment; } public void onTabSelected(Tab tab, FragmentTransaction ft) { ft.replace(R.id.fragment_container, fragment, null); } public void onTabUnselected(Tab tab, FragmentTransaction ft) { ft.remove(fragment); getFragmentManager().popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE); }
Podsumowanie public void onTabReselected(Tab tab, FragmentTransaction ft) { Log.d("Zakładka", String.valueOf(tab.getPosition()) + "wybrana ponownie"); } } }
Jak widzisz, w metodzie onStart() pobierane jest narzędzie GoogleAnalytics z wykorzystaniem bieżącego kontekstu. Następnie włączany jest tryb debugowania, aby informacje z debugowania były zapisywane w dzienniku. Za pomocą metody getTracker() tworzony jest nowy tracker na podstawie wygenerowanego identyfikatora śledzenia. Nowy tracker jest ustawiany jako domyślny przy użyciu metody setDefaultTracker(). Do śledzenia odsłon stron aplikacji wykorzystywana jest metoda trackView(). Odsłony stron dla dwóch fragmentów (CreateActivity oraz UpdateActivity) są śledzone za pomocą metody trackView(). W celu śledzenia zdarzeń dwóch kontrolek Button (Przycisk start i Przycisk stop) wywoływana jest metoda trackEvent() w metodzie wywołania zwrotnego onClick() nasłuchiwacza zdarzeń setOnClickListener, który jest powiązany z tymi kontrolkami. Zdarzenia kliknięcia na kontrolkach Button będą śledzone. W ten sposób pozyskasz informacje, takie jak całkowita liczba wystąpień zdarzenia kliknięcia na przyciskach oraz całkowita liczba użytkowników, którzy kliknęli te kontrolki.
Uwaga Chociaż w narzędziu GoogleAnalytics można stosować wiele trackerów, jeden z nich musi być domyślny. Jako domyślny traktowany jest pierwszy utworzony tracker.
Podsumowanie W tym rozdziale nauczyłeś się analizować aplikacje Android i implementować w nich opcje śledzenia. Zobaczyłeś, w jaki sposób jest wykorzystywana do śledzenia biblioteka EasyTracker. Poznałeś również rolę narzędzia GoogleAnalytics w poprawianiu wydajności śledzenia. Starałem się, aby zawartość tej książki była łatwa do zrozumienia. Posiadasz już wszystkie niezbędne informacje do rozwiązywania różnych problemów, które możesz napotkać podczas tworzenia i aktualizowania aplikacji przeznaczonych na tablety z systemem Android. Dobrej zabawy przy tworzeniu własnych aplikacji. Dziękuję za lekturę!
589
590
Rozdział 20. Analityka i śledzenie aplikacji
Skorowidz A ActionBar, Patrz: pasek akcji activity, Patrz: aktywność adapter ArrayAdapter, 145, 166, 168 baseadapter, 145 ImageAdapter, 162 NfcAdapter, 563 SimpleCursorAdapter, 145 adres URI, 232, 237, 251 URL, 489, 491, 496 ADT, 32 akceleracja sprzętowa, 309, 323, 324 włączanie, 310 wyłączanie, 310 akcelerometr, 445, 446, 447, 450, 461 akcja element, 119, 121, 123 dodawanie, 124 wyświetlanie, 121 MediaStore.ACTION_ IMAGE_CAPTURE, 351, 352, 354 MediaStore.ACTION_ VIDEO_CAPTURE, 379 MediaStore.Audio.Media. RECORD_SOUND_ ACTION, 362, 363, 365
pasek, Patrz: pasek akcji widok, 121 niestandardowy, 127 zwijanie, 128 aktywność, 49, 68, 429 cykl życia, 49, 51 danych przekazywanie, 58, 59 inicjowanie, 50, 53, 54 konfiguracyjna widżetu, 542 kończenie, 50, 53 na pierwszym planie, 50 nazwa, 34 niszczenie, 50 stos, Patrz: stos aktywności tytuł, 121 w tle, 50 zawieszanie, 50, 52 algorytm szeregowania wątków, Patrz: wątek planista Allocation Tracker, 426 Android Beam, 555, 570, 572 przesyłanie danych, 571 Android Developer Tools, Patrz: ADT Android Support Library, 511, 518, 521 Android Virtual Device, Patrz: AVD Androida wersja, 513
animacja, 257, 258, 267 alfa, 283, 287 długość, 258 generowania klatek pośrednich, 258, 283, 286 interpolator, Patrz: interpolator liczba powtórzeń, 258 odwracanie, 257, 259 poklatkowa, 258, 279 powtarzanie, 257, 259 przesunięcia, 283, 287, 297 restart, 259 rotacji, 283, 288, 289 sekwencja, Patrz: animacja złożona skalowania, 283, 289 układu, 258, 293, 296 widoku, 257, 258 właściwości, 257 złożona, 273, 275, 301 API, 327, 393 OpenGL ES, Patrz: OpenGL ES poziom uruchomienia aplikacji, 221, 310, 514, 515, 518 aplikacja analiza, 577 asynchroniczna, 225 ekranu głównego, 535 logo, 119, 121, 126
592
Android na tablecie. Receptury
aplikacja personalizowanie, 109 strona główna, 126 śledzenie, 577 Widget Preview, 542 wyłącznie dla tabletów, 48 app widget, Patrz: widżet aplikacji application programming interface, Patrz: API atrybut checkable, 136 checkableBehavior, 137 match_parent, 27 minSdkVersion, 120, 514, 517, 520, 566 requiresSmallestWidthDp, 48 resizeMode, 541 showAsAction, 132 targetSdkVersion, 514, 520, 566 updatePeriodMillis, 541 windowActionBarOverlay, 120 wrap_content, 27 audio, 362, 372 kodowanie, 365, 372 nagrywanie, 363, 365, 373 przechwytywanie, 365, 372 AVD, 27
B background fragment, Patrz: fragment w tle biblioteka EasyTracker, 577, 578, 582, 585 json.org, 465 bitmapa, Patrz też: obraz alternatywna, 513 Bluetooth, 393 łączenie urządzeń, 394
łączenie z komputerem z systemem Windows, 399 przesyłanie plików, 397, 410 zasięg, 556 broadcast receiver, Patrz: odbiornik rozgłoszeniowy
C clip, Patrz: wycinek ContentResolver, 242, 251 czujnik ciśnienia atmosferycznego, 446 grawitacji, 446 odległości, 446 orientacji, 445 oświetlenia, 446, 455 pola magnetycznego, 445, 446 przyspieszenia liniowego, 446 siły ciężkości, 461 temperatury, 446 wilgotności, 446 zbliżeniowy, 443, 445, 455 czytnik kart elektronicznych, 556
D Dalvik, 426 Dalvik Debug Monitor Service), Patrz: DDMS DDMS, 426, 427 debuger, 427 density, Patrz: gęstość dialog asynchroniczny, 102 modalny, 103, 109 synchroniczny, 102
dostawca kontaktów, 232 treści, 225, 226, 227 Browser, 227 CallLog, 227 Contacts, 225, 227, 228 dodawanie wiersza, 242 funkcjonalność, 237 Media Store, 227 niestandardowy, 233, 234, 243, 246, 248, 252 Settings, 227 utrzymywanie zawartości, 246, 248 dp, 27, 512 drag and drop, Patrz: operacja przeciągnij i upuść drop zone, Patrz: strefa upuszczania
E ekran, 68, 511 duży, 26, 37, 48, 512 ekstraduży, 26, 37, 48, 280, 512 gęstość, Patrz: gęstość mały, 26, 512 normalny, 26, 512 orientacja, 524, 525, 526, 527, 528, 529, 531 preferencji, 109 rozdzielczość pikselowa, Patrz: rozdzielczość pikselowa szerokość, 68 najmniejsza, 38 wysokość, 68 element rysowalny, 513 supports-screens, 48 explicit intent, Patrz: intencja jawna
Skorowidz
F flat color, Patrz: kolor jednoodcieniowy folder assets, 36 bin, 36 gen, 34 gen/com.androidtablet. firstandroidtabletapp/R. java, 35 res, 36 res/anim, 281 res/drawable-hdpi, 36 res/drawable-ldpi, 36 res/drawable-mdpi, 36 res/drawable-xhdpi, 36 res/drawable-xxhdpi, 36 res/layout, 36, 529 res/layout/activity_first_ android_tablet_app.xml, 36 res/layout-land, 529 res/layout-large/, 37 res/layout-sw600dp/, 37 res/layout-sw720dp/, 38 res/layout-xlarge/, 37 res/menu, 36 res/values, 36, 236 res/values-sw600dp, 236, 280 res/values-sw720dp, 236, 280 res/values-v11, 37 res/values-v14, 37 src, 34 src/com.androidtablet. firstandroidtabletapp, 34 src/com.androidtablet. firstandroidtabletapp/ FirstAndroidTablet AppActivity.java, 34 foreground fragment, Patrz: fragment pierwszego planu
format JSON, Patrz: JSON NDEF, Patrz: NDEF fragment, 67, 68, 512 cykl życia, 68 DialogFragment, 103 dołączanie do aktywności, 70 interfejs użytkownika, 67 komunikacja, 94 ListFragment, 98, 99 menedżer, Patrz: menedżer fragmentów nieaktywny, 70 niewidoczny dla użytkownika, 70 niszczenie, 70 odłączanie od aktywności, 70 pierwszego planu, 70 PreferenceFragment, 109 wyświetlanie przy użyciu kodu, 110 wyświetlanie za pomocą pliku XML, 109 stos, Patrz: stos fragmentów tworzenie, 70, 78 dynamiczne, 85, 86 statyczne, 86 usuwanie, 78 w tle, 70 widoczny dla użytkownika, 70 widoku tworzenie, 70 zapisywanie, 70
G garbage collector, Patrz: pamięć odzyskiwanie gęstość, 25, 36, 511, 512 hdpi, 25, 513 ldpi, 513
mdpi, 25, 513 xhdpi, 25, 513 xxhdpi, 513 Google Analytics, 578, 587 Google Analytics SDK, 577, 579 Google Nexus 7, 10, 23, 24 Google Play, 572 GPU, 309, 327 grafika, 309, 327 2D, 327 3D, 327 optymalizacja, 382 przesuwanie, 346 renderowanie, 327, 328, 339 rotacja, 337, 339, 340, 342 skalowanie, 342 graphical user interface, Patrz: GUI GUI, 309, 317
H home screen widget, Patrz: widżet ekranu głównego
I implicit intent, Patrz: intencja niejawna intencja, 49, 53, 351 dane, 207 rozszerzone, 60 filtr, 561 jawna, 53 nasłuchiwanie, 205, 208 niejawna, 54 oczekująca, 205, 206, 218, 548 PendingIntent, 548, 553 regularna, 205 rozgłaszanie, 205, 207, 209, 212 wiązka dodatkowa, 563
593
594
Android na tablecie. Receptury
intent, Patrz: intencja interfejs CharSequence, 198 definiowanie, 90 LoaderManager.Loader Callbacks, 225 programowania aplikacji, Patrz: API SpinnerAdapter, 145 SurfaceHolder, 321 SurfaceHolder.Callback, 317, 356, 357, 382 TextureView.Surface TextureListener, 323 TypeEvaluator, 260 użytkownika, Patrz: UI graficzny, Patrz: GUI fragmentu, Patrz: fragment interfejs użytkownika kontrolka, 26, 40, 70 View.OnDragListener, 178 Internet, 490 interpolator, 257, 296, 300 accelerate_interpolator, 298 AccelerateDecelerate Interpolator, 300 AccelerateInterpolator, 300, 305 AnticipateInterpolator, 300 AnticipateOvershoot Interpolator, 300 BounceInterpolator, 300 CycleInterpolator, 300 DecelerateInterpolator, 300 LinearInterpolator, 300 liniowy, 296 OvershootInterpolator, 300
J JavaScript Object Notation, Patrz: JSON jednostka dp, 27, 512 JSON, 465 przechowywanie informacji, 468 tablica, Patrz: tablica JSONArray
K kalendarz, 151 kamera, 323, 325, 326, 351 domyślna, 324 karta czytnik, 556 kredytowa, 556 SD, 360, 375, 379 klasa AlarmManager, 551 AlphaAnimation, 286, 287 AnimationDrawable, 283 AnimationSet, 301 AnimatorSet, 273, 275 AppWidgetProvider, 536 AsyncTask, 425, 437, 438 BluetoothAdapter, 400 BroadcastReceiver, 208, 211, 536, 538, 545 Build, 514, 515 Button, 153 CalendarView, 153 CamcorderProfile, 365, 368, 388 Camera, 356 ClipboardManager, 198 ClipData, 178, 198 ContentResolver, 237, 251 Context, 208 CursorLoader, 225, 228 DialogFragment, 102
DragEvent, 178 DragShadowBuilder, 178 FragmentActivity, 524 FragmentManager, 83 FragmentTransaction, 83, 85 GLSurfaceView, 328 GLSurfaceView.Renderer, 328 Handler, 430 JsonReader, 478, 479 JsonWriter, 478, 479 LayoutAnimation Controller, 293, 295, 296 ListFragment, 98 LoaderManager, 225 MediaRecorder, 362, 372, 373, 379, 382 MyGLSurfRenderer, 336 Notification, 215 Notification.Builder, 216 NotificationManager, 215, 218 ObjectAnimator, 267 ObjectAnimator, 258, 267, 271 PendingIntent, 205, 206 PreferenceFragment, 109 RemoteViews, 536 RotateAnimation, 286, 288, 289 ScaleAnimation, 286, 289 SensorManager, 445, 450 SharedPreferences, 109 SQLiteQueryBuilder, 237 SurfaceTexture, 323 SurfaceView, 309, 317, 323, 356, 357, 382 TextureView, 323 TranslateAnimation, 286, 287, 305 ValueAnimator, 257, 259 WebSettings, 490 WebView, 491
Skorowidz
WebViewClient, 489, 496 WebViewFragment, 489, 499, 502 WebViewFragment Activity, 502, 503 wewnętrzna, 550 WifiManager, 412, 414 klatka pośrednia, 258 klawiatury konfiguracja, 443 klucz username, 58 kolejka komunikatów, 430 MessageQueue, 430 kolor jednoodcieniowy, 327, 334 wieloodcieniowy, 327, 334, 337 komunikacja małego zasięgu, Patrz: NFC peer-to-peer, 556 komunikat, 536 asynchroniczny, Patrz: intencja dziennika, 130 NDEF, 557, 571 NdefMessage, 557 wymagający uwagi użytkownika, Patrz: powiadomienie konstruktor obiektu JSONObject, 466 kontakty, 227, 228, 231 kontrolka Button, 55, 58, 62, 107, 109, 151, 153, 199, 209, 210, 235, 236, 242, 248, 249, 261, 285, 352, 500 widżetu, 547 EditText, 109, 165, 170, 235, 236, 248, 249, 250, 496, 500 Fragment, 500 GridView, 188
ImageView, 160, 161, 162, 275, 285, 352 skalowanie, 301 interfejsu użytkownika, Patrz: interfejs użytkownika kontrolka kotwiczenie, 524, 525 ListView, 98, 99, 179, 231 rozmiar elementów, 166 selectedopt TextView, 71 TextView, 44, 47, 55, 62, 71, 98, 107, 140, 141, 154, 275, 492, 500 animacja, 267, 272 animowanie, 262 ToggleButton, 272, 279, 283 WebView, 489, 490, 496, 499, 501, 503 kursor, 225
L lista opcji, 165, 170 loader, Patrz: ładowarka
Ł ładowarka, 225 wywołanie zwrotne, 226
M magnetometr, Patrz: czujnik pola magnetycznego maszyna wirtualna Dalvik, 426 urządzenia fizycznego, 427 mechanizm ContentResolver, Patrz: ContentResolver
menedżer fragmentów, 83 pakietów, 521 menu, 119 przepełnienia, 121 metoda adaptera, 162 addCallback, 321 addPreferencesFrom Resource, 113 AsyncTasksAppActivity. java, 439 beginObject, 479 beginTransaction, 85 canGoBack, 490 canGoBackOrForward, 491 canGoForward, 490 clearCache, 491 clearHistory, 491 Context.getSystemService, 445 delete, 237, 251 doInBackground, 438, 440 enableForeground Dispatch, 563 endObject, 479 get, 467, 474 getAction, 178, 208 getActionBar, 120 getActivity, 74, 206 getAddress, 402 getAnimatedValue, 259 getBondedDevices, 407 getBoolean, 115 getCount, 162 getDefaultAdapter, 400 getDefaultSensor, 445, 448, 450 getExtras, 60 getFragmentManager, 83 getHolder, 321 getInt, 115 getItem, 162
595
596
Android na tablecie. Receptury
getItemId, 134, 162 getLoaderManager, 225 getName, 402 getPrimaryClip, 198 getSensorList, 448 getSettings, 490 getState, 402 getString, 115 getStringExtra, 208 getSurfaceTexture, 323 getSystemService, 198, 218, 412, 418 getType, 237 getWifiState, 414 glClear, 333 glClearColor, 330, 331, 332 glColorPointer, 336 glDrawArrays, 331, 333 glEnableClientState, 332 glLoadIdentity, 341 glMatrixMode, 341 glPopMatrix, 344 glPushMatrix, 344 glRotatef, 341 glScalef, 344 glVertexPointer, 333 goBack, 490 goBackOrForward, 491 goForward, 490 handleMessage, 430 has, 467 hasNext, 479 initLoader, 225, 226 insert, 237 invalidate, 260, 313 isEnabled, 402 isHardwareAccelerated, 310 isNull, 467 isWifiEnabled, 414 join, 474 length, 467, 474 loadURL, 490 name, 479
nextName, 479 nextString, 479 notifyChange, 237 ofFloat, 259, 265, 267, 269 ofInt, 259, 267 ofObject, 259, 267 onAccuracyChanged, 447, 454 onActivityCreated, 70 onAttach, 70 onCancelled, 438 onCreate, 50, 70, 241, 321 onCreateLoader, 226 onCreateView, 70, 73, 104 onDeleted, 538, 542, 544 onDestroy, 50, 70 onDestroyView, 70 onDetach, 70 onDisabled, 538, 542, 544 onDrawFrame, 328, 331, 334 onEditorAction, 495 onEnabled, 538, 542 onItemClick, 81 onKey, 495 onListItemClick, 246, 252 onLoaderReset, 226 onLoadFinished, 226 onNavigationItem Selected, 146 onOptionItemSelected, 126 onOptionSelected, 82, 92 onOptionsItemSelected, 134 onPause, 50, 70, 321 onPauseMySurfaceView, 321, 322 onPostExecute, 438, 440 onPreExecute, 438, 440 onProgressUpdate, 438, 440 onQueryTextChange, 128, 130
onQueryTextSubmit, 128 onReceive, 209, 211, 542 onResume, 50, 70, 321 onSensorChanged, 447, 454 onStart, 50, 70 onStop, 50, 70 onSurfaceChanged, 328, 331, 334 onSurfaceCreated, 328, 330, 334 onSurfaceTexture Available, 325 onSurfaceTexture Destroyed, 325 onSurfaceTextureSize Changed, 325 onSurfaceTexture Updated, 325 onTabSelected, 143 onTabUnselected, 144 onUpdate, 538, 542 peek, 479 PendingIntent.getActivity, 206 PendingIntent.get Broadcast, 206 PendingIntent.getService, 206 play, 273 playSequentially, 273, 277 playTogether, 273, 276 prepare, 383 publishProgress, 440 put, 467, 474 putExtra, 207 query, 237, 251 registerListener, 447 release, 356, 372, 373, 383 reload, 490 remove, 467 replace, 86 sendBroadcast, 206, 208 setAction, 207
Skorowidz
setAudioEncoder, 372, 373, 383 setAudioSource, 372, 373, 382 setAutoCancel, 216 setContentIntent, 217 setContentText, 217 setContentTitle, 217 setDisplayShowHome Enabled, 120 setDisplayShowTitle Enabled, 120 setDisplayZoomControls, 490 setDuration, 258 setJavaScriptEnabled, 490 setLayerType, 313 setListNavigationCall backs, 145 setMaxDuration, 383 setMaxFileSize, 383 setName, 402 setNavigationMode, 145 setOutputFile, 372, 373, 383 setOutputFormat, 372, 373, 382 setPreviewDisplay, 356, 372, 383 setPrimaryClip, 198 setRepeatCount, 258 setRepeatMode, 258 setSavePassword, 490 setSmallIcon, 216 setSupportZoom, 490 setTextScaleX, 265 setTextZoom, 490 setTicker, 216 setVideoEncoder, 383 setVideoFrameRate, 382 setVideoSource, 382 setWebViewClient, 503 setWhen, 216 setWifiEnabled, 414 setWrapSelectorWheel, 157
skipValue, 479 start, 283, 372, 373, 383, 385 startActivity, 54, 206 startDiscovery, 407 startDrag, 178, 179 startPreview, 356 startService, 206 statyczna, 267 stop, 283, 372, 373, 383, 385 stopPreview, 356 supportMultipleWindows, 490 surfaceChanged, 358, 384 surfaceCreated, 322, 358, 384 surfaceDestroyed, 358, 384 takePicture, 356 toString, 474 update, 237 value, 479 wywołania zwrotnego onDragEvent, 178 mikrofon, 351, 362 motyw Theme, 158 Theme.Black.NoTitleBar, 159 Theme.Holo, 120 Theme.Holo.NoAction Bar, 120 Theme_Holo, 158 Theme_Holo_Light, 158
N nasłuchiwacz zdarzeń, 177 addUpdateListener, 262 przeciągania, 178 sensora, 443, 447, 450 setOnClickListener, 153, 261, 363
setOnEditActionListener, 496 setOnErrorListener, 385 setOnInfoListener, 385 setOnQueryTextListener, 128 setOnValueChanged Listener, 157 setSurfaceTextureListener, 325 TabListener, 143 NDEF, 557 near field communication, Patrz: NFC NFC, 555, 556, 560, 570 zasięg, 556 NFC data exchange format, Patrz: NDEF NFC tag, Patrz: znacznik NFC notification, Patrz: powiadomienie
O obiekt ActionBar, 120 AnimatorSet, 273 BroadcastReceiver, 536, 542 Builder, 273 ClipData, 198, 199 ClipData.Item, 198 ClipDescription, 198 JSONObject, 465 konstruktor, 466 przechowywanie informacji, 468 pusty, 477 serializacja, 468 zagnieżdżanie, 471 pendIntent, 206 SharedPreferences, 115 tworzenie, 58
597
598
Android na tablecie. Receptury
obraz, 160, 162, 227 podgląd, 351 przechwytywanie, 351, 356, 360 przeciągnij i upuść, 188, 199 rozmiar, 163, 164, 190 zapisywanie, 360, 361 obszar powiadomień, 205, 214 odbiornik rozgłoszeniowy, 205, 207, 208, 210, 211, 212, 429 dodawanie dynamiczne, 212 okno DialogFragment, 103, 105, 107 dialogowe, 103, 105, 107, 109 LogCat, 52 modalne, 103, 109, 170 metoda, 127 onResumeMySurfaceView, 321 OpenGL ES, 327, 328 operacja kopiuj wklej, 198 przeciągnij i upuść, 177, 179 obraz, 188, 199 tekst, 179 wytnij, 198
P pamięć alokowana dynamicznie, 426 odzyskiwanie, 425, 426 para klucz-wartość, 465 pasek akcji, 119, 121, 122, 134, 143 komponenty, 121
tytuł, 121 widoczność, 120 widok niestandardowy, 127 wyświetlanie podmenu, 132 z rozwijaną listą, 145, 146 zakładka, 121, 143 systemowy, 205, 214 tytułu, 119 zadań z zakładkami, 139, 140 pending intent, Patrz: intencja oczekująca piksel, 25 niezależny od urządzenia, 25 pixel resolution, Patrz: rozdzielczość pikselowa playlista, 379 plik .jar, 35 ActionBarOnOlderApp Activity.java, 516 activity_action_bar_on_ older_app.xml, 515 activity_action_bar_ submenu.xml, 132 activity_alternate_layout_ app.xml, 528 activity_blue_tooth_ paired_list_app.xml, 405 activity_btfile_transfer_ app.xml, 410 activity_consume_jsonweb service_app xml, 484 activity_fragment_on_ older_app.xml, 518 activity_handle_orienta tion_app.xml, 525 activity_jsonreader_writer _app.xml, 480 activity_multiple_threads_ app.xml, 434
activity_pref_fragment. xml, 113 activity_sensor_acc_ app.xml, 451 activity_sensor_gyro scope_app.xml, 458 activity_web_view_app. xml, 491 activity_web_view_frag_ app.xml, 501 activity_wi_fi_app.xml, 415 AlternateLayoutApp Activity.java, 531 analytics.xml, 582 AndroidManifest.xml, 27, 37, 64, 83, 127, 233, 514 audio, 227, 365, 372 BlueToothAppActivity. java, 402 BlueToothPairedListApp Activity.java, 407 BTFileTransferApp Activity.java, 411 build, 37 dimens.xml, 37, 67, 236, 276 główny aktywności Java, 61 JSONArrayAppActivity. java, 476 JSONReaderWriterApp Activity.java, 481 konfiguracyjny, 37 konfiguracyjny widżetu, Patrz: widżet plik konfiguracyjny layout.xml, 38 manifestu, 211 multimedialny, 227 MultipleThreadsApp Activity.java, 435 MyFragmentActivity.java, 519
Skorowidz
popupmenu.xml, 170 preferences.xml, 110, 112 Proguard-project.txt, 37 przesyłanie Bluetooth, 397, 410 ReceiveBroadcastActivity. java, 211 SensorGyroscopeApp Activity.java, 459 SensorsListAppActivity. java, 449 strings.xml, 36 ThreadAppActivity.java, 431 układu, 36, 113, 242 dla tabletu, 41 dla telefonu, 41 o orientacji pionowej, 38, 44, 45 o orientacji poziomej, 38 tworzenie, 41 wideo, 227, 365, 372 WifiAppActivity.java, 416 WiFiBroadcastReceiver. java, 422 WiFiDirectAppActivity. java, 420 XML, 27 menu, 36 podmenu w pasku akcji, 132 pole RF, 555, 556 wyboru, 109 zaznaczania, 136 połączenie nieodebrane, 227 powiadomienie, 205, 215 szuflada, 205, 214 tworzenie, 215, 216, 217, 218 preference view, Patrz: widok preferencji preferencji grupowanie w kategorie, 109
procesor czterordzeniowy, 425 dwurdzeniowy, 425 graficzny, Patrz: GPU wielordzeniowy, 425 projekt Android, 32 przeglądarka, 490, 491 historia, 227, 490, 491 zakładka, 227 przycisk opcji, 109, 137 dźwięki dzwonka, 109
R radio frequency identification, Patrz: RFID rekord NdefRecord, 557, 559 RFID, 555 rozdzielczość pikselowa, 512
S schowek systemowy, 198, 199 SDK, 577 sensor, 443 dokładność, 447 sprzętowy, 445 tryb próbkowania, 455 przerwania, 455 TYPE_ACCELERO METER, 446 TYPE_AMBIENT_ TEMPERATURE, 446 TYPE_GRAVITY, 446 TYPE_GYROSCOPE, 446 TYPE_LINEAR_ACCELE RATION, 446 TYPE_MAGNETIC_ FIELD, 446 TYPE_PRESSURE, 446 TYPE_PROXIMITY, 446
TYPE_RELATIVE_ HUMIDITY, 446 TYPE_ROTATION_ VECTOR, 446 metoda, 139 sklep Google Play, Patrz: Google Play smooth coloring, Patrz: kolor wieloodcieniowy Software Development Kit, Patrz: SDK stos, 160 aktywności, 49, 94 fragmentów, 94 obrazów, 160 strefa upuszczania, 177, 178 strona WWW, 490, 491 sw, Patrz: ekran szerokość najmniejsza system powiadomień, 214 szuflada powiadomień, Patrz: powiadomienie szuflada
T tablet, 25, 38, 67 tablica JSONArray, 473, 474, 475, 478 telefon, 25, 38, 67 transmisja danych, 465
U UI, 24, 26, 70, 429, 511 controls, Patrz: interfejs użytkownika kontrolka układ, 68 alternatywny, 513, 528, 529 definiowanie, 37 dla podaktywności, 62 nazwa, 34 plik, Patrz: plik układu wieloekranowy, 26
599
600
Android na tablecie. Receptury
uniform resource identifier, Patrz: zapytanie URI URI, Patrz: zapytanie URI urządzenie Bluetooth, 393, 397, 399, 405, 408 włączanie, 400, 402 konfiguracja, 27 NFC, 555 PIN, 395 powiązane, 393, 395, 405 preferencje, 227 ustawienia, 227 wirtualne Android, Patrz: AVD user interface, Patrz: UI usługa DDMS, Patrz: DDMS sieciowa JSON, 483
W warstwa LAYER_TYPE_HARD WARE, 313, 314 LAYER_TYPE_NONE, 313, 315 LAYER_TYPE_SOFT WARE, 313 programowa, 309 sprzętowa, 309 wątek, 432 główny, 429, 430 planista, 425, 426 szeregowanie, Patrz: wątek planista w tle, 429, 437 wiadomość powitalna, 55 wideo, 372 kodowanie, 365, 372, 382 nagrywanie, 379, 382 przechwytywanie, 351, 365, 372 widok AdapterView, 293 akcji, Patrz: akcja widok
hosta, 165 preferencji, 109 CheckBoxPreference, 109, 110, 115 EditTextPreference, 109, 111, 115 ListPreference, 109, 111, 112 Preference, 109 PreferenceCategory, 109 PreferenceScreen, 109 RingtonePreference, 109, 111, 117 wyświetlanie za pomocą pliku XML, 109 przeciąganie, 177, 178 SurfaceView, 382, 385 warstwa, 313 widżet aktualizacja, 551 aktywność konfiguracyjna, 542 aplikacji, 535, 536 cykl życia, 538 host, 535 CalendarView, 151, 154 cykl życia, 542 definicja, Patrz: widżet plik konfiguracyjny ekranu głównego, 535, 539 ikona, 542 instancja, 536, 538, 545 kontrolka Button, 547 ListPopupWindow, 165, 170 ListView, 71 NumberPicker, 154, 157 plik konfiguracyjny, 539 PopupMenu, 170 SearchView, 130 StackView, 160 usuwanie, 538
WebView, Patrz: kontrolka WebView wyboru, 71, 145 wielozadaniowość, 425, 432 Wi-Fi, 393, 412, 414 Wi-Fi Direct, 418 wycinek, 198
Z zapytanie URI, 227, 228 zasób alternatywny, 513 wymiarów, 236, 276, 280, 285, 318 zaznaczanie i zamiatanie, 426 zdarzenie, 49 aplikacji, 207 nasłuchiwanie, 128, 177 onActivityCreated, 70 onAttach, 70 onCreate, 50, 70 onCreateView, 70 onDestroy, 50, 70 onDestroyView, 70 onDetach, 70 onPause, 50, 70 onResume, 50, 70 onStart, 50, 70 onStop, 50 systemowe, 207 znacznik, 555 NFC, 556, 557, 560, 561 odczyt danych, 572 zapisywanie danych, 566, 572 odczyt, 555 zapis, 555
Ż żyroskop, 443, 446, 458 dokładność, 461