124
|
Rozdz ał 3. Testy
Używana w testach klasa Main to główna aktywność projektu nadrzędnej aplikacji. W konstruktorze testów używane są nazwa pakietu głównej aplikacji i klasa głównej aktywności. Następnie można tworzyć przypadki testowe, a referencje do elementów aktywności da się uzyskać za pomocą standardowych metod interfejsu API Androida. Przedstawiony test sprawdza, czy główna aktywność obejmuje kontrolkę TextView z tekstem Witaj, świecie!.
Zobacz także Dokumentacja Androida (http://developer.android.com/tools/testing/testing_android.html).
3.6. Rozwiązywanie problemów z awariami aplikacji Ulysses Levy
Problem W aplikacji z niewiadomego powodu występuje awaria (rysunek 3.14).
Rysunek 3.14. Efekt awarii aplikacji
Rozwiązanie Należy zacząć od przejrzenia pliku dziennika.
Omówienie Po awarii aplikacji można zastosować instrukcję adb logcat lub okno LogCat w środowisku Eclipse, aby wyświetlić dziennik urządzenia AVD. Na listingu 3.3 pokazano miejsce wystąpienia awarii w śladzie stosu wyświetlonym za pomocą instrukcji adb logcat.
3.6. Rozw ązywan e problemów z awar am apl kacj
|
125
Listing 3.3. Ślad stosu zarejestrowany po odmowie uprawnień E/DatabaseUtils( 53): Writing exception to parcel E/DatabaseUtils( 53): java.lang.SecurityException: Permission Denial: writing com.android.providers.settings.SettingsProvider uri content://settings/system from pid=430, uid=10030 requires android.permission.WRITE_SETTINGS E/DatabaseUtils( 53): at android.content.ContentProvider$Transport. enforceWritePermission(ContentProvider.java:294) E/DatabaseUtils( 53): at android.content.ContentProvider$Transport. insert(ContentProvider.java:149) E/DatabaseUtils( 53): at android.content.ContentProviderNative. onTransact(ContentProviderNative.java:140) E/DatabaseUtils( 53): at android.os.Binder.execTransact(Binder.java:287) E/DatabaseUtils( 53): at com.android.server.SystemServer.init1(Native Method) E/DatabaseUtils( 53): at com.android.server.SystemServer.main(SystemServer.java:497) E/DatabaseUtils( 53): at java.lang.reflect.Method.invokeNative(Native Method) E/DatabaseUtils( 53): at java.lang.reflect.Method.invoke(Method.java:521) E/DatabaseUtils( 53): at com.android.internal.os. ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:860) E/DatabaseUtils( 53): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:618) E/DatabaseUtils( 53): at dalvik.system.NativeStart.main(Native Method) D/AndroidRuntime( 430): Shutting down VM W/dalvikvm( 430): threadid=3: thread exiting with uncaught exception (group=0x4001b188) ...
Informacje z listingu 3.3 wskazują na problem z uprawnieniami. Rozwiązaniem tego konkretnego problemu jest dodanie uprawnień WRITE_SETTINGS do pliku AndroidManifest.xml.
Inny często występujący błąd to NPE (ang. Null Pointer Exception). Na listingu 3.4 przedstawiono informacje z okna LogCat dotyczące tego problemu. Listing 3.4. Dane z okna LogCat I/ActivityManager( 53): Displayed activity com.android.launcher/.Launcher: 28640 ms (total 28640 ms) I/ActivityManager( 53): Starting activity: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10200000 cmp=com.aschyiel.disp/.Disp } I/ActivityManager( 53): Start proc com.aschyiel.disp for activity com.aschyiel.disp/.Disp: pid=214 uid=10030 gids={1015} I/ARMAssembler( 53): generated scanline__00000177:03515104_00000001_00000000 [ 73 ipp] (95 ins) at [0x47c588:0x47c704] in 2087627 ns I/ARMAssembler( 53): generated scanline__00000077:03545404_00000004_00000000 [ 47 ipp] (67 ins) at [0x47c708:0x47c814] in 1834173 ns I/ARMAssembler( 53): generated scanline__00000077:03010104_00000004_00000000 [ 22 ipp] (41 ins) at [0x47c818:0x47c8bc] in 653016 ns D/AndroidRuntime( 214): Shutting down VM W/dalvikvm( 214): threadid=3: thread exiting with uncaught exception (group=0x4001b188) E/AndroidRuntime( 214): Uncaught handler: thread main exiting due to uncaught exception E/AndroidRuntime( 214): java.lang.RuntimeException: Unable to start activity ComponentInfo{com.aschyiel.disp/com.aschyiel.disp.Disp}:java.lang.NullPointerException E/AndroidRuntime( 214): at android.app.ActivityThread.performLaunchActivity( ActivityThread.java:2496) E/AndroidRuntime( 214): at android.app.ActivityThread.handleLaunchActivity( ActivityThread.java:2512) E/AndroidRuntime( 214): at android.app.ActivityThread.access$2200( ActivityThread.java:119)
126
|
Rozdz ał 3. Testy
E/AndroidRuntime( 214): at android.app.ActivityThread$H.handleMessage( ActivityThread.java:1863) E/AndroidRuntime( 214): at android.os.Handler.dispatchMessage(Handler.java:99) E/AndroidRuntime( 214): at android.os.Looper.loop(Looper.java:123) E/AndroidRuntime( 214): at android.app.ActivityThread.main(ActivityThread.java:4363) E/AndroidRuntime( 214): at java.lang.reflect.Method.invokeNative(Native Method) E/AndroidRuntime( 214): at java.lang.reflect.Method.invoke(Method.java:521) E/AndroidRuntime( 214): at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run( ZygoteInit.java:860) E/AndroidRuntime( 214): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:618) E/AndroidRuntime( 214): at dalvik.system.NativeStart.main(Native Method) E/AndroidRuntime( 214): Caused by: java.lang.NullPointerException E/AndroidRuntime( 214): at com.aschyiel.disp.Disp.onCreate(Disp.java:66) E/AndroidRuntime( 214): at android.app.Instrumentation.callActivityOnCreate( Instrumentation.java:1047) E/AndroidRuntime( 214): at android.app.ActivityThread.performLaunchActivity( ActivityThread.java:2459) E/AndroidRuntime( 214): ... 11 more
Oto przykładowy kod, który może prowadzić do błędu tego rodzaju: import ... public class Disp extends Activity { private TextView foo; @Override public void onCreate( Bundle savedInstanceState ) { ... foo.setText("bar"); } }
Kod prowadzi do problemów, ponieważ nie wywołano instrukcji findViewById(). Przykładowy kod można naprawić w następujący sposób: import ... public class Disp extends Activity { private TextView foo; @Override public void onCreate( Bundle savedInstanceState ) { ... foo = (TextView) findViewById( R.id.id_foo ); foo.setText("bar"); } }
Wprowadzone poprawki powinny rozwiązać problem.
Zobacz także Materiały z konferencji Google I/O 2009 — Debugging Arts of the Ninja Masters (http://developer. android.com/develop/index.html#v=Dgnx0E7m1GQ), http://groups.google.com/group/android-developers/ browse_thread/thread/92ea776cfd42aa45.
3.6. Rozw ązywan e problemów z awar am apl kacj
|
127
3.7. Debugowanie z wykorzystaniem instrukcji Log.d i okna LogCat Rachee Singh
Problem Kod Javy zwykle kompiluje się bez błędów, jednak czasem w działającej aplikacji występuje błąd, co prowadzi do wyświetlenia komunikatu o błędzie Force Close (lub podobnych informacji).
Rozwiązanie W opisanej sytuacji przydatną techniką jest debugowanie kodu za pomocą komunikatów z okna LogCat.
Omówienie Większość programistów Javy prawdopodobnie korzystała kiedyś z instrukcji System.out.println w trakcie debugowania kodu. W podobny sposób można ułatwić sobie debugowanie aplikacji na Android — należy zastosować metodę Log.d(). Pozwala ona wyświetlać potrzebne wartości i komunikaty w oknie LogCat. Najpierw należy zaimportować klasę Log: import android.util.Log;
Następnie wstaw poniższy wiersz w miejscach, w których chcesz sprawdzać stan aplikacji: Log.d("Testy", "Punkt kontrolny 1");
Testy to tag wyświetlany w kolumnie Tag w oknie LogCat (rysunek 3.15).
Rysunek 3.15. Dane wyjściowe w trakcie debugowania 128
|
Rozdz ał 3. Testy
Zwykle tagi definiuje się jako stałe w głównej klasie, co gwarantuje spójną pisownię. Punkt kontrolny 1 to komunikat widoczny w oknie Text w oknie LogCat. Metoda Log.d przyjmuje dwa wspomniane argumenty. W oknie LogCat pojawia się odpowiadający im komunikat. Jeśli więc wstawiłeś instrukcję Log.d jako punkt kontrolny i w oknie LogCat zobaczysz komunikat Punkt kontrolny 1, możesz zakładać, że kod do danego punktu działa prawidłowo. Metoda Log.d() nie przyjmuje zmiennych, dlatego jeśli chcesz sformatować więcej niż jedną wartość, zastosuj złączanie łańcuchów znaków lub metodę String.format (pomiń jednak końcową sekwencję %n): Log.d("Testy", String.format("x0 = %5.2f, x1=%5.2f", x0, x1));
3.8. Automatyczne otrzymywanie raportów o błędach od użytkowników za pomocą mechanizmu BugSense Ian Darwin
Problem Użytkownicy nie zawsze informują o awarii aplikacji, a nawet jeśli to robią, często pomijają ważne szczegóły. Przydatna byłaby usługa, która wykrywa każdy wyjątek i przesyła szczegółowe raporty.
Rozwiązanie Zarejestruj się w usłudze BugSense (możesz wybrać wersję Free lub Premium), a następnie dodaj do aplikacji plik JAR i jedno wywołanie. Następnie możesz spokojnie usiąść i czekać na powiadomienia lub zapoznać się z listą błędów i szczegółowymi informacjami na ich temat w dostępnym w internecie panelu kontrolnym.
Omówienie W usłudze BugSense nie ma nic niezwykłego. Nie udostępnia żadnych informacji, których nie mógłbyś zdobyć samodzielnie. Jest jednak gotowa, dlatego możesz ją wykorzystać! Oto podstawowe etapy korzystania z niej:
1. Utwórz konto (Free lub Premium) w usłudze BugSense (http://www.bugsense.com). 2. Zarejestruj aplikację i pobierz unikatowy klucz z witryny. 3. Pobierz plik JAR i dodaj go do projektu. 4. Dodaj jedno wywołanie (z unikatowym kluczem aplikacji) do metody onCreate() głównej aktywności.
5. Udostępnij aplikację użytkownikom.
3.8. Automatyczne otrzymywan e raportów o błędach od użytkown ków
|
129
Etapy 1. i 2. są proste, dlatego nie wymagają dalszego omówienia. Pozostałe kroki wymagają wyjaśnień, dlatego opisano je w dalszych podpunktach.
Dodawanie pliku JAR do projektu Potrzebny plik JAR to bugsense-trace.jar. Można go pobrać ze strony https://github.com/bugsense/ bugsense-android/blob/master/bugsense-trace.jar?raw=true lub http://www.bugsense.com. Prawdopodobnie wiesz, jak dodawać pliki JAR do projektu. Jeśli nie, zajrzyj do receptury 1.10. Ponieważ mechanizm zgłasza błędy przez internet, nie trzeba chyba wspominać, że do korzystania z usługi aplikacja potrzebuje uprawnień do komunikacji z internetem. Dodaj do pliku AndroidManifest.xml następujący kod:
Wywoływanie usługi BugSense przy uruchamianiu aplikacji Teraz wystarczy dodać tylko jedno wywołanie w metodzie onCreate(). Wywołanie to zwykle umieszcza się po wywołaniu metody setContentView(). Oto przykład z pierwszej części metody onCreate() z napisanego przeze mnie programu JPS Track: private static final String OUR_BUGSENSE_API_KEY = ""; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); // Włączanie śledzenia błędów przez usługę BugSense BugSenseHandler.setup(this, OUR_BUGSENSE_API_KEY); ... }
Oczywiście trzeba zaimportować klasę BugSenseHandler, jednak środowisko Eclipse robi to automatycznie (jeśli tak się nie stanie, wybierz opcję Source/Organize Imports.
Udostępnianie aplikacji i przeglądanie raportów o błędach Przeglądać raporty można tylko na stronie internetowej dostępnej po zalogowaniu się.
Zobacz także Zacznij od witryny usługi BugSense (http://www.bugsense.com/). Więcej informacji na temat możliwości tej usługi znajdziesz na stronie Features (http://www.bugsense.com/features). W serwisie Google Code istnieje projekt ACRA (http://code.google.com/p/acra/), który udostępnia podobne funkcje z zakresu rejestrowania danych, jednak gorzej radzi sobie z tworzeniem raportów (przynajmniej było tak w czasie, gdy powstawała ta książka).
130
|
Rozdz ał 3. Testy
3.9. Korzystanie z lokalnego dziennika czasu wykonania do analizowania błędów i innych sytuacji Atul Nene
Problem Użytkownicy zgłaszają niepożądane (ich zdaniem) działanie aplikacji, jednak gdy na rynku dostępna jest wersja produkcyjna aplikacji, nie ma sposobu na ustalenie, co dzieje się w środowisku użytkowników. Dlatego raporty o błędach trafiają do przegródki „nie można odtworzyć”.
Rozwiązanie Opracuj wbudowany mechanizm zapewniający dodatkowe informacje. Znasz ważne zdarzenia lub zmiany stanu oraz zapotrzebowanie aplikacji na zasoby. Zapisanie informacji w dzienniku aplikacji pozwala uzyskać dodatkowe, bardzo potrzebne dane na temat zgłaszanego problemu. Ten prosty środek zapobiegawczy pomaga poprawić niskie oceny przyznawane aplikacji przez użytkowników z uwagi na występowanie nieprzewidzianych sytuacji. Dzięki dodatkowym danym można poprawić komfort pracy użytkowników. Jednym z rozwiązań jest zastosowanie standardowego pakietu java.util.logging. W tej recepturze przedstawiono przykładową klasę RuntimeLog, w której wykorzystano pakiet java.util. logging do zapisu informacji w pliku dziennika urządzenia. Wspomniana klasa zapewnia programiście znaczną kontrolę nad poziomem szczegółowości rejestrowanych danych.
Omówienie Zaprojektowałeś, napisałeś i przetestowałeś aplikację oraz udostępniłeś ją w sklepie Google Play. Możesz sądzić, że zasłużyłeś już na wakacje. Nie tak szybko! Tylko w najprostszych sytuacjach można uwzględnić w trakcie testowania wszystkie możliwe scenariusze (ponadto programiści nie mają czasu na testowanie wszystkich możliwości). Dlatego użytkownicy zawsze zgłaszają nieoczekiwane działanie aplikacji. Nie musi to być błąd — może to być sytuacja w środowisku wykonawczym, na którą programista nie natrafił w trakcie testów. Należy z góry przygotować się na takie zgłoszenia przez zaprojektowanie dziennika czasu wykonania. W dzienniku należy rejestrować najważniejsze zdarzenia aplikacji — np. zmianę stanu, przekroczenie czasu oczekiwania na zasoby (dostęp do sieci, wątek) lub przekroczenie limitu liczby prób. Warto nawet zapobiegawczo rejestrować nieoczekiwane ścieżki wykonania kodu w dziwnych scenariuszach, a także wybrane ważne powiadomienia wyświetlane użytkownikowi. W dzienniku zapisuj tylko informacje pomagające zrozumieć działanie aplikacji. W przeciwnym razie duży rozmiar dziennika może powodować problemy. Choć w podpisanych aplikacjach wywołania Log.d() są ignorowane, nadmierna liczba takich instrukcji może spowalniać pracę programu.
3.9. Korzystan e z lokalnego dz enn ka czasu wykonan a do anal zowan a błędów nnych sytuacj
|
131
Możesz się zastanawiać, dlaczego do obsługi tego zadania nie zastosować narzędzia LogCat, BugSense lub ACRA. Rozwiązania te nie są odpowiednie z kilku powodów: • Standardowy mechanizm LogCat nie przydaje się po udostępnieniu aplikacji,
ponieważ zwykle nie można podłączyć debugera do urządzenia użytkownika. Ponadto zbyt duża liczba instrukcji Log.d i Log.i w kodzie może negatywnie wpływać na wydajność programu. Dlatego nie należy kompilować instrukcji Log.* w wersji produkcyjnej aplikacji.
• Narzędzia ACRA i BugSense działają dobrze, jeśli urządzenie podłączone jest
do internetu. Warunek ten nie zawsze jest spełniony. W niektórych programach dostęp do internetu potrzebny jest tylko ze względu na wymienione narzędzia. Ponadto ślad stosu rejestrowany przez narzędzie ACRA obejmuje szczegółowe informacje tylko na temat zgłoszonego wyjątku, natomiast technika opisana w recepturze zapewnia długoterminowy obraz działającej aplikacji.
Na listingu 3.5 przedstawiono kod klasy RuntimeLog. Listing 3.5. Klasa RuntimeLog // Należy zastosować następujące wbudowane mechanizmy import java.util.logging.FileHandler; import java.util.logging.Formatter; import java.util.logging.Level; import java.util.logging.LogRecord; import java.util.logging.Logger; public class RuntimeLog { public static final int public static final int public static final int public static final int public static final int public static final int public static final int
MODE_DEBUG = 1; MODE_RELEASE = 2; ERROR = 3; WARNING = 4; INFO = 5; DEBUG = 6; VERBOSE = 7;
// Zmień na MODE_DEBUG na potrzeby debugowania w trakcie programowania static boolean Mode = MODE_RELEASE; static logfileName = "/sdcard/YourAppName.log" static Logger logger; static LogRecord record; // Inicjowanie dziennika przy pierwszym użyciu klasy oraz // tworzenie niestandardowego obiektu Formatter static { try { FileHandler fh = new FileHandler(logfileName, true); fh.setFormatter(new Formatter() { public String format(LogRecord rec) { StringBuffer buf = new StringBuffer(1000); buf.append(new java.util.Date().getDate()); buf.append('/'); buf.append(new java.util.Date().getMonth()); buf.append('/'); buf.append((new java.util.Date().getYear())%100); buf.append(' '); buf.append(new java.util.Date().getHours()); buf.append(':'); buf.append(new java.util.Date().getMinutes()); buf.append(':'); buf.append(new java.util.Date().getSeconds());
132
|
Rozdz ał 3. Testy
buf.append('\n'); return buf.toString(); } }); logger = Logger.getLogger(logfileName); logger.addHandler(fh); } catch (IOException e) { e.printStackTrace(); } } // Metoda log public static void log(int logLevel,String msg) { // W wersji produkcyjnej nie należy zapisywać komunikatów typu DEBUG i VERBOSE if (Mode == MODE_RELEASE) && (logLevel >= DEBUG)) return; record=new LogRecord(Level.ALL, msg); record.setLoggerName(logfileName); try { switch(logLevel) { case ERROR: record.setLevel(Level.SEVERE); logger.log(record); break; case WARNING: record.setLevel(Level.WARNING); logger.log(record); break; case INFO: record.setLevel(Level.INFO); logger.log(record); break; // Poziomy FINE i FINEST w niektórych wersjach interfejsu API nie działają. // W zamian należy zastosować poziom INFO case DEBUG: record.setLevel(Level.INFO); logger.log(record); break; case VERBOSE: record.setLevel(Level.INFO); logger.log(record); break; } } catch(Exception exception) { exception.printStackTrace(); } } }
Oczywiście można zastosować też kilka odmian tego rozwiązania: • Ten sam mechanizm można wykorzystać do wykrywania skomplikowanych problemów
z wykonaniem na etapie rozwijania aplikacji. W tym celu zmienną Mode należy ustawić na wartość MODE_DEBUG. • W skomplikowanych aplikacjach z wieloma modułami przydatne może być dodanie do
wywołania metody log nazwy modułu i dodatkowych parametrów. • Wartości ClassName i MethodName można wyodrębnić z rekordu LogRecord i dodać do dzien-
nika. Nie zaleca się jednak stosowania tej techniki do dzienników czasu wykonania.
3.9. Korzystan e z lokalnego dz enn ka czasu wykonan a do anal zowan a błędów nnych sytuacj
|
133
Na listingu 3.6 pokazano, że stosowanie tego mechanizmu w prostych sytuacjach jest równie łatwe jak używanie wywołań Log.d. Listing 3.6. Korzystanie z klasy RuntimeLog RuntimeLog.log (RuntimeLog.ERROR, "Brak dostępu do sieci"); RuntimeLog.log (RuntimeLog.WARNING, "Zmiana stanu aplikacji na DZIWNY_STAN"); ...
W razie potrzeby można poprosić użytkowników o pobranie plików dziennika z karty SD i przesłanie ich do pracowników pomocy technicznej. Jeszcze lepszym pomysłem jest napisanie kodu, który pozwala użytkownikom wykonać potrzebne operacje przez kliknięcie przycisku. Oto kilka dodatkowych usług: • Mechanizm nie musi być zawsze włączony. Można rejestrować dane w zależności od te-
go, jakie ustawienie wybierze użytkownik, i włączać mechanizm tylko przy próbie odtworzenia określonego scenariusza. • Jeśli mechanizm zawsze jest włączony, należy zapisywać dziennik w pliku, którego nazwa
obejmuje bieżącą datę (ustalaną w momencie uruchamiania aplikacji). Warto też usuwać starsze pliki dziennika, które nie będą już przydatne. Pomaga to ograniczyć łączną wielkość plików dziennika.
Zobacz także Witryna narzędzia ACRA (http://code.google.com/p/acra/). Receptura 3.7 i receptura 3.8.
3.10. Odtwarzanie scenariuszy cyklu życia aktywności na potrzeby testów Daniel Fowler
Problem Aplikacje powinny być odporne na zdarzenia z cyklu życia aplikacji. Programiści muszą wiedzieć, jak odtwarzać różne scenariusze z cyklu życia.
Rozwiązanie Zastosuj rejestrowanie, aby dobrze zrozumieć cykl życia aktywności. Pozwala to na łatwiejsze odtwarzanie scenariuszy z cyklu życia w kontekście testowania aplikacji.
Omówienie Android zaprojektowano pod kątem życia w ruchu. Użytkownik może wykonywać różne zadania — odbierać połączenia telefoniczne, sprawdzać pocztę elektroniczną, wysyłać SMS-y, korzystać z sieci społecznościowej, robić zdjęcia, przeglądać strony internetowe, uruchamiać
134
|
Rozdz ał 3. Testy
aplikacje itd. Może nawet uda mu się ukończyć niektóre zadania! Dlatego w pamięci urządzenia wczytanych może być kilka aplikacji, a tym samym i kilka aktywności. Aplikacja działająca na pierwszym planie i jej aktywność mogą zostać w dowolnym momencie przerwane oraz wstrzymane. Aplikacja ma cykl życia, którego nie może kontrolować, ponieważ to system operacyjny Android uruchamia, śledzi, wstrzymuje, wznawia i usuwa aktywności aplikacji. Jednak aktywność ma informacje o tym, co się dzieje, ponieważ w momencie tworzenia egzemplarza, ukrywania i usuwania aktywności wywoływane są różne funkcje. Umożliwia to śledzenie w aktywności tego, co system operacyjny robi z aplikacją (opisano to w recepturze 1.6). Z tego powodu programiści aplikacji powinni znać funkcje wywoływane w momencie uruchamiania aktywności: • onCreate(Bundle savedInstanceState){...}, • onStart(){...}, • onResume(){...}
i funkcje wywoływane w chwili wstrzymywania aktywności i jej usuwania z pamięci: • onPause(){...}, • onStop(){...}, • onDestroy(){...}.
Aby zobaczyć działanie tego rodzaju metod, otwórz program z receptury 1.4. Następnie w klasie głównej aktywności przesłoń wszystkie sześć wymienionych wcześniej funkcji. Umieść w nich wywołania do wersji z nadklasy. Dodaj wywołanie funkcji Log.d(), aby zapisać nazwę aplikacji i wywoływanej funkcji. Kod powinien wyglądać tak jak na listingu 3.7. Listing 3.7. Rejestrowanie zdarzeń z cyklu życia public class Main extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); Log.d("MyAndroid", "onCreate"); } @Override public void onStart() { super.onStart(); Log.d("MyAndroid", "onStart"); } @Override public void onResume() { super.onResume(); Log.d("MyAndroid","onResume"); } @Override public void onPause() { super.onPause(); Log.d("MyAndroid","onPause"); } public void onStop() { super.onStop(); Log.d("MyAndroid","onStop"); }
3.10. Odtwarzan e scenar uszy cyklu życ a aktywnośc na potrzeby testów
|
135
public void onDestroy() { super.onDestroy(); Log.d("MyAndroid","onDestroy"); } }
Uruchom program. Aby zobaczyć komunikaty diagnostyczne, otwórz widok LogCat. Domyślnie jest on widoczny w perspektywie DDMS (ang. Dalvik Debug Monitor Server). Okno LogCat można też otworzyć za pomocą opcji z menu Window. Wybierz opcję Window/Show View/Other, rozwiń węzeł Android i kliknij opcję LogCat. Widok LogCat pojawi się w zakładkach w dolnej części ekranu. Aby otworzyć perspektywę DDMS, kliknij przycisk DDMS w prawym górnym rogu środowiska Eclipse. Przycisk ten przedstawiono na rysunku 3.16.
Rysunek 3.16. Perspektywa DDMS
Widok LogCat powinien znajdować się w zakładkach w dolnej części ekranu. Jeśli jest inaczej, wybierz wspomnianą wcześniej opcję z menu Window lub kliknij opcję Window/Reset Perspective. Widok LogCat można wyświetlić w odrębnym oknie, przeciągając zakładkę poza środowisko Eclipse. Po uruchomieniu programu zobaczysz trzy komunikaty diagnostyczne dodane w funkcjach wywoływanych przy uruchamianiu aktywności (rysunek 3.17).
Rysunek 3.17. Komunikaty wyświetlane przy uruchamianiu aktywności
Po wciśnięciu klawisza Back pojawiają się trzy komunikaty wyświetlane przy usuwaniu aktywności (rysunek 3.18). Aby wyświetlać tylko komunikaty z danej aplikacji, należy dodać filtr w oknie LogCat. Kliknij zielony znak plus w prawym górnym rogu tego okna. Określ nazwę filtru i w polu by Log Tag wpisz tekst MyAndroid (rysunek 3.19).
136
|
Rozdz ał 3. Testy
Rysunek 3.18. Komunikaty związane z usuwaniem aktywności
Rysunek 3.19. Filtrowanie w oknie LogCat
W oknie LogCat pojawia się wtedy nowa zakładka, w której znajdują się komunikaty bezpośrednio zgłoszone przez aplikację (rysunek 3.20).
Rysunek 3.20. Odfiltrowane komunikaty
Aby wyczyścić dane w oknie LogCat, należy kliknąć ikonę w prawym górnym rogu (ze stroną i małym czerwonym symbolem ×). Czasem warto usunąć zawartość okna przed wykonaniem operacji i oczekiwaniem na dalsze komunikaty.
3.10. Odtwarzan e scenar uszy cyklu życ a aktywnośc na potrzeby testów
|
137
Aby zobaczyć funkcje wywoływane w trakcie wstrzymywania programu, najpierw otwórz aplikację MyAndroid. W metodzie onRestart() dodaj potrzebne wywołanie oraz komunikat diagnostyczny. @Override public void onRestart() { super.onRestart(); Log.d("MyAndroid","onRestart"); }
W urządzeniu (lub emulatorze) uruchom program, kliknij przycisk Home, a następnie ponownie uruchom program. W oknie LogCat pojawia się standardowa sekwencja funkcji wywoływanych w trakcie uruchamiania aplikacji. Po wciśnięciu przycisku Home uruchamiane są metody onPause() i onStop() zamiast onDestroy(). Aplikacja nie kończy pracy, tylko przechodzi w stan wstrzymania. Przejście do programu nie prowadzi wtedy do ponownego jego wczytywania, dlatego zamiast metody onCreate() wywoływana jest metoda onRestart(). Ponownie uruchom program (w urządzeniu lub emulatorze), a następnie przejdź do narzędzia Manage Applications (Settings/Applications), wybierz program i kliknij przycisk Force Close. Następnie jeszcze raz uruchom program. Wywoływana jest standardowa sekwencja funkcji potrzebnych przy uruchamianiu aplikacji, a następnie aktywność zostaje wstrzymana. Metoda onDestroy() nie pojawia się po uruchomieniu drugiego egzemplarza aplikacji. W tej recepturze opisano następujące scenariusze cyklu życia: • normalne uruchomienie i późniejsze zamknięcie aplikacji; • uruchomienie aplikacji, wstrzymanie jej i późniejsze ponowne włączenie (rysunek 3.21);
Rysunek 3.21. Ponowne uruchamianie aplikacji • uruchomienie aplikacji, wstrzymanie jej, wymuszone usunięcie z pamięci i ponowne uru-
chomienie (rysunek 3.22). Scenariusze te prowadzą do wywoływania różnych sekwencji funkcji cyklu życia. Uwzględnienie opisanych scenariuszy w trakcie testów gwarantuje, że aplikacja będzie działać prawidłowo. Opisane tu techniki można wzbogacić przez zaimplementowanie dodatkowych przesłanianych
138
|
Rozdz ał 3. Testy
Rysunek 3.22. Komunikaty wyświetlane przy wymuszonym zamknięciu
funkcji. Przedstawione techniki są poprawne także dla fragmentów w aktywnościach oraz dla testowania cyklu życia fragmentów.
Zobacz także Receptura 1.4, receptura 1.6, http://developer.android.com/reference/android/app/Activity.html, http:// developer.android.com/reference/android/util/Log.html, http://developer.android.com/guide/topics/ fundamentals/fragments.html.
3.11. Rozwijanie płynnie działających aplikacji za pomocą narzędzia StrictMode Adrian Cowham
Problem Programista chce się upewnić, że interfejs GUI aplikacji będzie płynnie reagował.
Rozwiązanie Android obejmuje narzędzie StrictMode (wprowadzono je w wersji Gingerbread), przeznaczone do wykrywania wszystkich sytuacji, które mogą prowadzić do błędu ANR (ang. Application Not Responding). Narzędzie na przykład wykrywa i rejestruje w oknie LogCat wszystkie operacje odczytu i zapisu danych w bazie wykonywane w wątku głównym (czyli wątku interfejsu GUI).
Omówienie Żałuję, że nie mogłem korzystać z narzędzia w rodzaju StrictMode, gdy pisałem aplikacje na komputery stacjonarne za pomocą frameworku Java Swing. Tworzenie płynnie działających
3.11. Rozw jan e płynn e dz ałających apl kacj za pomocą narzędz a Str ctMode
|
139
aplikacji przy użyciu tego frameworku było kłopotliwe. Zarówno początkujący, jak i doświadczeni inżynierowie wykonywali operacje na bazie danych w wątku interfejsu użytkownika, co doprowadzało do braku reakcji aplikacji. Zwykle zawieszanie się programu wynikało z tego, że dział kontroli jakości (lub klient) uruchamiał aplikację dla znacznie większego zbioru danych niż używany w trakcie testów prowadzonych przez inżynierów. Wykrywanie takich drobnych błędów przez dział kontroli jakości było nie do przyjęcia i prowadziło do marnowania czasu (oraz pieniędzy firmy). Ostatecznie rozwiązaliśmy problem przez poświęcenie większej ilości czasu na przegląd kodu przez współpracowników, jednak zastosowanie narzędzia w rodzaju StrictMode byłoby tańsze. W poniższym przykładowym kodzie pokazano, jak łatwo jest włączyć narzędzie StrictMode w aplikacji: // Koniecznie zaimportuj klasę StrictMode import android.os.StrictMode; // W klasie android.app.Application aplikacji należy umieścić // poniższe wiersze w metodzie onCreate(...). if ( Build.VERSION.SDK_INT >= 9 && isDebug() ) { StrictMode.enableDefaults(); }
Warto zauważyć, że celowo pominięto implementację metody onDebug(), ponieważ nie wszyscy programiści ją stosują. Zachęcam do włączania narzędzia StrictMode tylko dla aplikacji działających w trybie diagnostycznym. Udostępnianie aplikacji w sklepie Google Play z działającym w tle narzędziem StrictMode, które niepotrzebnie zużywa zasoby, to zły pomysł.
Zobacz także Narzędzie StrictMode udostępnia wiele opcji konfiguracyjnych. Można precyzyjnie określić, jakie problemy ma wykrywać. Szczegółowe informacje na temat dostosowywania reguł narzędzia StrictMode znajdziesz na stronie http://developer.android.com/reference/android/os/ StrictMode.html.
3.12. Korzystanie z programu Monkey Adrian Cowham
Problem Programista chce przetestować aplikację w sposób losowy.
Rozwiązanie Można zastosować uruchamiane z wiersza poleceń narzędzie Android Monkey, aby przetestować rozwijaną aplikację.
140
|
Rozdz ał 3. Testy
Omówienie Testowanie jest tak łatwe, że nawet małpa sobie z tym poradzi (nazwa narzędzia, Monkey, oznacza właśnie małpę). Choć istnieje stosunkowo mało narzędzi do testowania aplikacji na Android, Monkey to całkiem dobry program. Jest to narzędzie testowe dostępne w pakiecie SDK Androida, które symuluje korzystanie z urządzenia z Androidem przez małpę lub dziecko. Wyobraź sobie małpę siedzącą przy klawiaturze i wciskającą losowe klawisze. Czy istnieje lepszy sposób na uzyskanie niespodziewanych komunikatów ANR? Aby zastosować narzędzie Monkey, wystarczy włączyć emulator (lub podłączyć urządzenie do komputera używanego do programowania), a następnie uruchomić skrypt narzędzia. Niechętnie się do tego przyznaję, ale przez codzienne korzystanie z narzędzia Monkey udało nam się znaleźć usterki, których dział kontroli jakości prawdopodobnie by nie wykrył. Gdyby to użytkownicy znaleźli takie błędy po wprowadzeniu aplikacji na rynek, naprawienie problemów byłoby bardzo trudne. Co gorsza, użytkownicy mogliby przestać korzystać z programu. Oto kilka najlepszych praktyk stosowania narzędzia Monkey w trakcie rozwijania aplikacji: • Utwórz własny skrypt narzędzia Monkey, będący nakładką na androidową wersję. Po-
zwala to zagwarantować, że wszyscy programiści w zespole będą korzystać z narzędzia Monkey z ustawionymi tymi samymi parametrami. Jeśli pracujesz sam, takie podejście pozwala zwiększyć przewidywalność (zagadnienie to opisano dalej). • Skonfiguruj narzędzie Monkey w odpowiedni sposób. Powinno działać wystarczająco
długo, żeby wykryć defekty, a przy tym na tyle krótko, aby nie obniżać produktywności. W trakcie rozwijania aplikacji skonfigurowaliśmy narzędzie Monkey w taki sposób, aby generowało w sumie 50 000 zdarzeń. Jednokrotne uruchomienie narzędzia w urządzeniu Samsung Galaxy Tab zajmowało około 40 minut. To niezły wynik, jednak wolałbym mieścić się w 30 minutach. Oczywiście na szybszych tabletach czas działania narzędzia będzie krótszy. • Narzędzie Monkey działa losowo, dlatego gdy zaczęliśmy z niego korzystać, każdy pro-
gramista otrzymywał inne wyniki i nie można było odtworzyć problemów. Następnie odkryliśmy, że narzędzie pozwala określić ziarno w generatorze liczb losowych. Dlatego w nakładce należy podać takie ziarno. Pozwala to zapewnić jednolitość i przewidywalność przy korzystaniu z narzędzia przez poszczególnych członków zespołu.
Oto nakładka na skrypt Monkey. Dalej opisano argumenty narzędzia. #!/bin/bash # Skrypt do uruchamiania narzędzia Monkey # # Zobacz http //developer.android.com/guide/developing/tools/monkey.html rm tmp/monkey.log adb shell monkey -p nazwa.pakietu –throttle 100 –s 43686 –v 50000 | tee tmp/monkey.log
• Argument –p nazwa.pakietu gwarantuje, że narzędzie Monkey sprawdzi tylko podany pakiet. • Argument --throttle określa opóźnienie między zdarzeniami. • Argument –s to wartość ziarna. • Argument –v służy do włączania zapisywania pełnych komunikatów. • 50000 to liczba zdarzeń symulowanych przez narzędzie.
3.12. Korzystan e z programu Monkey
|
141
Dostępnych jest też wiele innych opcji konfiguracyjnych. Celowo pominąłem typy generowanych zdarzeń, ponieważ uważam, że warto samemu się z nimi pomęczyć. Ustawiona u mnie w firmie wartość ziarna spowodowała, że narzędzie Monkey w połowie testów wyłączyło sieć Wi-Fi. Początkowo było to frustrujące, ponieważ wydawało się, że aplikacja nie jest wystarczająco dokładnie testowana. Okazało się jednak, że wyłączając sieć Wi-Fi i bezwzględnie sprawdzając aplikację, narzędzie wyświadczyło nam przysługę. Po wykryciu i naprawieniu kilku błędów szybko uzyskaliśmy całkowitą pewność, że po utracie połączenia sieciowego aplikacja działa w oczekiwany sposób. Dobra małpka.
Zobacz także http://developer.android.com/guide/developing/tools/monkey.html.
3.13. Wysyłanie komunikatów tekstowych i przekazywanie wywołań między urządzeniami AVD Johan Pelgrim
Problem Programista opracował aplikację, która musi kierować lub odbierać wywołania albo przesyłać lub otrzymywać komunikaty testowe, i chce przetestować potrzebne funkcje.
Rozwiązanie Należy uruchomić dwa urządzenia AVD i wykorzystać numer portu do przesyłania komunikatów tekstowych oraz zgłaszania wywołań.
Omówienie W trakcie rozwijania aplikacji odbierającej wywołania lub komunikaty tekstowe (podobnej do tej z receptury 12.2) można oczywiście symulować zgłaszanie wywołań i przesyłanie komunikatów w perspektywie DDMS w środowisku Eclipse, jednak można też uruchomić drugie urządzenie AVD. W nagłówku okna urządzenia AVD przed nazwą znajduje się liczba. Jest to numer portu, który można wykorzystać do połączenia się z powłoką urządzenia AVD za pomocą instrukcji telnet (np. telnet localhost 5554). Wygodne jest to, że w kontekście testów numer portu jest też numerem telefonu urządzenia AVD. Można więc wykorzystać ten numer do nawiązywania połączeń telefonicznych (rysunek 3.23) lub przesyłania wiadomości tekstowych (rysunek 3.24).
Zobacz także Receptura 12.2. 142
|
Rozdz ał 3. Testy
Rysunek 3.23. Dzwonienie z jednego urządzenia AVD do drugiego
Rysunek 3.24. Wysyłanie SMS-a z jednego urządzenia AVD do drugiego
3.13. Wysyłan e komun katów tekstowych przekazywan e wywołań m ędzy urządzen am AVD
|
143
144
|
Rozdz ał 3. Testy
ROZDZIAŁ 4.
Komunikacja wewnątrzi międzyprocesowa
4.1. Wprowadzenie — komunikacja wewnątrzi międzyprocesowa Ian Darwin
Omówienie Android udostępnia wyjątkowy zestaw mechanizmów do komunikacji w ramach aplikacji i między nimi. W tym rozdziale omówiono następujące zagadnienia: Intencje Określają, co należy zrobić w następnym kroku — czy wywołać klasę z danej aplikacji, czy wykorzystać inną aplikację, którą użytkownik wybrał do przetwarzania określonego żądania dla konkretnego typu danych. Odbiorniki rozgłoszeniowe W połączeniu z filtrami intencji umożliwiają zdefiniowanie, że aplikacja potrafi przetwarzać określone żądania dla konkretnego typu danych (czyli jest docelowym odbiorcą intencji). Klasa AsyncTask Umożliwia pisanie długo działającego kodu, którego nie należy wykonywać w wątku interfejsu GUI ani w głównym wątku obsługi zdarzeń, ponieważ mogłoby to spowolnić aplikację — nawet w takim stopniu, że może wystąpić błąd ANR. Komponenty obsługi Umożliwiają tworzenie kolejek komunikatów zgłaszanych przez wątki tła. Komunikaty te są przetwarzane przez inne wątki, na przykład przez wątek głównej aktywności, zwykle w celu bezpiecznego zaktualizowania zawartości ekranu na podstawie danych.
145
4.2. Obsługiwanie strony internetowej, numeru telefonu lub innych elementów za pomocą intencji Ian Darwin
Problem Element z danego programu ma być przetwarzany w innej aplikacji bez określania w programie, która z aplikacji ma to zrobić.
Rozwiązanie Należy wywołać konstruktor klasy Intent, a następnie wywołać metodę startActivity dla utworzonego obiektu Intent.
Omówienie Konstruktor klasy Intent przyjmuje dwa argumenty — akcję oraz element, na którym należy ją wykonać. Pierwszy argument możesz potraktować jak czasownik, a drugi — jak podmiot. Najczęściej stosowaną akcją jest Intent.ACTION_VIEW (reprezentujący ją łańcuch znaków to android.intent.action.VIEW). Drugim argumentem jest zwykle adres URL lub (jak mniej precyzyjnie określa się to w Androidzie) identyfikator URI. Identyfikatory URI można tworzyć za pomocą statycznej metody parse() klasy URI. Jeśli zmienna data obejmuje łańcuch znaków z lokalizacją, którą program ma wyświetlić, obiekt Intent można utworzyć za pomocą następującego kodu: Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(data));
I to już wszystko! Na tym przykładzie widać piękno Androida. Nie ma znaczenia, czy łańcuch znaków data obejmuje adres URL z członem http:, numer telefonu z członem tel: lub nawet rzadko spotykany format. O ile w systemie zarejestrowano aplikację do przetwarzania intencji określonego rodzaju, Android po wywołaniu intencji znajdzie potrzebną aplikację. Jak wywołuje się intencje? Warto pamiętać, że Android w celu przetworzenia intencji uruchamia nową aktywność. Jeśli kod znajduje się w intencji, wystarczy wywołać odziedziczoną metodę startActivity, np.: startActivity(intent);
Jeśli wszystko przebiega poprawnie, użytkownik zobaczy przeglądarkę, aplikację do obsługi połączeń telefonicznych, mapy lub inny potrzebny ekran. Google zdefiniował także wiele innych akcji, np. ACTION_OPEN (próbuje otworzyć nazwany obiekt). W niektórych sytuacjach wywołanie akcji VIEW i OPEN prowadzi do tych samych skutków, jednak czasem pierwsza akcja powoduje wyświetlenie danych, a druga pozwala użytkownikowi zmodyfikować lub zaktualizować dane.
146
|
Rozdz ał 4. Komun kacja wewnątrz- m ędzyprocesowa
Jeśli wystąpi błąd, użytkownik nie zobaczy żadnych informacji. Dlaczego? Ponieważ w instrukcji poinformowano Android, że nie ma znaczenia, czy przetwarzanie intencji zakończy się powodzeniem, czy porażką. Aby uzyskać informacje zwrotne, trzeba wywołać metodę startActivityForResult: startActivityForResult(intent, requestCode);
Wartość requestCode to dowolnie określona wartość służąca do śledzenia żądań wielu intencji. Zwykle należy wybrać unikatową liczbę dla każdego uruchamianego obiektu Intent. Liczby te można później wykorzystać do sprawdzania efektów przetwarzania intencji (jeśli w programie istnieje tylko jeden obiekt Intent, którego skutki przetwarzania są ważne, podaj wartość 1). Jednak samo dodanie wartości nie przyniesie żadnych skutków, jeśli programista nie przesłoni pewnej ważnej metody klasy Activity: @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { // Wykorzystanie wyników }
Wynik przetwarzania obiektu Intent jest znany dopiero po zakończeniu pracy aplikacji, która za to odpowiada (może to mieć miejsce dopiero po jakimś czasie). Może się to wydawać oczywiste, ale warto o tym pamiętać. Ostatecznie jednak metoda onActivityResult zostaje wywołana. Parametr resultCode służy oczywiście do określania, czy przetwarzanie zakończyło się powodzeniem, czy porażką. Istnieją stałe określające efekt przetwarzania, takie jak Activity.RESULT_OK i Activity.RESULT_CANCELED. Dla niektórych obiektów Intent dostępne są specjalne stałe oznaczające skutki przetwarzania (zobacz na przykład recepturę 10.9). Informacje o wykorzystaniu przekazanej intencji znajdziesz w recepturach dotyczących przekazywania dodatkowych danych, np. w recepturze 4.5.
4.3. Wysyłanie e-maili z poziomu widoku Wagied Davids
Problem Aplikacja ma umożliwiać wysyłanie z poziomu widoku e-maili z tekstem lub obrazkami.
Rozwiązanie Przeznaczone do wysłania dane należy przekazać do klienta pocztowego jako parametr, posługując się intencją.
Omówienie Aby z poziomu widoku przesłać tekst pocztą elektroniczną, wystarczy wykonać stosunkowo proste operacje. Oto one:
4.3. Wysyłan e e-ma l z poz omu w doku
|
147
1. Zmodyfikuj plik AndroidManifest.xml, aby umożliwić nawiązywanie połączeń internetowych, co pozwoli na wysyłanie e-maili. Potrzebny kod pokazano na listingu 4.1.
2. Utwórz warstwę prezentacji z przyciskiem E-mail, który użytkownik może kliknąć. Kod
układu znajdziesz na listingu 4.2. Łańcuchy znaków dla tego układu przedstawiono na listingu 4.3.
3. Dodaj odbiornik onClickListener, aby umożliwić wysyłanie e-maili w reakcji na kliknięcie przycisku E-mail przez użytkownika. Kod odbiornika przedstawiono na listingu 4.4.
Listing 4.1. Plik AndroidManifest.xml
Listing 4.2. Plik Main.xml
148
|
Rozdz ał 4. Komun kacja wewnątrz- m ędzyprocesowa
android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="@string/my_text" />
Listing 4.3. Plik Strings.xml
Listing 4.4. Plik Main.java import import import import import import
android.app.Activity; android.content.Intent; android.os.Bundle; android.view.View; android.view.View.OnClickListener; android.widget.Button;
public class Main extends Activity implements OnClickListener { private static final String tag = "Main"; private Button emailButton; /** Wywoływana, gdy aktywność tworzona jest po raz pierwszy */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Ustawianie warstwy z widokiem setContentView(R.layout.main); // Pobieranie referencji do przycisku E-mail this.emailButton = (Button) this.findViewById(R.id.emailButton); // Ustawianie odbiornika kliknięć this.emailButton.setOnClickListener(this); } @Override public void onClick(View view) { if (view == this.emailButton) { Intent emailIntent = new Intent(android.content.Intent.ACTION_SEND);
4.3. Wysyłan e e-ma l z poz omu w doku
|
149
emailIntent.setType("text/html"); emailIntent.putExtra(android.content.Intent.EXTRA_TITLE, "Tytuł"); emailIntent.putExtra(android.content.Intent.EXTRA_SUBJECT, "Temat"); // Pobieranie referencji do łańcucha znaków i przekazywanie jej do intencji emailIntent.putExtra(android.content.Intent.EXTRA_TEXT, getString(R.string.my_text)); startActivity(emailIntent); } } }
4.4. Wysyłanie e-maili z załącznikami Marco Dinacci
Problem Aplikacja ma umożliwiać wysyłanie e-maili z załącznikami.
Rozwiązanie Należy utworzyć obiekt Intent, dołączyć dodatkowe dane określające plik załącznika i uruchomić nową aktywność, która pozwala użytkownikowi wysłać e-mail.
Omówienie Najłatwiejszy sposób na wysłanie e-maila polega na utworzeniu obiektu Intent z akcją ACTION_SEND: Intent intent = new Intent(Intent.ACTION_SEND); intent.putExtra(Intent.EXTRA_SUBJECT, "Test dla jednego załącznika"); intent.putExtra(Intent.EXTRA_EMAIL, new String[]{recipient_address}); intent.putExtra(Intent.EXTRA_TEXT, "E-mail z załącznikiem");
Aby dołączyć plik, należy umieścić w obiekcie Intent dodatkowe dane: intent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(new File("/path/to/file"))); intent.setType("text/plain");
Typ MIME zawsze można ustawić na text/plain, czasem jednak warto sprecyzować typ, aby aplikacja przetwarzająca komunikat działała prawidłowo. Przy załączaniu rysunku w formacie JPEG typ MIME należy ustawić na image/jpeg. Przy wysyłaniu większej liczby załączników proces należy nieco zmodyfikować, co pokazano na listingu 4.5. Listing 4.5. Większa liczba załączników Intent intent = new Intent(Intent.ACTION_SEND_MULTIPLE); intent.setType("text/plain"); intent.putExtra(Intent.EXTRA_SUBJECT, "Test dla kilku załączników"); intent.putExtra(Intent.EXTRA_TEXT, "E-mail z kilkoma załącznikami"); intent.putExtra(Intent.EXTRA_EMAIL, new String[]{recipient_address}); ArrayList
150
|
Rozdz ał 4. Komun kacja wewnątrz- m ędzyprocesowa
uris.add(Uri.fromFile(new File("/path/to/first/file"))); uris.add(Uri.fromFile(new File("/path/to/second/file"))); intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris);
Najpierw należy zastosować akcję Intent.ACTION_SEND_MULTIPLE (jest ona dostępna od Androida 1.6). Następnie trzeba utworzyć obiekt ArrayList z identyfikatorami URI plików dołączanych do wiadomości oraz wywołać metodę putParcelableArrayListExtra. Przy wysyłaniu plików różnego rodzaju warto ustawić typ MIME na multipart/mixed. Niezależnie od liczby załączników nowy obiekt Activity można uruchomić za pomocą poniższej instrukcji: startActivity(Intent.createChooser(intent, "Wyślij e-mail"));
Instrukcja Intent.createChooser jest opcjonalna, pozwala jednak użytkownikowi wybrać aplikację do wysyłania e-maili.
4.5. Przekazywanie łańcuchów znaków za pomocą instrukcji Intent.putExtra() Ulysses Levy
Problem Programista chce przekazywać parametry do aktywności w momencie jej uruchamiania.
Rozwiązanie Szybkim rozwiązaniem jest zastosowanie instrukcji Intent.putExtra() do przesłania danych. Następnie należy wywołać metodę getIntent().getExtras().getString() w celu ich pobrania.
Omówienie Na listingu 4.6 przedstawiono kod służący do przesyłania danych. Listing 4.6. Przesyłanie danych import android.content.Intent; ... Intent intent = new Intent( this, MyActivity.class ); intent.putExtra( "paramName", "paramValue" ); startActivity( intent );
Kod ten może znajdować się w głównej aktywności. MyActivity.class to druga aktywność, którą należy uruchomić. Trzeba ją bezpośrednio podać w pliku AndroidManifest.xml.
4.5. Przekazywan e łańcuchów znaków za pomocą nstrukcj ntent.putExtra()
|
151
Na listingu 4.7 pokazano, jak pobierać dane. Listing 4.7. Pobieranie danych import android.os.Bundle; ... Bundle extras = getIntent().getExtras(); if (extras != null) { String myParam = extras.getString("paramName"); } else { // Ups! }
Kod z tego listingu powinien znajdować się w głównym pliku Activity.java. Przedstawiona technika ma kilka ograniczeń; między innymi pozwala na przekazywanie tylko łańcuchów znaków. Dlatego jeśli chcesz na przykład przekazać obiekt ArrayList do aktywności ListActivity, należy zastosować pewną sztuczkę — przekazać listę łańcuchów znaków rozdzielonych przecinkami i złączyć je po pobraniu. Inna możliwość to zastosowanie współużytkowanych preferencji (klasy SharedPreferences).
Zobacz też http://mylifewithandroid.blogspot.com/2007/12/playing-with-intents.html, http://developer.android. com/guide/appendix/faq/commontasks.html.
4.6. Pobieranie danych z aktywności podrzędnej do aktywności głównej Ulysses Levy
Problem W aktywności głównej trzeba pobierać dane z aktywności podrzędnej.
Rozwiązanie W aktywności głównej należy zastosować metody startActivityForResult() i onActivityResult(), a w aktywności podrzędnej — metodę setResult().
Omówienie W tym przykładzie łańcuch znaków z aktywności podrzędnej (MySubActivity) jest zwracany do aktywności głównej (MyMainActivity).
152
|
Rozdz ał 4. Komun kacja wewnątrz- m ędzyprocesowa
Pierwszy krok polega na przesłaniu danych z aktywności MyMainActivity za pomocą obiektu Intent (listing 4.8). Listing 4.8. Przesyłanie danych z aktywności public class MyMainActivity extends Activity { // Na potrzeby rejestrowania danych private static final String TAG = "MainActivity"; // Kod żądania powinien być unikatowy public static final int MY_REQUEST_CODE = 123; @Override public void onCreate( Bundle savedInstanceState ) { ... } private void pushFxn() { Intent intent = new Intent( this, MySubActivity.class ); startActivityForResult( intent, MY_REQUEST_CODE );
}
protected void onActivityResult( int requestCode, int resultCode, Intent pData) { if ( requestCode == MY_REQUEST_CODE ) { if (resultCode == Activity.RESULT_OK ) { final String zData = pData.getExtras().getString ( MySubActivity.EXTRA_STRING_NAME ); // Wykorzystanie pobranej wartości Log.v( TAG, "Pobrana wartość zData to "+zData ); // W oknie LogCat "Pobrana wartość zData to returnValueAsString" } }
}
}
Na listingu 4.8 wykonywane są następujące operacje: • Po wywołaniu MySubActivity.finish() wywoływana jest metoda onActivityResult() głów-
nej aktywności. • Pobrane dane to obiekt Intent, dlatego można w nich przekazać bardziej skomplikowane
dane, np. identyfikator URI osoby z książki adresowej z konta Google. Jednak na listingu 4.8 istotny jest tylko łańcuch znaków pobierany za pomocą metody Intent.getExtras().
• Kod requestCode (MY_REQUEST_CODE) powinien być unikatowy i może być przydatny w innych
miejscach aplikacji (np. w instrukcji Activity.finishActivity(MY_REQUEST_CODE)).
4.6. Pob eran e danych z aktywnośc podrzędnej do aktywnośc głównej
|
153
Drugi z ogólnych kroków to pobranie danych z aktywności MySubActivity do MyMainActivity (listing 4.9). Listing 4.9. Pobieranie danych z aktywności podrzędnej public class MySubActivity extends Activity { public static final String EXTRA_STRING_NAME = "extraStringName"; @Override public void onCreate( Bundle savedInstanceState ) { ... } private void pullFxn() { Intent iData = new Intent(); iData.putExtra( EXTRA_STRING_NAME, "returnValueAsString" ); setResult( android.app.Activity.RESULT_OK, iData ); // Zwraca sterowanie do nadrzędnej aktywności "MyMainActivity" finish(); } }
Oto zdarzenia zachodzące na listingu 4.9: • Także tu dane są przekazywane w obiektach Intent (np. w iData). • W metodzie setResult() potrzebny jest kod wyniku przetwarzania, np. RESULT_OK. • Wywołanie finish() powoduje przesłanie wyniku z metody setResult().
Warto zwrócić uwagę także na następujące zagadnienia: • Technicznie dane z aktywności MySubActivity nie są pobierane do momentu zwrócenia ste-
rowania do aktywności MyMainActivity. Dlatego można uznać, że dane są tu po raz drugi wysyłane (a nie pobierane). • Nie jest konieczne stosowanie publicznej statycznej zmiennej typu String z modyfikato-
rem final na nazwę pola extra…. Uznałem jednak, że jest to eleganckie podejście.
Przypadek użycia (nieformalny) W jednej z moich aplikacji używam aktywności ListActivity z menu ContextMenu (wyświetlanym, gdy użytkownik wykona długie kliknięcie w celu wykonania pewnej operacji). Chciałem informować aktywność MainActivity o wierszu, który użytkownik wybrał przy wykonywaniu akcji z menu ContextMenu (aplikacja udostępnia tylko jedną akcję). Ostatecznie zastosowałem dodatkowe dane intencji do przekazywania indeksu wybranego wiersza jako łańcucha znaków do aktywności nadrzędnej. W tej aktywności można przekształcić indeks na wartość typu int i wykorzystać go do określenia wiersza za pomocą metody ArrayList.get(index). W mojej aplikacji to rozwiązanie było skuteczne, jestem jednak przekonany, że istnieje też inny (lepszy) sposób.
154
|
Rozdz ał 4. Komun kacja wewnątrz- m ędzyprocesowa
Zobacz też Receptura 4.5, „sztuczka” z kodem ResultCode (http://androidforums.com/application-development/ 102689-startactivityforresult.html), przykład zastosowania metody startActivityForResult (w sekcji Returning a Result from a Screen na stronie http://developer.android.com/guide/faq/commontasks. html), metoda Activity.startActivityForResult() (http://developer.android.com/reference/android/ app/Activity.html#startActivityForResult(android.content.Intent, int)).
4.7. Podtrzymywanie działania usługi w trakcie wyświetlania innych aplikacji Ian Darwin
Problem Część aplikacji ma kontynuować działanie w tle, gdy użytkownik zacznie korzystać z innych programów.
Rozwiązanie Należy utworzyć usługową klasę Service do wykonywania operacji w tle i uruchomić usługę w głównej aplikacji. Opcjonalnie można udostępnić ikonę powiadomień, aby umożliwić użytkownikowi zatrzymanie usługi lub wznowienie pracy głównej aplikacji.
Omówienie Klasa Service (android.app.Service) działa w tym samym procesie co główna aplikacja, jednak może pracować nawet po przełączeniu się użytkownika do innej aplikacji lub przejściu do ekranu głównego i uruchomieniu nowego programu. Klasy Activity są uruchamiane albo przez intencje z pasującym dostawcą treści, albo przez intencje z bezpośrednio podaną nazwą klasy danej aktywności. To samo dotyczy usług. W tej recepturze opisano bezpośrednie uruchamianie usług. Pośrednie włączanie usług omówiono w recepturze 4.2. Przykładowy kod pochodzi z aplikacji JPSTrack (jest to program na Android służący do śledzenia lokalizacji za pomocą współrzędnych GPS). Po rozpoczęciu śledzenia aplikacja nie powinna przerywać tego procesu w momencie, gdy użytkownik musi odebrać telefon lub spojrzeć na mapę. Dlatego śledzenie odbywa się w usłudze. W kodzie z listingu 4.10 pokazano, że usługa jest uruchamiana w głównej aktywności po kliknięciu przycisku Rozpocznij śledzenie. Aby zakończyć pracę usługi, należy kliknąć przycisk Stop. Warto zauważyć, że operacje te stosuje się tak często, że klasa Activity udostępnia metody startService() i stopService(). Listing 4.10. Metoda onCreate @Override public void onCreate(Bundle savedInstanceState) { ...
4.7. Podtrzymywan e dz ałan a usług w trakc e wyśw etlan a nnych apl kacj
|
155
Intent theIntent = new Intent(this, TrackService.class); Button startButton = (Button) findViewById(R.id.startButton); startButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { startService(theIntent); Toast.makeText(Main.this, "Uruchamianie", Toast.LENGTH_LONG).show(); } }); Button stopButton = (Button) findViewById(R.id.stopButton); stopButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { stopService(theIntent); Toast.makeText(Main.this, "Zatrzymana", Toast.LENGTH_LONG).show(); } }); ... }
TrackService to klasa bezpośrednio pochodna od Service, dlatego trzeba w niej zaimplementować abstrakcyjną metodę onBind(). Nie jest ona potrzebna przy bezpośrednim uruchamianiu
klasy, dlatego metodę można utworzyć jako namiastkę. Zwykle przesłania się przynajmniej metody onStartCommand() i onUnbind() (związane z rozpoczynaniem i kończeniem określonych aktywności). Kod z listingu 4.11 uruchamia usługę śledzącą współrzędne GPS. Usługa przesyła powiadomienia zapisywane na dysku i powinna działać przez cały czas. To właśnie zapewnia przedstawiona klasa Service. Listing 4.11. Klasa TrackService (z usługą korzystającą ze współrzędnych GPS) public class TrackService extends Service { private LocationManager mgr; private String preferredProvider; @Override public IBinder onBind(Intent intent) { return null; } @Override public int onStartCommand(Intent intent, int flags, int startId) { initGPS(); // Ustawianie obiektu LocationManager if (preferredProvider != null) { mgr.requestLocationUpdates(preferredProvider, MIN_SECONDS * 1000, MIN_METRES, this); return START_STICKY; } return START_NOT_STICKY; } @Override public boolean onUnbind(Intent intent) { mgr.removeUpdates(this); return super.onUnbind(intent); }
Możliwe, że zwróciłeś uwagę na różne wartości zwracane przez metodę onStartCommand(). Wartość START_STICKY powoduje ponowne uruchomienie usługi przez Android, jeśli zostanie zamknięta. Wartość START_NOT_STICKY sprawia, że usługa nie jest automatycznie wznawiana. 156
|
Rozdz ał 4. Komun kacja wewnątrz- m ędzyprocesowa
Wartości te opisano dokładniej w internetowej dokumentacji klasy Service (http://developer. android.com/reference/android/app/Service.html). Warto pamiętać, że klasę pochodną klasy Service należy zadeklarować w elemencie Application w pliku AndroidManifest.xml:
4.8. Wysyłanie i odbieranie komunikatów rozgłoszeniowych Vladimir Kroz
Problem Programista chce utworzyć aktywność, która odbiera proste komunikaty rozgłoszeniowe wysyłane przez inną aktywność.
Rozwiązanie Należy skonfigurować odbiornik rozgłoszeniowy, utworzyć obiekt odbiornika komunikatów i utworzyć filtr IntentFilter. Następnie trzeba zarejestrować odbiornik w aktywności, która ma odbierać komunikaty rozgłoszeniowe.
Omówienie Kod z listingu 4.12 konfiguruje odbiornik rozgłoszeniowy, tworzy obiekt do odbierania komunikatów oraz filtr IntentFilter. Listing 4.12. Tworzenie i rejestrowanie odbiornika BroadcastReceiver // Tworzenie odbiornika komunikatów. Klasę odbiornika należy utworzyć przez // rozszerzenie klasy android.content.BroadcastReceiver. Po wysłaniu // komunikatu rozgłoszeniowego wywoływana jest metoda onReceive() nowej klasy MyBroadcastMessageReceiver _bcReceiver = new MyBroadcastMessageReceiver(); // Tworzenie filtra IntentFilter IntentFilter filter = new IntentFilter( MyBroadcastMessageReceiver.class.getName()); // Trzeba też zarejestrować odbiornik w aktywności, która ma odbierać komunikaty // rozgłoszeniowe. Jeśli gdzieś w systemie zostanie wygenerowany taki komunikat, // w wątku głównym aktywności myActivity aplikacja wywoła metodę _bcReceiver.onReceive() myActivity.registerReceiver(_bcReceiver, filter);
W kodzie z listingu 4.13 pokazano, jak publikować rozgłaszane zdarzenia. Listing 4.13. Publikowanie rozgłaszanych zdarzeń Intent intent = new Intent( MyBroadcastMessageReceiver.class.getName()); intent.putExtra("Dodatkowe dane", choice); someActivity.sendBroadcast(intent);
4.8. Wysyłan e odb eran e komun katów rozgłoszen owych
|
157
4.9. Uruchamianie usługi po ponownym uruchomieniu urządzenia Ashwini Shahapurkar
Problem Aplikacja obejmuje usługę, która ma być uruchamiana po ponownym rozruchu telefonu.
Rozwiązanie Należy odbierać intencje ze zdarzeniami związanymi z rozruchem i uruchamiać usługę po wystąpieniu takich zdarzeń.
Omówienie Po zakończeniu rozruchu platformy rozgłaszana jest intencja z akcją android.intent.action. BOOT_COMPLETED. W aplikacji trzeba zarejestrować chęć odbierania tej intencji. W tym celu w pliku AndroidManifest.xml należy umieścić następujący kod:
Aby klasa ServiceManager była odbiornikiem rozgłoszeniowym dla intencji ze zdarzeniami związanymi z rozruchem, jej kod powinien wyglądać tak jak na listingu 4.14. Listing 4.14. Kod klasy odbiornika rozgłoszeniowego public class ServiceManager extends BroadcastReceiver { Context mContext; private final String BOOT_ACTION = "android.intent.action.BOOT_COMPLETED"; @Override public void onReceive(Context context, Intent intent) { // Odbiera wszystkie zarejestrowane komunikaty rozgłoszeniowe mContext = context; String action = intent.getAction(); if (action.equalsIgnoreCase(BOOT_ACTION)) { // Wykrywanie zakończenia rozruchu i uruchamianie usługi startService(); } } private void startService() { // Tu usługa jest uruchamiana Intent mServiceIntent = new Intent(); mServiceIntent.setAction("com.bootservice.test.DataService"); mContext.startService(mServiceIntent); } }
158
|
Rozdz ał 4. Komun kacja wewnątrz- m ędzyprocesowa
4.10. Używanie wątków do tworzenia szybko reagujących aplikacji Amir Alagic
Problem Aplikacja wykonuje długie operacje, ale nie powinna w tym czasie przestawać reagować.
Rozwiązanie Za pomocą wątków można utworzyć aplikację, która reaguje nawet wtedy, gdy wykonuje czasochłonne operacje.
Omówienie Aby aplikacja reagowała w trakcie wykonywania czasochłonnych operacji w systemie operacyjnym Android, można zastosować kilka rozwiązań. Jeśli znasz Javę, wiesz, że można utworzyć klasę pochodną od klasy Thread, przesłonić metodę public void run(), a następnie wywołać metodę start() obiektu nowej klasy, aby uruchomić czasochłonne zadanie. Jeżeli dana klasa już rozszerza inną klasę, można zaimplementować interfejs Runnable. Jeszcze inna technika to utworzenie własnej klasy pochodnej od klasy AsyncTask Androida, jednak klasę AsyncTask omówiono w recepturze 4.11. Najpierw przyjrzyj się temu, jak stosować klasę Thread. Na listingu 4.15 pokazano, jak zastosować tę klasę dla aktywności korzystającej z sieci. Listing 4.15. Kod aktywności korzystającej z sieci public class NetworkConnection extends Activity { /** Wywoływana, gdy aktywność tworzona jest po raz pierwszy */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); Thread thread = new Thread(new Runnable(){ public void run() { getServerData(); } }); thread.start(); } }
Jak widać, w momencie uruchamiania aktywności w metodzie onCreate() należy utworzyć obiekt thread za pomocą obiektu Runnable. Po wywołaniu metody start() obiektu thread wywoływana jest metoda run() klasy Runnable. W metodzie run() można wywołać inne (czasochłonne) metody i operacje, które mogłyby blokować wątek główny i prowadzić do problemów z reagowaniem aplikacji.
4.10. Używan e wątków do tworzen a szybko reagujących apl kacj
|
159
Często po zakończeniu pracy wątek zwraca dane, które należy wyświetlić użytkownikowi aplikacji. Próba aktualizacji interfejsu GUI z poziomu uruchomionego wątku (poza wątkiem głównym) prowadzi do awarii programu. Komunikat o błędzie informuje, że przyczyną problemu jest próba modyfikacji interfejsu użytkownika poza głównym wątkiem odpowiedzialnym za ten interfejs. Aby zmodyfikować interfejs użytkownika na podstawie wygenerowanych danych, należy zastosować klasę Handler. Technikę tę opisano w recepturze 4.12. Wątki tworzone i uruchamiane w przedstawiony sposób działają nawet po zakończeniu korzystania z aplikacji przez użytkownika. Można śledzić uruchomione wątki i nakazać im zakończenie pracy (zwykle robi się to przez ustawienie zmiennej logicznej oznaczającej koniec operacji). Istnieje też prostsze rozwiązanie. Aby mieć pewność, że wątki przestaną działać po zakończeniu pracy aplikacji, należy przed wywołaniem metody start() obiektu thread ustawić wątek jako demon: thread.setDaemon(true);
W niektórych sytuacjach przydatne jest przypisywanie nazw do obiektów thread. Nazwę wątku można podać w czasie tworzenia obiektu thread: Thread thread = new Thread(); Thread thread = new Thread(runnable, "ThreadName1");
Można też wywołać metodę setName() obiektu thread: thread.setName("ThreadName2");
Nazwy wątków są niewidoczne dla użytkowników, jednak pojawiają się w różnych dziennikach diagnostycznych i pomagają ustalić, który wątek jest przyczyną problemów.
4.11. Korzystanie z klasy AsyncTask do wykonywania operacji w tle Johan Pelgrim
Problem Programista chce wykonywać długotrwałe operacje lub wczytywać zasoby z sieci, a jednocześnie wyświetlać informacje o postępie i wyniki w interfejsie użytkownika.
Rozwiązanie Należy zastosować klasy AsyncTask i ProgressDialog.
Omówienie Wprowadzenie W punkcie Processes and Threads poradnika Android Dev Guide wyjaśniono, że nigdy nie należy blokować wątku interfejsu użytkownika ani manipulować androidowym interfejsem użyt-
160
|
Rozdz ał 4. Komun kacja wewnątrz- m ędzyprocesowa
kownika poza wątkiem odpowiedzialnym za ten interfejs. Niestosowanie się do tych zaleceń prowadzi do problemów. Można jednak uruchamiać procesy w tle i aktualizować interfejs użytkownika w odpowiedzialnym za niego wątku (czyli w wątku głównym). Służy do tego kilka technik. Podejście oparte na klasie AsyncTask jest bardzo wygodne i każdy programista aplikacji na Android powinien je znać. Aby uzyskać pożądany efekt, należy utworzyć klasę pochodną od AsyncTask. AsyncTask to klasa abstrakcyjna z jedną metodą abstrakcyjną — Result doInBackground(Params... params);. Klasa AsyncTask tworzy wątek roboczy, w którym uruchamiany jest kod metody doInBackground. Result i Params to dwa typy, które trzeba zdefiniować we wspomnianej klasie pochodnej. Trzecim takim typem jest opisany dalej Progress. W recepturze 11.15 omówiono potencjalnie długie zadanie, które obejmuje przetwarzanie zawartości strony internetowej (dokumentu XML) i zwracanie wyniku jako listy obiektów typu Datum. Zwykle operacje tego typu należy wykonywać poza wątkiem interfejsu użytkownika. W pierwszej wersji wszystkie operacje wykonywane są w tle. Użytkownik widzi na pasku tytułu kręcące się kółko, a aktualizacja widoku ListView ma miejsce po zakończeniu przetwarzania. Jest to typowe podejście, które nie zakłóca operacji wykonywanych przez użytkownika. Aktualizacja interfejsu następuje tu po otrzymaniu wyników. W drugiej wersji zastosowano modalne okno dialogowe, aby wyświetlić informacje o postępie wykonywanego w tle zadania. W niektórych sytuacjach użytkownik nie powinien wykonywać żadnych operacji w trakcie przetwarzania. Opisana technika pozwala uzyskać taki efekt. Interfejs użytkownika obejmuje trzy przyciski (obiekty Button) i widok ListView. Pierwszy przycisk uruchamia pierwszy proces odświeżania. Drugi przycisk powiązany jest z drugim procesem odświeżania, a trzeci służy do usuwania danych z widoku ListView (zobacz listing 4.16). Listing 4.16. Główny układ
Na listingu 4.17 poszczególne elementy interfejsu użytkownika są przypisywane do różnych pól (w metodzie onCreate). Ponadto ustawiane są odbiorniki kliknięć.
4.11. Korzystan e z klasy AsyncTask do wykonywan a operacj w tle
|
161
Listing 4.17. Metody onCreate() i onItemClick() ListView mListView; Button mClear; Button mRefresh1; Button mRefresh2; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); mListView = (ListView) findViewById(R.id.listView1); mListView.setTextFilterEnabled(true); mListView.setOnItemClickListener(this); mRefresh1 = (Button) findViewById(R.id.button1); mClear = (Button) findViewById(R.id.button3); mClear.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { mListView.setAdapter(null); } }); } public void onItemClick(AdapterView parent, View view, int position, long id) { Datum datum = (Datum) mListView.getItemAtPosition(position); Uri uri = Uri.parse("http://androidcookbook.com/Recipe.seam?recipeId=" + datum.getId()); Intent intent = new Intent(Intent.ACTION_VIEW, uri); this.startActivity(intent); }
W dwóch kolejnych podpunktach opisano dwa przypadki użycia — przetwarzanie w tle i przetwarzanie na pierwszym planie.
Pierwszy przypadek użycia — przetwarzanie w tle Najpierw należy utworzyć klasę wewnętrzną pochodną od AsyncTask: protected class LoadRecipesTask1 extends AsyncTask
Jak widać, w definicji klasy należy określić trzy typy. Pierwszy z nich to typ parametru podawanego przy uruchamianiu zadania wykonywanego w tle (tu jest to parametr typu String obejmujący adres URL). Drugi typ służy do aktualizowania informacji o postępie (będzie potrzebny dalej). Trzeci typ to typ wartości zwracanej przez metodę doInBackground. Zwykle jest to typ, który można wykorzystać do zaktualizowania określonego elementu interfejsu użytkownika (tu jest nim widok ListView). Pora zaimplementować metodę doInBackground: @Override protected ArrayList
162
|
Rozdz ał 4. Komun kacja wewnątrz- m ędzyprocesowa
} catch (IOException e) { e.printStackTrace(); } catch (XmlPullParserException e) { e.printStackTrace(); } return datumList; }
Jak widać, kod jest prosty. Metodę parse (która tworzy listę obiektów Datum) omówiono w recepturze 11.15. Wynik wykonania metody doInBackground przekazywany jest jako argument do metody onPostExecute tej samej klasy wewnętrznej. Ta ostatnia metoda służy do aktualizowania elementów interfejsu użytkownika, dlatego należy wyświetlić wyniki za pomocą adaptera widoku ListView. @Override protected void onPostExecute(ArrayList
Teraz trzeba uruchomić zadanie. Można to zrobić w metodzie onClickListener powiązanej z przyciskiem mRefresh1. W tym celu należy wywołać metodę execute(Params... params) klasy AsyncTask (tu wywołanie to execute(String... urls)). mRefresh1.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { LoadRecipesTask1 mLoadRecipesTask = new LoadRecipesTask1(); mLoadRecipesTask.execute( "http://androidcookbook.com/seam/resource/rest/recipe/list"); } });
Teraz uruchomiona aplikacja pobiera receptury i zapełnia widok ListView, jednak użytkownik nie wie, że w tle wykonywane są operacje. Aby to zmienić, można zastosować pasek postępu z małą animacją, widoczny w prawym górnym rogu paska tytułowego aplikacji. Pożądany efekt można uzyskać przez wywołanie w metodzie onCreate metody requestWindow Feature(Window.FEATURE_INDETERMINATE_PROGRESS);. Następnie należy uruchomić animację informującą o postępie. W tym celu trzeba wywołać metodę setProgressBarIndeterminateVisibility(Boolean visibility) w nowej metodzie onPre Execute klasy wewnętrznej. protected void onPreExecute() { MainActivity.this.setProgressBarIndeterminateVisibility(true); }
Animację informującą o postępie, wyświetlaną na pasku tytułowym okna, można zatrzymać przez wywołanie wspomnianej wcześniej metody w metodzie onPostExecute: protected void onPostExecute(ArrayList
Gotowe! Zobaczmy, jakie postępy poczyniliśmy z aplikacją.
4.11. Korzystan e z klasy AsyncTask do wykonywan a operacj w tle
|
163
Jak widać, jest to elegancki mechanizm, pozwalający zwiększyć komfort pracy użytkowników.
Drugi przypadek użycia — przetwarzanie na pierwszym planie W tej wersji aplikacja wyświetla modalne okno dialogowe z informacjami o postępie wczytywania receptur w tle. Okna tego rodzaju oparte są na klasie ProgressDialog. Najpierw należy dodać do aktywności pole tego typu: ProgressDialog mProgressDialog;
Następnie trzeba dodać metodę onCreateDialog, aby móc reagować na wywołania showDialog i tworzyć okno dialogowe. protected Dialog onCreateDialog(int id) { switch (id) { case DIALOG_KEY: mProgressDialog = new ProgressDialog(this); mProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); mProgressDialog.setMessage("Pobieranie receptur..."); mProgressDialog.setCancelable(false); return mProgressDialog; } return null; }
n o p q
n W tym miejscu należy obsługiwać żądanie i tworzenie wszystkich okien dialogowych. DIA LOG_KEY to stała typu int o arbitralnej wartości (tu jest to 0) określającej dane okno dialogowe. o Styl animacji o postępie ustawiono na STYLE_HORIZONTAL, co prowadzi do wyświetlania poziomego paska postępu. Domyślny styl to STYLE_SPINNER. p Ustawiany jest też niestandardowy komunikat, wyświetlany nad paskiem postępu. q Wywołanie metody setCancelable z argumentem false powoduje wyłączenie przycisku Back, przez co okno dialogowe staje się modalne. Nową implementację klasy AsyncTask pokazano na listingu 4.18. Listing 4.18. Implementacja klasy AsyncTask protected class LoadRecipesTask2 extends AsyncTask
n
@Override protected ArrayList
164
|
Rozdz ał 4. Komun kacja wewnątrz- m ędzyprocesowa
} catch (IOException e) { e.printStackTrace(); } catch (XmlPullParserException e) { e.printStackTrace(); } } return datumList; } @Override protected void onProgressUpdate(Integer... values) { mProgressDialog.setProgress(values[0]); } @Override protected void onPostExecute(ArrayList
q r
s
}
Występuje tu kilka nowych elementów. n Przed uruchomieniem działającego w tle procesu wyświetlane jest modalne okno dialogowe. o W działającym w tle procesie aplikacja w pętli przechodzi po wszystkich adresach URL (oczekiwana jest większa ich liczba). Pozwala to wyświetlać trafne informacje o postępie. p Pasek postępu można aktualizować za pomocą metody publishProgress. Warto zauważyć, że typ argumentu to int. Wartości tego typu są przekształcane na wartości drugiego typu zdefiniowanego w klasie, czyli Integer. q Wywołanie metody publishProgress prowadzi do wywołania metody onProgressUpdate, przyjmującej argumenty typu Integer. Można oczywiście zastosować też typ String (lub inny) przez zmianę drugiego typu w definicji klasy wewnętrznej i typu w wywołaniu metody publishProgress na String. r Pierwsza wartość typu Integer służy do ustawienia nowej wartości na pasku postępu w oknie ProgressDialog. s Okno dialogowe jest zamykane, co prowadzi do jego usunięcia. Teraz można połączyć wszystkie elementy w implementacji odbiornika onClickListener dla drugiego przycisku odświeżania. mRefresh2.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { LoadRecipesTask2 mLoadRecipesTask = new LoadRecipesTask2(); String url = "http://androidcookbook.com/seam/resource/rest/recipe/list"; showDialog(DIALOG_KEY); n mLoadRecipesTask.execute(url, url, url, url, url); o } });
n Okno dialogowe jest wyświetlane przez wywołanie metody showDialog z argumentem DIA LOG_KEY. W efekcie wywoływana jest zdefiniowana wcześniej metoda onCreateDialog.
4.11. Korzystan e z klasy AsyncTask do wykonywan a operacj w tle
|
165
o Nowe zadanie jest wywoływane z pięcioma adresami URL, co pozwala wyświetlić informacje o postępie. Efekt powinien wyglądać podobnie jak na rysunku 4.1.
Rysunek 4.1. Pobieranie receptur w tle
Wniosek Implementowanie wykonywania zadań w tle za pomocą klasy AsyncTask jest bardzo proste. Technikę tę należy stosować do wszystkich długich procesów, które powinny aktualizować interfejs użytkownika.
Zobacz też Receptura 11.15; http://developer.android.com/guide/topics/fundamentals/processes-and-threads.html.
4.12. Przesyłanie komunikatów między wątkami za pomocą kolejki wątków aktywności i komponentu obsługi Vladimir Kroz
Problem Programista chce przekazywać dane z usługi lub innego wykonywanego w tle zadania do aktywności. Ponieważ aktywności działają w wątku interfejsu użytkownika, niebezpieczne jest wywoływanie ich w wątkach tła. 166
|
Rozdz ał 4. Komun kacja wewnątrz- m ędzyprocesowa
Rozwiązanie Można napisać klasę zagnieżdżoną pochodną od androidowej klasy Handler. Następnie należy przesłonić metodę handleMessage() wczytującą komunikaty z kolejki wątków. Obiekt Handler trzeba przekazać do wątku roboczego, zwykle przez konstruktor klasy roboczej. W wątku roboczym do przesyłania komunikatów można stosować różne metody obtainMessage() i send Message(). Dzięki temu aktywność wywoływana jest wprawdzie w metodzie handleMessage(), ale w wątku obsługi zdarzenia, co pozwala na bezpieczną aktualizację interfejsu GUI.
Omówienie W wielu sytuacjach wątek musi działać w tle i przesyłać informacje do wątku interfejsu użytkownika z głównej aktywności. Na poziomie architektury można zastosować jedno z dwóch podejść: • wykorzystać androidową klasę AsyncTask; • uruchomić nowy wątek.
Choć korzystanie z klasy AsyncTask jest bardzo wygodne, czasem trzeba samodzielnie utworzyć wątek roboczy. W takich sytuacjach zwykle przesyła się informacje z powrotem do wątku aktywności. Warto pamiętać, że Android nie zezwala innym wątkom na modyfikowanie zawartości głównego wątku interfejsu użytkownika. Dlatego dane trzeba umieścić w komunikatach, a następnie przesyłać je za pomocą kolejki komunikatów. Aby uzyskać pożądany efekt, należy najpierw dodać egzemplarz klasy Handler — np. w klasie MapActivity (listing 4.19). Listing 4.19. Komponent obsługi public class MyMap extends MapActivity { . . . public Handler _handler = new Handler() { @Override public void handleMessage(Message msg) { Log.d(TAG, String.format("Handler.handleMessage(): msg=%s", msg)); // Tu wątek głównej aktywności odbiera komunikaty // Należy umieścić tu kod obsługi komunikatów od innych wątków super.handleMessage(msg); } }; . . . }
Teraz w wątku roboczym należy przesyłać komunikat do kolejki aktywności, gdy do głównej aktywności trzeba dodać komponent obsługi (listing 4.20). Listing 4.20. Przekazywanie obiektu Runnable do kolejki /** * Wykonywanie zadań w tle */ class MyThreadRunner implements Runnable { // @Override public void run() {
4.12. Przesyłan e komun katów m ędzy wątkam
|
167
while (!Thread.currentThread().isInterrupted()) { // Przykładowy komunikat – w prawdziwym kodzie // należy umieścić w nim sensowne dane Message msg = Message.obtain(); msg.what = 999; MyMap.this._handler.sendMessage(msg); // Przykładowy kod symulujący opóźnienie w trakcie pracy ze zdalnym serwerem try { Thread.sleep(5000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } }
4.13. Tworzenie androidowej wersji kalendarza Epoch (napisanego w HTML-u i JavaScripcie) Wagied Davids
Problem Programista potrzebuje niestandardowego kalendarza napisanego w JavaScripcie i chce wiedzieć, jak zapewnić komunikację między kodem w JavaScripcie a kodem w Javie.
Rozwiązanie Za pomocą komponentu WebView należy wczytać plik HTML z napisanym w JavaScripcie kalendarzem Epoch. Oto krótki opis kroków, które powinieneś wykonać:
1. Pobierz kalendarz Epoch DHTML/JavaScript ze strony http://www.javascriptkit.com/script/ script2/epoch/index.shtml.
2. Utwórz katalog assets w katalogu projektu androidowego (np. TestCalendar/assets). 3. Napisz główny plik HTML, w którym korzystasz z kalendarza Epoch. 4. Utwórz aktywność Androida przeznaczoną do uruchamiania kalendarza Epoch. Pliki umieszczone w katalogu assets Androida wskazuje się za pomocą ścieżki file:///android_ asset/ (zwróć uwagę na trzy początkowe ukośniki i liczbę pojedynczą w słowie asset).
Omówienie Aby umożliwić interakcję między warstwą widoku (opartą na JavaScripcie) a warstwą logiki (napisaną w Javie), trzeba zastosować interfejs Java – JavaScript. Tu jest nim klasa wewnętrzna MyJavaScriptInterface. W funkcji onDayClick() (listing 4.22) pokazano, jak wywoływać funkcje JavaScriptu w aktywności Androida. Odbywa się to na przykład tak: webview.loadUrl("java script:popup();");. Na listingu 4.21 pokazano komponent napisany w HTML-u i JavaScripcie.
168
|
Rozdz ał 4. Komun kacja wewnątrz- m ędzyprocesowa
Listing 4.21. Plik calendarview.html
Listing 4.22. Plik CalendarView.java import java.util.Date; import import import import import import import import import import import import import import
android.app.Activity; android.content.Intent; android.os.Bundle; android.os.Handler; android.util.Log; android.view.View; android.view.View.OnClickListener; android.webkit.JsResult; android.webkit.WebChromeClient; android.webkit.WebSettings; android.webkit.WebView; android.widget.Button; android.widget.ImageView; android.widget.Toast;
import com.pfizer.android.R; import com.pfizer.android.utils.DateUtils; import com.pfizer.android.view.screens.journal.CreateEntryScreen; public class CalendarViewActivity extends Activity { private static final String tag = "CalendarViewActivity"; private ImageView calendarToJournalButton; private Button calendarDateButton; private WebView webview; private Date selectedCalDate; private final Handler jsHandler = new Handler(); /** Wywoływana, gdy aktywność tworzona jest po raz pierwszy */
170
|
Rozdz ał 4. Komun kacja wewnątrz- m ędzyprocesowa
@Override public void onCreate(Bundle savedInstanceState) { Log.d(tag, "Tworzenie widoku..."); super.onCreate(savedInstanceState); // Ustawianie warstwy widoku Log.d(tag, "Ustawianie warstwy widoku"); setContentView(R.layout.calendar_view); // Tworzenie wpisów w dzienniku calendarToJournalButton = (ImageView) this.findViewById (R.id.calendarToJournalButton); calendarToJournalButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { Log.d(tag, "Przekierowanie -> CreateEntryScreen ..."); Intent intent = intent = new Intent(getApplicationContext(), CreateEntryScreen.class); startActivity(intent); } }); // Data wybrana przez użytkownika calendarDateButton = (Button) this.findViewById(R.id.calendarDateButton); // Dostęp do obiektu WebView webview = (WebView) this.findViewById(R.id.webview); // Pobieranie ustawień WebSettings settings = webview.getSettings(); // Włączanie obsługi JavaScriptu settings.setJavaScriptEnabled(true); // Włączanie kontrolek przybliżania settings.setSupportZoom(true); // Dodawanie interfejsu do korzystania z JavaScriptu webview.addJavaScriptInterface(new MyJavaScriptInterface(), "android"); // Ustawianie klasy pochodnej od WebChromeClient webview.setWebChromeClient(new MyWebChromeClient()); // Wczytywanie adresu URL pliku HTML webview.loadUrl("file:///android_asset/calendarview.html"); } public void setCalendarButton(Date selectedCalDate) { Log.d(tag, jsHandler.obtainMessage().toString()); calendarDateButton.setText( DateUtils.convertDateToSectionHeaderFormat( selectedCalDate.getTime())); } /** *
4.13. Tworzen e andro dowej wersj kalendarza Epoch (nap sanego w HTML-u JavaScr pc e)
|
171
* @param selectedCalDate */ public void setSelectedCalDate(Date selectedCalDate) { this.selectedCalDate = selectedCalDate; } /** * * @return */ public Date getSelectedCalDate() { return selectedCalDate; } /** * INTERFEJS JAVA->JAVASCRIPT * * @author wagied * */ final class MyJavaScriptInterface { private Date jsSelectedDate; MyJavaScriptInterface() { // PUSTA } public void onDayClick() { jsHandler.post(new Runnable() { public void run() { // Java kieruje instrukcje do JavaScriptu webview.loadUrl("javascript: popup();"); } }); } /** * UWAGA Funkcja ta jest używana w JavaScripcie. * Przyjmuje datę ustawioną przez użytkownika w widoku WebView * * @param dateStr */ public void setSelectedDate(String dateStr) { Toast.makeText(getApplicationContext(), dateStr, Toast.LENGTH_SHORT).show(); Log.d(tag, "Wybrana data: JavaScript -> Java : " + dateStr); // Ustawianie daty wybranej przez użytkownika setJsSelectedDate(new Date(Date.parse(dateStr))); Log.d(tag, "java.util.Date Object: " + Date.parse(dateStr).toString()); } private void setJsSelectedDate(Date userSelectedDate) { jsSelectedDate = userSelectedDate; }
172
|
Rozdz ał 4. Komun kacja wewnątrz- m ędzyprocesowa
public Date getJsSelectedDate() { return jsSelectedDate; } } /** * Wyskakujące okno z ostrzeżeniami (na potrzeby debugowania) * * @author wdavid01 * */ final class MyWebChromeClient extends WebChromeClient { @Override public boolean onJsAlert(WebView view, String url, String message, JsResult result) { Log.d(tag, message); result.confirm(); return true; } } @Override public void onDestroy() { Log.d(tag, "Usuwanie widoku!"); super.onDestroy(); } }
Na potrzeby debugowania utworzono klasę MyWebChromeClient (jest to klasa wewnętrzna z modyfikatorem final, pochodna od klasy WebChromeClient i zdefiniowana w końcowej części głównej klasy) oraz przesłonięto metodę onJsAlert().
4.13. Tworzen e andro dowej wersj kalendarza Epoch (nap sanego w HTML-u JavaScr pc e)
|
173
174
|
Rozdz ał 4. Komun kacja wewnątrz- m ędzyprocesowa
ROZDZIAŁ 5.
Dostawcy treści
5.1. Wprowadzenie — dostawcy treści Ian Darwin
Omówienie Dostawcy treści to jeden z ciekawszych pomysłów wprowadzonych w Androidzie. Pozwala na współdzielenie danych przez zupełnie niepowiązane aplikacje. Takie dane zwykle są przechowywane w bazie SQLite, jednak nie są porządkowane w ustalony wcześniej specjalny sposób. Znane są tylko nazwy tabel i pól. Jednym z często stosowanych dostawców treści jest dostawca danych kontaktowych z Androida. W pierwszej recepturze z tego rozdziału pokazano, jak łatwo jest pobrać początkowy zestaw danych (służy do tego intencja, czego może się już domyśliłeś, jednak zwracany jest identyfikator URI, a nie konkretne dane). Następnie dotrzesz do konkretnych danych za pomocą kursorów bazy SQLite. W następnej recepturze zobaczysz, jak utworzyć własnego dostawcę danych. Także to zadanie (jak zapewne się spodziewasz) można wykonać za pomocą specjalnego interfejsu. Choć nie jest to bezpośrednio związane z dostawcami treści, Android udostępnia też ogólniejszy mechanizm zdalnego wywoływania procedur, oparty na AIDL-u (ang. Android Inteface Definition Language). Ponieważ dotyczy on podobnych zagadnień, opisano go w końcowej recepturze w tym rozdziale.
5.2. Pobieranie danych z dostawcy treści Ian Darwin
Problem Programista chce wczytywać dane z dostawcy treści (na przykład dostawcy danych kontaktowych).
175
Rozwiązanie Należy utworzyć identyfikator URI do wybierania osób z listy kontaktów, otworzyć go w intencji za pomocą metody startActivityForResult, pobrać identyfikator URI ze zwróconej intencji, wywołać metodę Activity.getContentProvider() i przetworzyć dane za pomocą metod klasy Cursor bazy SQLite.
Omówienie Tu przedstawiono kod służący do wskazywania danych kontaktowych. Jest to fragment kodu programu TabbyText opracowanej przeze mnie aplikacji do wysyłania SMS-ów z tabletów z systemem Honeycomb obsługujących tylko sieć Wi-Fi (pozostały kod znajdziesz w recepturze 11.17). Najpierw w głównym programie należy skonfigurować odbiornik OnClickListener, tak aby po wciśnięciu przycisku Znajdź osobę uruchamiał aplikację Contacts. b.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View arg0) { Uri uri = ContactsContract.Contacts.CONTENT_URI; System.out.println(uri); Intent intent = new Intent(Intent.ACTION_PICK, uri); startActivityForResult(intent, REQ_GET_CONTACT); } });
Adres URI jest wstępnie zdefiniowany. Jego wartość to content://com.android.contacts/contacts. Stała REQ_GET_CONTACT jest wybrana arbitralnie. Służy tylko do powiązania intencji z kodem komponentu obsługi, ponieważ w bardziej skomplikowanych aplikacjach często uruchamianych jest kilka intencji, wymagających obsługi wyników w różny sposób. Po wciśnięciu przycisku sterowanie przekazywane jest z omawianego programu do aplikacji Contacts. Użytkownik może wybrać osobę, do której chce wysłać SMS. Aplikacja Contacts jest wtedy przenoszona w tło, a sterowanie wraca do głównego programu, do metody onActivityResult(). Oznacza to, że uruchomiona aktywność zakończyła działanie i zwróciła wynik. W następnym fragmencie kodu pokazano, w jaki sposób metoda onActivityResult() przetwarza odpowiedź od aktywności na kursor bazy SQLite (listing 5.1). Listing 5.1. Metoda OnActivityResult @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == REQ_GET_CONTACT) { switch(resultCode) { case Activity.RESULT_OK: // Interfejs API aplikacji Contaxt jest jednym z najtrudniejszych w użyciu. // Najpierw należy pobrać dane kontaktowe, ponieważ intencja zwraca tylko // identyfikator URI Uri resultUri = data.getData(); // Na przykład content //contacts/people/123 Cursor cont = getContentResolver().query(resultUri, null, null, null, null); if (!cont.moveToNext()) { // Oczekiwany jest jeden wiersz Toast.makeText(this, "Brak danych w kursorze", Toast.LENGTH_LONG).show(); return; } ...
176
|
Rozdz ał 5. Dostawcy treśc
Należy zwrócić uwagę na kilka ważnych kwestii. Najpierw trzeba się upewnić, że kod żądania jest odpowiedni, a zmienna resultCode ma wartość RESULT_OK lub RESULT_CANCELED (po wykryciu innej wartości należy wyświetlić okno dialogowe z ostrzeżeniem). Następnie należy wyodrębnić identyfikator URI z otrzymanej odpowiedzi (z danych zwróconej intencji). Identyfikator ten należy podać w tworzonym zapytaniu. Używana jest tu odziedziczona metoda getContentResolver() aktywności (do pobierania obiektu ContentResolver) oraz metoda query() (tworzy kursor bazy SQLite). Aplikacja oczekuje, że użytkownik wybierze jedną osobę. Jeśli jest inaczej, należy zgłosić błąd. Jeżeli wszystko przebiega prawidłowo, należy przejść dalej i wczytać dane za pomocą kursora SQLite. Omawianie formatu bazy danych aplikacji Contacts wykracza poza zakres tej receptury. Zagadnienie to opisano w recepturze 11.17.
5.3. Pisanie dostawcy treści Ashwini Shahapurkar
Problem Aplikacje często generują dane, które można przetwarzać i analizować w innych programach. Programista chce zagwarantować, że aplikacja udostępnia dane w możliwie bezpieczny sposób (bez zapewniania bezpośredniego dostępu do swojej bazy danych).
Rozwiązanie Należy napisać niestandardowego dostawcę treści, który umożliwia innym programom dostęp do danych wygenerowanych w danej aplikacji.
Omówienie Dostawcy treści umożliwiają innym programom dostęp do danych wygenerowanych w danej aplikacji. Niestandardowy dostawca treści wymaga utworzenia bazy danych i zapewnia na nią nakładkę, używaną przez inne programy. Aby poinformować inne programy o dostępności dostawcy treści, należy zadeklarować go w pliku AndroidManifest.xml w następujący sposób:
Tu podano klasę MyContantProvider. Jest to klasa pochodna od klasy ContentProvider. W nowej klasie należy przesłonić następujące metody: onCreate(); delete(Uri, String, String[]); getType(Uri); insert(Uri, ContentValues); query(Uri, String[], String, String[], String); update(Uri, ContentValues, String, String[]);
Są to metody używane jako nakładki na SQL-owe zapytania do bazy SQLite. Metody te przetwarzają parametry wejściowe i kierują zapytania do bazy danych. Pokazano to na listingu 5.2. 5.3. P san e dostawcy treśc
|
177
Listing 5.2. Dostawca treści public class MyContentProvider extends ContentProvider { DatabaseHelper mDatabase; private static final int RECORDS = 1; public static final Uri CONTENT_URI = Uri .parse("content://com.example.android.contentprovider"); public static final String AUTHORITY = "com.example.android.contentprovider"; private static final UriMatcher matcher = new UriMatcher( UriMatcher.NO_MATCH); static { matcher.addURI(AUTHORITY, "records", RECORDS); } @Override public int delete(Uri uri, String selection, String[] selectionArgs) { // Tu należy umieścić kod do usuwania rekordów z bazy danych return 0; } @Override public String getType(Uri uri) { int matchType = matcher.match(uri); switch (matchType) { case RECORDS: return ContentResolver.CURSOR_DIR_BASE_TYPE + "/records"; default: throw new IllegalArgumentException("Nieznany lub błędny URI " + uri); } } @Override public Uri insert(Uri uri, ContentValues values) { // Tu należy umieścić kod do wstawiania danych. // Może być bardzo prosty — będzie wstawiać wszystkie wartości // do bazy i zwracać identyfikator rekordu long id = mDatabase.getWritableDatabase().insert(Helper.TABLE_NAME, null, values); uri = Uri.withAppendedPath(uri, "/" + id); return uri; } @Override public boolean onCreate() { // Inicjowanie komponentów bazy danych return true; } @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { // Tworzenie zapytania za pomocą obiektu SQLiteQueryBuilder SQLiteQueryBuilder qBuilder = new SQLiteQueryBuilder(); qBuilder.setTables(Helper.TABLE_NAME); int uriType = matcher.match(uri); // Kierowanie zapytania do bazy i pobieranie wyników w kursorze Cursor resultCursor = qBuilder.query(mDatabase.getWritableDatabase(),
178
|
Rozdz ał 5. Dostawcy treśc
projection, selection, selectionArgs, null, null, sortOrder, null); resultCursor.setNotificationUri(getContext().getContentResolver(), uri); return resultCursor; } @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { // Do zaimplementowania return 0; } }
Utworzenie dostawcy treści pozwala uniknąć przyznawania dostępu do bazy danych innym programistom i zmniejsza ryzyko powstawania niespójności w bazie.
5.4. Pisanie zdalnej usługi na Android Rupesh Chavan
Problem Programista chce napisać zdalną usługę i korzystać z niej w innej aplikacji.
Rozwiązanie Android pozwala utworzyć interfejs API oparty na języku AIDL. Taki interfejs należy zastosować po stronie klienta i w usłudze, aby umożliwić kontakt między tymi komponentami w ramach komunikacji międzyprocesowej (ang. inter-process communication — IPC).
Omówienie Komunikacja IPC to ważne narzędzie w modelu programowania obowiązującym w Androidzie. W ramach komunikacji IPC dostępne są dwa mechanizmy: • komunikacja oparta na intencjach; • komunikacja oparta na zdalnych usługach.
W tej recepturze omówiono komunikację opartą na zdalnych usługach. Ten androidowy mechanizm pozwala tworzyć wywołania metod wyglądające jak lokalne, ale wykonywane w innym procesie. Wymaga to zastosowania języka AIDL. Po stronie usługi trzeba zadeklarować interfejs w AIDL-u, a narzędzie AIDL na podstawie tego interfejsu automatycznie wygeneruje powiązany interfejs Javy. Narzędzie AIDL generuje też klasę namiastki z abstrakcyjną implementacją metod z interfejsu usługi. Konkretna klasa usługi powinna być klasą pochodną od klasy namiastki i obejmować konkretne implementacje metod udostępnianych w interfejsie. W kliencie usługi trzeba wywołać jej metodę onBind(), aby móc połączyć się z usługą. Metoda onBind() zwraca klientowi obiekt klasy namiastki. Na listingu 5.3 pokazano kod z pliku AIDL.
5.4. P san e zdalnej usług na Andro d
|
179
Listing 5.3. Plik AIDL package com.demoapp.service; interface IMyRemoteService { String getMessage(); }
Środowisko Eclipse automatycznie generuje zdalny interfejs odpowiadający plikowi AIDL. Zdalny interfejs obejmuje też wewnętrzną klasę namiastki z implementacją klasy RemoteService. Implementację klasy namiastki z klasy usługi pokazano na listingu 5.4. Listing 5.4. Namiastka zdalnej usługi private IMyRemoteService.Stub myRemoteServiceStub = new IMyRemoteService.Stub() { public int getMessage() throws RemoteException { return "Witaj, świecie!"; } }; // Metoda onBind() z klasy usługi public IBinder onBind(Intent arg0) { Log.d(getClass().getSimpleName(), "onBind()"); return myRemoteServiceStub; }
Przed przejściem do opisu łączenia się klienta z klasą usługi przyjrzyj się głównej części tej klasy. Przedstawiona tu klasa RemoteService tylko zwraca łańcuch znaków. Poniżej pokazano nowe wersje przesłanianych metod onCreate(), onStart() i onDestroy(). Metoda onCreate() usługi jest wywoływana tylko raz w cyklu życia usługi. Metoda onStart() jest wywoływana przy każdym uruchomieniu usługi. Zauważ, że usługa zwalnia wszystkie zasoby w metodzie onDestroy() (listing 5.5). Listing 5.5. Metody onCreate() i onDestroy() public void onCreate() { super.onCreate(); Log.d(getClass().getSimpleName(),"onCreate()"); } public void onStart(Intent intent, int startId) { super.onStart(intent, startId); Log.d(getClass().getSimpleName(), "onStart()"); } public void onDestroy() { super.onDestroy(); Log.d(getClass().getSimpleName(),"onDestroy()"); }
Pora przejść do omówienia klasy klienta. Metody do uruchamiania, zatrzymywania, wiązania i zwalniania usługi oraz wywoływania akcji umieszczono dla uproszczenia w jednym kliencie. Jednak w praktyce jeden klient może uruchamiać usługę, a inny łączyć się z już działającą usługą. W programie znajduje się pięć przycisków — po jednym do uruchamiania, zatrzymywania, wiązania i zwalniania usługi oraz wywoływania akcji. Klienta trzeba powiązać z usługą przed wywoływaniem jej metod. Metodę do uruchamiania usługi przedstawiono na listingu 5.6.
180
|
Rozdz ał 5. Dostawcy treśc
Listing 5.6. Metoda startService() private void startService(){ if (started) { Toast.makeText(RemoteServiceClient.this, "Usługa już działa", Toast.LENGTH_SHORT).show(); } else { Intent i = new Intent(); i.setClassName("com.demoapp.service", "com.demoapp.service.RemoteService"); startService(i); started = true; updateServiceStatus(); Log.d( getClass().getSimpleName(), "startService()" ); } }
Metoda bezpośrednio tworzy intencję i uruchamia usługę za pomocą metody Context.start Service(i). Reszta kodu aktualizuje stan w interfejsie użytkownika. Nie ma tu nic charakterystycznego dla wywoływania zdalnej usługi. To w metodzie bindService() widoczne są różnice względem lokalnej usługi (listing 5.7). Listing 5.7. Metoda bindService() private void bindService() { if(conn == null) { conn = new RemoteServiceConnection(); Intent i = new Intent(); i.setClassName("com.demoapp.service", "com.demoapp.service.RemoteService"); bindService(i, conn, Context.BIND_AUTO_CREATE); updateServiceStatus(); Log.d( getClass().getSimpleName(), "bindService()" ); } else { Toast.makeText(RemoteServiceClient.this, "Nie można powiązać — usługę już powiązano", Toast.LENGTH_SHORT).show(); } }
W tej metodzie klient łączy się z usługą za pomocą klasy RemoteServiceConnection (implementuje ona interfejs ServiceConnection). Obiekt połączenia jest potrzebny w metodzie bindService(). Przyjmuje ona intencję, obiekt połączenia i typ wiązania. W jaki sposób utworzyć połączenie z klasą RemoteService? Potrzebny kod przedstawiono na listingu 5.8. Listing 5.8. Implementacja interfejsu ServiceConnection class RemoteServiceConnection implements ServiceConnection { public void onServiceConnected(ComponentName className, IBinder boundService ) { remoteService = IMyRemoteService.Stub.asInterface((IBinder)boundService); Log.d( getClass().getSimpleName(), "onServiceConnected()" ); } public void onServiceDisconnected(ComponentName className) { remoteService = null; updateServiceStatus(); Log.d( getClass().getSimpleName(), "onServiceDisconnected" ); } };
5.4. P san e zdalnej usług na Andro d
|
181
Stała Context.BIND_AUTO_CREATE gwarantuje, że jeśli usługa jeszcze nie istnieje, aplikacja ją utworzy (choć metoda onstart() jest wywoływana tylko przy bezpośrednim uruchamianiu usługi). Po powiązaniu klienta z usługą i uruchomieniu usługi można wywoływać dowolne metody udostępniane przez usługę. Tu dostępna jest tylko jedna taka metoda — getMessage(). W przykładzie można ją wywołać przez kliknięcie przycisku Wywołaj. Usługa zwróci wtedy komunikat, który aplikacja wyświetla pod przyciskiem. Metodę do wywoływania usługi przedstawiono na listingu 5.9. Listing 5.9. Metoda invokeService() private void invokeService() { if(conn == null) { Toast.makeText(RemoteServiceClient.this, "Nie można wywołać — usługa nie jest powiązana", Toast.LENGTH_SHORT).show(); } else { try { String message = remoteService.getCounter(); TextView t = (TextView)findViewById(R.id.notApplicable); t.setText( "Komunikat: "+message ); Log.d( getClass().getSimpleName(), "invokeService()" ); } catch (RemoteException re) { Log.e( getClass().getSimpleName(), "RemoteException" ); } } }
Po wywołaniu metod usługi można ją zwolnić. Odbywa się to w sposób pokazany na listingu 5.10 (w reakcji na kliknięcie przycisku Zwolnij). Listing 5.10. Metoda releaseService() private void releaseService() { if(conn != null) { unbindService(conn); conn = null; updateServiceStatus(); Log.d( getClass().getSimpleName(), "releaseService()" ); } else { Toast.makeText(RemoteServiceClient.this, "Nie można zwolnić – usługa nie jest powiązana", Toast.LENGTH_SHORT).show(); } }
Można też zatrzymać usługę, klikając przycisk Zatrzymaj. Po wykonaniu tej operacji żaden klient nie może wywołać usługi. Potrzebny kod przedstawiono na listingu 5.11. Listing 5.11. Metoda stopService() private void stopService() { if (!started) { Toast.makeText(RemoteServiceClient.this, "Usługi nie uruchomiono", Toast.LENGTH_SHORT).show(); } else { Intent i = new Intent(); i.setClassName("com.demoapp.service", "com.demoapp.service.RemoteService"); stopService(i);
182
|
Rozdz ał 5. Dostawcy treśc
started = false; updateServiceStatus(); Log.d( getClass().getSimpleName(), "stopService()" ); } }
Jeśli klient i usługa znajdują się w innych pakietach, wówczas w pakiecie z klientem trzeba umieścić plik AIDL (podobnie jak w pakiecie z usługą).
Poznałeś właśnie podstawy korzystania ze zdalnych usług w Androidzie — powodzenia!
5.4. P san e zdalnej usług na Andro d
|
183
184
|
Rozdz ał 5. Dostawcy treśc
ROZDZIAŁ 6.
Grafika
6.1. Wprowadzenie — grafika Ian Darwin
Omówienie Grafika komputera to dowolne materiały wyświetlane poza komponentami interfejsu GUI. Mogą to być wykresy, rysunki itd. Android jest dobrze przystosowany do wyświetlania grafiki, ponieważ obejmuje kompletną implementację standardu OpenGL ES (jest to podzbiór standardu OpenGL przeznaczony dla mniejszych urządzeń). Ten rozdział zaczyna się od receptury dotyczącej stosowania niestandardowej czcionki w specjalnych efektach tekstowych. Dalsze receptury poświęcone są grafice GL i dotykowej obsłudze elementów graficznych. Następne receptury dotyczą plików graficznych. W ostatniej recepturze omówiono technikę przybliżania obrazu za pomocą gestu zbliżania do siebie palców (ang. pinch-in), która pozwala użytkownikom zmieniać wielkość grafiki przy użyciu dotyku.
6.2. Stosowanie niestandardowej czcionki Ian Darwin
Problem Liczba dostępnych czcionek w Androidach z serii 2.x jest zaskakująco mała. Dostępne są tylko trzy odmiany czcionki Droid. Programiści oczekują czegoś więcej.
Rozwiązanie W katalogu assets/fonts (trzeba go samodzielnie utworzyć) zainstaluj wersję TTF lub OTF wybranej czcionki. W kodzie utwórz krój czcionki na podstawie danego zasobu i wywołaj metodę setTypeface() klasy View. Gotowe!
185
Omówienie W aplikacji możesz udostępnić jedną lub więcej czcionek. Nie znalazłem jeszcze udokumentowanego sposobu na instalowanie czcionek systemowych. Wystrzegaj się dużych plików z czcionkami, ponieważ są one pobierane razem z aplikacją i zwiększają jej wielkość. Niestandardowe czcionki dostępne są w formatach TTF i OTF (TrueType i OpenTypeFace; ten drugi format jest rozszerzeniem pierwszego). W katalogu assets projektu należy utworzyć podkatalog fonts i umieścić w nim odpowiednią czcionkę. Choć w XML-u można określać predefiniowane czcionki, własnych czcionek nie da się ustawić w ten sposób. Kiedyś może się to zmieni, jednak na razie do atrybutu android:typeface w XML-u można przypisywać jedynie wartości wyliczeniowe normal, sans, serif i monospace. Dlatego własną czcionkę trzeba określić w kodzie. Istnieje kilka metod z rodziny Typeface.create(). Oto niektóre z nich: • create(String familyName, int style); • create(TypeFace family, inst style); • createFromAsset(AssetManager mgr, String path); • createFromFile(File path); • createFromFile(String path);
Można się domyślić, jak działają poszczególne metody. Parametr style (podobnie jak w Javie) przyjmuje jedną z kilku stałych zdefiniowanych w klasie reprezentującej czcionkę (tu jest to klasa Typeface). W przykładowym kodzie wykorzystano metodę createFromAsset(), dlatego lokalizacja czcionki jest znana. Za pomocą dwóch ostatnich metod można udostępnić współużytkowaną czcionkę. W tym celu należy podać pełną ścieżkę do lokalizacji w katalogu /sdcard. Trzeba wtedy pamiętać o zażądaniu w pliku AndroidManifest.xml uprawnień do odczytu danych z karty SD. Za pomocą dwóch pierwszych metod można tworzyć różne odmiany wbudowanych czcionek. W przykładzie zastosowałem elegancką czcionkę Iceberg firmy SoftMaker Software GmbH (http://www.softmaker.de/). Czcionka ta jest chroniona prawem autorskim i nie mam pozwolenia na jej rozpowszechnianie, dlatego jeśli zechcesz pobrać projekt i go uruchomić, zainstaluj też plik z wersją TrueType tej czcionki. Do tego pliku powinna prowadzić ścieżka assets/fonts/ fontdemo.ttf. Warto zauważyć, że jeśli czcionka jest nieprawidłowa, Android zignoruje błąd bez informowania o tym i zastosuje wbudowaną czcionkę Droid. W przykładowej aplikacji znajdują się dwa obszary z tekstem. W jednym zastosowano wbudowaną czcionkę szeryfową, a w drugim — czcionkę niestandardową. Obszary te zdefiniowano (wraz z różnymi atrybutami) w pliku main.xml (listing 6.1). Listing 6.1. Układ XML ze specyfikacją czcionki
186
|
Rozdz ał 6. Graf ka
Na listingu 6.2 przedstawiono kod źródłowy. Listing 6.2. Ustawianie czcionki niestandardowej public class FontDemo extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); TextView v = (TextView) findViewById(R.id.FontView); n Typeface t = Typeface.createFromAsset(getAssets(), o "fonts/fontdemo.ttf"); v.setTypeface(t, Typeface.BOLD_ITALIC); p } }
n Znajdowanie kontrolki View, dla której program ma zastosować nową czcionkę. o Tworzenie obiektu Typeface za pomocą jednej ze statycznych metod create() klasy Typeface. p Przekazanie obiektu Typeface do metody setTypeface klasy View. Jeśli aplikacja działa poprawnie, powinna wyglądać tak jak na rysunku 6.1.
6.3. Wyświetlanie obracającego się sześcianu za pomocą specyfikacji OpenGL ES Marco Dinacci
Problem Programista chce utworzyć prostą aplikację z wykorzystaniem specyfikacji OpenGL ES.
6.3. Wyśw etlan e obracającego s ę sześc anu za pomocą specyf kacj OpenGL ES
|
187
Rysunek 6.1. Niestandardowa czcionka
Rozwiązanie Utwórz obiekt GLSurfaceView oraz niestandardowy obiekt Renderer do wyświetlania obracającego się sześcianu.
Omówienie Android obsługuje grafikę trójwymiarową poprzez interfejs API OpenGL ES. Jest to odmiana specyfikacji OpenGL zaprojektowana specjalnie na urządzenia przenośne. Tej receptury nie należy traktować jak wprowadzenia do specyfikacji OpenGL. Zakładam, że masz już podstawową wiedzę na temat tej technologii. Na rysunku 6.2 pokazano ostateczny wygląd aplikacji.
Rysunek 6.2. Przykładowa grafika OpenGL
188
|
Rozdz ał 6. Graf ka
Najpierw należy napisać nową aktywność i w metodzie onCreate utworzyć dwa podstawowe obiekty potrzebne przy korzystaniu z interfejsu API OpenGL. Te obiekty to GLSurfaceView oraz Renderer (listing 6.3). Listing 6.3. Przykładowa aktywność wykorzystująca OpenGL public class OpenGLDemoActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Przejście w tryb pełnoekranowy requestWindowFeature(Window.FEATURE_NO_TITLE); getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
}
}
GLSurfaceView view = new GLSurfaceView(this); view.setRenderer(new OpenGLRenderer()); setContentView(view);
Na listingu 6.4 znajduje się kod klasy Renderer, w którym prosty obiekt Cube (opisany dalej) wykorzystano do wyświetlania obracającego się sześcianu. Listing 6.4. Implementacja renderera class OpenGLRenderer implements Renderer { private Cube mCube = new Cube(); private float mCubeRotation; @Override public void onSurfaceCreated(GL10 gl, EGLConfig config) { gl.glClearColor(0.0f, 0.0f, 0.0f, 0.5f); gl.glClearDepthf(1.0f); gl.glEnable(GL10.GL_DEPTH_TEST); gl.glDepthFunc(GL10.GL_LEQUAL); gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_NICEST); } @Override public void onDrawFrame(GL10 gl) { gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT); gl.glLoadIdentity(); gl.glTranslatef(0.0f, 0.0f, -10.0f); gl.glRotatef(mCubeRotation, 1.0f, 1.0f, 1.0f); mCube.draw(gl); gl.glLoadIdentity(); mCubeRotation -= 0.15f; } @Override
6.3. Wyśw etlan e obracającego s ę sześc anu za pomocą specyf kacj OpenGL ES
|
189
public void onSurfaceChanged(GL10 gl, int width, int height) { gl.glViewport(0, 0, width, height); gl.glMatrixMode(GL10.GL_PROJECTION); gl.glLoadIdentity(); GLU.gluPerspective(gl, 45.0f, (float)width / (float)height, 0.1f, 100.0f); gl.glViewport(0, 0, width, height); gl.glMatrixMode(GL10.GL_MODELVIEW); gl.glLoadIdentity(); } }
Metody onSurfaceChanged i onDrawFrame to odpowiedniki funkcji glutReshapeFunc oraz glutDis playFunc biblioteki GLUT. Metoda onSurfaceChanged jest wywoływana po zmianie wielkości powierzchni, na przykład przy przechodzeniu telefonu z trybu poziomowego w pionowy. Druga metoda wywoływana jest przy wyświetlaniu każdej klatki. To w tej metodzie znajduje się kod do wyświetlania sześcianu (listing 6.5). Listing 6.5. Klasa Cube class Cube { private FloatBuffer mVertexBuffer; private FloatBuffer mColorBuffer; private ByteBuffer mIndexBuffer; private float vertices[] = {
private float colors[] = {
-1.0f, -1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, -1.0f, -1.0f, 1.0f, -1.0f, -1.0f, -1.0f, 1.0f, 1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, -1.0f, 1.0f, 1.0f };
0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 1.0f,
1.0f, 1.0f, 0.5f, 0.5f, 0.0f, 0.0f, 0.0f, 0.0f,
0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f,
1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f
}; private byte indices[] = { 0, 1, 2, 3, 4, 3, };
4, 5, 6, 7, 7, 0,
5, 6, 7, 4, 6, 1,
0, 1, 2, 3, 4, 3,
5, 6, 7, 4, 6, 1,
1, 2, 3, 0, 5, 2
public Cube() { ByteBuffer byteBuf = ByteBuffer.allocateDirect(vertices.length * 4); byteBuf.order(ByteOrder.nativeOrder()); mVertexBuffer = byteBuf.asFloatBuffer(); mVertexBuffer.put(vertices); mVertexBuffer.position(0);
190
|
Rozdz ał 6. Graf ka
byteBuf = ByteBuffer.allocateDirect(colors.length * 4); byteBuf.order(ByteOrder.nativeOrder()); mColorBuffer = byteBuf.asFloatBuffer(); mColorBuffer.put(colors); mColorBuffer.position(0); mIndexBuffer = ByteBuffer.allocateDirect(indices.length); mIndexBuffer.put(indices); mIndexBuffer.position(0); } public void draw(GL10 gl) { gl.glFrontFace(GL10.GL_CW); gl.glVertexPointer(3, GL10.GL_FLOAT, 0, mVertexBuffer); gl.glColorPointer(4, GL10.GL_FLOAT, 0, mColorBuffer); gl.glEnableClientState(GL10.GL_VERTEX_ARRAY); gl.glEnableClientState(GL10.GL_COLOR_ARRAY); gl.glDrawElements(GL10.GL_TRIANGLES, 36, GL10.GL_UNSIGNED_BYTE, mIndexBuffer); gl.glDisableClientState(GL10.GL_VERTEX_ARRAY); gl.glDisableClientState(GL10.GL_COLOR_ARRAY); } }
W klasie Cube użyto dwóch obiektów FloatBuffer do przechowywania wierzchołków oraz informacji o kolorze. Ponadto wykorzystano obiekt ByteBuffer na indeksy ścian. Aby bufory działały prawidłowo, ważne jest, żeby ustawić odpowiednią dla platformy kolejność bajtów. Służy do tego metoda order. Po zapełnieniu buforów wartościami z tablic trzeba wewnętrzny kursor ustawić z powrotem na początek danych. W tym celu należy wywołać metodę buffer. position(0).
Zobacz też http://www.khronos.org/opengles.
6.4. Sterowanie obracającym się sześcianem Marco Dinacci
Problem Programista chce, aby użytkownik mógł za pomocą klawiatury sterować wielościanem utworzonym za pomocą OpenGL.
Rozwiązanie Należy utworzyć niestandardowy obiekt GLSurfaceView i przesłonić metodę onKeyUp, tak aby odbierać zdarzenia KeyEvent związane z kontrolerem D-pad.
6.4. Sterowan e obracającym s ę sześc anem
|
191
Omówienie Ta receptura to rozwinięcie receptury 6.3. Pokazano tu, jak kontrolować sześcian za pomocą kontrolera D-pad. Aplikacja ma umożliwiać zwiększanie za pomocą klawiszy kontrolera szybkości obrotu względem osi x i y. Najpoważniejszą zmianą jest utworzenie niestandardowej klasy pochodnej od GLSurfaceView. Pozwala to przesłonić metodę onKeyUp i otrzymywać powiadomienia o zdarzeniach związanych z kontrolerem D-pad. Metodę onCreate aktywności przedstawiono na listingu 6.6. Listing 6.6. Aktywność odpowiedzialna za obracanie sześcianu public class SpinningCubeActivity2 extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Przejście w tryb pełnoekranowy requestWindowFeature(Window.FEATURE_NO_TITLE); getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); // Utworzenie niestandardowego widoku GLSurfaceView view = new OpenGLSurfaceView(this); view.setRenderer((Renderer)view); setContentView(view); } }
Nowa klasa (pochodna od GLSurfaceView) także obejmuje implementację interfejsu Renderer. Metody onSurfaceCreated i onSurfaceChanged są dokładnie takie same jak w poprzedniej recepturze. Większość zmian dotyczy metody onDrawFrame, ponieważ wprowadzono cztery nowe parametry. Parametry mXrot i mYrot pozwalają kontrolować rotację sześcianu względem osi x oraz y, a parametry mXspeed i mYspeed przechowują szybkość obrotu względem tych osi. Każde kliknięcie przycisku z kontrolera D-pad powoduje zmianę wartości tych parametrów oraz szybkości sześcianu. Na listingu 6.7 znajduje się kompletny kod nowej klasy. Listing 6.7. Klasa pochodna od GLSurfaceView class OpenGLSurfaceView extends GLSurfaceView implements Renderer { private private private private private
Cube mCube; float mXrot; float mYrot; float mXspeed; float mYspeed;
public OpenGLSurfaceView(Context context) { super(context); // Zdarzenia będą kierowane do obiektu OpenGLSurfaceView requestFocus(); setFocusableInTouchMode(true);
192
|
Rozdz ał 6. Graf ka
mCube = new Cube(); } @Override public void onDrawFrame(GL10 gl) { gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT); gl.glLoadIdentity(); gl.glTranslatef(0.0f, 0.0f, -10.0f); gl.glRotatef(mXrot, 1.0f, 0.0f, 0.0f); gl.glRotatef(mYrot, 0.0f, 1.0f, 0.0f); mCube.draw(gl); gl.glLoadIdentity(); mXrot += mXspeed; mYrot += mYspeed; } @Override public boolean onKeyUp(int keyCode, KeyEvent event) { if(keyCode == KeyEvent.KEYCODE_DPAD_LEFT) mYspeed -= 0.1f; else if(keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) mYspeed += 0.1f; else if(keyCode == KeyEvent.KEYCODE_DPAD_UP) mXspeed -= 0.1f; else if(keyCode == KeyEvent.KEYCODE_DPAD_DOWN) mXspeed += 0.1f; return true; } // Bez zmian @Override public void onSurfaceCreated(GL10 gl, EGLConfig config) { gl.glClearColor(0.0f, 0.0f, 0.0f, 0.5f); gl.glClearDepthf(1.0f); gl.glEnable(GL10.GL_DEPTH_TEST); gl.glDepthFunc(GL10.GL_LEQUAL); gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_NICEST); } // Bez zmian @Override public void onSurfaceChanged(GL10 gl, int width, int height) { gl.glViewport(0, 0, width, height); gl.glMatrixMode(GL10.GL_PROJECTION); gl.glLoadIdentity(); GLU.gluPerspective(gl, 45.0f, (float)width / (float)height, 0.1f, 100.0f); gl.glViewport(0, 0, width, height); gl.glMatrixMode(GL10.GL_MODELVIEW); gl.glLoadIdentity(); } }
6.4. Sterowan e obracającym s ę sześc anem
|
193
Klasa Cube jest taka sama jak w poprzedniej recepturze. Nie zapominaj o wywołaniu metod requestFocus() i setFocusableInTouchMode(true) w konstruktorze widoku. Jeśli tego nie zrobisz, zdarzenia związane z klawiszami nie będą odbierane.
Zobacz też Receptura 6.3.
6.5. Odręczne rysowanie płynnych linii Ian Darwin
Problem Programista chce umożliwić użytkownikom rysowanie płynnych linii, np. odręczne rysowanie krzywych Béziera, wprowadzanie podpisów itd.
Rozwiązanie Należy utworzyć niestandardową klasę View z odpowiednim odbiornikiem OnTouchListener, obsługującym sytuację, w której dane wyjściowe są wprowadzane szybciej, niż można je przetworzyć. Wyniki należy zapisywać w tablicy i wyświetlać w metodzie onDraw().
Omówienie Autorem pierwotnej wersji kodu jest Eric Burke z firmy Square Inc. Eric napisał program do rejestrowania podpisów w aplikacji Square na potrzeby obsługi zakupów z wykorzystaniem karty kredytowej. Podpis musi być wysokiej jakości, aby można było go zgodnie z prawem zaakceptować jako dowód chęci dokonania zakupu. Dzięki uprzejmości firmy Square kod do rejestrowania podpisów dostępny jest na licencji Apache Software License 2.0. Kodu tego nie można jednak wykorzystać w omawianej recepturze. Później zaadaptowałem kod do rejestrowania podpisów na potrzeby aplikacji JabaGator. Jest to rozwijany przeze mnie bardzo prosty program graficzny, który w 2012 roku chcę udostępnić w sklepie Google Play. JabaGator to program graficzny ogólnego użytku przeznaczony na komputery z Javą oraz na Android. To, że nazwa aplikacji rymuje się z nazwą znanego programu firmy Adobe przeznaczonego do tworzenia ilustracji, jest oczywiście zbiegiem okoliczności. Napisana „w podręcznikowy sposób” początkowa wersja kodu Erica działa, ale bardzo powoli i nierówno. Po analizach ustalono, że warstwa grafiki w Androidzie wysyła zdarzenia dotknięcia w pakietach, jeśli nie może przesłać ich wystarczająco szybko pojedynczo. Każde zdarzenie MotionEvent przekazane do metody onTouchEvent() może obejmować zestaw współrzędnych zarejestrowanych od czasu poprzedniego wywołania tej metody. Aby narysować płynną krzywą, trzeba uwzględnić wszystkie współrzędne. Aby to zrobić, należy pobrać liczbę współrzędnych za pomocą metody getHistorySize() klasy TouchEvent, przejść po liście tych
194
|
Rozdz ał 6. Graf ka
współrzędnych i wywoływać metody getHistoricalX(int) i getHistoricalY(int) w celu określenia poszczególnych punktów (listing 6.8). Listing 6.8. Wyświetlanie wszystkich punktów // W metodzie onTouchEvent(TouchEvent) for (int i=0; i < event.getHistorySize(); i++) { float historicalX = event.getHistoricalX(i); float historicalY = event.getHistoricalY(i); // Dodawanie punktu (historicalX, historicalY) do ścieżki } // Dodawanie punktu (eventX, eventY) do ścieżki
To rozwiązanie jest znacznym usprawnieniem, nadal jednak jest zbyt wolne, aby umożliwiało rysowanie. Jeśli aplikacja nie nadąża z rysowaniem, wielu użytkowników czeka, aż program dotrze do miejsca, w którym zatrzymał się palec. Problem polegał na tym, że w prostej technice po każdym fragmencie linii wywoływana była metoda invalidate(). Jest to poprawne podejście, jednak znacznie spowalnia aplikację, ponieważ Android za każdym razem musi ponownie wyświetlić całą zawartość ekranu. Aby rozwiązać ten problem, należy wywoływać metodę invalidate() tylko dla obszaru, w którym narysowano fragment linii. Poprawne określenie tego obszaru wymaga obliczeń (zobacz metodę expandDirtyRect() na listingu 6.9). Oto, jak sam Eric opisał algorytm do wyznaczania zmodyfikowanego obszaru:
1. Należy utworzyć prostokąt reprezentujący zmodyfikowany obszar. 2. Punkty określające cztery rogi trzeba ustawić na współrzędne X i Y ze zdarzenia ACTION_DOWN. 3. Po zgłoszeniu zdarzeń ACTION_MOVE i ACTION_UP należy rozszerzyć prostokąt, tak aby obejmował nowe punkty. Trzeba przy tym pamiętać o „historycznych” współrzędnych.
4. Do metody invalidate() należy przekazać tylko zmodyfikowany prostokąt. Dzięki temu Android nie będzie ponownie wyświetlał pozostałej części ekranu.
To rozwiązanie sprawia, że kod obsługujący rysowanie działa płynnie, a aplikacja jest użyteczna. Na listingu 6.9 znajduje się moja wersja ostatecznego rozwiązania. Zastosowałem kilka odbiorników OnTouchListener — jeden do rysowania krzywych, jeden do zaznaczania obiektów, jeden do rysowania prostokątów itd. Kod w obecnej postaci nie jest kompletny, jednak fragment do rysowania krzywych działa prawidłowo. Listing 6.9. Plik DrawingView.java // Kod dostępny na licencjach Creative Commons i Apache Software License 2.0 public class DrawingView extends View { private static final float STROKE_WIDTH = 5f; /** Trzeba uwzględnić szerokość linii przy wyznaczaniu zmodyfikowanego obszaru **/ private static final float HALF_STROKE_WIDTH = STROKE_WIDTH / 2; private Paint paint = new Paint(); private Path path = new Path(); /** * Optymalizowanie rysowania przez wywoływanie metody invalidate() dla * możliwie najmniejszego obszaru */ private float lastTouchX; private float lastTouchY;
6.5. Odręczne rysowan e płynnych l n
|
195
private final RectF dirtyRect = new RectF(); final OnTouchListener selectionAndMoveListener = // Pominięto; final OnTouchListener drawRectangleListener = // Pominięto; final OnTouchListener drawOvalListener = // Pominięto; final OnTouchListener drawPolyLineListener = new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { // Log.d("jabagator", "onTouch: " + event); float eventX = event.getX(); float eventY = event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: path.moveTo(eventX, eventY); lastTouchX = eventX; lastTouchY = eventY; // Punkt końcowy nie jest znany, dlatego nie warto marnować cykli // na wywoływanie metody invalidate() return true; case MotionEvent.ACTION_MOVE: case MotionEvent.ACTION_UP: // Rozpoczynanie śledzenia zmodyfikowanego obszaru resetDirtyRect(eventX, eventY); // Jeśli sprzęt rejestruje zdarzenia szybciej, niż można je // przekazać do aplikacji, zdarzenie obejmuje listę // pominiętych punktów. int historySize = event.getHistorySize(); for (int i = 0; i < historySize; i++) { float historicalX = event.getHistoricalX(i); float historicalY = event.getHistoricalY(i); expandDirtyRect(historicalX, historicalY); path.lineTo(historicalX, historicalY); } // Na podstawie listy pominiętych punktów należy utworzyć linię do pierwszego punktu. path.lineTo(eventX, eventY); break; default: Log.d("jabagator", "Nieznane zdarzenie " + event.toString()); return false; } // Uwzględnianie połowy szerokości linii, aby uniknąć przycięcia invalidate( (int) (dirtyRect.left - HALF_STROKE_WIDTH), (int) (dirtyRect.top - HALF_STROKE_WIDTH), (int) (dirtyRect.right + HALF_STROKE_WIDTH), (int) (dirtyRect.bottom + HALF_STROKE_WIDTH)); lastTouchX = eventX; lastTouchY = eventY; return true; }
196
|
Rozdz ał 6. Graf ka
/** * Wywoływana w trakcie sprawdzania pominiętych punktów. Gwarantuje, że * zmodyfikowany obszar obejmuje wszystkie punkty */ private void expandDirtyRect(float historicalX, float historicalY) { if (historicalX < dirtyRect.left) { dirtyRect.left = historicalX; } else if (historicalX > dirtyRect.right) { dirtyRect.right = historicalX; } if (historicalY < dirtyRect.top) { dirtyRect.top = historicalY; } else if (historicalY > dirtyRect.bottom) { dirtyRect.bottom = historicalY; } } /** * Ponowne ustawianie zmodyfikowanego obszaru po wykryciu ruchu */ private void resetDirtyRect(float eventX, float eventY) { // Wartości lastTouchX i lastTouchY ustawiono przy wystąpieniu // zdarzenia ACTION_DOWN dirtyRect.left = Math.min(lastTouchX, eventX); dirtyRect.right = Math.max(lastTouchX, eventX); dirtyRect.top = Math.min(lastTouchY, eventY); dirtyRect.bottom = Math.max(lastTouchY, eventY); } }; /** Konstruktor klasy DrawingView */ public DrawingView(Context context, AttributeSet attrs) { super(context, attrs); paint.setAntiAlias(true); paint.setColor(Color.WHITE); paint.setStyle(Paint.Style.STROKE); paint.setStrokeJoin(Paint.Join.ROUND); paint.setStrokeWidth(STROKE_WIDTH); setMode(MotionMode.DRAW_POLY); } public void clear() { path.reset(); // Ponowne wyświetlenie całego widoku invalidate(); } @Override protected void onDraw(Canvas canvas) { canvas.drawPath(path, paint); } /** * Ustawienie DrawingView na jeden z kilku trybów, na przykład * "select" (na potrzeby przenoszenia lub zmiany wielkości obiektów), * "Draw polyline" (płynne krzywe), "draw rectangle" itd. */ private void setMode(MotionMode motionMode) { switch(motionMode) { case SELECT_AND_MOVE:
6.5. Odręczne rysowan e płynnych l n
|
197
setOnTouchListener(selectionAndMoveListener); break; case DRAW_POLY: setOnTouchListener(drawPolyLineListener); break; case DRAW_RECTANGLE: setOnTouchListener(drawRectangleListener); break; case DRAW_OVAL: setOnTouchListener(drawOvalListener); break; default: throw new IllegalStateException("Nieznany tryb MotionMode " + motionMode); } } }
Na rysunku 6.3 pokazano uruchomiony program JabaGator. Widać tu efekt próby zastosowania czytelnego pisma odręcznego (bez obaw, nie jest to mój oficjalny podpis).
Rysunek 6.3. Przykładowy tekst napisany za pomocą dotyku
Ta technika zapewnia wysoką wydajność rysowania oraz gładkie krawędzie. Kod do zapisywania krzywych w modelu danych używanym przy rysowaniu pominięto, ponieważ jest specyficzny dla aplikacji.
Zobacz też Oryginalny kod i jego opis przygotowany przez Erica można znaleźć w internecie na stronie http://corner.squareup.com/2010/07/smooth-signatures.html.
6.6. Robienie zdjęć za pomocą intencji Ian Darwin
Problem Programista zamierza umożliwić robienie zdjęć w aplikacji, ale nie chce pisać dużej ilości kodu.
198
|
Rozdz ał 6. Graf ka
Rozwiązanie Należy utworzyć obiekt Intent z akcją MediaStore.ACTION_IMAGE_CAPTURE, nieco go dostosować i wywołać dla niego metodę startActivityForResult. Należy też udostępnić wywołanie zwrotne onActivityResult(), aby otrzymywać powiadomienie o zakończeniu korzystania z aparatu przez użytkownika.
Omówienie Na listingu 6.10 znajduje się kompletna aktywność związana z obsługą aparatu z rozwijanej przeze mnie aplikacji JPSTrack. Listing 6.10. Aktywność obsługująca robienie zdjęć import jpstrack.android.MainActivity; import jpstrack.android.FileNameUtils; public class CameraNoteActivity extends Activity { private File imageFile; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Uruchamianie aplikacji Camera za pomocą intencji Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); // Ustawianie pliku, w którym zapisywane jest zdjęcie imageFile = new File(MainActivity.getDataDir(), FileNameUtils.getNextFilename("jpg")); Uri uri = Uri.fromFile(imageFile); intent.putExtra(MediaStore.EXTRA_OUTPUT, uri); intent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 1); // Przekazywanie sterowania na zewnątrz startActivityForResult(intent, 0); } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { switch(requestCode) { case 0: // Robienie zdjęcia switch(resultCode) { case Activity.RESULT_OK: if (imageFile.exists()) Toast.makeText(this, "Zapisano bitmapę w pliku " + imageFile.getAbsoluteFile(), Toast.LENGTH_LONG).show(); else { AlertDialog.Builder alert = new AlertDialog.Builder(this); alert.setTitle("Błąd").setMessage( "Zwrócono OK, ale nie utworzono zdjęcia!").show(); } break; case Activity.RESULT_CANCELED: // Bez zbędnego kodu break; default: Toast.makeText(this,
6.6. Rob en e zdjęć za pomocą ntencj
|
199
"Nieoczekiwana wartość resultCode: " + resultCode, Toast.LENGTH_LONG).show(); } break; default: Toast.makeText(this, "NIEOCZEKIWANE ZAKOŃCZENIE PRACY AKTYWNOŚCI", Toast.LENGTH_LONG).show(); } finish(); // Powrót do głównej aplikacji } }
Jeśli aplikacja ma zapisywać zdjęcie w swoich danych (a nie w katalogu programu Gallery), należy w instrukcji intent.putExtra(MediaStore.EXTRA_OUTPUT, uri); udostępnić identyfikator URI prowadzący do folderu docelowego. Według opinii internautów z różnych forów komponent obsługi tej intencji na platformach poszczególnych producentów może działać w różny sposób. W urządzeniu Motorola Milestone z systemem Android 2.1 od operatora Telus Canada kod z listingu 6.10 powoduje zapisanie w zdefiniowanym katalogu rysunku o wielkości używanej w podglądzie, a w programie Media Gallery — kopii o ¼ pełnej rozdzielczości (1280×960). Pozostaje mieć nadzieję, że w wersji 2.2 problem ten zostanie rozwiązany i kod będzie działał w jednolity sposób.
Zobacz też Robienie zdjęć w sposób opisany w recepturze 6.7 wymaga więcej kodu, ale zapewnia większą kontrolę.
6.7. Robienie zdjęć za pomocą klasy android.media.Camera Marco Dinacci
Problem Programista chce zachować większą kontrolę nad różnymi etapami robienia zdjęć.
Rozwiązanie Należy utworzyć obiekt SurfaceView i zaimplementować wywołania zwrotne zgłaszane w momencie robienia zdjęcia. Zapewniają one kontrolę nad procesem wykonywania zdjęcia.
Omówienie Czasem pożądana jest większa kontrola nad etapami robienia zdjęcia. Możliwe też, że potrzebny jest dostęp do nieprzetworzonych danych obrazu zarejestrowanych przez aparat (na przykład w celu ich zmodyfikowania). Wtedy zastosowanie prostego obiektu Intent nie pozwala uzyskać oczekiwanych efektów.
200
|
Rozdz ał 6. Graf ka
Zamiast tego należy utworzyć nową klasę pochodną od Activity i w jej metodzie onCreate przekształcić widok na pełnoekranowy (listing 6.11). Listing 6.11. Aktywność do robienia zdjęć public class TakePictureActivity extends Activity { private Preview mCameraView; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Przełączanie orientacji na poziomą, ponieważ nie na wszystkich // urządzeniach można łatwo wyświetlać filmy w orientacji pionowej setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); // Ukrywanie nagłówka okna i włączanie trybu pełnoekranowego requestWindowFeature(Window.FEATURE_NO_TITLE); getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); mCameraView= new Preview(this); setContentView(mCameraView); } }
Klasa Preview to najważniejszy element receptury. Klasa ta zarządza obiektami Surface (to tu wyświetlane są piksele) i Camera. W konstruktorze zdefiniowano odbiornik ClickListener, aby użytkownik mógł robić zdjęcia przez dotknięcie ekranu. Po otrzymaniu powiadomienia o kliknięciu aplikacja wykonuje zdjęcie, przekazując jako parametry cztery opcjonalne wywołania zwrotne (listing 6.12). Listing 6.12. Kod klasy pochodnej od SurfaceView class Preview extends SurfaceView implements SurfaceHolder.Callback, PictureCallback { private SurfaceHolder mHolder; private Camera mCamera; private RawCallback mRawCallback; public Preview(Context context) { super(context); mHolder = getHolder(); mHolder.addCallback(this); mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); mRawCallback = new RawCallback(); setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { mCamera.takePicture(mRawCallback, mRawCallback, null, Preview.this); } }); }
W klasie Preview zaimplementowano interfejs SurfaceHolder.Callback, aby otrzymywać powiadomienia o utworzeniu, zmianie i usunięciu powierzchni. Wywołania zwrotne z tego interfejsu umożliwiają poprawną obsługę obiektu Camera (listing 6.13).
6.7. Rob en e zdjęć za pomocą klasy andro d.med a.Camera
|
201
Listing 6.13. Metoda surfaceChanged() @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { Camera.Parameters parameters = mCamera.getParameters(); parameters.setPreviewSize(width, height); mCamera.setParameters(parameters); mCamera.startPreview(); } @Override public void surfaceCreated(SurfaceHolder holder) { mCamera = Camera.open(); configure(mCamera); try { mCamera.setPreviewDisplay(holder); } catch (IOException exception) { closeCamera(); } } @Override public void surfaceDestroyed(SurfaceHolder holder) { closeCamera(); }
Bezpośrednio po utworzeniu obiektu reprezentującego aparat aplikacja wywołuje metodę configure, aby ustawić parametry uwzględniane przy robieniu zdjęcia. Parametry te dotyczą trybu lampy błyskowej, efektów, formatu zdjęcia, wielkości zdjęcia, sceny itd. (listing 6.14). Ponieważ nie wszystkie urządzenia obsługują każdą z tych funkcji, należy zawsze najpierw sprawdzić dostępne mechanizmy. Listing 6.14. Metoda configure() private void configure(Camera camera) { Camera.Parameters params = camera.getParameters(); // Ustawianie formatu zdjęcia. Najczęściej stosuje się format RGB_565 List
202
|
Rozdz ał 6. Graf ka
if (sceneModes.contains(Camera.Parameters.SCENE_MODE_ACTION)) params.setSceneMode(Camera.Parameters.SCENE_MODE_ACTION); else params.setSceneMode(Camera.Parameters.SCENE_MODE_AUTO); // Po ustawieniu trybu FOCUS_MODE_AUTO należy pamiętać o wywołaniu // metody autoFocus() obiektu Camera przed zrobieniem zdjęcia params.setFocusMode(Camera.Parameters.FOCUS_MODE_FIXED); camera.setParameters(params); }
Po usunięciu powierzchni należy zamknąć obiekt reprezentujący aparat i zwolnić powiązane z nim zasoby. private void closeCamera() { if (mCamera != null) { mCamera.stopPreview(); mCamera.release(); mCamera = null; } }
Na końcu zgłaszane jest wywołanie zwrotne onPictureTaken. To tu aplikacja ponownie włącza podgląd i zapisuje plik na dysku. @Override public void onPictureTaken(byte[] jpeg, Camera camera) { // Po zgłoszeniu wszystkich wywołań zwrotnych można znów bezpiecznie włączyć podgląd mCamera.startPreview(); saveFile(jpeg); } }
Na końcu kodu znajduje się implementacja interfejsu ShutterCallback. Zaimplementowano tu także interfejs PictureCallback, aby uzyskać dostęp do nieskompresowanych i nieprzetworzonych danych obrazu (listing 6.15). Listing 6.15. Implementacja interfejsu ShutterCallback class RawCallback implements ShutterCallback, PictureCallback { @Override public void onShutter() { // Powiadamianie użytkownika (zwykle za pomocą dźwięku) o wykonaniu zdjęcia } @Override public void onPictureTaken(byte[] data, Camera camera) { // Manipulowanie nieskompresowanymi danymi obrazu } }
Zobacz też Receptura 6.6.
6.7. Rob en e zdjęć za pomocą klasy andro d.med a.Camera
|
203
6.8. Skanowanie kodu kreskowego lub kodu QR za pomocą programu Google ZXing Daniel Fowler
Problem Programista chce umożliwić w aplikacji skanowanie kodów kreskowych i kodów QR (ang. Quick Response).
Rozwiązanie Aby uzyskać dostęp do funkcji skanera kodów kreskowych Google ZXing, należy zastosować obiekt Intent.
Omówienie Jednym z bardzo wartościowych mechanizmów Androida jest łatwość wykorzystywania istniejących funkcji. Dotyczy to także skanowania kodów kreskowych i kodów QR. Google udostępnia bezpłatną aplikację skanującą, z której można korzystać poprzez obiekt Intent. Do aplikacji można więc łatwo dodać obsługę skanera, co otwiera nowe możliwości w obszarze interfejsu, komunikacji i funkcji. W programie przedstawionym w tej recepturze pokazano, jak za pomocą obiektu Intent uzyskać dostęp do google’owego skanera kodów kreskowych. Upewnij się, że skaner ten jest zainstalowany (można go pobrać ze strony https://market.android.com/details?id=com.google.zxing. client.android). Kod z listingu 6.16 tworzy trzy przyciski, za pomocą których można zeskanować kod QR, kod kreskowy produktu lub inne materiały. Typ zeskanowanego kodu kreskowego oraz informacje zapisane w kodzie wyświetlane są w dwóch polach TextView. Listing 6.16. Układ programu do skanowania
204 |
Rozdz ał 6. Graf ka
android:id="@+id/butOther" android:text="Inne" android:textSize="18sp"/>
W zależności od tego, który przycisk wciśnięto, program umieszcza odpowiednie parametry w obiekcie Intent przed uruchomieniem aktywności skanera ZXing. Następnie aplikacja czeka na zwrócony wynik (listing 6.17). Listing 6.17. Główna aktywność programu skanującego public class Main extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); HandleClick hc = new HandleClick(); findViewById(R.id.butQR).setOnClickListener(hc); findViewById(R.id.butProd).setOnClickListener(hc); findViewById(R.id.butOther).setOnClickListener(hc); } private class HandleClick implements OnClickListener{ public void onClick(View arg0) { Intent intent = new Intent("com.google.zxing.client.android.SCAN"); switch(arg0.getId()){ case R.id.butQR: intent.putExtra("SCAN_MODE", "QR_CODE_MODE"); break; case R.id.butProd: intent.putExtra("SCAN_MODE", "PRODUCT_MODE"); break; case R.id.butOther: intent.putExtra("SCAN_FORMATS", "CODE_39,CODE_93,CODE_128,DATA_MATRIX,ITF"); break; } startActivityForResult(intent, 0); // Uruchamianie skanera kodów kreskowych } } public void onActivityResult(int requestCode, int resultCode, Intent intent) { if (requestCode == 0) { TextView tvStatus=(TextView)findViewById(R.id.tvStatus); TextView tvResult=(TextView)findViewById(R.id.tvResult); if (resultCode == RESULT_OK) { tvStatus.setText(intent.getStringExtra("SCAN_RESULT_FORMAT")); tvResult.setText(intent.getStringExtra("SCAN_RESULT")); } else if (resultCode == RESULT_CANCELED) { tvStatus.setText("Wciśnij przycisk, aby rozpocząć skanowanie.");
6.8. Skanowan e kodu kreskowego lub kodu QR za pomocą programu Google ZX ng
|
205
tvResult.setText("Skanowanie anulowano."); } } } }
W poniższej tabeli pokazano, że można skanować kody z określonych rodzin kodów kreskowych (tryb SCAN_MODE) lub kody kreskowe konkretnego typu (tryb SCAN_FORMATS). Jeśli wiadomo, jakiego rodzaju kod kreskowy jest wczytywany, wybranie konkretnego typu może prowadzić do szybszego odczytania go (aplikacja nie musi wtedy wywoływać wszystkich algorytmów dekodowania kodów kreskowych). Technikę tę zastosowano w wywołaniu intent.putExtra ("SCAN_FORMATS", "CODE_39"). Przy stosowaniu trybu SCAN_FORMATS można podać różne formaty rozdzielone przecinkami (zobacz listing 6.17). SCAN_MODE
SCAN_FORMATS
QR_CODE_MODE
QR_CODE
PRODUCT_MODE
EAN_13 EAN_8 RSS_14 UPC_A UPC_E
ONE_D_MODE
Tak jak dla t ybu PRODUCT_MODE a ponadto
CODE_39 CODE_93 CODE_128 ITF DATA_MATRIX_MODE
DATA_MATRIX
Zespół odpowiedzialny za program ZXing pracuje też nad obsługą innych formatów dla trybu SCAN_FORMATS — CODABAR, RSS_EXPANDED, AZTEC i PDF_417. Teraz możesz zabrać się do pisania aplikacji do skanowania produktów w magazynie lub do tworzenia listy zakupów!
206
|
Rozdz ał 6. Graf ka
Zobacz też http://code.google.com/p/zxing/ i http://developer.android.com/guide/topics/intents/intents-filters.html.
6.9. Wyświetlanie diagramów i wykresów za pomocą klasy AndroidPlot Rachee Singh
Problem Programista chce w aplikacji na Android wyświetlać dane w formie graficznej.
Rozwiązanie Do tworzenia wykresów w Androidzie można wykorzystać jedną z wielu niezależnych bibliotek. W przykładowej aplikacji do narysowania prostego wykresu zastosowano bibliotekę o otwartym dostępie do kodu źródłowego AndroidPlot.
Omówienie Jeśli jeszcze tego nie zrobiłeś, pobierz bibliotekę AndroidPlot ze strony http://androidplot.com/ wiki/Download (możesz wybrać dowolną wersję). Teraz utwórz nowy projekt na Android i dodaj do niego bibliotekę AndroidPlot. W tym celu w folderze projektu należy utworzyć nowy katalog i nazwać go lib. W katalogu tym trzeba umieścić pobrany plik JAR z biblioteką AndroidPlot. Plik ten powinien nosić nazwę Android -plot-core-0.4a-release.jar lub podobną. Na tym etapie w projekcie powinny znajdować się katalogi src, res, gen i lib. Aby móc stosować omawianą bibliotekę, należy dodać ją do ścieżki budowania. W tym celu w środowisku Eclipse należy prawym przyciskiem myszy kliknąć dodany plik .jar i wybrać opcję Build Path/Add to Build Path. Wtedy w projekcie w środowisku Eclipse pojawi się nowy katalog — Referenced Libraries. Przykładowa aplikacja obejmuje pewne zapisane na stałe dane i wyświetla odpowiadający im wykres. Do układu w formacie XML (plik main.xml) trzeba więc dodać wykres typu xy. Na listingu 6.18 pokazano plik main.xml z komponentem XYPlot w układzie liniowym. Listing 6.18. Układ XML z komponentem XYPlot
6.9. Wyśw etlan e d agramów wykresów za pomocą klasy Andro dPlot
|
207
android:layout_height="wrap_content" title="Dane"/>
W kodzie należy pobrać referencję do zdefiniowanego w pliku XML wykresu XYPlot: mySimpleXYPlot = (XYPlot) findViewById(R.id.mySimpleXYPlot);
Dalej w aplikacji inicjowane są dwie tablice liczb używanych do wygenerowania wykresu: // Tworzenie dwóch tablic rysowanych wartości y Number[] series1Numbers = {1, 8, 5, 2, 7, 4}; Number[] series2Numbers = {4, 6, 3, 8, 2, 10};
Tablice należy przekształcić na obiekty XYSeries: XYSeries series1 = new SimpleXYSeries( // Konstruktor SimpleXYSeries przyjmuje listę, dlatego należy przekształcić tablicę na listę Arrays.asList(series1Numbers), // Stała Y_VALS_ONLY sprawia, że jako wartość x ustawiany jest indeks elementu SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, // Ustawianie nagłówka dla zestawu wartości "Seria1");
Trzeba też utworzyć obiekt formatujący typu LineAndPointRenderer, służący do wyświetlania danego zestawu wartości: LineAndPointFormatter series1Format = new LineAndPointFormatter( Color.rgb(0, 200, 0), // Kolor linii Color.rgb(0, 100, 0), // Kolor punktów Color.rgb(150, 190, 150)); // Kolor wypełnienia (opcjonalny)
Teraz do obiektu XYPlot można dodać zestawy liczb series1 i series2: mySimpleXYPlot.addSeries(series1, series1Format); mySimpleXYPlot.addSeries(series2, new LineAndPointFormatter(Color.rgb(0, 0, 200), Color.rgb(0, 0, 100), Color.rgb(150, 150, 190)));
Na koniec wystarczy zwiększyć przejrzystość wykresu: // Zmniejszanie liczby etykiet przedziałów mySimpleXYPlot.setTicksPerRangeLabel(3); // Domyślnie biblioteka AndroidPlot wyświetla pomoce mające ułatwiać tworzenie // wykresu. Aby je ukryć, należy wywołać metodę disableAllMarkup() mySimpleXYPlot.disableAllMarkup(); mySimpleXYPlot.getBackgroundPaint().setAlpha(0); mySimpleXYPlot.getGraphWidget().getBackgroundPaint().setAlpha(0); mySimpleXYPlot.getGraphWidget().getGridBackgroundPaint().setAlpha(0);
Możesz już uruchomić aplikację! Powinna wyglądać tak jak na rysunku 6.4.
6.10. Tworzenie ikony do androidowego launchera za pomocą programu Inkscape Daniel Fowler
Problem Programista chce utworzyć niestandardową ikonę do launchera dla rozwijanej aplikacji na Android. 208 |
Rozdz ał 6. Graf ka
Rysunek 6.4. Wykres wyświetlony za pomocą biblioteki AndroidPlot
Rozwiązanie Inkscape to bezpłatny i rozbudowany program graficzny, który umożliwia eksportowanie wyników pracy do bitmapy. Za pomocą Inkscape’a można tworzyć potrzebne w aplikacji ikony różnej wielkości.
Omówienie Zasoby graficzne na potrzeby aplikacji na Android można zaprojektować za pomocą programu graficznego. Inkscape to bezpłatny, wieloplatformowy program graficzny o rozbudowanych funkcjach. Można go wykorzystać do wygenerowania wysokiej jakości grafiki wektorowej, którą później można wyeksportować w dowolnej rozdzielczości. Program ten doskonale nadaje się do generowania ikon do androidowego launchera (a także innych zasobów graficznych). Więcej informacji o programie Inkscape oraz jego najnowszą wersję znajdziesz w poświęconej mu witrynie — http://inkscape.org/. W czasie tworzenia projektu w środowisku Eclipse w katalogu res/drawable generowana jest domyślna ikona o wymiarach 48×48 pikseli. Ikony zapisywane są w formacie PNG (ang. Portable Network Graphics). Android obsługuje ekrany o różnej gęstości (mierzonej w jednostkach dpi — ang. dots per inch, czyli punkty na cal). Gęstość ekranu może być niska (120 dpi), średnia (160 dpi), wysoka (240 dpi) i bardzo wysoka (320 dpi). Ikony o wymiarach 48×48 pikseli dobrze nadają się dla ekranów o średniej gęstości. Na potrzeby innych wyświetlaczy takie ikony trzeba przeskalować w górę lub w dół. W idealnych warunkach w celu uzyskania najlepszych efektów (wyraźnego obrazu bez pikselizacji) w projekcie należy umieścić ikony dla wszystkich możliwych gęstości ekranu. Podejście to wymaga utworzenia w folderze res czterech katalogów (po jednym dla każdej gęstości wyświetlacza) i zapisania w nich ikon o odpowiedniej wielkości: 6.10. Tworzen e kony do andro dowego launchera za pomocą programu nkscape
| 209
• w katalogu res/drawable-ldpi (dla ekranów o niskiej gęstości) należy umieścić ikonę o wy-
miarach 36×36 pikseli;
• w katalogu res/drawable-mdpi (dla ekranów o średniej gęstości) należy umieścić ikonę o wy-
miarach 48×48 pikseli;
• w katalogu res/drawable-hdpi (dla ekranów o wysokiej gęstości) należy umieścić ikonę o wy-
miarach 72×72 piksele;
• w katalogu res/drawable-xhdpi (dla ekranów o bardzo wysokiej gęstości) należy umieścić
ikonę o wymiarach 96×96 pikseli.
Każda ikona musi obejmować ramkę wokół głównej grafiki. Ramka ta pozwala uzyskać odstępy między elementami na ekranie i zapewnia miejsce na drobne fragmenty rysunków wystające poza kwadrat (rysunek 6.5). Zalecana grubość ramki to 1/12 szerokości ikony. Oznacza to, że obszar zajmowany przez samą grafikę jest mniejszy od wielkości całej ikony: • w ikonach o wymiarach 36×36 pikseli grafika zajmuje obszar 30×30 pikseli; • w ikonach o wymiarach 48×48 pikseli grafika zajmuje obszar 40×40 pikseli; • w ikonach o wymiarach 72×72 piksele grafika zajmuje obszar 60×60 pikseli; • w ikonach o wymiarach 96×96 pikseli grafika zajmuje obszar 80×80 pikseli.
Rysunek 6.5. Ikona z ramką
W trakcie projektowania ikon lepiej jest korzystać z obrazów o wielkości większej niż docelowa. W programach graficznych łatwiej pracuje się z większymi rysunkami. Obrazy takie można też łatwo zmniejszyć po zakończeniu pracy. Rysunek o wymiarach 576×576 pikseli dzieli się bez reszty przez szerokość ikon o każdym rozmiarze. Jest to dobra wielkość dla rysunku roboczego. W programach do obsługi grafiki wektorowej, takich jak Inkscape, wielkość obrazu nie ma znaczenia. Rysunki można skalować w górę i w dół bez utraty jakości. W programie Inkscape używany jest format SVG (ang. Scalable Vector Graphics). Utrata szczegółowości następuje w nim tylko w momencie tworzenia gotowych bitmap na podstawie grafiki wektorowej. Jeśli chcesz się nauczyć projektowania grafiki w programie Inkscape, możesz zajrzeć do jednego z wielu samouczków dostępnych poprzez menu Help oraz w internecie. Dobrym źródłem wiedzy jest strona http://inkscapetutorials.wordpress.com/. Po zaprojektowaniu obrazu w programie Inkscape można wyeksportować go do pliku PNG i wykorzystać jako ikonę aplikacji. W przykładowej aplikacji rysunek przekształcany na ikonę utworzono na podstawie samouczka http://vector.tutsplus.com/tutorials/illustration/creating-a-coffee-cup-with-inkscape/. Jeśli zastosujesz się do instrukcji z samouczka, otrzymasz grafikę z rysunku 6.6. Rysunek ten możesz przekształcić na ikonę aplikacji do zamawiania kawy, stopera używanego przy parzeniu kawy, gry rozgrywanej w czasie przerwy na kawę lub dowolnego innego związanego z kawą programu, nad którym aktualnie pracujesz. Osoby, które nie chcą wykonywać instrukcji z samouczka, mogą pobrać gotowy rysunek z witryny http://openclipart.org.
210
|
Rozdz ał 6. Graf ka
Rysunek 6.6. Filiżanka kawy
Witryna ta to doskonałe źródło ponad 33 000 bezpłatnych obrazków (rysunek 6.7). Gdy wpiszesz hasło „coffee”, zobaczysz różne rysunki związane z kawą — w tym obrazek z rysunku 6.6, przesłany przez autora tej receptury. Wybierz rysunek, kliknij przycisk View SVG i wybierz opcję Plik/Zapisz stronę jako (Firefox) lub Plik/Zapisz jako (Internet Explorer).
Rysunek 6.7. Poszukiwanie idealnego kubka kawy
Za pomocą opcji Export Bitmap programu Inkscape można wygenerować cztery ikony o pożądanych wymiarach. Program otwiera rysunek i określa odpowiednie proporcje na potrzeby eksportowania. Wyeksportować można dowolny rysunek otwarty w programie Inkscape. Warto pamiętać, że obrazki nie powinny być zbyt szczegółowe i mieć zbyt wielu kolorów (w trakcie zmiany wymiarów następuje utrata szczegółowości). Rysunki powinny też zajmować kwadratowy obszar. Ponadto według wytycznych dotyczących tworzenia ikon na Android warto tworzyć obrazki z cieniem u dołu i niewielkim odblaskiem w górnej części (zobacz http://developer.android.com/guide/practices/ui_guidelines/icon_design_launcher.html). Po otwarciu rysunku zmień jego wymiary na 576×576 pikseli. W tym celu wybierz opcję File/ Document Properties (zobacz rysunek 6.8). W polu Custom size ustaw opcje Width i Height na 576. Sprawdź też, czy opcja Units jest ustawiona na px (czyli że jednostką są piksele). Upewnij się, że pole Show page border jest zaznaczone. Przeciągnij po dwie linie pomocnicze z linijek dla osi x i y (wystarczy kliknąć i przeciągnąć dowolny punkt z linijki). Umieść je na ramce mniej więcej na 1/12 szerokości i wysokości od widocznej krawędzi strony. Dokładna pozycja linii pomocniczych określana jest na podstawie właściwości. Jeśli linijki są niewidoczne, wyświetl je za pomocą opcji View/Show/Hide/Rulers. Kliknij dwukrotnie każdą linię pomocniczą i wprowadź następujące wartości:
6.10. Tworzen e kony do andro dowego launchera za pomocą programu nkscape
|
211
Rysunek 6.8. Okno dialogowe Document Properties L n a pomocn cza
x
y
Gó na pozioma
0
528
Dolna pozioma
0
48
Lewa pionowa
48
0
P awa pionowa
528
0
Na tym etapie możliwe powinno być łatwe dostosowanie wielkości obrazu. Można nieco wyjść na obszar obramowania, jeśli jest to potrzebne do zachowania proporcji rysunku. Za pomocą opcji Edit/Select All (lub kombinacji Ctrl+A) zaznacz obrazek, przeciągnij go w odpowiednie miejsce i zmień wielkość, tak aby pasował do pola wyznaczonego przez linie pomocnicze (rysunek 6.9). Po przygotowaniu rysunku i ustaleniu pożądanych proporcji można utworzyć bitmapy na potrzeby projektu androidowego. W środowisku Eclipse otwórz projekt, w którym chcesz zastosować ikony. Przejdź do katalogu res i utwórz w nim cztery nowe podkatalogi (za pomocą opcji File/New/Folder z menu lub New/Folder z menu kontekstowego): • res/drawable-ldpi; • res/drawable-mdpi; • res/drawable-hdpi; • res/drawable-xhdpi.
Istniejący katalog drawable używany jest jako rezerwowy, jeśli nie można znaleźć ikon w innym miejscu lub aplikacja działa w systemie Android 1.5.
212
|
Rozdz ał 6. Graf ka
Rysunek 6.9. Zmiana wielkości w programie Inkscape
W programie Inkscape upewnij się, że rysunek nie jest zaznaczony (kliknij obszar poza nim). Za pomocą opcji File/Export Bitmap wyświetl okno dialogowe Export Bitmap (zobacz rysunek 6.10). Wybierz przycisk Page, a następnie w polu Bitmap Size ustaw Width i Height na 96. Nie musisz zmieniać ustawienia dpi — zostanie automatycznie dostosowane do wartości opcji Width i Height. W polu Filename przejdź do katalogu przeznaczonego na ikonę o bardzo wysokiej gęstości (res/drawable-xhdpi). Jako nazwę pliku wprowadź icon.png. Kliknij przycisk Export, aby wygenerować ikonę.
Rysunek 6.10. Okno dialogowe Export Bitmap 6.10. Tworzen e kony do andro dowego launchera za pomocą programu nkscape
|
213
Ustaw opcje Width i Height dla ikon o trzech innych rozdzielczościach (na wartości 72, 48 i 36). Przejdź do odpowiednich katalogów, aby wyeksportować każdą ikonę. W ostatnim kroku skopiuj ikonę z katalogu res/drawable-mdpi do katalogu drawable, aby zastąpić domyślną ikonę. W efekcie powstanie zestaw ikon o odmiennej wielkości, potrzebnych w urządzeniach o różnych wyświetlaczach (rysunek 6.11).
Rysunek 6.11. Kubki kawy o różnej wielkości
Jeśli w trakcie generowania ikon otwarte było środowisko Eclipse, należy odświeżyć projekt, tak aby zobaczyć w katalogach nowe ikony. Wybierz opcję File/Refresh lub wciśnij klawisz F5 (rysunek 6.12).
Rysunek 6.12. Lokalizacja ikon w projekcie
Przetestuj aplikację w urządzeniach fizycznych i wirtualnych, aby się upewnić, że ikony wyglądają w pożądany sposób (rysunek 6.13).
Rysunek 6.13. Ikony w użyciu
Plikowi z ikoną można nadać nazwę inną niż icon.png. Informacje o zmianie nazwy pliku z ikoną do launchera znajdziesz w recepturze 6.11.
Zobacz też Receptura 6.11, http://inkscape.org/, http://inkscapetutorials.wordpress.com/, http://vector.tutsplus. com/tutorials/illustration/creating-a-coffee-cup-with-inkscape/, http://openclipart.org, http://developer. android.com/guide/practices/ui_guidelines/icon_design_launcher.html. 214
|
Rozdz ał 6. Graf ka
6.11. Łatwe tworzenie ikon do launchera za pomocą programu Paint.NET i grafik z serwisu OpenClipArt.org Daniel Fowler
Problem Programista chce, aby aplikacja odróżniała się od innych i wyglądała bardziej profesjonalnie.
Rozwiązanie Serwis http://OpenClipArt.org to dobre źródło bezpłatnych plików graficznych, które można przekształcić na ikony aplikacji.
Omówienie Gdy programista jest gotowy do udostępnienia aplikacji, musi ustalić, czego potrzebuje, aby umieścić ją w sklepie Google Play. Jedną z potrzebnych rzeczy jest dobra ikona. Jest ona reprezentacją graficzną programu, z którą użytkownicy stykają się najczęściej. Odpowiada aplikacji na ekranach Applications i Manage Applications, a także pełni funkcję skrótu na ekranie głównym. Odpowiednia ikona sprawia, że aplikacja robi dobre pierwsze wrażenie i wyróżnia się spośród innych programów. Programiści, którzy mają zdolności artystyczne lub mogą skorzystać z usług grafika (odpłatnie lub po znajomości), mogą w większym stopniu kontrolować grafikę w aplikacji. Jednak dla wielu programistów tworzenie grafiki jest nieprzyjemnym obowiązkiem. W tej recepturze pokazano, jak szybko przygotować dobrą ikonę (choć technika ta nie daje takiej kontroli jak współpraca z zaangażowanym artystą). W serwisie Open Clipart Library (http://www.openclipart.org) znajduje się ponad 33 000 bezpłatnych grafik. Są to grafiki wektorowe, dlatego doskonale nadają się do przeskalowania do wymiarów ikony. Ikony mają format rastrowy, dlatego po wybraniu odpowiedniej grafiki trzeba przekształcić ją na format ikon Androida (czyli na format PNG). W tej recepturze zobaczysz, jak dodać ikonę do przykładowej aplikacji wyświetlającej tekst Witaj, świecie, utworzonej w recepturze 1.4. Najpierw znajdź odpowiednią bezpłatną grafikę, która będzie punktem wyjścia. Przejdź do strony http://www.openclipart.org i użyj pola Search. Wyniki wyszukiwania mogą obejmować grafiki, które nie pasują do wpisanego tekstu. Wynika to z tego, że w wyszukiwaniu uwzględniane są też znaczniki, opisy i fragmenty słów, a nie tylko nazwa pliku. Dlatego możesz zobaczyć rysunki niepowiązane z szukanym pojęciem, a także obrazki z literówkami w nazwach i z określeniami w obcych językach. Może się zdarzyć, że w ten sposób niespodziewanie natrafisz na odpowiednią grafikę. Przejrzyj strony z wynikami wyszukiwania. Wyniki te mają postać miniatur z tytułem, nazwą osoby, która dodała dany rysunek, datą dodania i liczbą pobrań. W trakcie wyszukiwania grafiki ikony warto pamiętać o kilku kwestiach:
6.11. Łatwe tworzen e kon do launchera
|
215
• Istnieje paleta zalecanych kolorów, zgodna z kompozycją Androida. Jest to tylko zalecenie,
jednak warto się do niego stosować (rysunek 6.14). Unikaj zbyt niestandardowych kolorów.
Rysunek 6.14. Paleta kolorów • Rysunki są znacznie pomniejszane, dlatego unikaj grafiki o zbyt dużej szczegółowości.
Dobrą wskazówką jest miniatura w wynikach wyszukiwania. • Przejrzyste i proste obrazki o płynnych liniach i jasnych, neutralnych kolorach dobrze się
skalują oraz dobrze wyglądają na ekranie urządzenia. • Warto pamiętać o poradach projektowych dla programistów Androida (http://developer.
android.com/guide/practices/ui_guidelines/icon_design_launcher.html). Grafika powinna być elegancka i mieć mały cień u dołu oraz niewielki odblask u góry.
• Ikony są kwadratowe, dlatego szukaj rysunków, które mają w przybliżeniu taki kształt.
Na potrzeby aplikacji wyświetlającej tekst Witaj, świecie wpisałem w polu wyszukiwania słowo earth (rysunek 6.15).
Rysunek 6.15. Wyniki wyszukiwania grafiki
Do tworzenia ikony wybrałem grafikę A simple globe, znalezioną na drugiej stronie wyników wyszukiwania. Należy kliknąć rysunek, aby wyświetlić szczegóły. Grafikę można zapisać na lokalnym komputerze przez kliknięcie jej (lub kliknięcie przycisku View SVG) i późniejsze wybranie opcji z menu Plik. W Firefoksie należy wybrać opcję Zapisz stronę jako i określić lokalizację pliku. W Internet Explorerze trzeba wybrać opcję Zapisz jako. Obie przeglądarki obsługują też skrót Ctrl+S. Plik zostanie zapisany jako grafika wektorowa. Wcześniej wspomniano
216
|
Rozdz ał 6. Graf ka
już, że nie jest to odpowiedni format dla ikon. Na szczęście na stronie grafiki w serwisie Open Clip Art można też zapisać plik w formacie PNG.
Ikony na Android należy udostępnić w czterech różnych wymiarach, aby Android mógł wyświetlić ikonę najlepiej dostosowaną do gęstości ekranu urządzenia. Zaleca się, aby w aplikacji umieszczać ikony we wszystkich potrzebnych rozmiarach. Pozwala to uniknąć wyświetlania nieatrakcyjnie wyglądających ikon w niektórych urządzeniach. Oto cztery wymiary ikon: • 36×36 pikseli dla wyświetlaczy o niskiej gęstości (120 dpi); • 48×48 pikseli dla wyświetlaczy o średniej gęstości (160 dpi); • 72×72 piksele dla wyświetlaczy o wysokiej gęstości (240 dpi); • 96×96 pikseli dla wyświetlaczy o bardzo wysokiej gęstości (320 dpi).
Trzeba też uwzględnić obramowanie. Umożliwia ono eleganckie oddzielanie ikon od siebie i zapewnia miejsce na fragmenty rysunku wykraczające poza kwadrat. Zgodnie z zaleceniami obramowanie powinno mieć 1/12 szerokości ikony (rysunek 6.16).
Rysunek 6.16. Obszar obramowania wokół ikony
Oznacza to, że w praktyce grafika w ikonach powinna zajmować obszar mniejszy niż cała ikona. Oto zalecane wymiary grafiki: • 30×30 pikseli dla ekranów o niskiej gęstości; • 40×40 pikseli dla ekranów o średniej gęstości; • 60×60 pikseli dla ekranów o wysokiej gęstości; • 80×80 pikseli dla ekranów o bardzo wysokiej gęstości.
W serwisie Open Clip Art na stronie z odpowiednią grafiką można kliknąć przycisk PNG, aby utworzyć pliki PNG z rysunkami o czterech odpowiednich wymiarach. W polu obok przycisku PNG należy wprowadzić wielkość pierwszego rysunku (80 dla ikon na ekrany o bardzo wysokiej gęstości; rysunek 6.17). Wartość ta powinna być mniejsza od wielkości ikony (96), aby pozostawić miejsce na obramowanie.
Rysunek 6.17. Przekształcanie grafiki na rysunek w formacie PNG o wielkości 80 pikseli 6.11. Łatwe tworzen e kon do launchera
|
217
Kliknij przycisk PNG, a następnie za pomocą menu Plik przeglądarki (lub kombinacji Ctrl+S) zapisz wygenerowany plik PNG. Aby wrócić do strony z grafiką, wciśnij w przeglądarce przycisk Wstecz. Wyczyść zawartość pola obok przycisku PNG i wprowadź wielkość grafiki odpowiednią dla następnej ikony (60 dla ikon na ekrany o wysokiej gęstości). Ponownie kliknij przycisk PNG i zapisz wygenerowany plik. Powtórz te operacje dla wartości 40 i 30, aby wygenerować dwie następne grafiki. Może tu wystąpić kilka problemów. Czasem w wyniku przekształcania powstaje grafika o ustawionej wcześniej wielkości. Wtedy trzeba ponownie otworzyć stronę grafiki w serwisie Open Clip Art (kliknij pasek adresu i po umieszczeniu kursora po adresie wciśnij klawisz Enter; samo wybranie klawisza F5 nie rozwiązuje problemu). Ponadto czasem przekształcanie grafiki na format PNG kończy się niepowodzeniem. W Firefoksie pojawia się wówczas komunikat z informacją, że grafika zawiera błędy. W Internet Explorerze widoczny jest wtedy mały kwadrat ze znakiem X. W takiej sytuacji możesz wybrać inną grafikę lub zapisać plik SVG i użyć programu graficznego z obsługą tego formatu. Jeszcze inne rozwiązanie to otwarcie menu kontekstowego na stronie grafiki w serwisie Open Clip Art i zapisanie jej jako pliku PNG o wielkości równej pierwotnej grafice. Potem w programie graficznym można zmienić wielkość grafiki i ustawić przezroczystość. Po zastosowaniu przycisku PNG dla wybranej grafiki otrzymasz cztery pliki — każdy z tym samym rysunkiem, ale w innej rozdzielczości (rysunek 6.18). Pliki graficzne nie muszą być idealnie kwadratowe (wymiary mogą być równe na przykład 39×40 zamiast 40×40), ponieważ ewentualne drobne różnice nie mają znaczenia.
Rysunek 6.18. Różnej wielkości ikony przedstawiające Ziemię
Następnie trzeba zmienić wielkość plików z ikonami przez dodanie pustego obramowania. Można to zrobić za pomocą programu graficznego. Przykładowe programy tego typu to GIMP (http://www.gimp.org), Inkscape (http://www.inkscape.org) lub Paint.NET (http://www.getpaint.net; tylko dla systemu Windows). W tej recepturze wykorzystano aplikację Paint.NET. W programie Paint.NET otwórz pierwszy plik graficzny. Ustaw kolor tła na przezroczystość. W tym celu wybierz opcję Window/Colors (lub wciśnij przycisk F8). W oknie dialogowym Colors upewnij się, że w menu rozwijanym wybrana jest opcja Secondary. Następnie kliknij przycisk More, aby wyświetlić zaawansowane opcje. Ustaw opcję Transparency w prawym dolnym rogu okna dialogowego Colors na zero (rysunek 6.19). Następnie otwórz okno dialogowe Canvas Dialog (wybierz opcję Image/Canvas Size lub zastosuj kombinację Ctrl+Shift+R). Zaznacz przycisk opcji By absolute size. Jeśli grafika jest kwadratem, możesz zaznaczyć pole Maintain aspect ratio; jeżeli rysunek ma inny kształt, pole to nie powinno być zaznaczone. W polu Pixel size ustaw szerokość i wysokość ikony odpowiednią dla danej grafiki (o wartości 36 dla grafiki o wymiarach 30×30, 48 dla grafiki o wymiarach 40×40, 72 dla grafiki o wymiarach 60×60 i wartości 96 dla grafiki o wymiarach 80×80). Opcję Anchor ustaw na Middle i kliknij przycisk OK.
218
|
Rozdz ał 6. Graf ka
Rysunek 6.19. Paleta do wybierania kolorów
Zapisz rysunek o zmienionym rozmiarze i powtórz cały proces dla trzech pozostałych grafik, tak aby uzyskać cztery pliki PNG z ikonami o wielkościach 36, 48, 72 i 96 pikseli (zobacz rysunek 6.18).
Cztery pliki trzeba skopiować do projektu, dla którego przeznaczone są ikony. Pliki należy umieścić w projekcie w katalogu res, w podkatalogach przeznaczonych na określone wartości dpi. Jeśli projekt rozwijany jest w środowisku Eclipse, możliwe, że katalog res obejmuje już podkatalogi drawable-hdpi, drawable-ldpi, drawable-mdpi z domyślną ikoną.
6.11. Łatwe tworzen e kon do launchera
|
219
Istniejące ikony należy zastąpić nowymi. Trzeba też dodać nowy katalog, drawable-xhdpi, przeznaczony dla ikony o rozdzielczości xhdpi. Jeśli aplikacja przeznaczona jest dla wersji 1.5 Androida, potrzebny jest też katalog drawable z ikoną o wymiarach 48×48 (rysunek 6.20). W tabeli 6.1 opisano katalogi i ich zawartość.
Rysunek 6.20. Katalogi z ikonami Tabela 6.1. Formaty ikon Katalog
W elkość kony
W elkość rysunku
dp
Gęstość w Andro dz e
Przykładowy wyśw etlacz
drawable ldpi
36×36
30×30
120
ldpi
Ma y QVGA
drawable mdpi
48×48
40×40
160
mdpi
No malny VGA
drawable hdpi
72×72
60×60
240
hdpi
No malny WVGA800
drawable xhdpi
96×96
80×80
320
xhdpi
Niestanda dowy
drawable
48×48
40×40
160
mdpi
No malny VGA
Uwag
Domyślna ikona (stosowana jeśli nie podano innych)
Domyślna ikona (stosowana jeśli nie podano innych)
Plik ikony nie musi nosić nazwy icon.png — można zastosować inną, jeśli jest poprawna i taka sama we wszystkich katalogach drawable. Pliki ikon można na przykład nazwać globe.png. Jeśli nazwa pliku jest inna niż domyślna, wartość atrybutu android:icon w elemencie application w pliku manifestu trzeba zmienić (na przykład z icon na globe). Otwórz plik AndroidManifest. xml, znajdź element application i zmień atrybut android:icon="@drawable/icon" na android:icon= "@drawable/globe".
Warto pamiętać o tym, aby podziękować za bezpłatne materiały. W tym przypadku dziękuję użytkownikowi jhnri4 — autorowi grafiki z serwisu Open Clipart Library.
220
|
Rozdz ał 6. Graf ka
Zobacz też Receptura 1.4, http://developer.android.com/guide/practices/ui_guidelines/icon_design_launcher.html, http://www.openclipart.org, http://www.getpaint.net, http://www.inkscape.org, http://www.gimp.org.
6.12. Korzystanie z plików NinePatch Daniel Fowler
Problem W trakcie projektowania interfejsu użytkownika programista chce zmienić tło domyślnego widoku, aby pasowało do ogólnego stylu aplikacji. Tło musi się poprawnie skalować dla widoków o różnej wielkości.
Rozwiązanie Wykorzystaj androidowe pliki NinePatch, aby umożliwić skalowanie tła wraz ze zmianą wielkości widoku.
Omówienie Na poniższym rysunku tłem słowa Tekst jest zaokrąglony prostokąt (szare pole z czarną ramką). Prostokąt przeskalowano na całej szerokości, aby zmieścił się w nim napis Dłuższy tekst. W efekcie skalowania rogi i pionowe krawędzie zostały zniekształcone, przez co zaokrąglony prostokąt wygląda nieelegancko. Porównaj jego wygląd z drugim prostokątem z napisem Dłuższy tekst, w którym tło ma zachowane proporcje.
Aby prawidłowo zmienić rozmiar tła, poszczególne fragmenty rysunku należy przeskalować w odpowiednim kierunku, a innych w ogóle nie należy modyfikować. To, które części są skalowane i w którym kierunku, pokazano na poniższym diagramie.
Symbol X oznacza, że rogi nie są skalowane. Krawędzie pionowe skalowane są w pionie, a krawędzie poziome — w poziomie. Środkowy obszar skalowany jest w obu kierunkach. Oto, skąd wzięła się nazwa „pliki NinePatch” (czyli pliki dziewięciopolowe): 6.12. Korzystan e z pl ków N nePatch
|
221
4 rogi + 2 krawędzie pionowe + 2 krawędzie poziome + 1 obszar środkowy --------------------łącznie 9 obszarów (pól)
W następnym przykładzie — zamiast domyślnego czarnego obramowania i szarego wypełnienia gradientem kontrolki EditText — zastosowano gładkie turkusowe tło i czarną ramkę. Zaokrąglony prostokąt narysowano w programie graficznym (takim jak GIMP, http://www.gimp. org, lub Paint.NET, http://www.getpaint.net/). Prostokąt jest maleńki (przypomina kółko), tak aby można było go wyświetlać także w małych widokach. Ma jednopikselową ramkę i przezroczyste tło. Wersja z pomarańczową ramką służy do informowania o wybraniu prostokąta za pomocą klawiszy.
W Androidzie trzeba określić, które części pionowych i poziomych krawędzi należy skalować, a także w którym miejscu ma się znajdować zawartość widoku. Aspekty te można ustawić za pomocą wskaźników umieszczanych na obrazku. Do dodawania takich wskaźników służy program draw9patch z katalogu z narzędziami pakietu SDK Androida. Uruchom program i otwórz rysunek tła (przeciągnij go do okna dialogowego programu draw9patch). Program doda do rysunku ramkę o szerokości jednego piksela. Jest to dodatkowa krawędź, na której należy umieścić wskaźniki. Powiększ rysunek za pomocą suwaka Zoom. Na lewej i górnej krawędzi umieść linie wskaźników określające, które piksele w poziomie i pionie mają być powielane w trakcie skalowania. Na prawej i dolnej krawędzi narysuj wskaźniki określające, gdzie ma się znajdować zawartość kontrolki.
Na poniższym rysunku na prawej i dolnej krawędzi widoczne są wskaźniki, określające miejsce na zawartość kontrolki. Jeśli zawartość nie mieści się w wyznaczonym prostokącie, rysunek tła jest rozciągany na podstawie wskaźników ustawionych na lewej i górnej krawędzi.
222
|
Rozdz ał 6. Graf ka
Plik ze wskaźnikami zapisz w katalogu res/drawable projektu. Rodzaj skalowania (dziewięciopolowy lub standardowy), jaki należy zastosować, Android określa na podstawie nazwy pliku. W nazwie plików NinePatch należy przed rozszerzeniem .png umieścić człon .9. Na przykład plik turquoise.png należy nazwać turquoise.9.png. Aby zastosować określony rysunek tła, należy podać go w układzie (android:background="@drawable/turquoise"). Jeśli aplikacja ma ustawiać inny rysunek po wybraniu danej kontrolki, należy zastosować plik XML selektora, np. selector.xml, i zapisać go w katalogu drawable. Oto przykładowy plik tego rodzaju:
Plik ten można wskazać za pomocą atrybutu — android:background="@drawable/selector".
Należy zauważyć, że nowe tło widoku zajmuje mniej miejsca niż jego domyślny odpowiednik (warto o tym wiedzieć, jeśli w projekcie potrzebna jest większa ilość miejsca). Pliki NinePatch można stosować nie tylko do tworzenia prostego tła. Poniższy plik pełni funkcję ramki fotografii.
Zauważ, że wskaźniki na lewej i górnej krawędzi są rozdzielone na fragmenty. Pominięto obszary, których nie należy skalować, ponieważ prowadziłoby to do zniekształceń.
6.12. Korzystan e z pl ków N nePatch
|
223
Zobacz też http://developer.android.com/guide/topics/graphics/2d-graphics.html#nine-patch.
6.13. Tworzenie wykresów na strony HTML5 za pomocą biblioteki RGraph Wagied Davids
Problem Programista chce przedstawić dane w formie wykresu, aby móc modyfikować go za pomocą JavaScriptu.
Rozwiązanie Zamiast tworzyć wykresy w Androidzie za pomocą samej Javy, można wykorzystać RGraph — javascriptową bibliotekę do tworzenia wykresów na strony HTML5. W bibliotece RGraph wykorzystano komponent Canvas z języka HTML5, który nie jest obsługiwany przez przeglądarkę z Androida 1.5. Biblioteka RGraph działa prawidłowo w Androidzie 2.1 i nowszych wersjach platformy (w nich też została przetestowana).
Omówienie Aby utworzyć wykres za pomocą biblioteki RGraph, wykonaj następujące operacje:
1. Utwórz katalog zasobów na pliki HTML. Android wewnętrznie odwzorowuje ten katalog na nazwę file:///android_asset (zwróć uwagę na trzy ukośniki i liczbę pojedynczą w słowie asset).
2. Skopiuj plik rgraphview.html (listing 6.19) do katalogu res/assets/.
224 |
Rozdz ał 6. Graf ka
3. Utwórz katalog res/assets/RGraph na pliki JavaScriptu. 4. Utwórz układ (listing 6.20) i aktywność (listing 6.21), tak jak w innych projektach Androida. Na listingu 6.19 pokazano kod z pliku HTML, w którym wykorzystano bibliotekę RGraph. Na rysunku 6.21 widoczny jest efekt działania tej biblioteki. Listing 6.19. Plik HTML, w którym wykorzystano bibliotekę RGraph
src="RGraph/libraries/RGraph.common.core.js" > src="RGraph/libraries/RGraph.common.annotate.js" > src="RGraph/libraries/RGraph.common.context.js" > src="RGraph/libraries/RGraph.common.tooltips.js" > src="RGraph/libraries/RGraph.common.zoom.js" > src="RGraph/libraries/RGraph.common.resizing.js" > src="RGraph/libraries/RGraph.pie.js" >
Rysunek 6.21. Wykresy wygenerowane przez bibliotekę RGraph
226
|
Rozdz ał 6. Graf ka
Listing 6.20. Plik main.xml
Listing 6.21. Główna aktywność import import import import import import
android.app.Activity; android.os.Bundle; android.webkit.WebChromeClient; android.webkit.WebSettings; android.webkit.WebView; android.webkit.WebViewClient;
public class Main extends Activity { /** Wywoływana, gdy aktywność tworzona jest po raz pierwszy */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); // Pobieranie referencji do obiektu WebView WebView webview = (WebView) this.findViewById(R.id.webview); // Pobieranie ustawień WebSettings webSettings = webview.getSettings(); // Włączanie JavaScriptu na potrzeby interakcji z użytkownikiem webSettings.setJavaScriptEnabled(true); // Wyświetlanie kontrolek do przybliżania i oddalania webSettings.setBuiltInZoomControls(true); webview.requestFocusFromTouch(); // Ustawianie klienta webview.setWebViewClient(new WebViewClient()); webview.setWebChromeClient(new WebChromeClient()); // Wczytywanie strony webview.loadUrl("file:///android_asset/rgraphview.html"); } }
6.13. Tworzen e wykresów na strony HTML5 za pomocą b bl otek RGraph
|
227
6.14. Dodawanie prostej animacji rastrowej Daniel Fowler
Problem Programista chce wyświetlić na ekranie animowany rysunek.
Rozwiązanie Android udostępnia wygodne mechanizmy dodawania animacji do interfejsu użytkownika. Za pomocą klasy AnimationDrawable można łatwo wyświetlać sekwencje rysunków.
Omówienie Aby utworzyć animację, najpierw trzeba przygotować sekwencję rysunków za pomocą programu graficznego. Każdy rysunek odpowiada jednej ramce animacji. Obrazki mają zwykle tę samą wielkość oraz treść, która zmienia się odpowiednio między klatkami. Z tej receptury dowiesz się, jak utworzyć animację zmiany świateł drogowych. Rysunki można wygenerować za pomocą programu do tworzenia grafiki wektorowej Inkscape (jest to aplikacja o otwartym dostępie do kodu źródłowego; zobacz stronę http://inkscape.org). Kopię potrzebnych rysunków można pobrać z serwisu Open Clipart Library (http://www.openclipart.org/). Wpisz w polu wyszukiwania tekst Traffic Lights Turned Off, wybierz rysunek, kliknij przycisk View SVG i zapisz plik z poziomu przeglądarki. Otwórz plik w programie Inkscape. Animacja składa się z czterech rysunków wyświetlanych w takiej kolejności, w jakiej światła zmieniają się w Wielkiej Brytanii — czerwone, czerwone i żółte, zielone, żółte i z powrotem czerwone. Na rysunku w formacie SVG widoczne są wszystkie światła, choć są one ukryte za półprzezroczystym kółkiem. Aby wygenerować pierwszy rysunek, zaznacz kółko zasłaniające czerwone światło i usuń je. Następnie wybierz opcję Edit/Select All, aby zaznaczyć cały rysunek. Potem wybierz opcję File/Export Bitmap. W oknie dialogowym Export Bitmap w obszarze Bitmap size wprowadź w polu Height wartość 150. Określ też nazwę generowanego pliku (np. red.png) oraz przeznaczony dla niego katalog. Kliknij przycisk Export, aby wyeksportować bitmapę. Usuń kółko zasłaniające żółte światło, ponownie wybierz opcję Select All i tak jak wcześniej wyeksportuj rysunek do pliku (np. red_yellow.png). Teraz dwukrotnie wybierz opcję Edit/Undo, aby zasłonić czerwone i żółte światło, a następnie usuń kółko zasłaniające zielone światło. Wyeksportuj rysunek do pliku green.png. Ponownie zastosuj opcję Undo, aby zasłonić zielone światło, po czym usuń kółko zasłaniające żółte światło. Wyeksportuj bitmapę do pliku yellow.png (rysunek 6.22). Cztery pliki potrzebne w animacji są już gotowe.
228
|
Rozdz ał 6. Graf ka
Rysunek 6.22. Okno dialogowe Export Bitmap
Utwórz projekt na Android. Skopiuj cztery wygenerowane pliki do katalogu res/drawable. W tym samym katalogu trzeba zdefiniować element animation-list. We wspomnianym katalogu utwórz plik uktrafficklights.xml. W nowym pliku umieść następujący kod:
W kodzie tym wymieniono używane w animacji rysunki w kolejności ich pojawiania się. Określono też czas wyświetlania każdego obrazka (w milisekundach). Jeśli aplikacja ma przerywać animację po pokazaniu rysunków, atrybut android:oneshot należy ustawić na true. W pliku układu programu należy umieścić widok ImageView. Źródło widoku trzeba ustawić na @drawable/uktrafficklights (czyli na utworzony wcześniej plik).
W klasie pochodnej od Activity zadeklarowany jest obiekt AnimationDrawable (jest to obiekt klasy Androida obsługującej animacje). W metodzie onCreate obiekt ten należy ustawić jako obiekt Drawable używany w widoku ImageView. W ostatnim kroku trzeba uruchomić animację przez wywołanie metody start() obiektu AnimationDrawable (istnieje też metoda stop(), która w razie potrzeby pozwala zakończyć animację). Metoda start() wywoływana jest w metodzie onWindowFocusChanged, co gwarantuje, że wszystkie elementy zostaną wczytane przed uruchomieniem animacji. Animację można też uruchamiać jako reakcję na kliknięcie przycisku lub wystąpienie innego zdarzenia wejścia. Na listingu 6.22 przedstawiono kod aktywności main. 6.14. Dodawan e prostej an macj rastrowej
|
229
Listing 6.22. Aktywność main public class main extends Activity { AnimationDrawable lightsAnimation; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); ImageView lights = (ImageView) findViewById(R.id.imageView1); lightsAnimation=(AnimationDrawable) lights.getDrawable(); } @Override public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); lightsAnimation.start(); } }
Animacje pomagają przyciągnąć uwagę do ekranu i przydają się w grach oraz kreskówkach.
Zobacz też http://inkscape.org, http://www.openclipart.org.
6.15. Przybliżanie obrazu za pomocą gestów dotykowych Pratik Rupwal
Problem Programista chce umożliwić zmianę pozycji rysunku na ekranie za pomocą gestów dotykowych, a także przybliżanie i oddalanie obrazu przez gesty zbliżania i oddalania palców (ang. pinch-in i pinch-out).
Rozwiązanie Aby móc stosować przekształcenia i wyświetlać różne efekty graficzne, należy skalować rysunek jako macierz.
230
|
Rozdz ał 6. Graf ka
Omówienie Najpierw należy prosty widok ImageView umieścić w elemencie FrameLayout w pliku main.xml, co pokazano poniżej.
Kod z listingu 6.23 skaluje widok ImageView jako macierz, co pozwala na przekształcanie obrazu. Listing 6.23. Odbiornik dotyku obsługujący skalowanie import import import import import import import import import import import import
android.app.Activity; android.graphics.Bitmap; android.graphics.Matrix; android.graphics.PointF; android.os.Bundle; android.util.FloatMath; android.util.Log; android.view.MotionEvent; android.view.View; android.view.View.OnTouchListener; android.widget.GridView; android.widget.ImageView;
public class Touch extends Activity implements OnTouchListener { private static final String TAG = "Touch"; // Poniższe macierze służą do przenoszenia i przybliżania obrazu Matrix matrix = new Matrix(); Matrix savedMatrix = new Matrix(); // Możliwy jest jeden z trzech stanów static final int NONE = 0; static final int DRAG = 1; static final int ZOOM = 2; int mode = NONE; // Należy zapamiętać kilka danych związanych z przybliżaniem PointF start = new PointF(); PointF mid = new PointF(); float oldDist = 1f; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); ImageView view = (ImageView) findViewById(R.id.imageView); // Dopasowanie obrazu do środka widoku
6.15. Przybl żan e obrazu za pomocą gestów dotykowych
|
231
view.setScaleType(ImageView.ScaleType.FIT_CENTER); view.setOnTouchListener(this); } public boolean onTouch(View v, MotionEvent event) { ImageView view = (ImageView) v; // Tworzenie skalowalnego obrazu jako macierzy view.setScaleType(ImageView.ScaleType.MATRIX); float scale; // Obsługa zdarzeń związanych z dotykiem switch (event.getAction() & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_DOWN: // Dotknięcie jednym palcem savedMatrix.set(matrix); start.set(event.getX(), event.getY()); Log.d(TAG, "mode=DRAG" ); mode = DRAG; break; case MotionEvent.ACTION_UP: // Podniesiono pierwszy palec case MotionEvent.ACTION_POINTER_UP: // Podniesiono drugi palec mode = NONE; Log.d(TAG, "mode=NONE" ); break; case MotionEvent.ACTION_POINTER_DOWN: // Opuszczono drugi palec // Obliczanie odległości między dotkniętymi punktami oldDist = spacing(event); Log.d(TAG, "oldDist=" + oldDist); // Minimalna odległość między palcami if (oldDist > 5f) { savedMatrix.set(matrix); // Ustawianie środkowego punktu między dwoma dotkniętymi miejscami midPoint(mid, event); mode = ZOOM; Log.d(TAG, "mode=ZOOM" ); } break; case MotionEvent.ACTION_MOVE: if (mode == DRAG) { // Poruszenie pierwszego palca matrix.set(savedMatrix); if (view.getLeft() >= -392) { matrix.postTranslate(event.getX() - start.x, event.getY() - start.y); } } else if (mode == ZOOM) { // Przybliżanie za pomocą gestu zbliżania palców float newDist = spacing(event); Log.d(TAG, "newDist=" + newDist); if (newDist > 5f) { matrix.set(savedMatrix); // Warto ograniczyć tę wartość scale = newDist/oldDist; matrix.postScale(scale, scale, mid.x, mid.y); } } break; } // Przeprowadzanie przekształceń view.setImageMatrix(matrix);
232
|
Rozdz ał 6. Graf ka
return true; // Informuje o tym, że zdarzenie przetworzono } private float spacing(MotionEvent event) { float x = event.getX(0) - event.getX(1); float y = event.getY(0) - event.getY(1); return FloatMath.sqrt(x * x + y * y); } private void midPoint(PointF point, MotionEvent event) { float x = event.getX(0) + event.getX(1); float y = event.getY(0) + event.getY(1); point.set(x / 2, y / 2); } }
6.15. Przybl żan e obrazu za pomocą gestów dotykowych
|
233
234 |
Rozdz ał 6. Graf ka
ROZDZIAŁ 7.
Graficzny interfejs użytkownika
7.1. Wprowadzenie — interfejs GUI Ian Darwin
Omówienie W trakcie rozwijania Androida projektanci musieli podjąć wiele decyzji, od których zależało to, czy platforma odniesie sukces, czy okaże się porażką. Gdy projektanci odrzucili wszystkie inne systemy operacyjne smartfonów (zarówno zastrzeżone, jak i o otwartym dostępie do kodu źródłowego) i zdecydowali się zbudować własny system oparty na jądrze Linuksa, mogli zastosować niemal dowolne rozwiązania. Jeden z ważnych wyborów dotyczył tego, którą technologię rozwijania interfejsu użytkownika zastosować: Java ME, Swing, SWT czy jeszcze inną. Java ME (Java Micro Edition; http://www.oracle.com/technetwork/java/javame/index.html) to zarządzany przez Sun i Oracle oficjalny, standardowy interfejs API dla telefonów komórkowych i innych małych urządzeń. Java ME odniosła duży sukces. Platforma ta działa w setkach milionów telefonów komórkowych. Każde urządzenie BlackBerry wyprodukowane od około 2000 roku i wszystkie aplikacje na smartfony BlackBerry (z okresu przed pojawieniem się platformy BBX) są oparte na Javie ME. Jednak interfejs GUI Javy ME okazał się zbyt ograniczający, ponieważ opracowano go w czasie, gdy telefony komórkowe miały naprawdę małe ekrany. Swing (http://docs.oracle.com/javase/6/docs/technotes/guides/swing/index.html) to interfejs GUI dla wersji Java Standard Edition (Java SE; jest to Java na komputery stacjonarne powiązana z technologiami JDK i JRE). Swing oparty jest na starszym pakiecie narzędzi do tworzenia kontrolek (AWT). W odpowiednich rękach pozwala budować fantastyczne interfejsy (http://filthyrichclients. org/), jest jednak zbyt duży i zużywa zbyt wiele zasobów, aby można go wykorzystać w Androidzie. SWT to warstwa interfejsu GUI dla środowiska IDE Eclipse (http://www.eclipse.org/) i bogatych klientów rozwijanych za pomocą tego środowiska. Jest to warstwa abstrakcyjna, zależna od technologii używanej w systemie operacyjnym (np. Win32 w systemach Microsoftu, GTK w Uniksie i Linuksie itd.).
235
Ostatnim rozważanym rozwiązaniem (i w końcu zastosowanym) była rezygnacja z gotowych systemów. Projektanci Androida opracowali więc własny pakiet narzędzi do tworzenia interfejsów GUI dla smartfonów, choć wykorzystali przy tym wiele dobrych pomysłów z innych pakietów i wyciągnęli wnioski z błędów popełnionych w trakcie rozwijania wcześniejszych rozwiązań. Poznawanie nowych frameworków do tworzenia interfejsów GUI jest pracochłonne. Jeszcze więcej wysiłku wymaga tworzenie za pomocą takich frameworków aplikacji z atrakcyjnym interfejsem użytkownika. Dlatego firma Google zbudowała witrynę Android Design66 (http:// developer.android.com/design/index.html; dotyczy głównie Androida 4. — Ice Cream Sandwich). Inny pomocny zestaw wytycznych projektowych znajdziesz w witrynie Android Patterns (http://www.androidpatterns.com/). Nie jest ona poświęcona pisaniu kodu, ale pokazuje projektantom, jak powinny wyglądać wizualne interfejsy w Androidzie. Witryna ta jest ilustrowana, rozwijana przez społeczność i warta polecenia. Oto uwaga dotycząca terminologii — pojęcie widżet ma dwa różne znaczenia. Wszystkie kontrolki interfejsu GUI, np. przyciski, etykiety i podobne elementy, to widżety. Pochodzą one z pakietu android.widget. Pakiet ten obejmuje też kontenery układu, przypominające połączenie obiektów JPanel i LayoutManager z frameworku Swing. Proste widżety i układy to klasy pochodne od View, dlatego często nazywa się je widokami. Widżety innego rodzaju pojawiają się na ekranie głównym Androida. Są to tzw. widżety aplikacji, pochodzące z odrębnego pakietu android.appwidget. Widżety tego rodzaju często stosuje się do wyświetlania różnych informacji, np. najnowszych wiadomości, prognozy pogody, komunikatów od znajomych lub z sieci społecznościowych itp. W końcowej części rozdziału znajduje się receptura poświęcona widżetom aplikacji. Choć staramy się odpowiednio używać nazw widżet (lub kontrolka) i widżet aplikacji, czasem trzeba z kontekstu wywnioskować, o którego rodzaju widżet chodzi. W tym rozdziale omówiono główne elementy interfejsów GUI Androida. W dwóch dalszych rozdziałach opisano niezwykle ważny komponent ListView oraz elementy „wyskakujące” w urządzeniu — menu, okna dialogowe, komunikaty toast i powiadomienia.
7.2. Poznawanie i przestrzeganie wytycznych tworzenia interfejsu użytkownika Ian Darwin
Problem Wielu programistów, nawet tych dobrych, nie radzi sobie z projektowaniem interfejsów użytkownika.
Rozwiązanie Należy zastosować się do wytycznych tworzenia interfejsów użytkownika. Ale do których?
236
|
Rozdz ał 7. Graf czny nterfejs użytkown ka
Omówienie Wytyczne tworzenia interfejsów użytkownika są znane niemal od czasu wymyślenia interfejsów GUI w Xerox PARC w latach 80. XX wieku, a następnie zaprezentowania ich firmom Microsoft oraz Apple. Zestaw wytycznych musi być dostosowany do platformy. Istnieje kilka źródeł z ogólnymi wytycznymi dotyczącymi urządzeń przenośnych. Porady tego rodzaju znajdziesz też w witrynie Android.com. Jednym z dobrych punktów wyjścia są oficjalne wytyczne tworzenia interfejsów użytkownika na Android (http://developer.android.com/guide/practices/ui_guidelines/index.html) — zwłaszcza jeśli masz doświadczenie w projektowaniu interfejsów użytkownika. Jeżeli nie masz potrzebnego doświadczenia, niektóre inne materiały omówione w tej recepturze mogą Ci pomóc w zrozumieniu zagadnień z obszaru projektowania interfejsów użytkownika. Na stronie http://android-developers.blogspot.com/2010/05/twitter-for-android-closer-look-at.html znajdziesz wnikliwe uwagi na temat wzorców projektowania interfejsów użytkownika. W firmie Research in Motion przygotowano tekst, który wprawdzie dotyczy głównie platformy BlackBerry, ale może być też przydatny dla każdego projektanta aplikacji mobilnych. Tekst ten znajdziesz na stronie http://na.blackberry.com/eng/developers/resources/Newsletter/2010/Featured_ Story_Jan_2010.jsp?html. Jednym z najstarszych poradników na temat interfejsów GUI jest The Gui Guide: International Terminology for the Windows Interface Microsoftu. Dotyczy on w mniejszym stopniu projektowania interfejsu użytkownika, a w większym umiędzynarodawiania. Poradnik ten dostępny był na dyskietkach (pamiętasz jeszcze, co to takiego?) i obejmował zalecane tłumaczenia elementów interfejsu GUI systemu Microsoft Windows na kilkanaście popularnych języków. Obecnie materiały te są już przestarzałe. W latach 80. i 90. XX wieku na sposób rozwijania interfejsów użytkownika za pomocą narzędzi firmy Sun duży wpływ miał ośrodek Xerox PARC, gdzie opracowano (od dawna nieużywany) interfejs użytkownika OPEN LOOK dla Uniksa oraz „wygląd i styl Javy”. Książka Java Look and Feel Design Guidelines to klasyczny, ale dotyczący konkretnej technologii tekst z owych czasów. Bardziej ogólną pozycją opracowaną w firmie Sun jest książka Designing Visual Interfaces: Communication-Oriented Techniques, napisana przez Mullera i Sano. Jest to dokładne omówienie zagadnień projektowych. Dotyczy ono głównie komputerów stacjonarnych (z systemami Mac, Unix i Windows), jednak przedstawione tu zasady przydadzą się przy projektowaniu różnych interakcji między człowiekiem a komputerem. Inne omówienie interfejsów dla komputerów stacjonarnych znajduje się w nowszej książce Microsoftu pt. About Face: The Essentials of Interaction Design. Obecnie dostępne jest trzecie wydanie tej pozycji. Autorem pierwszego wydania był Alan Cooper, nazywany „ojcem Visual Basica”.
7.2. Poznawan e przestrzegan e wytycznych tworzen a nterfejsu użytkown ka
|
237
7.3. Obsługa zmian konfiguracji przez oddzielenie widoku od modelu Alex Leffelman
Problem W momencie zmiany konfiguracji urządzenia (najczęściej dzieje się to z powodu zmiany orientacji) aktywność jest usuwana i ponownie tworzona, co utrudnia zachowywanie informacji o stanie.
Rozwiązanie Należy oddzielić interfejs użytkownika od modelu danych, tak aby usunięcie aktywności nie wpływało na dane związane ze stanem.
Omówienie Prawie wszyscy programiści aplikacji na Android (oprócz tych, którzy w porę zapoznali się z tą recepturą) w trakcie rozwijania pierwszego programu natrafiają na pewien problem — aplikacja działa doskonale, jednak po zmianie orientacji telefonu wszystkie informacje o stanie zostają utracone! W momencie zmiany konfiguracji (czytaj: orientacji) urządzenia framework interfejsu użytkownika Androida usuwa aktualnie uruchomioną aktywność i tworzy ją ponownie, stosując początkową konfigurację. Pozwala to projektantowi zoptymalizować układ pod kątem różnych orientacji i wielkości ekranu. Takie podejście utrudnia jednak pracę programiście, ponieważ pożądane jest zachowanie stanu aktywności sprzed zmiany orientacji, która doprowadziła do usunięcia zawartości ekranu. Próba poradzenia sobie z opisanym problemem może prowadzić do powstania różnych skomplikowanych rozwiązań — mniej lub bardziej eleganckich. Warto jednak zrobić krok wstecz i odpowiednio zaprojektować aplikację, co pozwoli zastosować przejrzysty, niezawodny kod, który ułatwi pracę wszystkim zainteresowanym. Graficzny interfejs użytkownika (ang. graphical user interface — GUI) jest dokładnie tym, na co wskazuje nazwa, czyli graficzną reprezentacją modelu danych, która umożliwia użytkownikowi komunikowanie się z danymi i manipulowanie nimi. Interfejs nie jest modelem danych. Warto na przykładzie wyjaśnić, dlaczego należy o tym pamiętać. Wyobraź sobie aplikację do gry w kółko i krzyżyk. Prosta główna aktywność powinna obejmować przynajmniej kontrolkę GridView (z odpowiednim adapterem) do wyświetlania planszy oraz kontrolkę TextView do informowania, który użytkownik ma ruch. Po kliknięciu przez użytkownika wybranego pola na planszy pojawia się w nim znak X lub O. Początkujący programista aplikacji na Android może zapisywać dane w dwuwymiarowej tablicy reprezentującej planszę. Pozwala to określić, czy gra została zakończona i kto wygrał (listing 7.1).
238
|
Rozdz ał 7. Graf czny nterfejs użytkown ka
Listing 7.1. Pierwsza wersja klasy TicTacToeActivity public class TicTacToeActivity extends Activity { private TicTacToeState[][] mBoardState; private GridView mBoard; private TextView mTurnText; @Override public void onCreate(Bundle savedInstanceState) { setContentView(R.layout.main); mBoardState = new TicTacToeState[3][3]; mBoard = (GridView)findViewById(R.id.board); mTurnText = (TextView)findViewById(R.id.turn_text); // Konfigurowanie adaptera, odbiorników OnClickListener itd. dla obiektu mBoard } }
To rozwiązanie jest łatwe do wymyślenia i zaimplementowania. Gra działa doskonale. Jeśli jednak użytkownik obróci telefon w trakcie rozgrywki, nagle zobaczy pustą planszę, co oznacza odsunięcie w czasie nieuniknionego zwycięstwa. Jak wcześniej opisano, framework interfejsu użytkownika usuwa aktywność, a następnie ponownie ją tworzy. Wywołuje przy tym metodę onCreate() i ustawia pustą planszę. Możliwe, że w trakcie analizy kodu z listingu 7.1 pomyślałeś: „Argument savedInstanceState typu Bundle wygląda obiecująco!”. Rzeczywiście tak jest. W tym niezwykle (może nawet nadmiernie) uproszczonym przykładzie można zapisać dane planszy w obiekcie Bundle i wykorzystać je przy odtwarzaniu zawartości ekranu. Istnieje nawet para metod (onRetainNonConfi gurationInstance() i getLastNonConfigurationInstance()), która pozwala przekazać dowolny obiekt Object z dawnej, usuwanej aktywności do nowej. W tym przykładzie wystarczy przekazać do nowej aktywności tablicę mBoardState i gotowe. W przyszłości chciałbyś jednak pisać rozbudowane, udane i wspaniałe aplikacje, a opisane podejście nie skaluje się dobrze, dlatego ciężko jest je stosować dla skomplikowanych interfejsów. Na szczęście można zastosować lepsze rozwiązanie! To dlatego oddzielenie interfejsu GUI od modelu danych jest tak przydatne. Interfejs GUI może zostać usunięty, odtworzony i zmodyfikowany, jednak same dane pozostają nienaruszone niezależnie od zmian w interfejsie użytkownika. Na listingu 7.2 pokazano, jak zapisać stan gry w odrębnej klasie z danymi. Listing 7.2. Wyodrębniona klasa z gry w kółko i krzyżyk public class TicTacToeGame { private TicTacToeState[][] mBoardState; public TicTacToeGame() { mBoardState = new TicTacToeState[3][3]; // Inicjowanie }
7.3. Obsługa zm an konf guracj przez oddz elen e w doku od modelu
|
239
public TicTacToeState getCellState(int row, int col) { return mBoardState[row][col]; } public void setCellState(int row, int col, TicTacToeState state) { mBoardState[row][col] = state; } // Inne metody narzędziowe do określania tego, czyj jest ruch, czy gra // została zakończona itd. }
To rozwiązanie nie tylko pomaga zachowywać stan aplikacji, ale ogólnie jest dobrym projektem obiektowym. Gdy dane są już bezpieczne poza zmienną aktywnością, można by zadać pytanie: „Jak można uzyskać do nich dostęp w trakcie tworzenia interfejsu?”. Stosuje się do tego dwa podejścia: 1) można zadeklarować wszystkie zmienne w klasie TicTacToeGame jako statyczne (z modyfikatorem static); 2) można zaprojektować klasę TicTacToeGame jako singleton, co umożliwia dostęp do jednego globalnego egzemplarza używanego w aplikacji. Z uwagi na moje preferencje projektowe skłaniam się do stosowania drugiego z tych podejść. Aby przekształcić klasę TicTacToeGame w singleton, należy dodać do konstruktora modyfikator private i w początkowej części klasy umieścić następujące wiersze: private static TicTacToeGame instance = new TicTacToeGame(); public static TicTacToeGame getInstance() { return instance; };
Teraz wystarczy pobrać dane gry i ustawić elementy interfejsu użytkownika, aby w odpowiedni sposób wyświetlić dane. Bardzo wygodne jest umieszczenie przeznaczonego do tego kodu w odrębnej funkcji, nazwanej na przykład refreshUI(), którą można zastosować po każdej modyfikacji danych przez aktywność. Na przykład do obsługi kliknięcia komórki planszy przez użytkownika wystarczą dwa wiersze kodu w odbiorniku — jeden z wywołaniem modyfikującym model danych (poprzez singleton TicTacToeGame) i jeden odświeżający interfejs użytkownika. Wprawdzie może wydawać się to oczywiste, ale warto przypomnieć, że gdy proces aplikacji kończy pracę, klasy z danymi są usuwane. Jeśli program zostanie zamknięty przez użytkownika lub system, prowadzi to oczywiście do utraty danych. Aby w takiej sytuacji zachować dane, potrzebna jest bardziej trwała pamięć, na przykład w postaci pliku lub bazy danych. Omawianie tych zagadnień wykracza jednak poza zakres tej receptury. Opisane tu podejście pozwala bardzo skutecznie oddzielić wizualną reprezentację danych od nich samych i sprawia, że obsługa zmiany orientacji jest bardzo prosta. Wystarczy wywołać metodę refreshUI() w onCreate(Bundle), aby zagwarantować, że w trakcie usuwania i odtwarzania aktywności możliwy będzie zarówno dostęp do danych, jak i poprawne wyświetlenie aktywności. Dodatkowo powstaje lepszy projekt obiektowy, a kod staje się bardziej przejrzysty, skalowalny i łatwiejszy w konserwacji.
240 |
Rozdz ał 7. Graf czny nterfejs użytkown ka
7.4. Tworzenie przycisku i odbiornika kliknięć Ian Darwin
Problem Programista chce, aby po wciśnięciu przycisku przez użytkownika program wykonywał określone operacje.
Rozwiązanie Utwórz w układzie przycisk. W metodzie onCreate() znajdź go na podstawie identyfikatora ViewID. Wywołaj metodę setOnClickListener() znalezionego przycisku. W implementacji metody OnClickListener sprawdź identyfikator ViewID (jeśli odbiornik używany jest dla kilku zdarzeń) i wykonaj odpowiednie operacje.
Omówienie Tworzenie przycisków w układach jest proste. Oto przykładowy przycisk w układzie w pliku XML:
W metodzie onCreate() aktywności znajdź przycisk na podstawie identyfikatora ViewID (tu ten identyfikator ma wartość R.id.start_button). Wywołaj metodę setOnClickListener(), przekazując obiekt z implementacją interfejsu OnClickListener. W implementacji interfejsu OnClickListener sprawdź identyfikator ViewID i wykonaj odpowiednie operacje: public class Main extends Activity implements OnClickListener { public void onCreate() { startButton = findViewById(R.id.start_button); startButton.setOnClickListener(this); ... } @Override public void onClick(View v) { switch (v.getId()) { case R.id.start_button: // Tu należy rozpocząć operacje związane z przyciskiem ... case R.id.some_other_button: // Itd. } } }
7.4. Tworzen e przyc sku odb orn ka kl kn ęć
|
241
Doświadczony programista Javy umieściłby odbiornik OnClickListener w anonimowej klasie wewnętrznej (tę technikę stosowano we frameworkach AWT i Swing od Javy 1.1). Jednak z uwagi na wydajność w dokumentacji do pierwszych wersji Androida odradzano to rozwiązanie. Zalecane podejście to implementacja interfejsu OnClickListener w aktywności i sprawdzanie identyfikatora ViewID (jest to technika z Javy 1.0). Obecnie urządzenia stały się znacznie wydajniejsze (podobnie jak w okresie po wprowadzeniu Swinga), dlatego starsze rozwiązanie straciło na popularności (choć przez pewien czas programiści będą stosować oba podejścia).
7.5. Pięć sposobów na dołączanie odbiornika zdarzeń Daniel Fowler
Problem Warto znać różne sposoby pisania komponentów obsługi zdarzeń — zarówno po to, aby wiedzieć, kiedy stosować poszczególne podejścia, a także z uwagi na to, że w tej książce i w innych miejscach możesz natknąć się na różne metody.
Rozwiązanie W trakcie pisania oprogramowania bardzo rzadko istnieje tylko jeden sposób na osiągnięcie celu. Dotyczy to także podłączania zdarzeń obiektów View. W tej recepturze pokazano pięć sposobów na wykonanie tego zadania.
Omówienie Jeśli aplikacja nie odbiera zdarzeń zgłaszanych przez obiekt View, nie może na nie zareagować. Na potrzeby wykrywania zdarzeń trzeba utworzyć egzemplarz klasy z implementacją odbiornika i powiązać go z obiektem View. Zastanów się na przykład nad zdarzeniem onClick — jednym z najczęściej stosowanych zdarzeń z aplikacji na Android. Prawie każdy obiekt View, który można umieścić na ekranie aplikacji, zgłasza zdarzenie w reakcji na dotknięcie go palcem (na wyświetlaczach dotykowych) lub kliknięcie za pomocą trackpadu lub trackballa (jeśli dany obiekt View jest aktywny). Zdarzenie to jest odbierane przez klasę z implementacją interfejsu OnClickListener. Egzemplarz tej klasy należy powiązać z odpowiednim obiektem View za pomocą metody setOnClickListener tego obiektu. W klasie wewnętrznej HandleClick, opisanej w poniższym podpunkcie „Metoda 1. Klasa wewnętrzna”, aktywność w reakcji na kliknięcie obiektu Button (button1) ustawia tekst w kontrolce TextView (textview1).
Metoda 1. Klasa wewnętrzna W aktywności main zadeklarowana jest klasa zagnieżdżona HandleClick z implementacją interfejsu OnClickListener. Podejście to jest przydatne, jeśli kilka odbiorników wymaga podobnego przetwarzania. Potrzebny kod można wtedy umieścić w jednej klasie.
242 |
Rozdz ał 7. Graf czny nterfejs użytkown ka
Listing 7.3. Klasa składowa public class Main extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); // Łączenie egzemplarza klasy HandleClick z obiektem Button findViewById(R.id.button1).setOnClickListener(new HandleClick()); } private class HandleClick implements OnClickListener{ public void onClick(View arg0) { Button btn = (Button)arg0; // Rzutowanie widoku na przycisk // Pobieranie referencji do kontrolki TextView TextView tv = (TextView) findViewById(R.id.textview1); // Aktualizowanie tekstu w kontrolce TextView tv.setText("Wcisnąłeś przycisk " + btn.getText()); } } }
Metoda 2. Interfejs jako typ W Javie interfejs można zastosować jako typ. Tu jako typ zmiennej zadeklarowano OnClick Listener. Do tworzenia obiektu służy instrukcja new OnClickListener(){...}, natomiast na zapleczu Java tworzy obiekt (anonimowej klasy) z implementacją interfejsu OnClickListener. Podejście to, przedstawione na listingu 7.4, ma podobne zalety co poprzednie. Listing 7.4. Interfejs jako typ public class Main extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); // Podłączanie odbiornika zdarzeń za pomocą zmiennej handleClick findViewById(R.id.button1).setOnClickListener(handleClick); } private OnClickListener handleClick = new OnClickListener(){ public void onClick(View arg0) { Button btn = (Button)arg0; TextView tv = (TextView) findViewById(R.id.textview1); tv.setText("Wcisnąłeś przycisk " + btn.getText()); } }; }
Metoda 3. Anonimowa klasa wewnętrzna Odbiornik OnClickListener często deklaruje się w wywołaniu metody setOnClickListener. Ta technika jest przydatna, jeśli poszczególne odbiorniki nie obejmują kodu przydatnego w innych odbiornikach. Dla niektórych początkujących programistów kod tego typu jest mało zrozumiały. Także tu po wywołaniu new OnClickListener(){...} Java tworzy obiekt z implementacją interfejsu. Podejście to przedstawiono na listingu 7.5.
7.5. P ęć sposobów na dołączan e odb orn ka zdarzeń
| 243
Listing 7.5. Anonimowa klasa wewnętrzna public class Main extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); findViewById(R.id.button1).setOnClickListener(new OnClickListener(){ public void onClick(View arg0) { Button btn = (Button)arg0; TextView tv = (TextView) findViewById(R.id.textview1); tv.setText("Wcisnąłeś przycisk " + btn.getText()); } }); } }
Metoda 4. Implementacja aktywności Interfejs OnClickListener można zaimplementować w samej aktywności. Ponieważ obiekt aktywności main już istnieje, można w ten sposób zaoszczędzić nieco pamięci, ponieważ nie trzeba umieszczać metody onClick w innym obiekcie. Wadą jest to, że powstaje publiczna metoda, która prawdopodobnie nie będzie używana nigdzie indziej. Dodanie implementacji większej liczby zdarzeń sprawia, że deklaracja aktywności main jest długa. Kod aktywności pokazano na listingu 7.6. Listing 7.6. Implementacja interfejsu w aktywności public class main extends Activity implements OnClickListener{ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); findViewById(R.id.button1).setOnClickListener(this); } public void onClick(View arg0) { Button btn = (Button)arg0; TextView tv = (TextView) findViewById(R.id.textview1); tv.setText("Wcisnąłeś przycisk " + btn.getText()); } }
Metoda 5. Atrybut zdarzenia onClick w układzie widoku W Androidzie 1.6 i nowszych wersjach (interfejs API 4. i nowsze) nazwę metody zdefiniowanej w aktywności (listing 7.7) można przypisać do atrybutu android:onClick w pliku układu. Dzięki temu nie trzeba pisać dużej ilości szablonowego kodu. Listing 7.7. Klasa określona w manifeście public class Main extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); } public void HandleClick(View arg0) { Button btn = (Button)arg0; TextView tv = (TextView) findViewById(R.id.textview1);
244 |
Rozdz ał 7. Graf czny nterfejs użytkown ka
tv.setText("Wcisnąłeś przycisk " + btn.getText()); } }
W pliku układu należy zadeklarować przycisk (element Button) z atrybutem android:onClick:
Pierwsze cztery techniki obsługi zdarzeń można stosować także dla innych rodzajów zdarzeń (onLongClick, onKey, onTouch, onCreateContextMenu, onFocusChange). Piąta technika, opisana w tym podpunkcie, dotyczy tylko zdarzeń onClick. W pliku układu z listingu 7.8 zadeklarowane są dwa dodatkowe przyciski. Dzięki zastosowaniu atrybutu android:onClick nie trzeba dodawać żadnego dodatkowego kodu — żadnych wywołań findViewById i setOnClickListener dla poszczególnych przycisków. Efekt działania kodu pokazano na rysunku 7.1. Listing 7.8. Zastosowanie atrybutu android:onClick do kilku przycisków
Rysunek 7.1. Zdarzenie onClick obsługiwane przez metodę z atrybutu android:onClick 7.5. P ęć sposobów na dołączan e odb orn ka zdarzeń
| 245
Wybór techniki używanej do podłączania odbiorników zależy od potrzebnych funkcji, ilości kodu, który można wielokrotnie wykorzystać dla poszczególnych obiektów View, a także czytelności kodu dla osób odpowiedzialnych za jego konserwację. W idealnych warunkach kod powinien być zwięzły i zrozumiały. Pominięto tu pewną technikę podobną do pierwszej z opisanych metod. W pierwszej metodzie klasę odbiornika można zapisać też w pliku z inną klasą jako klasę publiczną. Następnie egzemplarze klasy publicznej można stosować w innych aktywnościach, przekazując kontekst aktywności w konstruktorze. Aktywności powinny jednak być niezależne (ma to znaczenie, gdy są usuwane przez Android). Współużytkowanie odbiorników w różnych aktywnościach jest niezgodne z duchem Androida i może prowadzić do niepotrzebnej złożoności związanej z przekazywaniem referencji między klasami publicznymi.
7.6. Stosowanie kontrolek CheckBox i RadioButton Blake Meike
Problem Programista chce udostępnić użytkownikowi zestaw opcji krótszy niż lista.
Rozwiązanie Należy zastosować kontrolki CheckBox, RadioButton lub Spinner.
Omówienie Prawdopodobnie znasz już opisywane tu kontrolki z innych interfejsów użytkownika. Kontrolki te umożliwiają wybór opcji. Pola wyboru zwykle udostępnia się, aby pozwolić na zaznaczenie dowolnej liczby opcji typu tak – nie (lub prawda – fałsz). Przyciski opcji stosuje się wtedy, gdy w danym momencie zaznaczona może być tylko jedna opcja. Kontrolka Spinner przypomina pola kombi z niektórych frameworków do tworzenia interfejsów GUI. Opisano ją w recepturze 7.8. W Androidzie zaadaptowano niektóre popularne komponenty, aby były bardziej przydatne w środowisku dotykowym. Na rysunku 7.2 pokazano trzy rodzaje kontrolek wyboru w aplikacji na Android. Kontrolka Spinner wyświetla jedną z dostępnych opcji. Oto plik XML układu, który posłużył do utworzenia ekranu widocznego na rysunku:
246 |
Rozdz ał 7. Graf czny nterfejs użytkown ka
Rysunek 7.2. Pole wyboru i trzy przyciski opcji
W przedstawionym pliku XML znajdują się wyświetlane na ekranie widoki i ich atrybuty. Kontrolka RadioGroup jest jednocześnie kontrolką ViewGroup i obejmuje widoki typu RadioButton. Na listingu 7.9 przedstawiono kod Javy reagujący na kliknięcia użytkownika. Listing 7.9. Przykładowy kod obsługujący wybieranie elementów package com.oreilly.select; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import com.google.android.maps.GeoPoint;
7.6. Stosowan e kontrolek CheckBox Rad oButton
|
247
import import import import import import import import import import import import
android.app.Activity; android.os.Bundle; android.util.Log; android.view.View; android.widget.AdapterView; android.widget.ArrayAdapter; android.widget.CheckBox; android.widget.RadioButton; android.widget.RadioGroup; android.widget.Spinner; android.widget.TextView; android.widget.AdapterView.OnItemSelectedListener;
public class SelectExample extends Activity { private CheckBox checkBox; private TextView txtCheckBox, txtRadio; private RadioButton rb1, rb2, rb3; private Spinner spnMusketeers; /** Wywoływana, gdy aktywność tworzona jest po raz pierwszy */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); checkBox = (CheckBox) findViewById(R.id.cbxBox1); txtCheckBox = (TextView) findViewById(R.id.txtCheckBox); txtRadio = (TextView) findViewById(R.id.txtRadio); rb1 = (RadioButton) findViewById(R.id.RB1); rb2 = (RadioButton) findViewById(R.id.RB2); rb3 = (RadioButton) findViewById(R.id.RB3); spnMusketeers = (Spinner) findViewById(R.id.spnMusketeers); // Reagowanie na zdarzenia kontrolki CheckBox checkBox.setOnClickListener(new CheckBox.OnClickListener() { public void onClick(View v){ if (checkBox.isChecked()) { txtCheckBox.setText("CheckBox: pole jest zaznaczone"); } else { txtCheckBox.setText("CheckBox: bez zaznaczenia"); } } }); // Reagowanie na zdarzenia kontrolki RadioGroup rb1.setOnClickListener(new RadioGroup.OnClickListener() { public void onClick(View v){ txtRadio.setText("Radio: wybrano przycisk 1"); } }); rb2.setOnClickListener(new RadioGroup.OnClickListener() { public void onClick(View v){ txtRadio.setText("Radio: wybrano przycisk 2"); } }); rb3.setOnClickListener(new RadioGroup.OnClickListener() { public void onClick(View v){ txtRadio.setText("Radio: wybrano przycisk 3"); } }); // Określanie opcji kontrolki Spinner List
248 |
Rozdz ał 7. Graf czny nterfejs użytkown ka
lsMusketeers.add("Portos"); lsMusketeers.add("Aramis"); ArrayAdapter
Poszczególne widoki działają w następujący sposób: CheckBox
Widok CheckBox obsługuje zmianę stanu w obie strony i wyświetla „ptaszka”, jeśli stan to true. Programista musi tylko zaimplementować interfejs OnClickListener, aby przechwytywać kliknięcie, a następnie dodać kod wykonywany w reakcji na kliknięcie.
RadioGroup
Jak wcześniej wspomniano, widok RadioGroup jest widokiem ViewGroup, który obejmuje dowolną liczbę widoków RadioButton. W danym momencie zaznaczony może być tylko jeden z przycisków. Na potrzeby wykrywania zaznaczenia należy ustawić odbiornik OnClick Listener dla każdego obiektu RadioButton.Warto zauważyć, że kliknięcie jednego z obiektów RadioButton nie powoduje zgłoszenia kliknięcia widoku RadioGroup.
Trzy opisane rodzaje widoków pozwalają udostępniać krótkie zestawy opcji. Użytkownicy mogą następnie wybrać jedną lub kilka spośród zaprezentowanych opcji.
7.7. Wzbogacanie projektu interfejsu użytkownika za pomocą przycisków graficznych Rachee Singh
Problem Programista chce wzbogacić projekt interfejsu użytkownika, ale bez dodawania dużej ilości opisowego tekstu.
Rozwiązanie Można zastosować przycisk graficzny. Wymaga to mniej pracy niż utworzenie widoku tekstowego z opisem, ponieważ rysunek pozwala wyjaśnić sytuację znacznie lepiej niż nawet długi tekst.
7.7. Wzbogacan e projektu nterfejsu użytkown ka za pomocą przyc sków graf cznych
| 249
Omówienie Aby utworzyć własne przyciski graficzne, trzeba zdefiniować ich cechy w pliku XML, który należy umieścić w katalogu /res/drawable. W pliku tym określane są trzy stany przycisku graficznego: • wciśnięty, • aktywny, • inny.
Oto przykładowy kod:
Dla każdego ze stanów określony jest identyfikator rysunku (grafiki znajdują się w katalogu /res/drawable i są w formacie .png). Po wciśnięciu przycisku wyświetlany jest rysunek play_ pressed. W aplikacji występują dwa przyciski — odtwarzania i ustawień. W pliku .java aplikacji należy napisać metody onClick powiązane z przyciskami. W opisywanej recepturze kliknięcie przycisków prowadzi tylko do wyświetlania komunikatów toast z odpowiednim tekstem. Można też uruchomić nową aktywność, rozesłać intencję lub wykonać inne operacje. Na rysunku 7.3 pokazano przycisk odtwarzania w normalnym stanie, a na rysunku 7.4 — po wciśnięciu.
Rysunek 7.3. Przycisk odtwarzania w normalnym stanie
Rysunek 7.4. Wciśnięty przycisk odtwarzania 250
|
Rozdz ał 7. Graf czny nterfejs użytkown ka
7.8. Udostępnianie listy rozwijanej z opcjami za pomocą klasy Spinner Ian Darwin
Problem Programista chce udostępnić listę rozwijaną z opcjami.
Rozwiązanie Należy zastosować obiekt Spinner. Listę opcji do wyboru można przekazać za pomocą klasy Adapter.
Omówienie Kontrolka Spinner (często nazywana polem kombi) to odpowiednik elementu SELECT z języka HTML lub JComboBox z frameworku Swing. Kontrolka ta to lista rozwijana z opcjami, której pozycje pojawiają się na ekranie po kliknięciu kontrolki. Wybrać można tylko jedną opcję. Wtedy kontrolka jest zwijana i widoczna jest w niej wybrana opcja (rysunek 7.5).
Rysunek 7.5. Pokaz działania kontrolki Spinner (lista rozwijana)
Kontrolkę Spinner, podobnie jak wszystkie standardowe komponenty, można utworzyć i skonfigurować w XML-u. W omawianym przykładzie pojęcie kontekst dotyczy tego, kiedy dokonano pomiaru ciśnienia krwi pacjenta (po śniadaniu, po lunchu itd.). Dzięki temu pielęgniarka może zrozumieć wartość w kontekście przebiegu dnia pacjenta. Oto fragment kodu z pliku res/layout/main.xml:
7.8. Udostępn an e l sty rozw janej z opcjam za pomocą klasy Sp nner
|
251
W idealnych warunkach lista wartości nie jest zapisana na stałe, tylko pochodzi z pliku zasobu, co pozwala na umiędzynarodawianie aplikacji. Oto plik res/values/contexts.xml z zapisanymi w XML-u porami dnia do wyboru:
Aby powiązać listę łańcuchów znaków z kontrolką Spinner w czasie wykonywania programu, należy zlokalizować tę kontrolkę i ustawić wartości w pokazany poniżej sposób: Spinner contextChooser = (Spinner) findViewById(R.id.contextChooser); ArrayAdapter
To wystarczy, aby kontrolka Spinner pojawiła się na ekranie i umożliwiła użytkownikom wybór opcji (rysunek 7.5). Jeśli chcesz od razu poznać wybraną wartość, możesz przekazać implementację interfejsu OnItemSelectedListener do metody setOnItemSelectedListener kontrolki. Wspomniany interfejs ma dwie wywoływane zwrotnie metody — setItemSelected i setNothing Selected. Obie są wywoływane dla kontrolki Spinner (choć podany typ argumentu to View Adapter). Pierwsza z tych metod przyjmuje też dwa argumenty całkowitoliczbowe — pozycję listy i identyfikator wybranego elementu. contextChooser.setOnItemSelectedListener(new OnItemSelectedListener() { @Override public void onItemSelected(AdapterView spinner, View arg1, int pos, long id) { Toast.makeText(SpinnerDemoActivity.this, "Wybrałeś opcję " + contextChooser.getSelectedItem(), Toast.LENGTH_LONG).show(); } @Override public void onNothingSelected(AdapterView spinner) { Toast.makeText(SpinnerDemoActivity.this, "Nie wybrano żadnej opcji.", Toast.LENGTH_LONG).show(); } });
Czasem wartość z kontrolki Spinner potrzebna jest dopiero po ustawieniu przez użytkownika także innych opcji oraz po kliknięciu przycisku. Wtedy wystarczy wywołać metodę getSelec tedItem() kontrolki Spinner, aby poprzez obiekt Adapter uzyskać element umieszczony na danej pozycji. Jeśli na liście znajdują się łańcuchy znaków, wystarczy wywołać metodę toString(), żeby otrzymać potrzebną wartość typu String.
252
|
Rozdz ał 7. Graf czny nterfejs użytkown ka
7.9. Obsługa długiego kliknięcia Ian Darwin
Problem Programista chce, aby aplikacja odbierała długie kliknięcie i reagowała na nie. Zależy mu przy tym na uniknięciu konieczności ręcznego sprawdzania wystąpienia różnych zdarzeń.
Rozwiązanie W Androidzie 3.0 i nowszych wersjach można zastosować metody setLongClickable() i setOn LongClickListener() klasy View i udostępnić odbiornik OnLongClickListener.
Omówienie W wersjach starszych niż Honeycomb obsługa długich kliknięć sprawiała trudności. W recepturze 16.15 pokazano, jak obsługiwać długie kliknięcia przez połączenie wielu zdarzeń w jedno. Technika ta nie jest zbyt elegancka, dlatego w wersji 3.0 dodano bezpośrednią obsługę długich kliknięć. Obecnie klasa View udostępnia metodę setLongClickable(boolean), która pozwala włączyć i wyłączyć obsługę długich kliknięć. Dostępna jest też powiązana metoda setOnLong ClickListener(OnLongClickListener). Przykładowa aplikacja odbiera długie kliknięcia obiektu View i reaguje przez wyświetlenie menu wyskakującego (klasa PopupMenu), które jest modalne i pojawia się przed kontrolką ListView. final View myView = findViewById(R.id.myView); ... myView.setOnLongClickListener(new OnLongClickListener() { @Override public boolean onLongClick(View view) { PopupMenu p = new PopupMenu(Main.this, view); p.getMenuInflater().inflate( R.layout.main_popup_menu, p.getMenu()); p.show(); return true; } });
Menu wyskakujące jest ukrywane po kliknięciu jednego z jego elementów. Lista pozycji z menu pochodzi z pliku XML res/menu/main_popup_menu.xml, który obejmuje elementy item z tekstem opcji menu. Warto zauważyć, że wywołanie metody setOnLongClickListener() prowadzi do automatycznego wywołania metody setLongClickEnabled(true). Ponadto dodanie odbiornika OnClickListener do obiektu ListView (lub innego widoku z wieloma elementami) ma dość nieoczekiwane skutki — elementy listy są obsługiwane jak przy normalnym kliknięciu. Dlatego należy zastosować metodę setOnItemLongClickListener, która — co nie jest zaskoczeniem — przyjmuje obiekt z implementacją interfejsu OnItemLongClick Listener, wywoływany w odpowiedzi na długie kliknięcie elementu listy.
7.9. Obsługa dług ego kl kn ęc a
|
253
Rozwiązanie można uprościć dla widoku ListView przez wstępne rozwinięcie menu do klasy i przekazanie go do metody setContextMenu(view, menu) aktywności.
7.10. Wyświetlanie pól tekstowych TextView i EditText Ian Darwin
Problem Programista chce wyświetlać tekst na ekranie (w trybie tylko do odczytu lub z możliwością edycji).
Rozwiązanie Aby użytkownik miał dostęp do tekstu w trybie tylko do odczytu, należy zastosować klasę TextView. Jest ona odpowiednikiem klasy, która w większości pakietów API do tworzenia interfejsów GUI nosi nazwę Label (w pakiecie android.widget nie ma klasy o takiej nazwie). Natomiast klasa EditText pozwala użytkownikowi na dostęp do tekstu z możliwością edycji. W innych pakietach klasy pełniące podobną funkcję noszą zwykle nazwę TextField lub TextArea.
Omówienie EditText to klasa pochodna bezpośrednio od TextView. Warto zauważyć, że klasa TextView ma też liczne inne pośrednie i bezpośrednie podklasy (np. CheckBox i RadioButton), z których wiele jest samodzielnymi kontrolkami interfejsu GUI. Jedną z podklas jest AutoCompleteTextView, która — jak wskazuje na to nazwa — umożliwia automatycznie uzupełnianie tekstu po wprowadzeniu przez użytkownika kilku pierwszych liter. W recepturach z rozdziału 9. wyjaśniono, że uzupełniane wyrazy należy podawać za pomocą adaptera.
Dodawanie kontrolek EditText i TextView jest niezwykle proste — wystarczy umieścić je w układzie w formacie XML. Za pomocą XML-a można też łatwo przypisać początkowo wyświetlane wartości. Do bezpośredniego ustawiania wartości służy następująca składnia:
Zaleca się jednak stosowanie wartości w rodzaju "@+string/welcome_text" i definiowanie łańcuchów znaków w pliku strings.xml. Ułatwia to późniejszą zmianę napisów i umiędzynarodawianie. Ponieważ kontrolki TextView i EditText są używane w innych miejscach książki, w recepturze nie zaprezentowano przykładowej aplikacji ilustrującej korzystanie z tych kontrolek. Jeśli chcesz zapoznać się z taką aplikacją, znajdziesz ją w pakiecie Android API Examples (nosi nazwę LabelView).
254 |
Rozdz ał 7. Graf czny nterfejs użytkown ka
7.11. Ograniczanie wartości pola EditText za pomocą atrybutów oraz interfejsu TextWatcher Daniel Fowler
Problem Programista chce ograniczyć przedziały i typy wprowadzanych wartości.
Rozwiązanie Należy zastosować odpowiednie atrybuty dla kontrolek EditText w układzie w formacie XML i wzbogacić te kontrolki przez zaimplementowanie interfejsu TextWatcher.
Omówienie Jeśli aplikacja pobiera dane od użytkownika, czasem obsługuje tylko wartości określonego typu. Mogą to być liczby całkowite, liczby dziesiętne, liczby z określonego przedziału lub słowa pisane wielkimi literami. W definicji kontrolki EditText w układzie można zastosować atrybuty w rodzaju android:inputType, aby ograniczyć rodzaj danych, które można wprowadzać. Dzięki temu ilość potrzebnego kodu się zmniejsza, ponieważ można pominąć sprawdzanie kilku aspektów danych. Do ograniczania wprowadzanych wartości przydaje się też interfejs TextWatcher. W poniższym przykładzie kontrolka EditText umożliwia wprowadzanie tylko liczb z przedziału od 0 do 100 (na przykład reprezentujących procenty). Wpisanej wartości nie trzeba sprawdzać, ponieważ odbywa się to w trakcie wprowadzania tekstu. Oto prosty układ z jedną kontrolką EditText:
Do kontrolki EditText jest tu przypisywana wartość początkowa 0 (android:text="0"), a liczba wprowadzanych znaków jest ograniczona do trzech (android:maxLength="3"), ponieważ największa z dozwolonych wartości, czyli 100, ma tylko trzy cyfry. Ponadto użytkownik może wprowadzać tylko liczby dodatnie (android:inputType="number"). W klasie aktywności z listingu 7.10 zastosowano klasę wewnętrzną z implementacją interfejsu TextWatcher (interfejs można też zaimplementować w samej aktywności). Metoda afterText Changed() jest przesłonięta, a aplikacja wywołuje ją w trakcie wprowadzania znaków przez
7.11. Ogran czan e wartośc pola Ed tText za pomocą atrybutów oraz nterfejsu TextWatcher
|
255
użytkownika. Wspomniana metoda sprawdza, czy wartość nie jest większa od 100. Jeżeli jest większa, metoda ustawia wartość na 100. Nie trzeba sprawdzać, czy liczba jest mniejsza od zera — z uwagi na ustawione atrybuty użytkownik nie może wprowadzać takich wartości. Blok try catch jest potrzebny w sytuacjach, gdy użytkownik usunął wszystkie cyfry. Wtedy sprawdzanie, czy wartość nie jest większa od 100, prowadziłoby do zgłoszenia wyjątku (w wyniku próby przetworzenia pustego łańcucha znaków). Interfejs TextWatcher obejmuje ponadto metody beforeTextChanged() i onTextChanged(), które też można przesłonić, jednak nie występują one w przykładowej aplikacji. Listing 7.10. Implementacja interfejsu TextWatcher class CheckPercentage implements TextWatcher{ @Override public void afterTextChanged(Editable s) { try { Log.d("Procent", "wprowadzono: " + s); if(Integer.parseInt(s.toString())>100) s.replace(0, s.length(), "100"); } catch(NumberFormatException nfe){} } @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { // Nieużywana w przykładzie. Przyjmuje informacje o tekście tuż przed jego zmianą. // Służy do śledzenia zmian wprowadzonych w tekście (na przykład w implementacji cofania zmian) } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { // Nieużywana w przykładzie. Przyjmuje informacje o tekście w momencie wprowadzania zmian } }
W metodzie onCreate() aktywności klasa z implementacją interfejsu TextWatcher łączona jest z kontrolką EditText. Służy do tego metoda addTextChangedListener(). @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); EditText percentage=(EditText) findViewById(R.id.percent); percentage.addTextChangedListener(new CheckPercentage()); }
Warto zauważyć, że w metodzie afterTextChanged() można zmienić wartość kontrolki EditText, ponieważ do metody przekazywana jest wewnętrzna klasa Editable kontrolki. Nie można jednak wprowadzać zmian przez modyfikowanie obiektu CharSequence przekazanego do metod beforeTextChanged() i onTextChanged(). Po uruchomieniu przykładowego kodu w oknie LogCat pojawią się ustawione wartości, co pokazano na rysunku 7.6. Więcej informacji o atrybutach kontrolki EditText znajdziesz w dokumentacji Androida dotyczącej klasy TextView (EditText to jej podklasa). Warto też pamiętać, że zmiana wartości w kontrolce EditText prowadzi do ponownego wywołania metody afterTextChanged(). Trzeba zachować ostrożność, aby uniknąć wejścia kodu korzystającego z interfejsu TextWatcher w pętlę nieskończoną.
256
|
Rozdz ał 7. Graf czny nterfejs użytkown ka
Rysunek 7.6. Działanie implementacji interfejsu TextWatcher
Warto zapoznać się z atrybutami widoków w Androidzie. Definiowanie takich atrybutów w układzie w formacie XML pozwala zmniejszyć ilość potrzebnego kodu.
Zobacz też http://developer.android.com/reference/android/widget/TextView.html, http://developer.android.com/ reference/android/widget/EditText.html, http://developer.android.com/reference/android/text/TextWa tcher.html.
7.12. Kontrolka AutoCompleteTextView Rachee Singh
Problem Programista chce, aby użytkownicy nie musieli wpisywać całych słów. Dlatego zamierza zastosować autouzupełnianie tekstu na podstawie kilku pierwszych wprowadzonych znaków.
Rozwiązanie Należy zastosować kontrolkę AutoCompleteTextView, która działa jak połączenie kontrolek Edit Text i Spinner oraz pozwala na automatyczne uzupełnianie tekstu.
Omówienie W poniższym układzie przedstawiono kontrolkę TextView z obsługą automatycznego uzupełniania tekstu. Uzupełnianie odbywa się z wykorzystaniem kontrolki AutoCompleteTextView. Na listingu 7.11 pokazano kod układu w formacie XML.
7.12. Kontrolka AutoCompleteTextV ew
|
257
Listing 7.11. Układ z automatycznym uzupełnianiem
Pole completionThreshold w elemencie AutoCompleteTextView określa minimalną liczbę znaków, które użytkownik musi wprowadzić w kontrolce TextView, aby pojawiły się opcje automatycznego uzupełniania pasujące do wpisanych znaków. W aktywności, w której stosowane jest automatyczne uzupełnianie, należy zaimplementować interfejs TextWatcher, tak aby przesłonić metodę onTextChanged(): public class AutoComplete extends Activity implements TextWatcher {
Należy przesłonić niezaimplementowane metody interfejsu: onTextChanged, beforeTextChanged i afterTextChanged. Potrzebne są też trzy pola: • uchwyt do kontrolki TextView, • uchwyt do kontrolki AutoCompleteTextView, • lista elementów String uwzględnianych przy automatycznym uzupełnianiu. private TextView field; private AutoCompleteTextView autocomplete; String autocompleteItems [] = {"jabłko", "banan", "mango", "ananas", "jagody", "pomarańcza", "gruszka", "winogrona"};
Metoda onTextChanged() kopiuje aktualną wartość pola do innego pola. Nie jest to konieczne, jednak przykładowy kod pokazuje, jakie wartości są ustawiane w komponencie automatycznego uzupełniania. @Override public void onTextChanged(CharSequence arg0, int arg1, int arg2, int arg3) { field.setText(autocomplete.getText()); }
Metoda onCreate omawianej aktywności pobiera uchwyt do kontrolek TextView i AutoComplete TextView z układu. Dla kontrolki AutoCompleteTextView należy ustawić adapter obsługujący obiekty String: setContentView(R.layout.main); field = (TextView) findViewById(R.id.field); autocomplete = (AutoCompleteTextView)findViewById(R.id.autocomplete); autocomplete.addTextChangedListener(this); autocomplete.setAdapter(new ArrayAdapter
258
|
Rozdz ał 7. Graf czny nterfejs użytkown ka
7.13. Zapełnianie kontrolki AutoCompleteTextView za pomocą zapytań do bazy SQLite Jonathan Fuerth
Problem Choć dokumentacja Androida obejmuje kompletny, działający przykład zastosowania kontrolki AutoCompleteTextView z adapterem ArrayAdapter, nie wystarczy podmienić tego adaptera na SimpleCursorAdapter (związany z bazą danych).
Rozwiązanie Przy stosowaniu adaptera SimpleCursorAdapter zamiast ArrayAdapter trzeba wykonać dwie dodatkowe operacje: • określić w adapterze, którą kolumnę należy zastosować do wypełnienia kontrolki TextView
przy korzystaniu z uzupełniania; • poinformować adapter o tym, jak ma ponawiać zapytania na podstawie najnowszych zna-
ków wprowadzonych przez użytkownika, gdyż bez tego zawsze wyświetlane są wszystkie wiersze z kursora i lista nigdy nie jest skracana do pożądanych elementów.
Omówienie Pokazany dalej przykładowy kod zwykle znajduje się w metodzie onCreate() aktywności obejmującej kontrolkę AutoCompleteTextView. Kod pobiera taką kontrolkę z układu aktywności, tworzy adapter SimpleCursorAdapter, konfiguruje ten adapter na potrzeby współdziałania z kontrolką AutoCompleteTextView, a następnie wiąże adapter z kontrolką. W listingu 7.12 wyróżniono dwie ważne różnice między tym kodem a przykładem z dokumentacji Android Dev Guide. Różnice te opisano pod listingiem. Listing 7.12. Kod metody onCreate() final AutoCompleteTextView itemName = (AutoCompleteTextView) findViewById(R.id.item_name_view); SimpleCursorAdapter itemNameAdapter = new SimpleCursorAdapter( this, R.layout.completion_item, itemNameCursor, fromCol, toView); itemNameAdapter.setStringConversionColumn( n itemNameCursor.getColumnIndexOrThrow(GroceryDBAdapter.ITEM_NAME_COL)); itemNameAdapter.setFilterQueryProvider(new FilterQueryProvider() { o public Cursor runQuery(CharSequence constraint) { String partialItemName = null; if (constraint != null) { partialItemName = constraint.toString(); }
7.13. Zapełn an e kontrolk AutoCompleteTextV ew za pomocą zapytań do bazy SQL te
|
259
return groceryDb.suggestItemCompletions(partialItemName); } }); itemName.setAdapter(itemNameAdapter);
n Przy korzystaniu z adaptera ArrayAdapter nie trzeba określać sposobu przekształcania wybranego elementu na typ String. Jednak adapter SimpleCursorAdapter obsługuje pobieranie jednej kolumny jako tekstu podpowiedzi, a innej jako tekstu umieszczanego w polu tekstowym po wybraniu przez użytkownika jakiejś pozycji. Choć najczęściej w obu sytuacjach stosuje się ten sam tekst, nie jest to domyślne rozwiązanie. Domyślnie pole tekstowe jest uzupełniane wynikiem wywołania metody toString() na kursorze, czyli danymi w rodzaju android.database.sqlite.SQLiteCursor@f00f00d0. o Przy stosowaniu adaptera ArrayAdapter to system zarządza filtrowaniem podpowiedzi, tak aby pojawiały się tylko te łańcuchy znaków, które rozpoczynają się od znaków wprowadzonych już w polu tekstowym. Adapter SimpleCursorAdapter daje więcej możliwości, jednak jego domyślne działanie nie jest przydatne. Jeśli nie napiszesz kodu dostawcy Filter QueryProvider dla adaptera, kontrolka AutoCompleteTextView wyświetli początkowy zbiór sugestii niezależnie od tego, jaki tekst użytkownik wprowadził. Dostawca FilterQueryProvider sprawia, że podpowiedzi działają w oczekiwany sposób.
7.14. Przekształcanie pól tekstowych w pola na hasło Rachee Singh
Problem Programista chce zastosować kontrolkę EditText jako pole na hasło, tak aby znaki wprowadzane przez użytkownika nie były widoczne dla podglądaczy.
Rozwiązanie Klasa EditText w Androidzie ma atrybut password, który pozwala uzyskać pożądany efekt.
Omówienie Jeśli aplikacja wymaga, aby użytkownicy wprowadzali hasło, należy zastosować kontrolkę EditText w specjalnej postaci — kontrolka powinna ukrywać wprowadzone znaki. Aby uzyskać ten efekt, należy w XML-u dodać do elementu EditText następujący atrybut: android:password="True"
Na rysunku 7.7 pokazano wygląd kontrolki EditText przeznaczonej na hasło.
260
|
Rozdz ał 7. Graf czny nterfejs użytkown ka
Rysunek 7.7. Kontrolka EditText z hasłem
7.15. Zmiana klawisza Enter na Next na klawiaturze programowej Jonathan Fuerth
Problem W niektórych aplikacjach, takich jak Browser i Contacts, klawisz Enter na klawiaturze programowej jest zastępowany klawiszem Next, który pozwala przejść do następnego pola na dane. Programista chce dodać tego rodzaju udogodnienie do własnego programu.
Rozwiązanie Należy ustawić odpowiedni atrybut IME (ang. Input Method Editor) dla odpowiednich kontrolek.
Omówienie Na rysunku 7.8 pokazano prosty układ z trzema polami tekstowymi (kontrolki EditText) i przyciskiem Wyślij. Zwróć uwagę na klawisz Enter w prawym dolnym rogu. Wciśnięcie go prowadzi do rozwinięcia obecnie wybranego pola tekstowego, tak aby zmieścił się w nim następny wiersz tekstu. Zwykle nie jest to pożądane rozwiązanie. Oto kod pokazanego układu:
7.15. Zm ana klaw sza Enter na Next na klaw aturze programowej
|
261
Rysunek 7.8. Trzy pola tekstowe i przycisk wysyłania
Na rysunku 7.9 przedstawiono lepszą wersję tego samego interfejsu użytkownika. Zamiast klawisza Enter znajduje się tu klawisz Next. Nowy interfejs jest nie tylko wygodniejszy dla użytkowników, ale zapobiega też wprowadzaniu kilku wierszy tekstu w polach, które są przeznaczone na pojedyncze wiersze. Poniżej pokazano, jak nakazać Androidowi wyświetlanie klawisza Next na klawiaturze. Zwróć uwagę na atrybuty android:imeOptions przy każdej z trzech kontrolek EditText.
262
|
Rozdz ał 7. Graf czny nterfejs użytkown ka
Rysunek 7.9. Usprawniony interfejs użytkownika (z klawiszem Next)
Zwróć ponadto uwagę na ustawienie actionDone w trzecim polu tekstowym. Następny przycisk nie przyjmuje tekstu, a nawet gdyby przyjmował, klawiatura zostanie ukryta. Jak się może domyślasz, ustawienie actionDone powoduje wyświetlenie klawisza Done w miejscu klawisza Enter. Po wciśnięciu klawisza Done aplikacja ukrywa klawiaturę. Aby poprawić wygląd klawiatury programowej, można wprowadzić też wiele innych zmian, np. dodać wskazówki dotyczące typu wprowadzanych danych, automatycznie ustawiać wielkie lub małe litery, a nawet zaznaczać cały tekst po kliknięciu kontrolki. Warto zapoznać się z tymi możliwościami. Każdy drobiazg może zwiększyć atrakcyjność aplikacji.
7.15. Zm ana klaw sza Enter na Next na klaw aturze programowej
|
263
Zobacz też Opis klasy TextView w dokumentacji interfejsu API Androida (http://developer.android.com/ reference/android/widget/TextView.html; zwłaszcza fragment dotyczący atrybutu ImeOptions — http://developer.android.com/reference/android/widget/TextView.html#attr_android:imeOptions).
7.16. Obsługa w aktywności zdarzeń związanych z klawiszami Rachee Singh
Problem Programista chce przechwytywać klawisze wybrane przez użytkownika i wykonywać odpowiednie operacje.
Rozwiązanie Należy przesłonić metodę onKeyDown w aktywności.
Omówienie Jeśli aplikacja ma reagować w różny sposób na wciśnięcie poszczególnych klawiszy, należy przesłonić metodę onKeyDown w kodzie aktywności (w Javie). Metoda ta przyjmuje argument KeyCode, co pozwala wykonywać różne akcje w bloku switch-case. Pokazano to na listingu 7.13. Listing 7.13. Metoda onKeyDown public boolean onKeyDown(int keyCode, KeyEvent service) { switch(keyCode) { case KeyEvent.KEYCODE_HOME: keyType.setText("Wciśnięto klawisz Home!"); break; case KeyEvent.KEYCODE_DPAD_CENTER : keyType.setText("Wciśnięto środkowy klawisz!"); break; case KeyEvent.KEYCODE_DPAD_DOWN : keyType.setText("Wciśnięto dolny klawisz!"); break; // I tak dalej } }
264 |
Rozdz ał 7. Graf czny nterfejs użytkown ka
7.17. Pokaż im gwiazdy — kontrolka RatingBar Ian Darwin
Problem Programista chce, aby użytkownicy mogli zaznaczać grupy identycznych elementów interfejsu GUI w celu określenia oceny lub liczby punktów.
Rozwiązanie Należy zastosować kontrolkę RatingBar. Pozwala ona określić liczbę wyświetlanych gwiazdek i domyślną ocenę, powiadamia o zmianie wartości przez użytkownika i umożliwia pobranie oceny.
Omówienie Kontrolka RatingBar pozwala utworzyć spopularyzowany niedawno interfejs do przyznawania ocen. Interfejs ten pozwala użytkownikowi ocenić określoną rzecz za pomocą gwiazdek (kontrolka RatingBar nie wyświetla ocenianej rzeczy — to trzeba zrobić w kodzie aplikacji). RatingBar to klasa pochodna od ProgressBar, która wyświetla określoną liczbę ikon (gwiazdek) na pasku. Oto główne właściwości tej klasy: numStars
Liczba wyświetlanych gwiazdek (wartość typu int).
rating
Ocena przyznana przez użytkownika (wartość typu float z uwagi na właściwość stepSize).
stepSize
Wartość kroku między kolejnymi ocenami (wartość typu float; standardowo wynosi 1.0 lub 0.5 i zależy od tego, jak precyzyjne mają być oceny).
isIndicator
Wartość typu boolean. Należy ustawić ją na true, jeśli ocena ma być tylko do odczytu.
Właściwości te zwykle ustawia się w XML-u:
7.17. Pokaż m gw azdy — kontrolka Rat ngBar
|
265
Kontrolka RatingBar przechowuje przyznaną ocenę. Do pobrania tej oceny można zastosować jedną z dwóch technik: • wywołać metodę getRating(); • udostępnić odbiornik powiadomień o zmianie typu OnRatingBarChangeListener.
Odbiornik OnRatingChangeListener ma jedną metodę, a mianowicie onRatingChanged, która przyjmuje trzy argumenty: RatingBar rBar
Źródło zdarzenia (referencja do konkretnej kontrolki RatingBar).
float fRating
Przyznana ocena.
boolean fromUser
Ma wartość true, jeśli ocena została przyznana przez użytkownika, i false, jeżeli ocenę ustawiono programowo.
Kod z listingu 7.14 wyświetla formularz dla klientów. Kod tworzy dwie kontrolki RatingBar. Jedna służy do oceny jakości usług, a druga — do oceny ceny (w XML-u obie kontrolki różnią się tylko wartością atrybutu android:id). W głównym programie tworzony jest odbiornik OnRatingBarChangeListener, wyświetlający informacje odpowiadające przyznanej ocenie. Ocena jest przekształcana na wartość typu int, a w instrukcji switch aplikacja generuje komunikaty toast. Listing 7.14. Aplikacja demonstrująca korzystanie z kontrolki RatingBar public class Main extends Activity { /** Wywoływana, gdy aktywność tworzona jest po raz pierwszy */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); OnRatingBarChangeListener barChangeListener = new OnRatingBarChangeListener() { @Override public void onRatingChanged(RatingBar rBar, float fRating, boolean fromUser) { int rating = (int) fRating; String message = null; switch(rating) { case 1: message = "Przykro nam, że Cię zawiedliśmy"; break; case 2: message = "Przykro nam, że nie jesteś zadowolony"; break; case 3: message = "Możemy się poprawić"; break; case 4: message = "Cieszymy się, że jesteś zadowolony"; break; case 5: message = "Fantastycznie - dziękujemy!"; break; } Toast.makeText(Main.this, message, Toast.LENGTH_LONG).show(); } }; final RatingBar sBar = (RatingBar) findViewById(R.id.serviceBar); sBar.setOnRatingBarChangeListener(barChangeListener); final RatingBar pBar = (RatingBar) findViewById(R.id.priceBar); pBar.setOnRatingBarChangeListener(barChangeListener); Button doneButton = (Button) findViewById(R.id.doneButton); doneButton.setOnClickListener(new OnClickListener() {
266
|
Rozdz ał 7. Graf czny nterfejs użytkown ka
@Override public void onClick(View arg0) { String message = String.format( "Ostateczna ocena: Cena %.0f/%d, Jakość %.0f/%d%nDziękujemy!", sBar.getRating(), sBar.getNumStars(), pBar.getRating(), pBar.getNumStars() ); // Podziękowania dla użytkownika Toast.makeText(Main.this, message, Toast.LENGTH_LONG).show(); // Zapisywanie danych w bazie // To już koniec aktywności, a tym samym i aplikacji finish(); } }); } }
W aplikacji znajduje się więcej niż jedna kontrolka RatingBar, dlatego wartość nie jest zapisywana w odbiorniku, ponieważ formularze uzupełnione tylko częściowo nie są przydatne. W odbiorniku przycisku Gotowe pobierane i wyświetlane są obie wartości. W tym samym miejscu można je również zapisać (choć w niektórych aplikacjach sensowniejsze jest zapisywanie ich w odbiorniku OnRatingBarChangeListener). Jeśli nie masz doświadczenia w formatowaniu łańcuchów znaków za pomocą instrukcji printf, wiedz, że w poleceniu String.format zastosowano sekwencję %.0f do wyświetlenia wartości typu float jako wartości typu int. Wartości nie zrzutowano, ponieważ i tak trzeba sformatować dane. W idealnym rozwiązaniu łańcuch określający formatowanie powinien znajdować się w pliku strings.xml, jednak przedstawiony program to tylko przykład. Główny interfejs użytkownika przedstawiono na rysunku 7.10.
Rysunek 7.10. Wyświetlanie informacji zwrotnych na temat ocen
7.17. Pokaż m gw azdy — kontrolka Rat ngBar
|
267
Gdy użytkownik kliknie przycisk Gotowe, zobaczy komunikat końcowy wyświetlony na pulpicie (rysunek 7.11).
Rysunek 7.11. Komunikat wyświetlany po wypełnieniu formularza
Aby zarówno wyświetlić średnią ocen przyznanych przez członków społeczności, jak i umożliwić danemu użytkownikowi wyrażenie opinii, zwykle wyświetla się obecne wyniki w trybie tylko do odczytu i tworzy wyskakujące okno dialogowe, w którym dana osoba może wprowadzić własną ocenę. Technikę tę opisano w witrynie Android Patterns (http://www.android patterns.com/uap_pattern/rating-stars).
Zobacz też Omówienie kontrolek RatingBar w samouczku Form Stuff w witrynie Android.com (http:// developer.android.com/guide/topics/ui/controls.html#RatingBar); samouczek dotyczący modelu MVC, gdzie pokazano także, jak utworzyć własny komponent View działający podobnie jak kontrolka RatingBar (http://www.wiseandroid.com/post/2010/07/19/Use-MVC-and-develop-a-simple-Star-Rating-widget-on-Android.aspx).
7.18. Drgający widok Ian Darwin
Problem Programista chce, aby komponent View drgał przez kilka sekund w celu przyciągnięcia uwagi użytkownika.
268 |
Rozdz ał 7. Graf czny nterfejs użytkown ka
Rozwiązanie Należy utworzyć animację w pliku XML, a następnie wywołać metodę startAnimation() obiektu View i użyć pomocniczą metodę loadAnimation() do wczytania kodu w XML-u.
Omówienie Specyfikację animacji należy utworzyć w plikach XML w katalogu anim. W tym przykładzie pole tekstowe ma drgać w płaszczyźnie poziomej (co imituje kręcenie głową, oznaczające w wielu częściach świata niezgodę) lub pionowej (co odpowiada potakiwaniu). Należy więc utworzyć dwie animacje — horizontal.xml i vertical.xml. Oto kod z pliku horizontal.xml:
Plik vertical.xml jest niemal identyczny (jedyne różnice to zastosowanie atrybutów fromYDelta i toYDelta). Interpolator (funkcja sterująca animacją) znajduje się w innym pliku, a mianowicie cycler.xml,
którego kod przedstawiono poniżej.
Aby zastosować jedną z dwóch przygotowanych animacji do komponentu View, trzeba najpierw uzyskać do niego referencję. Można oczywiście zastosować w tym celu standardowe wywołanie findViewById(R.id.*). Można też wykorzystać metodę getCurrentFocus() aktywności, jeśli obsługiwany jest komponent, w którym w danym momencie wprowadzane są dane. To drugie podejście pozwala uniknąć wiązania kodu z nazwą konkretnego komponentu, co jest przydatne, jeśli animacja zawsze ma dotyczyć aktualnie używanej kontrolki. W przykładowym kodzie warunek ten jest spełniony, ponieważ animacja jest uruchamiana w metodzie onClick(). Jeszcze inna możliwość to zastosowanie widoku przekazanego do metody onClick(), jednak wtedy drgającym komponentem jest przycisk, a nie pole tekstowe. Nie przedstawiono tu całej aplikacji, a tylko metodę onClick(), w której znajduje się cały kod związany z animacją (listing 7.15). Listing 7.15. Kod animacji @Override public void onClick(View v) { String answer = answerEdit.getText().toString(); if ("tak".equalsIgnoreCase(answer)) { getCurrentFocus().startAnimation( AnimationUtils.loadAnimation(getApplicationContext(), R.anim.vertical)); return; }
7.18. Drgający w dok
|
269
if ("nie".equalsIgnoreCase(answer)) { getCurrentFocus().startAnimation( AnimationUtils.loadAnimation(getApplicationContext(), R.anim.horizontal)); return; } Toast.makeText(this, "Podaj konkretną odpowiedź", Toast.LENGTH_SHORT).show(); }
Efekt drgania pozwala przyciągnąć uwagę użytkownika do niepoprawnych danych wejściowych, łatwo jednak przesadzić z jego stosowaniem. Korzystaj z niego z rozwagą!
7.19. Wyświetlanie dotykowych informacji zwrotnych Adrian Cowham
Problem Programista chce udostępniać w aplikacji dotykowe informacje zwrotne.
Rozwiązanie Aby zapewnić natychmiastowe fizyczne informacje zwrotne, należy zastosować kontrolki dotykowe Androida.
Omówienie Informowanie użytkowników, że ich działania przyniosły efekt, jest niezbędne w każdej aplikacji na każdej platformie. Standardowym przykładem jest wyświetlanie paska postępu informującego użytkowników o tym, że ich poczynania odniosły skutek i że aplikacja wykonuje odpowiednie operacje. Technikę tę można stosować także w interfejsach dotykowych. Zaletą takich interfejsów jest to, że można udostępnić fizyczne informacje zwrotne, ponieważ użytkownicy mogą odczuć rzeczywiste reakcje urządzenia. Korzystałem już z wielu aplikacji na telefony i tablety z Androidem. Rzeczą, którą najbardziej w nich doceniam, jest informowanie mnie o tym, że dotknięcie ekranu przyniosło efekt. Lubię natychmiast wiedzieć, że aplikacja wykryła dotknięcie i zareagowała na nie. Reakcja może mieć trzy formy: wizualną, dźwiękową i fizyczną. W tej recepturze pokazano, jak upewniać użytkowników o podjęciu działań przez aplikację poprzez natychmiastowe wygenerowanie fizycznych informacji zwrotnych. Służą do tego kontrolki dotykowe Androida. Android udostępnia gotowe kontrolki dotykowe. Jeśli jednak potrzebujesz czegoś innego, możesz sterować mechanizmem wibracji urządzenia i udostępnić niestandardowe informacje zwrotne. Sterowanie mechanizmem wibracji urządzenia wymaga uprawnień. Trzeba to bezpośrednio określić w pliku AndroidManifest.xml. Jeśli nie chcesz żądać uprawnień lub lista potrzebnych uprawnień jest już długa, możesz wykorzystać gotowe kontrolki dotykowe Androida.
270
|
Rozdz ał 7. Graf czny nterfejs użytkown ka
Niektóre urządzenia (np. Motorola Xoom) nie mają mechanizmu wibracji. W takich urządzeniach przykładowy kod z tej receptury będzie wprawdzie działał, ale użytkownik nie otrzyma fizycznych informacji zwrotnych.
Najpierw zapoznaj się z bardziej skomplikowaną aplikacją, która generuje niestandardowe dotykowe informacje zwrotne.
Niestandardowe fizyczne informacje zwrotne oparte na wibracjach Pierwszy krok polega na zażądaniu niezbędnych uprawnień. Dodaj do pliku AndroidManifest. xml poniższy wiersz:
Teraz należy zdefiniować odbiornik do reagowania na dotknięcia. Nie pokazano tego na listingu 7.16, jednak klasa odbiornika, CustomHapticListener, to prywatna, niestatyczna klasa wewnętrzna aktywności. Podejście to wynika z tego, że potrzebny jest dostęp do metody Context. getSystemService(...). Listing 7.16. Implementacja interfejsu OnTouchListener zapewniająca fizyczne informacje zwrotne private class CustomHapticListener implements OnTouchListener { // Czas wibracji w milisekundach private final int durationMs; public CustomHapticListener( int ms ) { durationMs = ms; } @Override public boolean onTouch( View v, MotionEvent event ) { if( event.getAction() == MotionEvent.ACTION_DOWN ){ Vibrator vibe = ( Vibrator ) getSystemService( VIBRATOR_SERVICE );n vibe.vibrate( durationMs ); o } return true; } }
Ważne są tu wiersze z oznaczeniami n i o. Wiersz n pobiera referencję do usługi Vibrator, a wiersz o włącza wibracje. Jeśli aplikacja nie zażądała uprawnień do sterowania mechanizmem wibracji, wiersz o spowoduje zgłoszenie wyjątku. Teraz należy zarejestrować odbiornik. W metodzie onCreate(...) aktywności aplikacja pobiera referencję do elementu interfejsu GUI, który ma zwracać fizyczne informacje zwrotne. Następnie trzeba zarejestrować zdefiniowany wcześniej odbiornik z implementacją interfejsu OnTouchListener. @Override public void onCreate( Bundle savedInstance ) { Button customBtn = ( Button ) findViewById( R.id.btn_custom ); customBtn.setOnTouchListener( new CustomHapticListener( 100 ) ); }
Gotowe — teraz możesz sterować dotykowymi informacjami zwrotnymi. Pora przejść do stosowania wbudowanego androidowego mechanizmu generowania dotykowych informacji zwrotnych. 7.19. Wyśw etlan e dotykowych nformacj zwrotnych
|
271
Gotowe zdarzenia generujące dotykowe informacje zwrotne Należy zacząć od najważniejszego — aby móc zastosować zdarzenia generujące dotykowe informacje zwrotne, trzeba włączyć ten mechanizm w poszczególnych widokach. Można zrobić to deklaratywnie (w pliku układu) lub programowo (w kodzie Javy). Aby włączyć dotykowe informacje zwrotne w układzie, wystarczy dodać atrybut android:hapticFeedbackEnabled="true" dla odpowiednich widoków. Oto krótki przykład:
W Javie ten sam efekt można uzyskać za pomocą następujących instrukcji: Button keyboardTapBtn = ( Button ) findViewById( btnId ); keyboardTapBtn.setHapticFeedbackEnabled( true );
Po włączeniu dotykowych informacji zwrotnych następny krok polega na zarejestrowaniu odbiornika OnTouchListener i wygenerowaniu odpowiednich informacji zwrotnych. Na listingu 7.17 pokazano, jak zarejestrować odbiornik OnTouchListener i jak generować dotykowe informacje zwrotne w odpowiedzi na dotknięcie widoku przez użytkownika. Listing 7.17. Aplikacja generująca dotykowe informacje zwrotne // Inicjowanie przycisków za pomocą wartości oznaczających wbudowany // mechanizm generowania dotykowych informacji zwrotnych z Androida private void initializeButtons() { // Inicjowanie przycisków za pomocą standardowych opcji dotykowych informacji zwrotnych initializeButton( R.id.btn_keyboardTap, HapticFeedbackConstants.KEYBOARD_TAP );n initializeButton( R.id.btn_longPress, HapticFeedbackConstants.LONG_PRESS ); o initializeButton( R.id.btn_virtualKey, HapticFeedbackConstants.VIRTUAL_KEY ); p } // Metoda pomocnicza inicjująca pojedyncze przyciski i rejestrująca odbiornik // OnTouchListener do generowania dotykowych informacji zwrotnych private void initializeButton( int btnId, int hapticId ) { Button btn = ( Button ) findViewById( btnId ); btn.setOnTouchListener( new HapticTouchListener( hapticId ) ); } // Klasa do obsługi dotknięć i generowania dotykowych informacji zwrotnych private class HapticTouchListener implements OnTouchListener { private final int feedbackType; public HapticTouchListener( int type ) { feedbackType = type; } public int feedbackType() { return feedbackType; } @Override public boolean onTouch(View v, MotionEvent event) { // Informacje zwrotne należy generować tylko po dotknięciu widoku, a nie // na przykład po podniesieniu palca if( event.getAction() == MotionEvent.ACTION_DOWN ){ // Generowanie informacji zwrotnych v.performHapticFeedback( feedbackType() );q } return true; } }
272
|
Rozdz ał 7. Graf czny nterfejs użytkown ka
W wierszach od n do p aplikacja inicjuje trzy różne przyciski za pomocą trzech różnych stałych, które oznaczają dotykowe informacje zwrotne. Stałe te są wbudowane w Android. Dwie z nich generują dokładnie takie same informacje zwrotne. Kod z listingu 7.17 to fragment aplikacji testowej, którą napisałem w celu zademonstrowania dotykowych informacji zwrotnych. Nie udało mi się znaleźć różnicy między efektem zastosowania stałych HapticFeedbackConstants. LONG_PRESS i HapticFeedbackConstants.KEYBOARD_TAP. Ponadto w trakcie testów stała HapticFeedback Constants.VIRTUAL_KEY nie prowadziła do uzyskania żadnych informacji zwrotnych. W punkcie q aplikacja generuje informacje dotykowe. Udostępnianie takich informacji jest więc proste, powinieneś jednak pamiętać, że jeśli chcesz sterować wibracjami urządzenia, musisz zażądać odpowiednich uprawnień w pliku AndroidManifest.xml. Jeśli zdecydujesz się na wbudowane w Android opcje generowania dotykowych informacji zwrotnych, pamiętaj o włączeniu tego mechanizmu dla widoków w układzie lub programowo.
Zobacz także http://mytensions.blogspot.com/2011/03/androids-haptic-feedback.html.
7.20. Przełączanie się między różnymi aktywnościami w widoku TabView Pratik Rupwal
Problem Programista chce umożliwić przełączanie aktywności w oknie z zakładkami.
Rozwiązanie Należy zastąpić zawartość zakładki nową, docelową aktywnością.
Omówienie Gdy wywołująca aktywność w widoku TabView wywołuje inną aktywność za pomocą intencji, widok TabView jest zastępowany widokiem opartym na wywołanej aktywności. Aby wyświetlić wywołaną aktywność w widoku TabView, można zastąpić widok wywołującej aktywności widokiem wywoływanej aktywności. Dzięki temu widok TabView pozostaje niezmienny. Aby uzyskać taki efekt, wywołująca aktywność powinna dziedziczyć po klasie ActivityGroup, a nie po klasie Activity. Na listingu 7.18 aktywność Calling (pochodna od ActivityGroup) jest umieszczana w widoku TabView. Listing 7.18. Zastępowanie aktywności w zakładce // Aktywność Calling public class Calling extends ActivityGroup implements OnClickListener {
7.20. Przełączan e s ę m ędzy różnym aktywnośc am w w doku TabV ew
|
273
Button b1; Intent i1; /** Wywoływana, gdy aktywność tworzona jest po raz pierwszy */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.calling); b1=(Button)findViewById(R.id.changeactivity); b1.setOnClickListener(); } public void onClick(View view) { // Ta instrukcja tworzy intencję wywołującą aktywność Called i1=new Intent(this.getBaseContext(),Called.class); // Wywołanie metody w celu zastąpienia widoku replaceContentView("Called", i1); } // Ta metoda służy do zastępowania widoku aktywności Calling widokiem aktywności Called public void replaceContentView(String id, Intent newIntent) { // Tworzenie widoku aktywności Called za pomocą intencji newIntent View view = getLocalActivityManager().startActivity(id, newIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)) .getDecorView(); // Ustawianie widoku na zawartość aktywności Calling this.setContentView(view); } }
Aktywność Called może wywoływać jeszcze inną aktywność (np. CalledSecond). Oto potrzebny do tego kod: // Aktywność Called public class Called extends Activity implements OnClickListener { Button b1; Intent i1; Calling caller; /** Wywoływana, gdy aktywność tworzona jest po raz pierwszy */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.called); b1=(Button)findViewById(R.id.changeactivity); b1.setOnClickListener(); } public void onClick(View view) { // Ta instrukcja tworzy intencję wywołującą aktywność CalledSecond i1=new Intent(this.getBaseContext(),CalledSecond.class); /* CalledSecond może być dowolną aktywnością — nawet aktywnością * Calling, jeśli trzeba wrócić do wcześniejszej aktywności */ // Inicjowanie obiektu Calling caller=(Calling)getParent(); // Wywołanie metody zastępującej widok caller.replaceContentView("CalledSecond", i1); } }
274
|
Rozdz ał 7. Graf czny nterfejs użytkown ka
7.21. Tworzenie niestandardowego paska tytułu Shraddha Shravagi
Problem W standardowym pasku tytułu nie można umieszczać żadnych przycisków ani własnego tekstu. Pasek ten zwykle obejmuje nazwę aplikacji i znajduje się w górnej części okna.
Rozwiązanie Aby utworzyć własny pasek tytułu, należy wykonać następujące operacje:
1. Utworzyć plik XML z kodem paska tytułu. 2. Utworzyć klasę, która korzysta z danego paska tytułu, i zaimplementować działanie przycisku. 3. Zmodyfikować pliki układu. 4. Utworzyć aktywności jako klasy pochodne od niestandardowej klasy przygotowanej w kroku 2.
Omówienie Na listingu 7.19 pokazano plik maintitlebar.xml, który obejmuje jeden widok tekstowy i trzy przyciski graficzne. Orientacja jest tu ustawiona na poziomą. Listing 7.19. Plik maintitlebar.xml
7.21. Tworzen e n estandardowego paska tytułu
|
275
android:layout_toLeftOf="@+id/linkedinBtn" />
Na listingu 7.20 przedstawiono najważniejszą klasę — klasę aktywności wyświetlającej okno. Jak widać, najpierw trzeba zażądać niestandardowego paska tytułu, następnie ustawić plik układu, a w ostatnim kroku ustawić pasek tytułu. Listing 7.20. Aktywność wyświetlająca okno public class CustomWindow extends Activity { protected TextView title; protected ImageView icon; protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Żądanie niestandardowego paska tytułu requestWindowFeature(Window.FEATURE_CUSTOM_TITLE); // Ustawianie pliku układu setContentView(R.layout.main); // Ustawianie układu z paskiem tytułu getWindow().setFeatureInt(Window.FEATURE_CUSTOM_TITLE, R.layout.maintitlebar); } public void facebookBtnClicked(View v) { // Obsługa kliknięć przycisku } public void twitterBtnClicked(View v) { // Obsługa kliknięć przycisku } public void linkedinBtnClicked(View v) { // Obsługa kliknięć przycisku } }
W każdym pliku układu, w którym widoczny ma być niestandardowy pasek tytułu, należy podać wartość match_parent w atrybutach layout_height i layout_width:
Oto, jak powinna wyglądać aktywność rozszerzająca przygotowaną wcześniej niestandardową klasę: // Aktywność CustomWindow odpowiada za wczytywanie paska tytułu public class Credentials extends CustomWindow { // Ustawianie pliku układu setContentView(R.layout.login); }
Na rysunku 7.12 przedstawiono wygląd aktywności.
276
|
Rozdz ał 7. Graf czny nterfejs użytkown ka
Rysunek 7.12. Niestandardowy pasek tytułu
Nie trzeba stosować odrębnej klasy, aby zaimplementować pasek tytułu, jednak przedstawione tu podejście należy do dobrych praktyk pisania kodu.
7.22. Formatowanie liczb Ian Darwin
Problem Programista chce samodzielnie formatować liczby, ponieważ domyślne formatowanie zapewniane przez metodę Double.toString() i podobne instrukcje nie zapewnia wystarczającej kontroli nad wyglądem wartości.
Rozwiązanie Należy zastosować metodę String.format() lub jedną z klas pochodnych od NumberFormat.
Omówienie Funkcję printf() wprowadzono w języku C w latach 70. XX wieku. Od tej pory pojawiła się ona w wielu innych językach programowania, w tym w Javie. Oto przykładowe wywołanie funkcji printf w Javie SE: System.out.printf("Witaj, %s, o %s%n", userName, time);
Instrukcja ta wyświetla komunikaty podobne do poniższego: Witaj, Aniu, o Wed Jun 16 08:38:46 EDT 2010
7.22. Formatowan e l czb
|
277
W Androidzie nie stosuje się przestrzeni nazw System.out, jednak ten sam wyjściowy łańcuch znaków można uzyskać (na potrzeby wyświetlenia w widoku) za pomocą poniższej instrukcji: String msg = String.format("Witaj, %s, o %s%n", userName, time);
Jeśli wcześniej nie stosowałeś instrukcji printf, wiedz, że pierwszy argument to łańcuch znaków z kodami formatującymi, a dalsze argumenty (tu userName i time) to formatowane wartości. Kody formatujące rozpoczynają się od znaku procentów (%) i obejmują przynajmniej jeden kod określający typ. W tabeli 7.1 przedstawiono często używane kody tego rodzaju. Tabela 7.1. Wybrane kody formatujące Znak
Znaczen e
s
Łańcuch znaków (p zekształca wa tości typów p ostych w domyślny sposób p zekształca obiekty za pomocą metody toString)
d
Liczba całkowita w systemie dziesiętnym (int long)
f
Liczba zmiennop zecinkowa (float double)
n
Nowy wie sz
t
Data i czas (fo maty specyficzne dla Javy) zobacz tekst podany w punkcie „Zobacz także” w końcowej części eceptu y
Daty formatowane w domyślny sposób wyglądają nieelegancko, dlatego często warto wyświetlać je za pomocą innych technik. Możliwości funkcji printf są określone w klasie java.util. Formatter. To do niej należy zajrzeć, aby poznać wszystkie szczegóły mechanizmów formatowania. Inaczej niż w funkcjach printf z innych języków, w Javie wszystkie metody formatujące pozwalają opcjonalnie wskazywać argumenty za pomocą numerów. Należy umieścić numer i znak dolara po początkowym symbolu %, ale przed właściwym kodem formatującym. Na przykład sekwencja %2$3.1f oznacza, że drugi argument należy sformatować jako liczbę dziesiętną o trzech cyfrach w części całkowitej i jednej cyfrze po przecinku. Numery argumentów można wykorzystać w dwóch celach — do zmiany kolejności wyświetlanych argumentów (często jest to przydatne przy umiędzynarodawianiu) i do kilkukrotnego wskazywania jednego argumentu. Kod formatujący t, oznaczający datę i czas, wymaga dodatkowego znaku, np. Y (dla roku), m (dla miesiąca) itd. Poniższa instrukcja pobiera argument time i wyodrębnia z niego kilka pól: msg = String.format("Witaj dnia %1$tB %1$td, %1$tY%n", time);
Ten kod wyświetla daty w następującym formacie: July 4, 2010. Aby wyświetlać wartości z określoną precyzją, należy zastosować kod f oraz podać długość liczby i precyzję, np.: msg = String.format("Szerokość geograficzna: %10.6f", latitude);
Ta instrukcja może wyświetlić następujący tekst: Długość geograficzna: -79.281818
Choć taki sposób formatowania jest odpowiedni w niektórych zastosowaniach, np. do wyświetlania szerokości i długości geograficznej, czasem daje niepotrzebnie dużo kontroli.
Ogólne metody formatujące Java udostępnia cały pakiet (java.text) pełen metod formatujących, które są bardzo ogólne i dają duże możliwości. Podobnie jak dla funkcji printf, tak i tu istnieje język formatowania,
278
|
Rozdz ał 7. Graf czny nterfejs użytkown ka
opisany na stronie internetowej z dokumentacją. Zastanów się nad sposobem wyświetlania liczb. W Ameryce Północnej wartość „tysiąc dwadzieścia cztery i jedna czwarta” zapisywana jest tak: 1,024.25. W większości państw europejskich taka wartość to 1 024,25, natomiast w niektórych krajach pisze się ją tak: 1.024,25. Formatowanie walut i procentów jest równie zróżnicowane. Próba samodzielnego zarządzania wszystkimi możliwościami z tego obszaru szybko przyprawiłaby programistę o ból głowy. Na szczęście w pakiecie java.text znajduje się klasa Locale. Ponadto środowisko uruchomieniowe Androida ustawia domyślny obiekt Locale na podstawie środowiska użytkownika. Przedstawiony tu kod działa zarówno w Javie na komputery stacjonarne, jak i w Androidzie. Aby można było udostępniać metody formatujące dostosowane do liczb, walut i procentów, w klasie NumberFormat znajdują się statyczne metody fabryczne, które standardowo zwracają obiekt DecimalFormat z już ustawionym odpowiednim wzorcem. Odpowiedni dla ustawień użytkownika obiekt DecimalFormat można pobrać z metody fabrycznej NumberFormat.getInstance(). Do zarządzania takim obiektem służą metody set. Co zaskakujące, metoda setMinimumIntegerDigits() pozwala w łatwy sposób utworzyć format z początkowymi zerami. Przykładowy kod pokazano na listingu 7.21. Listing 7.21. Przykładowy program do formatowania liczb /* * Formatowanie liczb w niestandardowy i domyślny sposób */ public class NumFormat2 { /** Formatowana wartość */ public static final double data[] = { 0, 1, 22d/7, 100.2345678 }; public static void main(String[] av) { // Pobieranie obiektu NumberFormat NumberFormat form = NumberFormat.getInstance(); // Dostosowanie obiektu, aby liczby wyglądały tak 999.99[99] form.setMinimumIntegerDigits(3); form.setMinimumFractionDigits(2); form.setMaximumFractionDigits(4); // Teraz obiekt można wykorzystać do formatowania for (int i=0; i
Pokazany kod wyświetla zawartość tablicy za pomocą obiektu NumberFormat. Kod ten utworzono jako główny program (a nie jako fragment aplikacji na Android), aby pokazać efekty działania obiektu NumberFormat. Na przykład instrukcja $ java NumFormat2 0.0 powoduje wyświetlenie tekstu 000.00. Dla argumentu 1.0 wyświetlany jest tekst 001.00, dla argumentu 3.142857142857143 — 003.1429, a dla 100.2345678 — 100.2346. Można też utworzyć obiekt DecimalFormat z konkretnym wzorcem lub dynamicznie zmieniać wzorce za pomocą instrukcji applyPattern(). W tabeli 7.2 przedstawiono wybrane symbole z często stosowanych symboli z wzorców.
7.22. Formatowan e l czb
|
279
Tabela 7.2. Symbole z wzorców dla obiektu DecimalFormat Symbol
Wyjaśn en e
#
Cyf a (z pomijaniem początkowych ze )
0
Cyf a (z wyświetlaniem początkowych ze )
.
Specyficzny dla języka sepa ato części dziesiętnej (punkt dziesiętny)
,
Specyficzny dla języka sepa ato g up cyf (w angielskim jest to p zecinek)
-
Specyficzny dla języka symbol oznaczający liczby ujemne (znak minus)
%
Wyświetla wa tości jako p ocenty
;
Oddziela dwa fo maty (pie wszy dla wa tości dodatnich a d ugi
'
Sp awia że jeden z powyższych znaków pojawia się w pie wotnej postaci
nne znaki
Pojawiają się bez zmian
dla ujemnych)
W programie NumFormatTest wykorzystano jeden obiekt DecimalFormat do wyświetlania w liczbach tylko dwóch cyfr po przecinku, a drugi — do formatowania liczby zgodnie z ustawieniami dla języka domyślnego. Kod programu przedstawiono na listingu 7.22. Listing 7.22. Program w Javie SE ilustrujący stosowanie klasy NumberFormat import java.text.DecimalFormat; import java.text.NumberFormat; public class NumFormatDemo { /** Formatowana liczba */ public static final double intlNumber = 1024.25; /** Inna formatowana liczba */ public static final double ourNumber = 100.2345678; public static void main(String[] av) { NumberFormat defForm = NumberFormat.getInstance(); NumberFormat ourForm = new DecimalFormat("##0.##"); // Instrukcja toPattern() pozwala zobaczyć wzorzec używany // w języku domyślnym System.out.println("Wzorzec obiektu defForm to " + ((DecimalFormat)defForm).toPattern()); System.out.println(intlNumber + " po sformatowaniu to " + defForm.format(intlNumber)); System.out.println(ourNumber + " po sformatowaniu to " + ourForm.format(ourNumber)); System.out.println(ourNumber + " w domyślnym formacie to " + defForm.format(ourNumber)); } }
Przedstawiony program pokazuje dany wzorzec, a następnie wyświetla liczbę sformatowaną na kilka sposobów: $ java NumFormatTest Wzorzec defForm to #,##0.### 1024.25 po sformatowaniu to 1,024.25 100.2345678 po sformatowaniu to 100.23 100.2345678 w domyślnym formacie to 100.235
280 |
Rozdz ał 7. Graf czny nterfejs użytkown ka
Zobacz także Rozdział 10. książki Java Cookbook1 Iana F. Darwina (wydawnictwo O’Reilly), część VI książki Java I/O Elliotte’a Rusty’ego Harolda (wydawnictwo O’Reilly).
7.23. Poprawne stosowanie liczby mnogiej Ian Darwin
Problem Aplikacja ma wyświetlać tekst w rodzaju "Found " + n + " reviews", jednak w języku angielskim forma Found 1 reviews jest niepoprawna gramatycznie. Dla n==1 aplikacja ma wyświetlać tekst Found 1 review.
Rozwiązanie W prostej sytuacji, gdy wyniki wyświetlane są tylko w języku angielskim, można zastosować instrukcję warunkową. Lepsza technika, umożliwiająca umiędzynarodawianie, polega na zastosowaniu obiektu ChoiceFormat. W Androidzie można też zastosować element
Omówienie Prosta technika polega na zastosowaniu trójargumentowego operatora Javy (cond ? trueval : falseval) przy złączaniu łańcuchów znaków. Ponieważ w języku angielskim do większości rzeczowników dodawana jest litera s w przypadku, gdy liczba przedmiotów to zero lub więcej niż jeden (np. „no books, one book, two books”), trzeba sprawdzać tylko to, czy n==1. // Plik FormatPlurals.java public static void main(String argv[]) { report(0); report(1); report(2); } /** report – zastosowanie operatora warunkowego */ public static void report(int n) { System.out.println("Found " + n + " item" + (n==1?"":"s")); }
Po uruchomieniu kodu w środowisku Java SE główny program wyświetli następujące informacje: $ java FormatPlurals Found 0 items Found 1 item Found 2 items $
1
Wydanie polskie: Ian Darwin, Java. Receptury, Helion, Gliwice 2003. — przyp. tłum.
7.23. Poprawne stosowan e l czby mnog ej
|
281
Ostatnia instrukcja println() to skrócona forma poniższego kodu: if (n==1) System.out.println("Found " + n + " item"); else System.out.println("Found " + n + " items");
Wersja ta jest znacznie dłuższa, dlatego warto nauczyć się korzystać z trójargumentowego operatora Javy. Oczywiście, nie zawsze można stosować to rozwiązanie, ponieważ język angielski jest dziwny i występują w nim wyjątki. Niektóre rzeczowniki, np. bus, wymagają dodania członu es w liczbie mnogiej. Inne, takie jak cash, nie mają liczby mnogiej (można powiedzieć „two flocks of geese” lub „two stacks of cash”, ale formy „two geeses” lub „two cashes” są niepoprawne). Niektóre rzeczowniki, np. fish, mogą oznaczać liczbę mnogą, choć forma fishes też jest prawidłowa.
Lepsze podejście Klasa ChoiceFormat z pakietu java.text doskonale nadaje się do obsługi liczby mnogiej. Pozwala ona określić wersje rzeczowników dla liczby pojedynczej i mnogiej (lub, w bardziej ogólnym ujęciu, dla przedziałów). Klasa ta ma duże możliwości, ale na listingu 7.23 pokazano tylko kilka prostych przykładów. Określono tu wartości 0, 1 i 2 (lub więcej) oraz łańcuchy znaków wyświetlane dla poszczególnych liczb. Tekst jest formatowany zależnie od przedziału, do jakiego należy wartość. Listing 7.23. Formatowanie tekstu z liczbą mnogą za pomocą klasy ChoiceFormat import java.text.*; /** * Poprawne formatowanie tekstu z liczbą mnogą za pomocą klasy ChoiceFormat */ public class FormatPluralsChoice extends FormatPlurals { // Ten obiekt ChoiceFormat zwraca słowo w odpowiedniej formie static double[] limits = { 0, 1, 2 }; static String[] formats = { "reviews", "review", "reviews"}; static ChoiceFormat pluralizedFormat = new ChoiceFormat(limits, formats); // Ten obiekt ChoiceFormat zwraca angielski tekst określający liczbę ocen static ChoiceFormat quantizedFormat = new ChoiceFormat( "0#no reviews|1#one review|1
}
System.out.println("Określanie liczby ocen"); for (int i : data) { System.out.println("Found " + quantizedFormat.format(i)); }
}
282
|
Rozdz ał 7. Graf czny nterfejs użytkown ka
Obie pętle generują podobne dane wyjściowe jak podstawowa wersja. Kod wykorzystujący klasę ChoiceFormat jest nieco dłuższy, ale bardziej uniwersalny i ułatwia umiędzynarodawianie. Wystarczy w pliku strings.xml umieścić łańcuch znaków z konstruktora wersji z określaniem liczby ocen i wykorzystać go przy umiędzynarodawianiu.
Najlepsze rozwiązanie (możliwe tylko w Androidzie) Należy utworzyć plik /res/values/
Następnie w kodzie należy zastosować następujące podejście: int count = getNumberOfsongsAvailable(); Resources res = getResources(); String songsFound = res.getQuantityString(R.plurals.numberOfSongsAvailable, count);
To rozwiązanie zasugerował Tomas Persson.
Zobacz także Technikę dostępną tylko w Androidzie opisano na stronie http://developer.android.com/guide/ topics/resources/string-resource.html#Plurals.
7.24. Wyświetlanie drugiego ekranu z poziomu pierwszego Daniel Fowler
Problem Początkujący programiści aplikacji potrzebują prostego przykładu ilustrującego otwieranie nowego ekranu. Pomaga to zrozumieć obsługę interfejsu użytkownika w Androidzie.
Rozwiązanie Należy zmodyfikować aplikację „Hello, World” ze środowiska Eclipse, tak aby wczytywała nowy ekran w reakcji na kliknięcie przycisku. Pozwoli to poznać reguły uruchamiania nowych ekranów w interfejsie użytkownika.
Omówienie Aplikacje na Android komunikują się z użytkownikiem przez jeden lub kilka ekranów. Każdy ekran wyświetla informacje i elementy interfejsu użytkownika, np. przyciski, listy, suwaki, 7.24. Wyśw etlan e drug ego ekranu z poz omu p erwszego
| 283
pola tekstowe itd. Liczba ekranów zależy od funkcji udostępnianych przez aplikację oraz rodzaju urządzenia z Androidem. Tanie telefony z Androidem mają wyświetlacze 2,5-calowe, w drogich telefonach wyświetlacze mają 4,5 cala, a w tabletach — 7 lub 10 cali. Dlatego na tablecie funkcje aplikacji mogą się mieścić na jednym ekranie, w zaawansowanym telefonie — na dwóch lub trzech, a w tanim telefonie — na czterech lub pięciu. Każdy ekran wyświetlany użytkownikowi jest kontrolowany przez aktywność. To aktywność odpowiada za tworzenie i wyświetlanie ekranu oraz zarządzanie elementami interfejsu użytkownika. Podstawowymi cegiełkami do tworzenia interfejsów użytkownika w Androidzie są widoki. Każdy element ekranu, np. przycisk (obiekt Button) lub pole tekstowe (obiekt EditText), jest dostępny w pakiecie android.widget. Elementy ekranu dziedziczą po klasie View. Na ekranie można umieszczać je w kontenerach pochodnych od klasy ViewGroup, np. w układzie Linear Layout (sama klasa ViewGroup dziedziczy po klasie View). Dostępne są rozmaite układy z rodziny ViewGroup, między innymi poziome, pionowe, tabelowe, w formie siatki itd. (rysunek 7.13).
Rysunek 7.13. Widoki dostępne na palecie w interfejsie graficznym
Na ekranie głównym urządzenia można umieszczać specjalne widoki nazywane widżetami aplikacji. Są to małe elementy interfejsu użytkownika, które udostępniają użytkownikom informacje z aplikacji bez konieczności uruchamiania danego programu. Widżetów aplikacji nie należy mylić z kontrolkami dostępnymi w pakiecie android.widget. Pakiet ten obejmuje różnego rodzaju elementy wyświetlane w programach, natomiast widżety aplikacji to narzędzia dostępne na ekranie głównym urządzenia. Widżety aplikacji tworzy się za pomocą klas z rodziny RemoteView (które też należą do pakietu android.widget). Długą listę elementów typu View i ViewGroup dostępnych w Androidzie można zobaczyć po otwarciu lub utworzeniu pliku zasobu układu w środowisku Eclipse (w katalogu res/layout projektu). Po otwarciu takiego pliku należy kliknąć zakładkę Graphical Layout w dolnej części
284 |
Rozdz ał 7. Graf czny nterfejs użytkown ka
edytora. Po lewej stronie edytora pojawi się wtedy pasek narzędzi ze wszystkimi dostępnymi elementami interfejsu użytkownika. Elementy można filtrować na podstawie wersji interfejsu API, używając listy rozwijanej w prawej górnej części edytora. Można też zdefiniować fragment (obiekt Fragment), czyli część ekranu przeznaczoną do wielokrotnego użytku. Do tworzenia fragmentów można wykorzystać obiekty ViewGroup i View. Ten sam fragment można następnie wyświetlać na kilku ekranach. Pozwala to jednokrotnie zdefiniować część interfejsu użytkownika, która jest potrzebna w różnych miejscach. Jeśli aplikacja obejmuje więcej niż jeden ekran, konieczne jest wyświetlanie jednego ekranu z poziomu drugiego. W innych systemach operacyjnych drugi ekran często wczytuje się bezpośrednio w pierwszym. Z uwagi na projekt Androida aplikacja nigdy nie może bezpośrednio otworzyć nowego ekranu — zamiast tego musi zażądać wyświetlenia ekranu przez Android. Wynika to z tego, że Android od początku zaprojektowano z myślą o środowisku mobilnym. Android musi mieć pełną kontrolę nad aplikacją, aby można było wydajnie obsługiwać zdarzenia spoza danego programu. O niektórych z tych zdarzeń (na przykład o telefonie lub niskim poziomie baterii) trzeba użytkownika poinformować natychmiast. Inne zdarzenia, takie jak powiadomienia o odebraniu e-maila lub przypomnienie, powodują, że użytkownik wychodzi z aplikacji, aby wykonać odpowiednie operacje. Możliwe też, że użytkownik jednocześnie korzysta z kilku programów. Android w wielu sytuacjach potrzebuje ścisłej kontroli nad pracą i reagowaniem aplikacji. Gdy Android wyświetla ekran, ma informacje o jego działaniu i stanie. Android może kierować komunikaty do aktywności, które odpowiednio reagują na nieoczekiwane zdarzenia. To dlatego w aplikacjach na Android (inaczej niż w innych systemach) nie ma metody głównej (wspomniano o tym w recepturze 1.6). Metoda główna nie jest potrzebna, ponieważ sam Android kontroluje uruchamianie aplikacji. Oto warunki, które trzeba spełnić, aby otworzyć ekran w aplikacji:
1. Trzeba utworzyć definicję ekranu w formie układu. 2. W pliku klasy Javy należy zdefiniować aktywność; posłuży ona do obsługi ekranu. 3. Android musi mieć informacje o istnieniu aktywności (należy umieścić je w pliku manifestu). 4. Aplikacja musi informować Android, kiedy należy wyświetlić nowy ekran. W ramach przykładu można dodać nowy ekran do aplikacji MyAndroid z receptury 1.4. Nowy ekran także zawiera prosty komunikat, a jest wyświetlany po wciśnięciu przycisku na pierwszym ekranie. Uruchom środowisko Eclipse i otwórz projekt MyAndroid utworzony w recepturze poświęconej aplikacji Witaj, świecie. Najpierw należy dodać trzy łańcuchy znaków — pierwszy na tytuł nowego ekranu, drugi na komunikat z tego ekranu i trzeci na tekst przycisku uruchamiającego ten ekran. W drzewie projektu w oknie Package Explorer otwórz plik strings.xml z katalogu res/values. Dodaj trzy łańcuchy znaków — pierwszy o nazwie screen2Title i wartości Ekran 2., drugi o nazwie hello2 i wartości Witaj ponownie! oraz trzeci o nazwie next i wartości Następny. Plik strings.xml powinien wyglądać następująco:
7.24. Wyśw etlan e drug ego ekranu z poz omu p erwszego
| 285
W menu File (lub w menu kontekstowym z poziomu drzewa projektu) wybierz opcję New/ Android XML File. W oknie dialogowym, które się pojawi, ustaw następujące pola (dla pozostałych zachowaj wartości domyślne; zobacz rysunek 7.14): File
secondscreen xml
Type of resource
Layout
Folder
/res/layout
Rysunek 7.14. Tworzenie nowego androidowego pliku XML
Wybierz opcję Finish. Po otwarciu pliku secondscreen.xml przeciągnij na ekran kontrolkę TextView (w zakładce Graphical Layout) lub wpisz kod kontrolki TextView (w zakładce z kodem w XML-u). Właściwości kontrolki ustaw w następujący sposób: layout_width
fill_parent
layout_height
wrap_content
text
@string/hello2
textSize
10pt
Plik secondscreen.xml powinien obejmować następujący kod:
286
|
Rozdz ał 7. Graf czny nterfejs użytkown ka
Otwórz plik main.xml z katalogu res/layout. Przeciągnij na ekran kontrolkę Button (w zakładce Graphical Layout) lub dodaj kod tej kontrolki w widoku z kodem w XML-u. Właściwości kontrolki ustaw w poniższy sposób: layout_width
wrap_content
layout_height
wrap_content
text
@+id/nextButton
id
@string/next
W pliku Main.xml powinien znajdować się następujący kod:
W menu File (lub w menu kontekstowym z poziomu drzewa projektu) wybierz opcję New/ Class. W oknie dialogowym, które się pojawi, ustaw następujące pola (dla pozostałych zachowaj wartości domyślne; zobacz rysunek 7.15): Source folder
MyAndroid/src
Package
com example
Name
Screen2
Wybierz opcję Finish. W pliku Screen2.java należy utworzyć klasę pochodną od Activity i przesłonić metodę onCreate (w podobny sposób jak w klasie Main). W metodzie tej należy wywołać metodę setContentView i przekazać do niej nowy układ (secondscreen). Referencje do wszystkich zasobów można uzyskać poprzez R, czyli wygenerowaną klasę Javy. Dlatego referencja do układu nowego ekranu to R.layout.secondscreen (klasa R jest generowana na podstawie plików i katalogów z folderu res). Oto kod z pliku Screen2.java (razem z potrzebnymi instrukcjami import): package com.example; import android.app.Activity; import android.os.Bundle;
7.24. Wyśw etlan e drug ego ekranu z poz omu p erwszego
|
287
Rysunek 7.15. Definiowanie nowej klasy Javy public class Screen2 extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.secondscreen); } }
Przycisk potrzebuje kodu, aby poinformować Android o zamiarze uruchomienia aktywności powiązanej z nowym ekranem. Aby uzyskać pożądany efekt, należy w momencie wciśnięcia przycisku przekazać w obiekcie Intent nazwę aktywności do metody startActivity. Metoda startActivity należy do obiektu Context. Obiekt ten obejmuje szereg przydatnych metod, które zapewniają dostęp do środowiska wykonywania aplikacji. Activity to klasa pochodna od Context, dlatego metoda startActivity w aktywnościach jest zawsze dostępna. Zastosowanie metody startActivity umożliwia Androidowi wykonanie niezbędnych operacji porządkujących oraz późniejsze uruchomienie klasy aktywności zdefiniowanej w aplikacji. W recepturze 7.4 opisano, jak dodawać komponenty obsługi wciśnięcia przycisku. Tu, zamiast metodę onClick implementować w klasie Main, zrobiono to w klasie wewnętrznej. W metodzie onClick należy umieścić kod do uruchamiania aktywności Screen2. W deklaracji intencji trzeba określić kontekst i aktywność (Screen2). Ponieważ Main jest klasą pochodną od Activity, która z kolei jest pochodna od Context, można zastosować słowo kluczowe this (tu w formie Main.this, ponieważ metoda onClick znajduje się w klasie wewnętrznej). Oto kod z pliku Main.java (ze wszystkimi instrukcjami import):
288 |
Rozdz ał 7. Graf czny nterfejs użytkown ka
package com.example; import import import import import
android.app.Activity; android.content.Intent; android.os.Bundle; android.view.View; android.view.View.OnClickListener;
public class Main extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); findViewById(R.id.nextButton).setOnClickListener(new OnClickListener() { public void onClick(View v) { Intent intent = new Intent(Main.this, Screen2.class); startActivity(intent); }}); } }
Można też napisać bardziej zrozumiałą wersję kodu i osobno zadeklarować obiekt do obsługi wciśnięcia przycisku: public class Main extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); findViewById(R.id.nextButton).setOnClickListener(new handleButton()); } class handleButton implements OnClickListener { public void onClick(View v) { Intent intent = new Intent(Main.this, Screen2.class); startActivity(intent); } } }
Na potrzeby tego przykładu można też zaadaptować komponent obsługi z receptury 7.4. W ostatnim kroku (w celu zarejestrowania nowego ekranu w Androidzie) należy do pliku AndroidManifest.xml projektu dodać element activity. Element ten powinien znaleźć się po deklaracji aktywności Main. Oto fragment z deklaracjami aktywności:
Kropka przed nazwami Main i Screen2 oznacza, że aktywność znajduje się w pakiecie aplikacji. Jeśli aktywność jest zdefiniowana w innym pakiecie, trzeba podać także pełną nazwę pakietu. Na rysunku 7.16 przedstawiono aplikację z uruchomionym pierwszym ekranem.
7.24. Wyśw etlan e drug ego ekranu z poz omu p erwszego
| 289
Rysunek 7.16. Pierwszy ekran z przyciskiem Następny
Ekran wyświetlany po kliknięciu przycisku Następny pokazano na rysunku 7.17.
Rysunek 7.17. Następne okno aplikacji
Aby umożliwić powrót do pierwszego ekranu, nie trzeba tworzyć specjalnego przycisku. Android zarządza stosem aktywności i udostępnia przycisk Back (albo w samym urządzeniu, albo w dolnej części ekranu pod oknem aplikacji).
290
|
Rozdz ał 7. Graf czny nterfejs użytkown ka
Zobacz także Receptura 1.4, receptura 7.4.
7.25. Tworzenie ekranu wczytywania, wyświetlanego przy przełączaniu aktywności Shraddha Shravagi
Problem Przed wczytaniem aktywności pojawia się pusty ekran.
Rozwiązanie Należy utworzyć prostą aktywność, która zamiast czarnego pola wyświetla ekran wczytywania.
Omówienie Pobranie żądanych przez użytkownika danych z bazy lub internetu oraz wyświetlenie ich na ekranie może zająć aktywności pewien czas. W trakcie oczekiwania użytkownika na pojawienie się danych ekran jest zwykle czarny. Taka sytuacja ma miejsce na przykład w następującym scenariuszu: ProfileList (użytkownik wybiera jeden z profili)
Czarny ekran
ProfileData
Zamiast w trakcie wczytywania danych wyświetlać użytkownikowi czarny ekran, można pokazać rysunek, jak ma to miejsce w poniższym scenariuszu: ProfileList (użytkownik wybiera jeden z profili)
LoadingScreenActivity
ProfileData
Z tej receptury dowiesz się, jak utworzyć prosty ekran wczytywania, widoczny na ekranie przez 2,5 sekundy w trakcie uruchamiania następnej aktywności. Najpierw należy utworzyć plik układu dla ekranu wczytywania. Układ ten tworzy ekran z komunikatem o wczytywaniu i paskiem postępu.
7.25. Tworzen e ekranu wczytywan a, wyśw etlanego przy przełączan u aktywnośc
|
291
Następnie należy utworzyć plik z klasą LoadingScreen (listing 7.24). Listing 7.24. Klasa LoadingScreen public class LoadingScreenActivity extends Activity { // Wprowadzanie opóźnienia private final int WAIT_TIME = 2500; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); System.out.println("LoadingScreenActivity screen started"); setContentView(R.layout.loading_screen); findViewById(R.id.mainSpinner1).setVisibility(View.VISIBLE); new Handler().postDelayed(new Runnable() { @Override public void run() { // Symulowanie długiego zadania try { Thread.sleep(1000); } catch (InterruptedException e) { // Niemożliwe } System.out.println("Wyświetlanie danych z profilu"); /* Tworzenie intencji uruchamiającej aktywność ProfileData */ Intent mainIntent = new Intent(LoadingScreenActivity.this, ProfileData.class); LoadingScreenActivity.this.startActivity(mainIntent); LoadingScreenActivity.this.finish(); } }, WAIT_TIME); }
}
Ten kod powoduje wczytanie następnej aktywności po upływie czasu WAIT_TIME. Teraz wystarczy utworzyć intencję uruchamiającą aktywność z ekranem wczytywania: protected void onListItemClick(ListView l, View v, int position, long id) { super.onListItemClick(l, v, position, id); Intent intent = new Intent(ProfileList.this, LoadingScreenActivity.class); startActivity(intent); }
7.26. Zakrywanie innych komponentów za pomocą klasy SlidingDrawer Mike Rowehl
Problem Komponent SlidingDrawer umożliwia użytkownikowi otwarcie w interfejsie GUI kontenera z komponentami innymi niż te widoczne w pierwotnym widoku. Tak działa na przykład 292
|
Rozdz ał 7. Graf czny nterfejs użytkown ka
Application Drawer z Androida 2.x. Jednak w dokumentacji pakietu SDK nie opisano zbyt dobrze prawidłowego układu komponentu SlidingDrawer. Warto wiedzieć, jak wykorzystać ten komponent do zakrywania innych elementów w układzie, a także jak rozmieszczać elementy układu, aby nie kolidowały one z uchwytem omawianej kontrolki.
Rozwiązanie Należy komponent SlidingDrawer umieścić w układzie FrameLayout lub RelativeLayout. Zastosowanie układu LinearLayout sprawia, że trudno jest zasłonić pozostałe kontrolki na ekranie. Aby komponent SlidingDrawer nie zasłaniał danych z kontrolki ListView, należy zastosować wypełniacz w podstawowym układzie.
Omówienie Najpierw warto przyjrzeć się układowi obejmującemu sam komponent SlidingDrawer. Na listingu 7.25 zwróć uwagę na wypełniacz w postaci kontrolki TextView. Jest ona wyrównana względem dolnej części układu RelativeLayout za pomocą stylu DrawerButton. Uchwyt komponentu SlidingDrawer to także kontrolka TextView o tym stylu. Umieszczenie głównej kontrolki ListView układu nad wypełniaczem gwarantuje, że po zamknięciu komponentu SlidingDrawer elementy listy nie są zasłaniane. Listing 7.25. Układ komponentu SlidingDrawer
7.26. Zakrywan e nnych komponentów za pomocą klasy Sl d ngDrawer
|
293
Na listingu 7.26 pokazano ustawienia komponentu DrawerButton wyodrębnione do pliku ze stylami (xml/styles.xml). Zastosowanie takiego pliku pozwala na jednoczesną zmianę wyglądu wypełniacza i uchwytu. Listing 7.26. Ustawienia komponentu DrawerButton
Teraz komponent SlidingDrawer powinien wysuwać się nad kontrolkę ListView, jednak po zamknięciu nie może zakrywać jej zawartości. Na rysunku 7.18 pokazano trzy widoki — pierwotny (z listą kontaktów), w trakcie przeciągania komponentu oraz po pełnym otwarciu komponentu (z przykładowym alfabetem fonetycznym).
Rysunek 7.18. Przesuwanie komponentu SlidingDrawer
Zobacz także Komponent SlidingDrawer można aktywować programowo za pomocą metod open(), close(), toggle() i animateOpen(). Dokumentację komponentu znajdziesz na stronie http://developer. android.com. Metoda animateOpen() standardowo otwiera komponent od dołu do góry. Można też otwierać komponent od góry do dołu (zobacz recepturę 7.27).
294 |
Rozdz ał 7. Graf czny nterfejs użytkown ka
7.27. Otwieranie komponentu SlidingDrawer od góry do dołu Wagied Davids
Problem Gdy użytkownik przeciąga komponent SlidingDrawer, aby go otworzyć, lub aplikacja żąda otwarcia komponentu, wywołując metodę open(), jest on wysuwany od dołu kontenera. Programista chce jednak, aby efekt przejścia działał od góry do dołu.
Rozwiązanie Aby utworzyć efekt przejścia od góry do dołu, należy zastosować pakiet org.panel (dostępny jako oprogramowanie o otwartym dostępie do kodu źródłowego).
Omówienie Należy wykonać następujące operacje:
1. Dołączyć pakiet org.panel, obsługujący interpolację typu easing. 2. W androidowym pliku XML z widokiem dodać nową przestrzeń nazw, np. panel. 3. Wykorzystać ustawiony znacznik zamiast komponentu SlidingDrawer Androida. Na listingu 7.27 przedstawiono kod układu z pliku Main.xml. Listing 7.27. Plik układu Main.xml
7.27. Otw eran e komponentu Sl d ngDrawer od góry do dołu
|
295
android:id="@id/panelHandle" android:layout_width="fill_parent" android:layout_height="33dip" />
Na listingu 7.28 pokazano główną aktywność. Listing 7.28. Główna aktywność import android.app.Activity; import android.os.Bundle; public class Test extends Activity { /** Wywoływana, gdy aktywność tworzona jest po raz pierwszy */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); } }
7.28. Dodawanie do układu obramowania z zaokrąglonymi rogami Daniel Fowler
Problem Programista chce umieścić obramowanie wokół ekranu lub zwiększyć atrakcyjność interfejsu użytkownika.
296
|
Rozdz ał 7. Graf czny nterfejs użytkown ka
Rozwiązanie W pliku XML należy zdefiniować typowy dla Androida kształt i przypisać go do atrybutu background układu.
Omówienie Katalog drawable (w folderze res) w projekcie na Android może obejmować nie tylko bitmapy (pliki w formatach PNG i JPEG), ale też kształty zdefiniowane w plikach XML. Kształty te można wielokrotnie stosować w projekcie. Za pomocą kształtu można na przykład umieścić obramowanie wokół układu. Tu pokazano, jak wyświetlić prostokątne obramowanie z zaokrąglonymi rogami. W katalogu drawable utwórz nowy plik o nazwie customborder.xml. W tym celu w środowisku Eclipse wybierz opcję File/New/File. Jeśli wybrany jest katalog drawable, wprowadź nazwę pliku i kliknij przycisk Finish. Oto kod w XML-u z definicją kształtu ramki:
Atrybut android:shape ma wartość rectangle (możliwe inne kształty to oval, line i ring). Wartość domyślna tego atrybutu to właśnie rectangle, dlatego przy definiowaniu prostokątów atrybut można pominąć. Szczegółowe informacje o plikach z kształtami znajdziesz w dokumentacji Androida poświęconej kształtom (adres URL strony z tą dokumentacją podano w punkcie „Zobacz także”). Element corners powoduje ustawienie w prostokącie zaokrąglonych rogów. Dla każdego rogu można ustawić inny kąt (zobacz dokumentację Androida). Atrybuty elementu padding pozwalają odsunąć zawartość widoku, dla którego stosowany jest kształt. Dzięki temu zawartość widoku nie nachodzi na obramowanie Kolor obramowania jest tu ustawiony na jasnoszary (wartość szesnastkowa tego koloru w formacie RGB to CCCCCC). Dla kształtów można też ustawić gradient, jednak tu tego nie zrobiono. Opis definiowania gradientu znajdziesz w dokumentacji Androida. Aby zastosować kształt, należy wykorzystać atrybut android:background="@drawable/customborder". W układzie można w standardowy sposób umieszczać inne widoki. Tu jest to jeden widok
TextView. Kolor tekstu jest biały (wartość szesnastkowa w formacie RGB to FFFFFF), a tło usta-
wiono na kolor niebieski z przezroczystością, co pozwala zmniejszyć jasność (wartość szesnastkowa w formacie alfa RGB to A00000FF).
7.28. Dodawan e do układu obramowan a z zaokrąglonym rogam
|
297
Układ odsunięto od krawędzi ekranu przez umieszczenie go w innym układzie o niewielkim marginesie wewnętrznym. Oto kompletna zawartość pliku układu:
Na rysunku 7.19 pokazano efekt działania tego kodu.
Rysunek 7.19. Obramowanie z zaokrąglonymi rogami
Zobacz także http://developer.android.com/guide/topics/resources/drawable-resource.html#Shape.
298 |
Rozdz ał 7. Graf czny nterfejs użytkown ka
7.29. Wykrywanie gestów w Androidzie Pratik Rupwal
Problem Programista chce umożliwić poruszanie się po różnych ekranach za pomocą prostych gestów, takich jak przerzucanie lub przewijanie stron.
Rozwiązanie Należy zastosować klasę GestureDetector do wykrywania prostych gestów, takich jak dotykanie, przewijanie, szybkie przesuwanie palcem po ekranie lub obracanie urządzenia.
Omówienie W przykładowej aplikacji znajdują się cztery widoki (każdy w innym kolorze). Aplikacja działa w dwóch trybach — SCROLL i FLIP. Początkowo włączony jest tryb FLIP. W tym trybie podczas wykonywania gestów przeciągania od lewej do prawej lub z góry do dołu aplikacja przełącza widoki. Po wykryciu długiego dotknięcia aplikacja przełącza się w tryb SCROLL, co pozwala przewijać wyświetlane widoki. W tym trybie można dwukrotnie dotknąć ekran, aby wrócić do początku widoku. Po ponownym wykryciu długiego dotknięcia aplikacja wraca do trybu FLIP. W tej recepturze najważniejsze jest wykrywanie gestów, dlatego nie opisano animacji. Efekt drgania widoku za pomocą animacji opisano w recepturze 7.18. Więcej informacji o animacjach znajdziesz w dokumentacji pakietu android.view.animation (zobacz stronę http://developer. android.com/reference/android/view/animation/package-summary.html). Listing 7.29 to wprowadzenie do prostego wykrywania gestów w Androidzie. Klasa Gesture Detector wykrywa gesty na podstawie wykrytych zdarzeń typu MotionEvent. Klasa Gesture Detector używana jest wraz z metodą onTouchEvent. W metodzie tej wywoływana jest metoda GestureDetector.onTouchEvent. Klasa GestureDetector wykrywa gesty lub zdarzenia i zwraca informacje o nich za pomocą interfejsu do obsługi wywołań zwrotnych, czyli GestureDetector. OnGestureListener. Aby utworzyć obiekt GestureDetector, należy przekazać do konstruktora obiekt Context i odbiornik GestureDetector.OnGestureListener. W interfejsie wywołań zwrotnych GestureDetector.OnGestureListener nie występuje zdarzenie dwukrotnego kliknięcia. Jest ono zgłaszane przez inny interfejs wywołań zwrotnych — GestureDetector.OnDoubleTapListener. Aby zastosować ten interfejs, należy zarejestrować go dla odpowiednich zdarzeń za pomocą metody GestureDetector.setOnDoubleTapListener. Klasa MotionEvent obejmuje wszystkie wartości dla zdarzeń związanych z ruchem i dotykiem. Wartości te to między innymi współrzędne X i Y miejsca zdarzenia, czas zdarzenia, a także indeks urządzenia wejścia. Listing 7.29. Wykrywanie gestów ... import android.view.GestureDetector; ... import android.view.animation.OvershootInterpolator;
7.29. Wykrywan e gestów w Andro dz e
|
299
import android.view.animation.TranslateAnimation; public class FlipperActivity extends Activity implements GestureDetector.OnGestureListener, GestureDetector.OnDoubleTapListener{ final private int SWIPE_MIN_DISTANCE = 100; final private int SWIPE_MIN_VELOCITY = 100; private ViewFlipper flipper = null; private ArrayList
300 |
Rozdz ał 7. Graf czny nterfejs użytkown ka
0.0f, 0.0f);
animleftout = new TranslateAnimation( Animation.RELATIVE_TO_PARENT, 0.0f, Animation.RELATIVE_TO_PARENT, -1.0f, Animation.RELATIVE_TO_PARENT, 0.0f, Animation.RELATIVE_TO_PARENT, 0.0f); animrightin = new TranslateAnimation( Animation.RELATIVE_TO_PARENT, -1.0f, Animation.RELATIVE_TO_PARENT, Animation.RELATIVE_TO_PARENT, 0.0f, Animation.RELATIVE_TO_PARENT,
0.0f, 0.0f);
animrightout = new TranslateAnimation( Animation.RELATIVE_TO_PARENT, 0.0f, Animation.RELATIVE_TO_PARENT, +1.0f, Animation.RELATIVE_TO_PARENT, 0.0f, Animation.RELATIVE_TO_PARENT, 0.0f); animupin = new TranslateAnimation( Animation.RELATIVE_TO_PARENT, 0.0f, Animation.RELATIVE_TO_PARENT, 0.0f, Animation.RELATIVE_TO_PARENT, +1.0f, Animation.RELATIVE_TO_PARENT, 0.0f); animupout = new TranslateAnimation( Animation.RELATIVE_TO_PARENT, 0.0f, Animation.RELATIVE_TO_PARENT, 0.0f, Animation.RELATIVE_TO_PARENT, 0.0f, Animation.RELATIVE_TO_PARENT, -1.0f); animdownin = new TranslateAnimation( Animation.RELATIVE_TO_PARENT, 0.0f, Animation.RELATIVE_TO_PARENT, 0.0f, Animation.RELATIVE_TO_PARENT, -1.0f, Animation.RELATIVE_TO_PARENT, 0.0f); animdownout = new TranslateAnimation( Animation.RELATIVE_TO_PARENT, 0.0f, Animation.RELATIVE_TO_PARENT, 0.0f, Animation.RELATIVE_TO_PARENT, 0.0f, Animation.RELATIVE_TO_PARENT, +1.0f); animleftin.setDuration(1000); animleftin.setInterpolator(new OvershootInterpolator()); animleftout.setDuration(1000); animleftout.setInterpolator(new OvershootInterpolator()); animrightin.setDuration(1000); animrightin.setInterpolator(new OvershootInterpolator()); animrightout.setDuration(1000); animrightout.setInterpolator(new OvershootInterpolator()); animupin.setDuration(1000); animupin.setInterpolator(new OvershootInterpolator()); animupout.setDuration(1000); animupout.setInterpolator(new OvershootInterpolator()); animdownin.setDuration(1000); animdownin.setInterpolator(new OvershootInterpolator()); animdownout.setDuration(1000); animdownout.setInterpolator(new OvershootInterpolator()); } private void prepareViews() { TextView view = null; views = new ArrayList
7.29. Wykrywan e gestów w Andro dz e
|
301
views.add(view); } } private void addViews() { for (int index=0; index
float float float float float float float float
ev1x = event1.getX(); ev1y = event1.getY(); ev2x = event2.getX(); ev2y = event2.getY(); xdiff = Math.abs(ev1x - ev2x); ydiff = Math.abs(ev1y - ev2y); xvelocity = Math.abs(velocityX); yvelocity = Math.abs(velocityY);
if(xvelocity > this.SWIPE_MIN_VELOCITY && xdiff > this.SWIPE_MIN_DISTANCE) { if(ev1x > ev2x) // Przesunięcie w lewo { --currentview; if(currentview < 0) { currentview = views.size() - 1; }
302
|
Rozdz ał 7. Graf czny nterfejs użytkown ka
flipper.setInAnimation(animleftin); flipper.setOutAnimation(animleftout); } else // Przesunięcie w prawo { ++currentview; if(currentview >= views.size()) { currentview = 0; } flipper.setInAnimation(animrightin); flipper.setOutAnimation(animrightout); } flipper.scrollTo(0,0); flipper.setDisplayedChild(currentview); } else if (yvelocity > this.SWIPE_MIN_VELOCITY && ydiff > this.SWIPE_MIN_DISTANCE) { if(ev1y > ev2y) // Przesunięcie w górę { --currentview; if(currentview < 0) { currentview = views.size() - 1; } flipper.setInAnimation(animupin); flipper.setOutAnimation(animupout); } else // Przesunięcie w dół { ++currentview; if(currentview >= views.size()) { currentview = 0; } flipper.setInAnimation(animdownin); flipper.setOutAnimation(animdownout); } flipper.scrollTo(0,0); flipper.setDisplayedChild(currentview); } return false; } /** Metoda onLongPress jest wywoływana, gdy użytkownik dotyka ekranu i przez pewien czas nie podnosi palca. Parametr MotionEvent reprezentuje zdarzenie dotknięcia */ @Override public void onLongPress(MotionEvent e) { vibrator.vibrate(200); flipper.scrollTo(0,0); isDragMode = !isDragMode;
7.29. Wykrywan e gestów w Andro dz e
| 303
setViewText(); } /** Metoda onScroll jest wywoływana, gdy użytkownik dotyka ekranu i przenosi palec w inne miejsce ekranu */ @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,float distanceY) { if(isDragMode) flipper.scrollBy((int)distanceX, (int)distanceY); return false; } /** Metoda onShowPress jest wywoływana, gdy użytkownik dotknął ekranu, ale * jeszcze nie przesunął palca. To zdarzenie używane jest przede wszystkim do * generowania informacji zwrotnych dotyczących działań użytkowników */ @Override public void onShowPress(MotionEvent e) { } /** Metoda onSingleTapUp wywoływana jest po dotknięciu ekranu przez użytkownika */ @Override public boolean onSingleTapUp(MotionEvent e) { return false; } /** Metoda onDoubleTap wywoływana jest po dwukrotnym dotknięciu ekranu. * Jej jedyny parametr, a mianowicie MotionEvent, odpowiada zdarzeniu dwukrotnego * dotknięcia */ @Override public boolean onDoubleTap(MotionEvent e) { flipper.scrollTo(0,0); return false; } /** Metoda onDoubleTapEvent wywoływana jest dla wszystkich zdarzeń, które * wystąpiły w ramach dwukrotnego dotknięcia (czyli dla opuszczenia, * przesunięcia i podniesienia palca) */ @Override public boolean onDoubleTapEvent(MotionEvent e) { return false; } /** Metoda onSingleTapConfirmed wywoływana jest po wystąpieniu i potwierdzeniu jednego dotknięcia. Jedno dotknięcie prowadzące do wywołania tej metody to inne zdarzenie niż pojedyncze dotknięcie wykrywane przez odbiornik GestureDetector.onGestureListener. Ta metoda wywoływana jest wtedy, gdy obiekt GestureDetector wykryje, że dane dotknięcie nie prowadzi do podwójnego dotknięcia */ @Override public boolean onSingleTapConfirmed(MotionEvent e) { return false; } }
O zmianie trybu aplikacja informuje użytkownika za pomocą wibracji. Aby zastosować mechanizm wibracji, należy w pliku AndroidManifest.xml programu ustawić następujące uprawnienie:
304 |
Rozdz ał 7. Graf czny nterfejs użytkown ka
W aplikacji wykorzystywane są łańcuchy znaków zadeklarowane w pliku res/values/string.xml:
Zobacz także Do obsługi skomplikowanych gestów w Androidzie służy klasa GestureOverlayView (http:// developer.android.com/reference/android/gesture/GestureOverlayView.html).
7.30. Tworzenie interfejsu użytkownika w Androidzie 1.6 i nowszych wersjach za pomocą fragmentów z Androida 3.0 Saketkumar Srivastav
Problem Fragmenty to małe porcje interfejsu użytkownika tworzące jedną aktywność. Fragmenty początkowo były dostępne tylko w Androidzie 3.0 i nowszych wersjach platformy. Programista chce zastosować fragmenty w interfejsie użytkownika w Androidzie 1.6 i nowszych wersjach.
Rozwiązanie W Androidzie 2.0 i nowszych wersjach należy zastosować pakiet Android Compatibility Google’a do budowania aplikacji za pomocą interfejsu API Fragments.
Omówienie Fragment można traktować jak portlet na stronie portalu. Pod względem wyglądu, cyklu życia i innych cech przypomina on w dużym stopniu aktywność, różni się jednak od niej, ponieważ musi działać w ramach aktywności. Fragmenty — w odróżnieniu od aktywności — nie mogą działać samodzielnie. Aby zbudować fragment, trzeba utworzyć klasę pochodną od jednej z klas z rodziny Fragment. Istnieją różne rodzaje fragmentów, w tym ListFragment (odpowiednik dla ListActivity), Dialog
7.30. Tworzen e nterfejsu użytkown ka w Andro dz e 1.6
| 305
Fragment (odpowiednik dla DialogInterface) i PreferenceFragment (odpowiednik dla Preference Activity).
Jako pierwsza opisana jest tu klasa FragmentTestActivity (listing 7.30). W metodzie onCreate() ustawiany jest adapter listy, który przechowuje tablicę łańcuchów znaków z tytułami magazynów wydawnictwa EFY Group. Ustawiany jest też odbiornik dla elementów listy, co pozwala wykonać określone operacje po wybraniu jednego z tych elementów. Listing 7.30. Plik FragmentTestActivity.java public class FragmentTestActivity extends FragmentActivity implements OnItemClickListener { /** Wywoływana, gdy aktywność tworzona jest po raz pierwszy */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); ListView l = (ListView) findViewById(R.id.number_list); ArrayAdapter
W metodzie onItemClickListener() wykonywane są główne operacje związane z zarządzaniem fragmentem. Aplikacja tworzy egzemplarz fragmentu, przekazując pozycję klikniętego elementu. Następnie trzeba zastąpić element z fragmentem z pliku main.xml nowym fragmentem TestFragment, z którym powiązany jest odpowiedni interfejs użytkownika. Aby uzyskać ten efekt, należy utworzyć egzemplarz klasy FragmentTransaction. Pozwala on dodawać, usuwać i zastępować fragmenty w sposób programowy. Obiekt R.id.the_frag odpowiadający elementowi
306
|
Rozdz ał 7. Graf czny nterfejs użytkown ka
Pora przejść do klasy TestFragment (listing 7.31). Tu pozycja klikniętego elementu listy zapisywana jest w zmiennej magznumber. Jak wcześniej wyjaśniono, jeśli fragment powiązany jest z interfejsem użytkownika, należy zastosować metodę onCreateView() do rozwinięcia widoku do fragmentu. Przykładowa aplikacja tworzy fragment z układem liniowym, a następnie wczytuje ten fragment wraz z odpowiednią grafiką magazynu w kontrolce ImageView (kontrolka ta jest dodawana do wspomnianego układu liniowego). Listing 7.31. Plik TestFragment.java public class TestFragment extends Fragment { private int magznumber; public TestFragment() { } /** * Konstruktor do bezpośredniego tworzenia fragmentów */ public TestFragment(int position) { this.magznumber = position; } /** * Jeśli fragment tworzony jest na podstawie zapisanego stanu, należy go przywrócić */ @Override public void onCreate(Bundle saved) { super.onCreate(saved); if (null != saved) { magznumber = saved.getInt("magznumber"); } } /** * Zapisywanie liczby wyświetlanych androidów */ @Override public void onSaveInstanceState(Bundle toSave) { toSave.putInt("magznumber", magznumber); } /** * Tworzenie siatki do wyświetlania magazynów */ @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle saved) { Context c = getActivity().getApplicationContext(); LinearLayout l = new LinearLayout(c); LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT, 0); l.setLayoutParams(params); ImageView i = new ImageView(c); switch(magznumber){ case 1: i.setImageResource(R.drawable.efymag); break; case 2:
7.30. Tworzen e nterfejsu użytkown ka w Andro dz e 1.6
|
307
i.setImageResource(R.drawable.lfymag); break; case 3: i.setImageResource(R.drawable.ffymag); break; } l.addView(i); return l; } }
Na rysunku 7.20 pokazano efekt uruchomienia przedstawionego kodu.
Rysunek 7.20. Przykład zastosowania interfejsu API Fragments
Zobacz także Zapoznaj się z oficjalnymi artykułami na temat interfejsu API fragmentów z Androida (http:// developer.android.com/guide/components/fragments.html) oraz pakietu Android Compatibility (http://android-developers.blogspot.com/2011/03/fragments-for-all.html).
7.31. Korzystanie z galerii zdjęć w Androidzie 3.0 Wagied Davids
Problem W aplikacji znajdują się statyczne grafiki. Programista chce wyświetlać je w galerii zdjęć, tak aby użytkownik mógł wykonywać na grafikach określone operacje.
308 |
Rozdz ał 7. Graf czny nterfejs użytkown ka
Rozwiązanie Można wykorzystać galerię zdjęć z Androida 3.0 do wyświetlania grafik, którymi użytkownik może później manipulować.
Omówienie Aby wykorzystać galerię zdjęć z Androida 3.0, należy wykonać następujące operacje:
1. Pobrać pakiet SDK Androida 3.x, używając menedżera pobierania pakietów SDK lub z poziomu środowiska Eclipse (za pomocą programu Android SDK Manager).
2. Utworzyć urządzenie AVD do uruchamiania w emulatorze. 3. Utworzyć projekt na Android (ważne — opcję Min. SDK Version należy ustawić na wersję Honeycomb) i kliknąć przycisk Finish.
4. Utworzyć plik Javy z głównym punktem wejścia do aplikacji (np. plik Main.java). 5. Utworzyć plik ImageAdapter.java. 6. Utworzyć plik XML z układem (listing 7.32). Listing 7.32. Główny plik układu, main.xml
7. Przygotować pakiet i uruchomić aplikację na Android. Na listingu 7.33 przedstawiono kod głównej aktywności. Listing 7.33. Główna aktywność import import import import import import import import import import
android.app.Activity; android.graphics.Bitmap; android.graphics.BitmapFactory; android.graphics.drawable.Drawable; android.os.Bundle; android.view.View; android.widget.AdapterView; android.widget.AdapterView.OnItemClickListener; android.widget.Gallery; android.widget.Toast;
public class Main extends Activity implements OnItemClickListener
7.31. Korzystan e z galer zdjęć w Andro dz e 3.0
| 309
{ private static final String tag = "Main"; private Gallery _gallery; private ImageAdapter _imageAdapter; /** Wywoływana, gdy aktywność tworzona jest po raz pierwszy */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); setTitle("Aplikacja z galerią zdjęć z Androida Honeycomb"); _gallery = (Gallery) this.findViewById(R.id.gallery1); _imageAdapter = new ImageAdapter(this); _gallery.setAdapter(_imageAdapter); _gallery.setOnItemClickListener(this); } @Override public void onItemClick(AdapterView arg0, View view, int position, long duration) { int resourcId = (Integer) _imageAdapter.getItem(position); Drawable drawable = getResources().getDrawable(resourcId); Bitmap bitmap = BitmapFactory.decodeResource(getResources(), resourcId); Toast.makeText(this, "Wybrana grafika: " + getResources().getText(resourcId) + "\n" + "Wysokość: " + bitmap.getHeight() + "\nSzerokość: " + bitmap.getWidth(), Toast.LENGTH_SHORT).show(); } }
Na listingu 7.34 pokazano kod klasy ImageAdapter. Listing 7.34. Klasa ImageAdapter public class ImageAdapter extends BaseAdapter { private Context _context = null; private final int[] imageIds = { R.drawable.formula, R.drawable.hollywood, R.drawable.mode1, R.drawable.mode2, R.drawable.mother1, R.drawable.mother2, R.drawable.nights, R.drawable.ontwerpje1,R.drawable.ontwerpje2, R.drawable.relation1, R.drawable.relation2, R.drawable.renaissance, R.drawable.renaissance_zoom }; public ImageAdapter(Context context) { this._context = context; } @Override public int getCount() { return imageIds.length; } @Override public Object getItem(int index) { return imageIds[index]; } @Override public long getItemId(int index) {
310
|
Rozdz ał 7. Graf czny nterfejs użytkown ka
return index; } @Override public View getView(int postion, View view, ViewGroup group) { ImageView imageView = new ImageView(_context); imageView.setImageResource(imageIds[postion]); imageView.setScaleType(ScaleType.FIT_XY); imageView.setLayoutParams(new Gallery.LayoutParams(400, 400)); return imageView; } }
Na rysunku 7.21 przedstawiono efekt działania kodu.
Rysunek 7.21. Przykładowa aplikacja z galerią zdjęć
7.32. Tworzenie prostego widżetu aplikacji Catarina Reis
Problem Programista chce ułatwić użytkownikom interakcję z aplikacją.
Rozwiązanie Należy utworzyć widżet aplikacji, czyli prostą kontrolkę interfejsu GUI, która pojawia się na ekranie głównym i umożliwia użytkownikom łatwą interakcję z istniejącą aplikacją, aktywnością lub usługą.
7.32. Tworzen e prostego w dżetu apl kacj
|
311
Omówienie W tej recepturze dowiesz się, jak utworzyć widżet, który uruchamia usługę aktualizującą wizualne komponenty. Widżet ten (CurrentMoodWidget) wyświetla informacje o aktualnym nastroju użytkownika w formie emotikonu tekstowego. Po kliknięciu przez użytkownika przycisku z grafiką emotikon ten zmienia się na losowy. Na rysunku 7.22 pokazano początkowy wygląd widżetu, a na rysunku 7.23 widoczny jest widżet po losowej zmianie emotikonu.
Rysunek 7.22. Początkowy wygląd widżetu
Rysunek 7.23. Widżet z informacjami o bieżącym nastroju
312
|
Rozdz ał 7. Graf czny nterfejs użytkown ka
1. Najpierw utwórz nowy projekt na Android (CurrentMoodWidgetProject). Jako nazwę aplika-
cji podaj Current Mood, a jako nazwę pakietu — oreillymedia.cookbook.android.spikes. Nie twórz aktywności. Minimalną wersję pakietu SDK ustaw na 8. (odpowiada ona Androidowi 2.2, w którym wprowadzono widżety aplikacji).
2. Dodaj napisy potrzebne w widżecie. Umieść je w pliku zasobów res/values/string.xml. Potrzebne są następujące pary nazwa – wartość:
• widgettext – Aktualny nastrój:; • widgetmoodtext – :).
3. Dodaj rysunki wyświetlane na przycisku widżetu. Umieść je w katalogu res/drawable. Oto rysunek smile_icon.png:
4. W folderze res/layout w hierarchii katalogów projektu utwórz nowy plik układu, widget layout.xml. Plik ten ma obejmować definicję układu widżetu:
5. Teraz należy utworzyć konfigurację dostawcy widżetu. Wymaga to utworzenia folderu res/xml w hierarchii katalogów projektu i umieszczenia w nowym folderze pliku widget providerinfo.xml o następującej zawartości:
6. Następnie należy utworzyć usługę reagującą na kliknięcie przycisku z rysunkiem przez użytkownika. Kod usługi (plik CurrentMoodService.java) przedstawiono na listingu 7.35. Listing 7.35. Kod usługi powiązanej z widżetem @Override public int onStartCommand(Intent intent, int flags, int startId) { super.onStart(intent, startId); updateMood(intent); stopSelf(startId);
7.32. Tworzen e prostego w dżetu apl kacj
|
313
return START_STICKY; } private void updateMood(Intent intent) { if (intent != null){ String requestedAction = intent.getAction(); if (requestedAction != null && requestedAction.equals(UPDATEMOOD)){ this.currentMood = getRandomMood(); int widgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, 0); AppWidgetManager appWidgetMan = AppWidgetManager.getInstance(this); RemoteViews views = new RemoteViews(this.getPackageName(),R.layout.widgetlayout); views.setTextViewText(R.id.widgetMood, currentMood); appWidgetMan.updateAppWidget(widgetId, views); } } }
7. Teraz należy zaimplementować klasę dostawcy widżetu. Jej kod umieść w pliku Current MoodWidgetProvider.java (listing 7.36).
Listing 7.36. Klasa dostawcy widżetu @Override public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { super.onUpdate(context, appWidgetManager, appWidgetIds); for (int i=0; i
8. W ostatnim kroku należy zadeklarować usługę i dostawcę widżetu aplikacji w pliku manifestu (AndroidManifest.xml).
314
|
Rozdz ał 7. Graf czny nterfejs użytkown ka
ROZDZIAŁ 8.
Alerty w interfejsach GUI — menu, okna dialogowe, komunikaty toast i powiadomienia
8.1. Wprowadzenie — alerty w interfejsach GUI Omówienie W rozmaitych pakietach do tworzenia interfejsów użytkownika, np. w Swingu dla Javy, narzędziach dla komputerów Macintosh Apple’a czy systemów Microsoft Windows, a także w JavaScripcie, dostępne są menu wyskakujące. Zwykle pojawiają się w wersji wyświetlanej w ramce okna i kontekstowej (udostępnianej wewnątrz okna). Takie menu dostępne są też w Androidzie, choć wyglądają tu nieco inaczej z uwagi na mniejsze wyświetlacze urządzeń (menu wyskakujące i kontekstowe zajmują tu większą część ekranu). Ponadto menu rozwijane z ramki okna znajduje się zwykle w dolnej części ekranu, a nie w górnej. We wspomnianych systemach okienkowych wszechobecne są też okna dialogowe. Nie zajmują one całego ekranu i pojawiają się, aby poinformować użytkownika o wystąpieniu pewnego warunku lub zdarzenia. Użytkownik może wtedy zatwierdzić operację, wybrać jedną z kilku opcji, udostępnić informacje itd. Android udostępnia stosunkowo standardowy mechanizm wyświetlania okien dialogowych. Oprócz tego udostępnia też mniejsze okna wyskakujące, tzw. komunikaty toast. Pojawiają się one na ekranie tylko przez kilka sekund i samoczynnie znikają. Służą do pasywnego powiadamiania o mniej istotnych zdarzeniach. Programiści często korzystają z komunikatów toast do powiadamiania o błędach — uważam jednak, że nie należy tak postępować. To jeszcze nie wszystko. Android udostępnia też mechanizm powiadomień, który pozwala umieszczać tekst oraz ikony na pasku powiadomień (w prawej górnej części ekranu w Androidzie Gingerbread i prawej dolnej części w Androidzie Honeycomb). Powiadomieniom mogą towarzyszyć diody LED, dźwięki i wibracje urządzenia.
315
W tym rozdziale omówiono wszystkie wymienione mechanizmy interakcji. Opisane są one w kolejności przedstawionej we wprowadzeniu. Najpierw zapoznasz się z menu, następnie z oknami dialogowymi i komunikatami toast, a na końcu z powiadomieniami.
8.2. Tworzenie i wyświetlanie menu Rachee Singh
Problem Programista chce wyświetlać menu w urządzeniu z Androidem po wybraniu przez użytkownika przycisku Menu.
Rozwiązanie Należy skonfigurować menu w XML-u i dołączyć je do aktywności przez przesłonięcie metody onCreateOptionsMenu().
Omówienie Najpierw w katalogu res projektu utwórz katalog menu. W katalogu menu umieść plik Menu.xml. Zawartość tego pliku przedstawiono na listingu 8.1. Listing 8.1. Definicja menu
W XML-u można utworzyć menu i dodać do niego dowolną liczbę opcji. Można też określić grafikę dla każdej opcji (w przykładowej aplikacji wykorzystano domyślne ikony). W kodzie aktywności w Javie należy przesłonić metodę onCreateOptionsMenu. @Override public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.menu, menu); return true; }
Na rysunku 8.1 pokazano wygląd menu.
316
|
Rozdz ał 8. Alerty w nterfejsach GU — menu, okna d alogowe, komun katy toast pow adom en a
Rysunek 8.1. Niestandardowe menu
8.3. Obsługa wyboru opcji menu Rachee Singh
Problem Po utworzeniu niestandardowego menu należy sprawić, aby program reagował na kliknięcie opcji przez użytkownika.
Rozwiązanie Trzeba przesłonić metodę onOptionsItemSelected.
Omówienie W aktywności w Javie należy przesłonić metodę onOptionsItemSelected. Metoda ta przyjmuje jako argument obiekt MenuItem (czyli opcję menu) i sprawdza jego identyfikator. Identyfikator klikniętej opcji można wykorzystać w instrukcji switch-case. W zależności od wybranej opcji należy podjąć odpowiednie działania. Wygląd niestandardowego menu pokazano na rysunku 8.2. W tym przykładzie kliknięcie opcji prowadzi tylko do wyświetlenia komunikatu toast. Oto potrzebny kod: @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) {
8.3. Obsługa wyboru opcj menu
|
317
Rysunek 8.2. Niestandardowe menu case R.id.icon1: Toast.makeText(this, break; case R.id.icon2: Toast.makeText(this, break; case R.id.icon3: Toast.makeText(this, break; case R.id.icon4 : Toast.makeText(this, break;
"Wybrano ikonę 1.", Toast.LENGTH_LONG).show(); "Wybrano ikonę 2.", Toast.LENGTH_LONG).show(); "Wybrano ikonę 3.", Toast.LENGTH_LONG).show(); "Wybrano ikonę 4.", Toast.LENGTH_LONG).show();
} return true; }
Efekty przedstawiono na rysunku 8.3.
8.4. Tworzenie podmenu Rachee Singh
Problem Aplikacja ma wyświetlać użytkownikom opcje w istniejącym menu.
Rozwiązanie Opcje można udostępnić użytkownikom poprzez zaimplementowanie podmenu.
318
|
Rozdz ał 8. Alerty w nterfejsach GU — menu, okna d alogowe, komun katy toast pow adom en a
Rysunek 8.3. Potwierdzenie wybrania opcji
Omówienie Podmenu to część menu wyświetlająca opcje w hierarchiczny sposób. W systemach operacyjnych na komputery stacjonarne podmenu pojawiają się kaskadowo, zwykle po prawej stronie menu nadrzędnego. W urządzeniach z Androidem nie zawsze dostępne jest miejsce na zastosowanie takiego rozwiązania, dlatego podmenu pojawia się w formie okna dialogowego, wyświetlanego nad głównym ekranem aplikacji. Pod tym względem przypomina nieco kontrolkę Spinner (zobacz recepturę 7.8). Menu można tworzyć w następujący sposób:
1. Przez rozwijanie układu w formacie XML do klasy. 2. Przez tworzenie opcji menu w Javie. W tej recepturze zobaczysz, jak zastosować drugie z tych podejść. Opcje menu i podmenu są tu tworzone w metodzie onCreateOptionsMenu(). Najpierw należy dodać podmenu do menu. Służy do tego metoda addSubMenu(). Aby zapobiec konfliktom z innymi opcjami menu, trzeba bezpośrednio podać identyfikator grupy i identyfikator opcji dla tworzonego podmenu (stałe używane jako identyfikatory opcji i grup są tu ustawiane bezpośrednio). Następnie należy ustawić ikonę podmenu, do czego służy metoda setIcon, i wskazać ikonę nagłówka podmenu (listing 8.2). Listing 8.2. Metody odbiornika obsługującego menu @Override public boolean onCreateOptionsMenu(Menu menu) { SubMenu sub1 = menu.addSubMenu(GROUP_ID, ITEM_ID , Menu.NONE, R.string.submenu_1); sub1.setHeaderIcon(R.drawable.icon); sub1.setIcon(R.drawable.icon);
8.4. Tworzen e podmenu
|
319
sub1.add(GROUP_ID , OPTION_1, 0, "Opcja 1. w podmenu"); sub1.add(GROUP_ID, OPTION_2, 1, "Opcja 2. w podmenu"); return super.onCreateOptionsMenu(menu); } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case OPTION_1: Toast.makeText(this, "Podmenu 1., opcja 1.", Toast.LENGTH_LONG).show(); break; case OPTION_2: Toast.makeText(this, "Podmenu 1., opcja 2.", Toast.LENGTH_LONG).show(); break; } return true; }
Do dodawania opcji do podmenu służy metoda add(). Metoda ta przyjmuje następujące argumenty: identyfikator grupy, identyfikator opcji, pozycję opcji w podmenu i tekst opcji. private private private private
static final int OPTION_1 = 0; static final int OPTION_2 = 1; int GROUP_ID = 4; int ITEM_ID =3;
Metoda onOptionsItemSelected() jest wywoływana po wybraniu opcji menu lub podmenu. W tej metodzie, w instrukcji switch-case, aplikacja sprawdza, którą opcję użytkownik kliknął, a następnie wyświetla odpowiedni komunikat. Na rysunku 8.4 pokazano początkowe menu, wyświetlane po kliknięciu przycisku Menu. Na rysunku 8.5 widoczne jest podmenu, które pojawia się po wybraniu odpowiedniej opcji z menu głównego.
Rysunek 8.4. Początkowe menu
320
|
Rozdz ał 8. Alerty w nterfejsach GU — menu, okna d alogowe, komun katy toast pow adom en a
Rysunek 8.5. Podmenu
8.5. Tworzenie wyskakujących okien dialogowych (okien z alertami) Rachee Singh
Problem Programista chce wyświetlać użytkownikom pytania (na przykład o niezapisane zmiany) za pomocą mechanizmu alertów.
Rozwiązanie Należy zastosować klasę AlertDialog. Umożliwia ona udostępnienie użytkownikowi odpowiednich opcji. Jeśli okno dotyczy niezapisanych zmian, można wyświetlić następujące opcje: • Zapisz, • Odrzuć zmiany, • Anuluj.
Omówienie Za pomocą klasy AlertDialog można udostępnić użytkownikowi do trzech opcji, mogących mieć zastosowanie w każdej sytuacji. Opcje te oznaczają:
8.5. Tworzen e wyskakujących ok en d alogowych (ok en z alertam )
|
321
• pozytywną reakcję, • neutralną reakcję, • negatywną reakcję.
Jeśli użytkownik wprowadził dane w kontrolce EditText, a następnie próbuje zamknąć aktywność, aplikacja powinna wyświetlić pytanie o to, czy użytkownik chce zapisać zmiany, odrzucić je czy anulować otwarte okno dialogowe. Ta ostatnia opcja powinna także prowadzić do anulowania zamykania aktywności. Oto kod z implementacją klasy AlertDialog. Znajdują się tu także odbiorniki kliknięć poszczególnych przycisków z okna dialogowego. alertDialog = new AlertDialog.Builder(this) .setTitle(R.string.unsaved) .setMessage(R.string.unsaved_changes_message) .setPositiveButton(R.string.save_changes, new AlertDialog.OnClickListener() { public void onClick(DialogInterface dialog, int which) { saveInformation(); } }) .setNeutralButton(R.string.discard_changes, new AlertDialog.OnClickListener() { public void onClick(DialogInterface dialog, int which) { finish(); } }) .setNegativeButton(android.R.string.cancel_dialog, new AlertDialog.OnClickListener() { public void onClick(DialogInterface dialog, int which) { alertDialog.cancel(); } }) .create(); alertDialog.show();
8.6. Kontrolka Timepicker Pratik Rupwal
Problem Programista chce, aby użytkownik wprowadzał czas, który będzie potem wykorzystywany przy wykonywaniu operacji przez aplikację. Pobieranie czasu w polu tekstowym jest nieeleganckie i wymaga sprawdzania poprawności.
Rozwiązanie Można wykorzystać standardową kontrolkę Timepicker do pobierania czasu od użytkownika. Dzięki temu aplikacja wygląda elegancko, a programista nie musi sprawdzać poprawności wprowadzonych danych. Kontrolka Datepicker działa podobnie, ale służy do pobierania dat.
322
|
Rozdz ał 8. Alerty w nterfejsach GU — menu, okna d alogowe, komun katy toast pow adom en a
Omówienie W kodzie z listingu 8.3 pokazano, jak wyświetlać na ekranie bieżący czas, a także jak utworzyć przycisk, który po kliknięciu udostępnia kontrolkę Timepicker, pozwalającą użytkownikowi ustawić czas. Listing 8.3. Główna aktywność public class Main extends Activity { private TextView mTimeDisplay; private Button mPickTime; private int mHour; private int mMinute; static final int TIME_DIALOG_ID = 0; /** Wywoływana, gdy aktywność tworzona jest po raz pierwszy */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); // Zapisywanie elementów widoku mTimeDisplay = (TextView) findViewById(R.id.timeDisplay); mPickTime = (Button) findViewById(R.id.pickTime); // Dodawanie odbiornika kliknięć dla przycisku mPickTime.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { showDialog(TIME_DIALOG_ID); } }); // Pobieranie bieżącego czasu final Calendar c = Calendar.getInstance(); mHour = c.get(Calendar.HOUR_OF_DAY); mMinute = c.get(Calendar.MINUTE); // Wyświetlanie bieżącej daty updateDisplay(); } // Poniższa wersja przesłoniętej metody wywoływana jest po wywołaniu metody // showDialog() w metodzie onClick() zdefiniowanej na potrzeby obsługi // kliknięcia przycisku służącego do zmiany czasu @Override protected Dialog onCreateDialog(int id) { switch (id) { case TIME_DIALOG_ID: return new TimePickerDialog(this, mTimeSetListener, mHour, mMinute, false); } return null; }
8.6. Kontrolka T mep cker
|
323
// Aktualizuje czas wyświetlany w kontrolce TextView private void updateDisplay() { mTimeDisplay.setText( new StringBuilder() .append(pad(mHour)).append(":") .append(pad(mMinute))); } // Wywołanie zwrotne, zgłaszane w momencie ustawienia czasu w oknie dialogowym przez użytkownika private TimePickerDialog.OnTimeSetListener mTimeSetListener = new TimePickerDialog.OnTimeSetListener() { public void onTimeSet(android.widget.TimePicker view, int hourOfDay, int minute) { mHour = hourOfDay; mMinute = minute; updateDisplay(); } }; private static String pad(int c) { if (c >= 10) return String.valueOf(c); else return "0" + String.valueOf(c); } }
Na rysunku 8.6 pokazano kontrolkę Timepicker wyświetlaną na ekranie po kliknięciu przycisku Zmień czas.
Rysunek 8.6. Ustawianie czasu
324 |
Rozdz ał 8. Alerty w nterfejsach GU — menu, okna d alogowe, komun katy toast pow adom en a
8.7. Tworzenie obrotowego mechanizmu wybierania (podobnego do tego z iPhone’ów) Wagied Davids
Problem Programista chce utworzyć komponent interfejsu użytkownika służący do wybierania opcji. Komponent ma przypominać obrotowy mechanizm wybierania z iPhone’ów.
Rozwiązanie Można utworzyć obrotowy mechanizm wybierania za pomocą kontrolki Android-Wheel udostępnianej przez niezależną firmę. Jest to androidowa kontrolka podobna do obrotowego mechanizmu wybierania z iPhone’ów.
Omówienie Kontrolkę Android-Wheel można pobrać ze strony http://code.google.com/p/android-wheel/. Niestety, aby ją zainstalować, nie wystarczy umieścić pliku JAR w katalogu libs. Ponieważ zasoby potrzebne do wyświetlania komponentów muszą znajdować się w katalogu res, należy wypakować plik android-wheel-xx.zip, a następnie skopiować katalogi wheel/src i wheel/res do projektu. Inna możliwość to utworzenie nowego projektu na Android na podstawie zawartości podkatalogu wheel. Android automatycznie utworzy wtedy projekt androidowej biblioteki. Bibliotekę tę należy następnie zastosować w głównym projekcie (zobacz recepturę 1.9). Potem można dodać jeden lub kilka obiektów WheelView do układu (należy przy tym podać pełną nazwę klasy). Kontrolka WheelView i powiązane klasy znajdują się w pakiecie kankan. wheel.widget. Pakiet podrzędny adapters obejmuje interfejs WheelViewAdapter i kilka implementacji. W pakiecie kontrolki znajdują się dwa interfejsy, które pozwalają zastosować standardowy wzorzec „setListener” dla komponentu WheelView. Te interfejsy to wheel.addChangingListener (OnWheelChangedListener) i wheel.addScrollingListener(OnWheelScrollListener). Kod na listingu 8.4 pochodzi z aplikacji medycznej i pozwala wybrać część ciała oraz stronę (P oznacza prawą stronę, a L — lewą). Opcje są tu zapisane na stałe w kodzie. W produkcyjnej wersji aplikacji powinny one znajdować się w pliku XML, co umożliwia umiędzynarodawianie. Wygląd aplikacji przedstawiono na rysunku 8.7.
Rysunek 8.7. Działanie obrotowego mechanizmu wybierania 8.7. Tworzen e obrotowego mechan zmu wyb eran a (podobnego do tego z Phone’ów)
|
325
Listing 8.4. Przykładowy kod, w którym zastosowano kontrolkę ScrollWheel import import import import import import import import import
kankan.wheel.widget.OnWheelChangedListener; kankan.wheel.widget.OnWheelScrollListener; kankan.wheel.widget.WheelView; kankan.wheel.widget.adapters.ArrayWheelAdapter; android.app.Activity; android.os.Bundle; android.util.Log; android.widget.EditText; android.widget.TextView;
public class WheelDemoActivity extends Activity { private final static String TAG = "WheelDemo"; String wheelMenu1[] = new String[]{ "P. ramię", "L. ramię", "P. część brzucha", "L. część brzucha", "P. udo", "L. udo"}; String wheelMenu2[] = new String[]{"Góra", "Środek", "Dół"}; String wheelMenu3[] = new String[]{"P. część", "L. część"}; // Flaga informująca, czy przesunięto mechanizm wybierania private boolean wheelScrolled = false; private private private private
TextView EditText EditText EditText
text; text1; text2; text3;
@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.empty_layout); initWheel(R.id.p1, wheelMenu1); initWheel(R.id.p2, wheelMenu2); initWheel(R.id.p3, wheelMenu3); text1 = (EditText) this.findViewById(R.id.r1); text2 = (EditText) this.findViewById(R.id.r2); text3 = (EditText) this.findViewById(R.id.r3); resultText = (TextView) this.findViewById(R.id.result); } // Odbiornik obsługujący przesunięcie mechanizmu wybierania OnWheelScrollListener scrolledListener = new OnWheelScrollListener() { @Override public void onScrollingStarted(WheelView wheel) { wheelScrolled = true; } @Override public void onScrollingFinished(WheelView wheel) { wheelScrolled = false; updateStatus(); } }; // Odbiornik zmiany w mechanizmie wybierania private final OnWheelChangedListener changedListener = new OnWheelChangedListener() { @Override
326
|
Rozdz ał 8. Alerty w nterfejsach GU — menu, okna d alogowe, komun katy toast pow adom en a
public void onChanged(WheelView wheel, int oldValue, int newValue) { Log.d(TAG, "onChanged, wheelScrolled = " + wheelScrolled); if (!wheelScrolled) { updateStatus(); } } }; /** * Aktualizowanie stanu */ private void updateStatus() { text1.setText(wheelMenu1[getWheel(R.id.p1).getCurrentItem()]); text2.setText(wheelMenu2[getWheel(R.id.p2).getCurrentItem()]); text3.setText(wheelMenu3[getWheel(R.id.p3).getCurrentItem()]); resultText.setText( wheelMenu1[getWheel(R.id.p1).getCurrentItem()] + " - " + wheelMenu2[getWheel(R.id.p2).getCurrentItem()] + " - " + wheelMenu3[getWheel(R.id.p3).getCurrentItem()]); } /** * Inicjowanie mechanizmu wybierania * * @param id * Identyfikator kontrolki */ private void initWheel(int id, String[] wheelMenu1) { WheelView wheel = (WheelView) findViewById(id); wheel.setViewAdapter(new ArrayWheelAdapter
8.8. Tworzenie okna dialogowego z zakładkami Rachee Singh
Problem Programista chce pokategoryzować informacje wyświetlane w niestandardowym oknie dialogowym.
8.8. Tworzen e okna d alogowego z zakładkam
|
327
Rozwiązanie Należy zastosować układ z podziałem na zakładki w niestandardowym oknie dialogowym.
Omówienie Klasa niestandardowego okna dialogowego dziedziczy po klasie Dialog: public class CustomDialog extends Dialog
W konstruktorze klasy trzeba przeprowadzić inicjalizację: public CustomDialog(final Context context) { super(context); setTitle("Niestandardowe okno dialogowe z zakładkami"); setContentView(R.layout.custom_dialog_layout);
Aby utworzyć dwie zakładki, należy umieścić w konstruktorze kod z listingu 8.5. Rysunki tab_image1 i tab_image2 trzeba zapisać w katalogu /res/drawable. Są to rysunki pełniące funkcję zakładek w niestandardowym oknie dialogowym. Listing 8.5. Kod z konstruktora służący do tworzenia i dodawania zakładek // Tworzenie obiektu tabHost na podstawie kodu z pliku XML TabHost tabHost = (TabHost)findViewById(R.id.TabHost01); tabHost.setup(); // Tworzenie pierwszej zakładki TabHost.TabSpec spec1 = tabHost.newTabSpec("tab1"); spec1.setIndicator("Profil", context.getResources().getDrawable(R.drawable.tab_image1)); spec1.setContent(R.id.TextView01); tabHost.addTab(spec1); // Tworzenie drugiej zakładki TabHost.TabSpec spec2 = tabHost.newTabSpec("tab2"); spec2.setIndicator("Profil", context.getResources().getDrawable(R.drawable.tab_image2)); spec2.setContent(R.id.TextView02); tabHost.addTab(spec2);
W ten sposób można utworzyć proste okno dialogowe z zakładkami. Wystarczy umieścić kilka wierszy kodu w konstruktorze. Aby umieścić w oknie dialogowym na przykład widok listy, trzeba zastosować adapter takiego widoku. Zawartość zakładek należy dostosować do wymagań stawianych aplikacji. Jak pokazano na listingu 8.6, w XML-owym kodzie okna dialogowego z zakładkami należy umieścić element
328
|
Rozdz ał 8. Alerty w nterfejsach GU — menu, okna d alogowe, komun katy toast pow adom en a
android:layout_width="fill_parent" android:layout_height="500dip">
8.8. Tworzen e okna d alogowego z zakładkam
|
329
8.9. Tworzenie okna ProgressDialog Rachee Singh
Problem Programista chce użytkownikom aplikacji wyświetlać alerty dotyczące przetwarzania wykonywanego w tle.
Rozwiązanie W trakcie przetwarzania należy wyświetlać okno ProgressDialog.
Omówienie W tej recepturze zobaczysz, jak udostępnić przycisk, którego kliknięcie prowadzi do wyświetlenia okna ProgressDialog. Dla okna ustawiany jest nagłówek Proszę czekać i zawartość Przetwarzanie w toku. Następnie można utworzyć nowy wątek i rozpocząć jego wykonywanie. W metodzie run(), uruchamianej po rozpoczęciu pracy wątku, wywoływana jest metoda sleep usypiająca wątek na cztery sekundy. Po czterech sekundach okno ProgressDialog jest zamykane, a tekst w kontrolce TextView się zmienia. complete = (TextView) this.findViewById(R.id.complete); complete.setText("Wciśnij przycisk, aby rozpocząć przetwarzanie"); processing = (Button)findViewById(R.id.processing); processing.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View arg0) { progressDialog = ProgressDialog.show(ProgressDialogExp.this, "Proszę czekać", "Przetwarzanie w toku", true,false); Thread thread = new Thread(ProgressDialogExp.this); thread.start(); } });
Do aktualizowania interfejsu użytkownika po zakończeniu pracy wątku używany jest obiekt Handler. Gdy wątek kończy działanie, do tego obiektu przesyłany jest pusty komunikat. Wtedy obiekt Handler zamyka okno ProgressDialog i aktualizuje tekst w kontrolce TextView. public void run() { try { Thread.sleep(4000); } catch (InterruptedException e) { e.printStackTrace(); } handler.sendEmptyMessage(0); } private Handler handler = new Handler() { @Override public void handleMessage(Message msg) { progressDialog.dismiss(); complete.setText("Zakończono przetwarzanie"); } };
330
|
Rozdz ał 8. Alerty w nterfejsach GU — menu, okna d alogowe, komun katy toast pow adom en a
8.10. Tworzenie niestandardowego okna dialogowego z przyciskami, rysunkami i tekstem Rachee Singh
Problem W aplikacji potrzebne jest okno dialogowe, które ma wyświetlać określone informacje i zastępować kompletną aktywność. W tym niestandardowym oknie dialogowym mają znajdować się tekst, rysunki i przycisk.
Rozwiązanie Należy utworzyć niestandardowe okno dialogowe z zakładkami. Ponieważ w oknie dialogowym można umieścić dowolne elementy zastępujące całą aktywność, zastosowane tu podejście pozwala utworzyć prostszą aplikację.
Omówienie CustomDialog to klasa bezpośrednio pochodna od Dialog: public class CustomDialog extends Dialog
W poniższych wierszach kodu z metody onCreate() klasy CustomDialog dodawany jest nagłówek i pobierane są uchwyty przycisków z okna dialogowego: setTitle("Nagłówek okna dialogowego"); setContentView(R.layout.custom_dialog_layout); // Uchwyty przycisków widocznych w oknie dialogowym Button button1 = (Button) findViewById(R.id.button1); Button button2 = (Button) findViewById(R.id.button2);
W następnych wierszach dla obu dodawanych przycisków definiowane są odbiorniki OnClick Listener. Przycisk button1 po kliknięciu zamyka okno dialogowe, a przycisk button2 uruchamia nową aktywność. button1.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { dismiss(); // Zamykanie okna dialogowego } }); button2.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // Wysyłanie intencji po kliknięciu przycisku Intent showQuickInfo = new Intent("com.android.oreilly.QuickInfo"); showQuickInfo.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(showQuickInfo); } });
8.10. Tworzen e n estandardowego okna d alogowego z przyc skam , rysunkam tekstem
|
331
Poniżej pokazano układ XML okna dialogowego, zapisany w pliku custom_dialog_layout w katalogu /res/layout. Wszystkie elementy znajdują się w układzie LinearLayout. Zawarty w nim układ RelativeLayout służy do określenia pozycji dwóch przycisków. Pod tym układem Relative Layout znajduje się drugi układ tego typu, obejmujący widok przewijany. Użyte w kodzie określenia android_button i thumbsup to nazwy rysunków z katalogu /res/drawable.
332
|
Rozdz ał 8. Alerty w nterfejsach GU — menu, okna d alogowe, komun katy toast pow adom en a
8.11. Klasa AboutBox do wielokrotnego użytku Daniel Fowler
Problem W aplikacjach często stosuje się okno O programie. Warto uniknąć konieczności tworzenia takiego okna od nowa w każdym rozwijanym programie.
Rozwiązanie Należy napisać klasę AboutBox, którą można umieścić w każdej nowej aplikacji.
Omówienie W każdym systemie operacyjnym w programach może znajdować się opcja O programie. Poświęcony jest jej wpis w Wikipedii, http://en.wikipedia.org/wiki/About_box. Okno O programie jest przydatne w kontekście udzielania pomocy technicznej: „Dzień dobry, w aplikacji występuje problem”. „Czy może pan wcisnąć przycisk O programie i podać numer wersji?” Ponieważ okno O programie jest potrzebne w różnych aplikacjach, warto utworzyć gotową klasę AboutBox, którą można łatwo dodać do dowolnego rozwijanego programu. Opcja O programie powinna przynajmniej wyświetlać okno dialogowe z nagłówkiem (np. Informacje o programie), nazwę wersji określoną w manifeście, opisowy tekst (wczytany z łańcucha znaków z zasobów) i przycisk OK. Nazwę wersji można wczytać z klasy PackageInfo. Obiekt tego typu można pobrać z obiektu PackageManager, który z kolei jest dostępny w obiekcie Context aplikacji. Oto metoda służąca do wczytywania łańcucha znaków z nazwą wersji aplikacji: static String VersionName(Context context) { try { return context.getPackageManager().getPackageInfo( context.getPackageName(),0).versionName; } catch (NameNotFoundException e) { return "Brak informacji"; } }
Klasa PackageInfo może zgłosić wyjątek NameNotFoundException (jeśli podano nazwę z innego pakietu). Wyjątek ten występuje bardzo rzadko. Tu jest obsługiwany przez zwrócenie łańcucha znaków z informacją o błędzie. Aby zwrócić kod wersji (wewnętrzny numer wersji aplikacji), należy w instrukcji zastąpić człon versionName członem versionCode i zwrócić liczbę całkowitą. Opcję O programie można szybko udostępnić za pomocą obiektu AlertDialog.Builder oraz metod setTitle(), setMessage() i show(). Można ją jednak usprawnić, stosując androidową klasę Linkify oraz niestandardowy układ. W ten sposób można umożliwić kliknięcie adresów stron
8.11. Klasa AboutBox do w elokrotnego użytku
|
333
internetowych (na przykład stron z informacjami z systemu pomocy) i adresów e-mail (przydatnych do kontaktu z pomocą techniczną) występujących w tekście wyświetlanym w oknie O programie. Układ z listingu 8.7 należy zapisać w pliku aboutbox.xml w katalogu res/layout. Listing 8.7. Plik aboutbox.xml
Jeśli tekst w oknie O programie jest długi, a ekran mały (QVGA), trzeba zastosować kontrolkę ScrollView. Inną zaletą stosowania niestandardowego układu dla okna O programie jest możliwość zmodyfikowania wyglądu tekstu. W tej recepturze tekst jest biały i ma niewielki margines wewnętrzny. W opisywanej tu klasie AboutBox zastosowano obiekt SpannableString, przechowujący tekst, który można przekazać do obiektu Linkify poprzez kontrolkę TextView z niestandardowego układu. Aplikacja rozwija układ do klasy, zapisuje tekst wyświetlany później w oknie O programie, a następnie za pomocą klasy AlertBuilder.Builder tworzy to okno dialogowe. Na listingu 8.8 przedstawiono kompletny kod klasy. Listing 8.8. Klasa AboutBox public class AboutBox { static String VersionName(Context context) { try { return context.getPackageManager().getPackageInfo( context.getPackageName(),0).versionName; } catch (NameNotFoundException e) { return "Brak informacji"; } } public static void Show(Activity callingActivity) { // Klasa SpannableString umożliwia wyróżnianie odnośników SpannableString aboutText = new SpannableString("Wersja " + VersionName(callingActivity)+ "\n\n" + callingActivity.getString(R.string.about)); // Generowanie widoków przekazywanych do obiektu AlertDialog.Builder // i używanych do ustawiania tekstu View about; TextView tvAbout; try { // Rozwijanie niestandardowego układu do klasy LayoutInflater inflater = callingActivity.getLayoutInflater();
334 |
Rozdz ał 8. Alerty w nterfejsach GU — menu, okna d alogowe, komun katy toast pow adom en a
about = inflater.inflate(R.layout.aboutbox, (ViewGroup) callingActivity.findViewById(R.id.aboutView)); tvAbout = (TextView) about.findViewById(R.id.aboutText); } catch(InflateException e) { // Mechanizm rozwijania układów do klas może zgłosić wyjątek. Jest to mało prawdopodobne, // jeśli jednak się zdarzy, domyślnie stosowana jest kontrolka TextView about = tvAbout = new TextView(callingActivity); } // Określanie tekstu okna O programie tvAbout.setText(aboutText); // Przekształcenie odpowiednich fragmentów tekstu na odnośniki Linkify.addLinks(tvAbout, Linkify.ALL); // Tworzenie i wyświetlanie okna dialogowego new AlertDialog.Builder(callingActivity) .setTitle("O programie " + callingActivity.getString(R.string.app_name)) .setCancelable(true) .setIcon(R.drawable.icon) .setPositiveButton("OK", null) .setView(about) .show(); // Dane zwracane przez metody obiektu Builder umożliwiają łańcuchowe wywoływanie tych metod } }
Warto zauważyć, że za pomocą instrukcji setIcon(R.drawable.icon) można ustawić ikonę wyświetlaną w polu O programie. Łańcuchy znaków określające nazwę aplikacji i tekst okna O programie należy umieścić w standardowym pliku res/values/strings.xml.
Wyświetlenie okna O programie wymaga tylko jednego wiersza kodu. Tu znajduje się on w metodzie obsługującej kliknięcie przycisku. public class Main extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); findViewById(R.id.button1).setOnClickListener(new OnClickListener(){ public void onClick(View arg0) { AboutBox.Show(Main.this); } }); } }
Efekt pokazano na rysunku 8.8. Aby ponownie wykorzystać okno O programie, wystarczy plik aboutbox.xml umieścić w katalogu res/layout projektu, dodać nową klasę AboutBox i zastąpić kod tej klasy kodem przedstawionym w recepturze. Następnie trzeba tylko wywołać metodę AboutBox.Show() w metodzie obsługi kliknięcia przycisku lub menu. Wyróżnione w tekście adresy stron internetowych i adresy e-mail można kliknąć, co prowadzi do wyświetlenia przeglądarki lub klienta poczty elektronicznej. Bywa to bardzo pomocne.
8.11. Klasa AboutBox do w elokrotnego użytku
|
335
Rysunek 8.8. Gotowe okno O programie
Zobacz także http://developer.android.com/reference/android/text/util/Linkify.html, http://developer.android.com/ guide/topics/ui/dialogs.html.
8.12. Modyfikowanie wyglądu komunikatów toast Rachee Singh
Problem Programista chce zmienić wygląd komunikatów toast.
Rozwiązanie Należy zdefiniować w XML-u układ komunikatu toast, a następnie rozwinąć widok do klasy w kodzie w Javie.
Omówienie Najpierw pokazano, jak w pliku XML (toast_layout.xml) zdefiniować układ niestandardowego komunikatu toast. Układ ten obejmuje kontrolki ImageView i TextView, a kod układu przedstawiono na listingu 8.9. Listing 8.9. XML-owy kod układu komunikatu toast
336
|
Rozdz ał 8. Alerty w nterfejsach GU — menu, okna d alogowe, komun katy toast pow adom en a
android:layout_width="fill_parent" android:layout_height="fill_parent" android:padding="10dp" android:background="#f0ffef" >
Następnie w kodzie Javy za pomocą obiektu LayoutInflater należy rozwinąć ten widok do klasy. W kodzie ustawiana jest lokalizacja komunikatu toast i czas jego wyświetlania. Do określania pozycji komunikatu służy metoda setGravity. Komunikat pojawia się w odpowiedzi na kliknięcie przycisku customToast (zobacz listing 8.10). Listing 8.10. Rozwijanie widoku do klasy customToast = (Button)findViewById(R.id.customToast); LayoutInflater inflater = getLayoutInflater(); View layout = inflater.inflate(R.layout.toast_layout, (ViewGroup) findViewById(R.id.toast_layout_root)); ImageView image = (ImageView) layout.findViewById(R.id.image); image.setImageResource(R.drawable.icon); TextView text = (TextView) layout.findViewById(R.id.text); text.setText("Witaj! To niestandardowy komunikat toast!"); final Toast toast = new Toast(getApplicationContext()); toast.setGravity(Gravity.CENTER_VERTICAL, 0, 0); toast.setDuration(Toast.LENGTH_LONG); toast.setView(layout); customToast.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View arg0) { toast.show(); } });
8.13. Tworzenie powiadomienia wyświetlanego na pasku stanu Ian Darwin
Problem Programista chce wyświetlać na pasku stanu ikonę powiadomienia, aby zwrócić uwagę użytkownika na wystąpienie zdarzenia lub przypomnieć o usłudze działającej w tle. 8.13. Tworzen e pow adom en a wyśw etlanego na pasku stanu
|
337
Rozwiązanie Należy utworzyć obiekt Notification i udostępnić go razem z obiektem PendingIntent, będącym nakładką na rzeczywisty obiekt Intent, który określa, co program ma robić po kliknięciu powiadomienia przez użytkownika. Przy przesyłaniu obiektu PendingIntent przekazywany jest też nagłówek oraz tekst wyświetlany w obszarze powiadomień. Należy też włączyć opcję AUTO_CANCEL, chyba że programista chce, aby powiadomienie trzeba było ręcznie usuwać z paska stanu. Trzeba też pobrać obiekt NotificationManager i zażądać od niego, aby wyświetlił powiadomienie (służy do tego metoda notify). Powiadomienie powiązane jest z identyfikatorem, co pozwala później je wskazać (na przykład w celu usunięcia).
Omówienie Powiadomień zwykle używa się w działających klasach typu Service do powiadamiania (stąd nazwa) użytkowników o określonych faktach. Możliwe, że wystąpiło pewne zdarzenie (odebranie komunikatu, utrata łączności z serwerem itd.) lub działająca przez długi czas usługa informuje o tym, że wciąż pracuje. Powiadomienia często służą do uruchamiania aktywności — jest to jedyna zalecana technika rozpoczynania aktywności przez usługi działające w tle (usługi nigdy nie powinny bezpośrednio uruchamiać aktywności!). Należy utworzyć obiekt Notification. Służący do tego konstruktor przyjmuje identyfikator ikony, tekst wyświetlany przez krótki czas na pasku stanu i czas wystąpienia zdarzenia (znacznik czasu podawany w milisekundach). Przed wyświetleniem powiadomienia trzeba powiązać je z obiektem PendingIntent, który określa, co należy zrobić po kliknięciu powiadomienia przez użytkownika. Potem można zażądać od obiektu NotificationManager, aby wyświetlił powiadomienie. Na listingu 8.11 pokazano kod służący do wyświetlania powiadomień. W poniższym kodzie pokazano, jak wykonać właściwe operacje w nieodpowiednim miejscu. Powiadomienia zwykle wyświetla się w usługach. W tej recepturze skoncentrowano się na interfejsie API powiadomień.
Listing 8.11. Kod do wyświetlania powiadomień public class Main extends Activity { private static final int NOTIFICATION_ID = 1; /** Wywoływana, gdy aktywność tworzona jest po raz pierwszy */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); int icon = R.drawable.icon;
// Ikona powinna się wyróżniać
// Tworzenie samego powiadomienia String noticeMeText = getString(R.string.noticeMe); Notification n = new Notification( icon, noticeMeText, System.currentTimeMillis()); // Tworzenie intencji, która określa, co należy zrobić po kliknięciu powiadomienia Context applicationContext = getApplicationContext();
338 |
Rozdz ał 8. Alerty w nterfejsach GU — menu, okna d alogowe, komun katy toast pow adom en a
Intent notifyIntent = new Intent(this, NotificationTarget.class); PendingIntent wrappedIntent = PendingIntent.getActivity(this, 0, notifyIntent, Intent.FLAG_ACTIVITY_NEW_TASK); // Określanie cech powiadomienia String title = getString(R.string.title); String message = getString(R.string.message); n.setLatestEventInfo(applicationContext, title, message, wrappedIntent); n.flags |= Notification.FLAG_AUTO_CANCEL; // Wywoływanie usługi wyświetlającej powiadomienia String notifService = Context.NOTIFICATION_SERVICE; NotificationManager mgr = (NotificationManager) getSystemService(notifService); mgr.notify(NOTIFICATION_ID, n); } }
Poniżej przedstawiono zawartość pliku strings.xml.
Łańcuch znaków noticeMe pojawia się na krótką chwilę (na kilka sekund) na pasku stanu. Tekst powiadomienia oraz ikony znajdują się w lewym górnym rogu w wersji Gingerbread (2.x) i w dolnym prawym rogu w wersji Honeycomb (3.x), co pokazano na rysunku 8.9. Następnie pojawia się główny widok, przedstawiony na rysunku 8.10. Pasek stanu po przeciągnięciu go przez użytkownika jest rozwijany i wyświetla szczegółowe informacje, w tym ikony, nagłówek i komunikat (rysunek 8.11). Można w tym miejscu wykorzystać niestandardowy widok. Więcej informacji znajdziesz w oficjalnej dokumentacji Androida (wspomnianej też w punkcie „Zobacz także” na stronie 342.). Jeśli ustawione jest automatyczne usuwanie powiadomienia, znika ono z paska stanu. Gdy użytkownik kliknie powiadomienie, wywoływana jest intencja PendingIntent. Tu prowadzi to do wyświetlenia prostego powiadomienia z podziękowaniami (rysunek 8.12). Jednak jeśli użytkownik kliknie przycisk Clear, intencja nie jest uruchamiana (jest tak też przy włączonym automatycznym usuwaniu powiadomień, co utrudnia użytkownikom pracę).
Dźwięki i inne irytujące efekty Jeśli użytkownik ma zwrócić uwagę na zdarzenie tylko raz, można określić dźwięk odtwarzany przy pierwszym wyświetleniu powiadomienia (można też generować dźwięk wielokrotnie, aby naprawdę zdenerwować użytkownika). W urządzeniach, które to umożliwiają, można ponadto włączyć wibracje. Domyślny dźwięk powiadomienia można odtworzyć w następujący sposób: notification.flags |= Notification.DEFAULT_SOUND;
8.13. Tworzen e pow adom en a wyśw etlanego na pasku stanu
|
339
Rysunek 8.9. Przykładowe powiadomienie (w Androidzie Gingerbread i Honeycomb)
Inna możliwość to podanie identyfikatora URI prowadzącego do pliku dźwiękowego. Plik ten może znajdować się na karcie SD lub w aplikacji. notification.sound = Uri.parse("file:///sdcard/mydata/zdenerwuj_uzytkownika.mp3");
Warto wiedzieć, że po ustawieniu opcji DEFAULT_SOUND i podaniu identyfikatora URI dźwięku zastosowany zostanie tylko domyślny sygnał. Aby naprawdę zdenerwować użytkownika, można wielokrotnie odtwarzać dźwięk. W tym celu wystarczy do pola flags dodać opcję FLAG_INSISTENT. notification.flags |= Notification.FLAG_INSISTENT;
Wzbudzanie wibracji w czasie wyświetlania powiadomień jest bardzo proste: notification.flags |= Notification.DEFAULT_VIBRATE;
Włączanie diod LED Jako ostatni dodatek można włączać diody LED w różnych kolorach i sekwencjach, o ile urządzenie udostępnia takie diody. W większości telefonów znajdują się one blisko dolnej części
340 |
Rozdz ał 8. Alerty w nterfejsach GU — menu, okna d alogowe, komun katy toast pow adom en a
Rysunek 8.10. Następny ekran przykładowego programu z powiadomieniami
Rysunek 8.11. Rozwinięte powiadomienie
8.13. Tworzen e pow adom en a wyśw etlanego na pasku stanu
|
341
Rysunek 8.12. Reakcja na kliknięcie powiadomienia
fizycznego ekranu lub w obszarze z klawiszami. Wywołać trzeba przynajmniej następujące instrukcje: notification.ledARGB = color; notification.flags |= Notification.FLAGS_SHOW_LIGHTS;
Kolor podawany jest jako liczba całkowita z czterema bajtami obejmującymi kanały alfa (przezroczystość), czerwony, zielony i niebieski. Oprócz części oznaczającej przezroczystość składnia jest podobna jak przy standardowym określaniu kolorów na stronach internetowych. 0xff0000ff to jasny odcień niebieskiego (bez przezroczystości oraz bez składowych czerwonej i zielonej). Można też określić wzorzec zapalania diod. Służą do tego instrukcje notification.ledOnMC i notification.ledOffMS. Przy ich użyciu można określić w milisekundach czas, przez jaki diody LED mają być zapalone i przez jaki zgaszone. Określenie tych wartości bez ustawionej opcji FLAGS_SHOW_LIGHTS nie przyniesie żadnego efektu.
Zobacz także Oficjalny samouczek znajdziesz na stronie http://developer.android.com/guide/topics/ui/notifiers/ notifications.html.
342 |
Rozdz ał 8. Alerty w nterfejsach GU — menu, okna d alogowe, komun katy toast pow adom en a
ROZDZIAŁ 9.
GUI — kontrolka ListView
9.1. Wprowadzenie — kontrolka ListView Ian Darwin
Omówienie Kontrolka ListView jest jednym z najważniejszych komponentów, dlatego poświęcenie jej odrębnego rozdziału nie powinno nikogo dziwić. Występuje w około 80% wszystkich aplikacji na Android. Ponadto daje bardzo duże możliwości. Przy jej użyciu można wiele osiągnąć, jednak nie zawsze łatwo jest ustalić, jak uzyskać pożądany efekt. W tym rozdziale omówiono zarówno proste, jak i zaawansowane zagadnienia dotyczące obszaru korzystania z kontrolki ListView. Oficjalną dokumentację znajdziesz na stronie http://developer.android.com/reference/android/widget/ ListView.html. Inne dobre omówienie kontrolki ListView zawiera prezentacja z konferencji Google I/O 2010. Można ją znaleźć w serwisie YouTube — http://www.youtube.com/watch?v=wDBM6wVEO70. Prezentację przeprowadzili Romain Guy i Adam Powell, pracownicy Google’a, którzy pracują nad kodem kontrolki ListView.
9.2. Używanie kontrolki ListView do tworzenia aplikacji opartych na listach Jim Blackler
Problem Wiele aplikacji mobilnych działa w podobny sposób — umożliwia użytkownikom przeglądanie wieloelementowych list i manipulowanie nimi. W jaki sposób programiści mogą wykorzystać standardowe klasy interfejsu użytkownika Androida do szybkiego zbudowania aplikacji, która działa w sposób oczekiwany przez użytkownika, czyli udostępnia dane na liście? 343
Rozwiązanie Należy zastosować kontrolkę ListView. Kontrolka ta daje bardzo duże możliwości, jest dobrze dostosowana do wielkości ekranu i ograniczeń związanych z aplikacjami mobilnymi, a służy do wyświetlania listy wierszy. W tej recepturze pokazano, jak skonfigurować kontrolkę ListView z wierszami obejmującymi dowolne standardowe kontrolki interfejsu użytkownika.
Omówienie Wiele aplikacji na Android opartych jest na kontrolce ListView. Kontrolka ta rozwiązuje problem wyświetlania dużych ilości informacji w taki sposób, aby użytkownik mógł je szybko przejrzeć. Omawiana kontrolka wyświetla dane na liście wierszy, którą użytkownik może przewijać. Gdy dochodzi on do wyników z końcowej części listy, można wygenerować lub dodać więcej informacji. Pozwala to na stronicowanie wyników w naturalny i intuicyjny sposób. Dzięki umieszczeniu obsługi przeglądania i edycji w odrębnych aktywnościach androidowa kontrolka ListView pomaga uporządkować kod. Użytkownik musi kliknąć dowolny fragment wiersza, co jest dobrym rozwiązaniem przy korzystaniu z małych ekranów obsługiwanych palcami. Po kliknięciu wiersza można uruchomić nową aktywność, która pozwala manipulować danymi z wybranego wiersza. Inną zaletą kontrolki ListView jest to, że umożliwia proste stronicowanie. Stronicowanie jest potrzebne, jeśli nie można jednocześnie wyświetlić wszystkich informacji żądanych przez użytkownika. Użytkownik może na przykład przeglądać zawartość skrzynki odbiorczej, w której znajduje się 2000 e-maili. Jednoczesne pobranie wszystkich 2000 wiadomości z serwera poczty elektronicznej jest niewykonalne. Nie jest to także konieczne, ponieważ użytkownik prawdopodobnie będzie chciał przejrzeć tylko około 10 pierwszych wpisów. W większości aplikacji sieciowych problem ten rozwiązywany jest przez podział wyników na strony. Kontrolki w dolnej części ekranu umożliwiają użytkownikom poruszanie się między tymi stronami. Jeśli programista korzysta z kontrolki ListView, aplikacja może pobierać pierwszą porcję wyników i wyświetlać je użytkownikowi. Gdy użytkownik dochodzi do końca listy, pojawia się ostatni wiersz z paskiem postępu. Wtedy aplikacja może pobrać w tle następną porcję wyników. Gdy dane są gotowe do wyświetlenia, ostatni wiersz (z paskiem postępu) zastępowany jest wierszami z nowymi danymi. Nie występują więc zakłócenia w wyświetlaniu listy, a nowe dane pobierane są wyłącznie na żądanie. Aby zastosować kontrolkę ListView w aplikacji na Android, trzeba utworzyć układ aktywności, która ma udostępniać taką kontrolkę. W układzie należy umieścić kontrolkę ListView skonfigurowaną w taki sposób, aby zajmowała większą część ekranu. W układzie mogą się też znajdować inne elementy, np. paski postępu lub dodatkowe nakładane kontrolki. Choć wielu ekspertów od Androida zachęca do stosowania aktywności typu ListActivity, ja do nich nie należę. Aktywności tego rodzaju udostępniają wprawdzie dodatkowe możliwości w porównaniu z aktywnościami typu Activity, zmniejszają jednak swobodę w tworzeniu hierarchii aktywności w aplikacji. Zdarza się na przykład bardzo często, że wszystkie aktywności dziedziczą po jednej wspólnej klasie aktywności, np. ApplicationActivity, która udostępnia standardowe mechanizmy (takie jak opcje menu O programie i Pomoc). Jeśli niektóre aktywności dziedziczą po klasie ListActivity, a inne bezpośrednio po klasie Activity, takiego podejścia nie można zastosować. 344 |
Rozdz ał 9. GU — kontrolka L stV ew
W aplikacji do kontrolowania danych dodawanych do kontrolki ListView służy obiekt List Adapter, dodawany za pomocą metody setListAdapter(). Obiekt ListAdapter powinien udostępniać 13 funkcji. Jednak zastosowanie klasy BaseAdapter pozwala zmniejszyć do czterech liczbę funkcji, które trzeba udostępnić. Jest to minimalny zestaw potrzebnych funkcji. Adapter określa liczbę wierszy na liście i powinien na podstawie numeru wiersza danego elementu udostępniać odpowiadający mu obiekt View. Adapter powinien zwracać zarówno obiekt, jak i identyfikator obiektu reprezentujący numer danego wiersza. Pomaga to udostępniać zaawansowany mechanizm obsługi list, np. pobieranie wierszy (zagadnienie to nie jest opisane w recepturze). Zachęcam, aby na początku zastosować najbardziej uniwersalny typ obiektu ListAdapter — BaseAdapter (android.widget.BaseAdapter). Dzięki temu w wierszu można zastosować dowolny
układ (dla różnych typów wierszy można stosować różne układy). W układzie dla wierszy można umieścić dowolne elementy View, które standardowo występują w układach.
Wiersze są generowane przez adapter na żądanie, wtedy gdy powinny pojawić się na ekranie. Adapter albo rozwija widok odpowiedniego typu do klasy, albo „odzyskuje” istniejący widok i wyświetla w nim odpowiednie dane. „Odzyskiwanie” to technika pozwalająca poprawić wydajność w systemie operacyjnym Android. Gdy na ekranie pojawiają się nowe wiersze, system operacyjny przekazuje do metody $ adaptera obiekt View z usuniętym z ekranu wierszem. To ta metoda określa, czy można ponownie wykorzystać dany obiekt do utworzenia nowego wiersza. Jest to możliwe, jeśli przekazany obiekt View ma układ pasujący do nowego wiersza. Jeden ze sposobów na sprawdzenie, czy tak jest, polega na zapisaniu za pomocą metody setTag() identyfikatora układu w polu Tag każdego obiektu View rozwijanego do klasy. W czasie sprawdzania, czy można ponownie wykorzystać dany obiekt View, należy wywołać metodę getTag() i ustalić, czy obiekt ten ma właściwy typ. Jeśli aplikacja potrafi odzyskać widok, przewijanie jest bardziej płynne, ponieważ nie trzeba poświęcać czasu procesora na rozwijanie widoku do klasy. Inny sposób na zwiększenie płynności przewijania polega na wykonywaniu jak najmniejszej ilości operacji w wątku interfejsu użytkownika. Metoda $ domyślnie wywoływana jest właśnie w tym wątku. Jeśli trzeba wykonać czasochłonne operacje, można to zrobić w nowym wątku tła ($example). Gdy w celu zaktualizowania kontrolek ponownie potrzebny jest wątek interfejsu użytkownika, można wykonać w nim operacje za pomocą metody $. Należy przy tym zachować ostrożność i sprawdzać, czy modyfikowany obiekt View nie został „odzyskany” i wykorzystany dla innego wiersza. Może się tak zdarzyć wtedy, gdy wiersz zostanie usunięty z ekranu w czasie wykonywania operacji. Jest to możliwe, jeżeli tą operacją jest długie pobieranie danych.
Konfigurowanie prostej kontrolki ListView W środowisku Eclipse za pomocą kreatora tworzenia nowych projektów na Android zbuduj nowy projekt tego rodzaju. Aktywność startową nazwij MainActivity. W układzie w pliku main.xml zastąp istniejący element TextView następującym kodem:
W końcowej części metody MainActivity.onCreate() wstaw kod z listingu 9.1. Kod ten obejmuje deklarację klasy anonimowej pochodnej od BaseAdapter oraz zastosowanie egzemplarza tej 9.2. Używan e kontrolk L stV ew do tworzen a apl kacj opartych na l stach
| 345
klasy do kontrolki ListView. W kodzie przedstawiono metody, które trzeba udostępnić, aby kontrolkę ListView móc zapełnić danymi. Listing 9.1. Implementacja adaptera ListView listView = (ListView) findViewById(R.id.ListView01); listView.setAdapter(new BaseAdapter(){ public int getCount() { return 0; } public Object getItem(int position) { return null; } public long getItemId(int position) { return 0; } public View getView(int position, View convertView, ViewGroup parent) { return null; }});
Dzięki ustawieniu składowych klasy anonimowej można modyfikować dane wyświetlane w kontrolce. Jednak przed pokazaniem danych trzeba określić układ informacji w wierszu. W katalogu res/layout umieść plik list_row.xml o następującej zawartości:
Do aktywności MainActivity dodaj przedstawione poniżej statyczne pole z tablicą obejmującą trzy łańcuchy znaków: static String[] words = {"jeden", "dwa", "trzy"};
Teraz w pokazany na listingu 9.2 sposób zmodyfikuj istniejący anonimowy adapter BaseAdapter. Pozwala to wyświetlić zawartość tablicy words w kontrolce ListView. Listing 9.2. Implementacja adaptera listView.setAdapter(new BaseAdapter(){ public int getCount() { return words.length; } public Object getItem(int position) { return words[position]; } public long getItemId(int position) { return position; } public View getView(int position, View convertView, ViewGroup parent) { LayoutInflater inflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE); View view = inflater.inflate(R.layout.list_row, null);
346 |
Rozdz ał 9. GU — kontrolka L stV ew
TextView textView = (TextView) view.findViewById(R.id.TextView01); textView.setText(words[position]); return view; }});
Metoda getCount() zwraca liczbę elementów na liście. Metody getItem() i getItemId() udostępniają w kontrolce ListView niepowtarzalne obiekty i identyfikatory pozwalające identyfikować dane w wierszach. Metoda getView() tworzy i modyfikuje androidowy obiekt View w taki sposób, aby reprezentował on wiersz. Jest to najbardziej złożony etap, dlatego warto krok po kroku opisać wykonywane w nim operacje. LayoutInflater inflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE);
Ta instrukcja pobiera systemowy obiekt LayoutInflater. Reprezentuje on usługę tworzącą widoki. View view = inflater.inflate(R.layout.list_row, null)
Tu nowy, utworzony wcześniej układ jest rozwijany do klasy. TextView textView = (TextView) view.findViewById(R.id.TextView01)
W tym wierszu znajdowana jest kontrolka TextView. textView.setText(words[position])
Tu kontrolka TextView jest modyfikowana za pomocą odpowiedniego elementu z tablicy words. return view;
Ta instrukcja pozwala użytkownikowi zobaczyć elementy z tablicy words w kontrolce ListView. W innych recepturach korzystanie z tej kontrolki opisano bardziej szczegółowo.
9.3. Tworzenie widoków „brak danych” dla kontrolek ListView Rachee Singh
Problem Jeśli kontrolka ListView nie zawiera żadnych elementów do wyświetlenia, ekran urządzenia z Androidem jest pusty. Warto wyświetlić wtedy na ekranie odpowiedni komunikat, informujący o braku danych w kontrolce.
Rozwiązanie Należy zastosować widok „brak danych” z układu w formacie XML.
Omówienie W aplikacjach na Android często korzysta się z kontrolek ListView. Przed wczytaniem danych kontrolka ListView jest pusta, co zwykle wiąże się z wyświetlaniem czarnego ekranu. Aby zwiększyć komfort korzystania z aplikacji, warto wyświetlać odpowiedni komunikat (lub nawet rysunek) z informacją, że lista jest pusta. Tu wykorzystano do tego widok „brak danych”. 9.3. Tworzen e w doków „brak danych” dla kontrolek L stV ew
|
347
Uzyskanie pożądanego efektu jest proste. Wystarczy dodać kilka wierszy kodu w XML-owym układzie aktywności obejmującej kontrolkę ListView.
Ważnym wierszem jest tu android:id="@id/android:empty". Wiersz ten sprawia, że gdy lista jest pusta, na ekranie pojawia się kontrolka TextView o podanym identyfikatorze. Kontrolka ta obejmuje łańcuch znaków Lista jest pusta (rysunek 9.1).
Rysunek 9.1. Pusta lista
348 |
Rozdz ał 9. GU — kontrolka L stV ew
9.4. Tworzenie zaawansowanych kontrolek ListView z rysunkami i tekstem Marco Dinacci
Problem Programista chce utworzyć kontrolkę ListView, która wyświetla rysunek obok łańcucha znaków.
Rozwiązanie Należy utworzyć aktywność pochodną od klasy ListActivity, przygotować XML-owe pliki zasobów i utworzyć niestandardowy adapter widoku przeznaczony do wczytywania zasobów do widoku.
Omówienie Według dokumentacji Androida kontrolka ListView jest łatwa w użyciu. Rzeczywiście tak jest, jeśli programista chce wyświetlać tylko prostą listę łańcuchów znaków. Jednak przy próbie dostosowania listy do potrzeb sytuacja się komplikuje. W tej recepturze wyjaśniono, jak napisać kontrolkę ListView wyświetlającą statyczną listę rysunków i łańcuchów znaków. Uzyskany rezultat przypomina listę ustawień z telefonu. Na rysunku 9.2 pokazano ostateczny efekt.
Rysunek 9.2. Kontrolka ListView z ikonami
9.4. Tworzen e zaawansowanych kontrolek L stV ew z rysunkam tekstem
| 349
Najpierw przyjrzyj się kodowi aktywności (listing 9.3). Dziedziczy ona po klasie ListActivity, a nie Activity, co pozwala łatwo podłączyć niestandardowy adapter. Listing 9.3. Implementacja klasy pochodnej od ListActivity public class AdvancedListViewActivity extends ListActivity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); Context ctx = getApplicationContext(); Resources res = ctx.getResources(); String[] options = res.getStringArray(R.array.country_names); TypedArray icons = res.obtainTypedArray(R.array.country_icons); setListAdapter(new ImageAndTextAdapter(ctx, R.layout.main_list_item, options, icons)); } }
W metodzie onCreate tworzona jest też tablica łańcuchów znaków (zawierająca nazwy państw) i tablica TypedArray, w której znajdują się obiekty typu Drawable z flagami. Tablice są tworzone na podstawie pliku XML. Oto zawartość pliku countries.xml:
Teraz można utworzyć adapter. W oficjalnej dokumentacji (http://developer.android.com/reference/ android/widget/Adapter.html) klasę Adapter opisano tak: Obiekt Adapter działa jak pomost między widokiem AdapterView a danymi widoku. Adapter zapewnia dostęp do danych, a także odpowiada za generowanie obiektu View dla każdego elementu ze zbioru danych. Istnieje kilka klas pochodnych od klasy Adapter. Na listingu 9.4 wykorzystano klasę ArrayAdapter, która jest konkretną implementacją klasy BaseAdapter i działa dla tablic dowolnych obiektów. Listing 9.4. Klasa ImageAndTextAdapter public class ImageAndTextAdapter extends ArrayAdapter
350
|
Rozdz ał 9. GU — kontrolka L stV ew
private String[] mStrings; private TypedArray mIcons; private int mViewResourceId; public ImageAndTextAdapter(Context ctx, int viewResourceId, String[] strings, TypedArray icons) { super(ctx, viewResourceId, strings); mInflater = (LayoutInflater)ctx.getSystemService( Context.LAYOUT_INFLATER_SERVICE); mStrings = strings; mIcons = icons; mViewResourceId = viewResourceId; } @Override public int getCount() { return mStrings.length; } @Override public String getItem(int position) { return mStrings[position]; } @Override public long getItemId(int position) { return 0; } @Override public View getView(int position, View convertView, ViewGroup parent) { convertView = mInflater.inflate(mViewResourceId, null); ImageView iv = (ImageView)convertView.findViewById(R.id.option_icon); iv.setImageDrawable(mIcons.getDrawable(position)); TextView tv = (TextView)convertView.findViewById(R.id.option_text); tv.setText(mStrings[position]); return convertView; } }
Konstruktor przyjmuje obiekt Context, identyfikator id układu używanego dla poszczególnych wierszy (więcej na ten temat znajdziesz dalej), tablicę łańcuchów znaków (nazw państw) oraz tablicę TypedArray (z flagami). W metodzie getView tworzone są wiersze listy. Najpierw za pomocą obiektu LayoutInflater na podstawie pliku XML tworzony jest obiekt View. Następnie aplikacja pobiera flagi państw (jako obiekty typu Drawable) i nazwy państw (jako obiekty typu String), a następnie zapełnia tymi elementami kontrolki ImageView i TextView zadeklarowane w układzie. Oto układ wierszy listy:
9.4. Tworzen e zaawansowanych kontrolek L stV ew z rysunkam tekstem
|
351
android:layout_width="48dp" android:layout_height="fill_parent"/>
Poniżej przedstawiono główny układ:
Warto zauważyć, że identyfikator kontrolki ListView musi mieć wartość @android:id/list — w przeciwnym razie wystąpi wyjątek RuntimeException.
9.5. Stosowanie nagłówków sekcji w kontrolkach ListView Wagied Davids
Problem Programista chce wyświetlać elementy podzielone na kategorie (np. według godzin i dat, rodzajów produktu, poziomu sprzedaży lub cen).
Rozwiązanie Można wykorzystać wymyślone przez Jeffa Sharkeya nagłówki sekcji do wyświetlania wpisów w pamiętniku według dni.
Omówienie Jeff Sharkey zaimplementował pierwsze nagłówki sekcji (http://jsharkey.org/blog/2008/08/18/ separating-lists-with-headers-in-android-09/) niedługo po pojawieniu się Androida — gdy dostępna była wersja 0.9. Jeff chciał odtworzyć styl standardowej aplikacji Settings, która wówczas wyglądała podobnie jak na rysunku poniżej (w tej recepturze zobaczysz, jak utworzyć widoczną na tym rysunku aplikację). Nadającą się do wielokrotnego użytku częścią aplikacji jest opracowana przez Jeffa klasa SeparatedListAdapter. Zaimplementowano w niej wzorzec projek-
352
|
Rozdz ał 9. GU — kontrolka L stV ew
towy Kompozyt (http://en.wikipedia.org/wiki/Composite_pattern). Klasa ta obejmuje kilka obiektów Adapter, a metoda getItem() określa, który z nich należy zastosować w danym momencie.
Zapoznaj się najpierw z plikami XML. Jeden z nich obejmuje główny układ (listing 9.5), a trzy dalsze — elementy listy. Możliwość zastosowania wbudowanych, ale dość skomplikowanych stylów Jeff zawdzięcza Romainowi Guyowi z firmy Google. Listing 9.5. Plik main.xml
W pliku list_header.xml (listing 9.6) umieszczono mniejsze separatory listy (np. „Zabezpieczenia”). Listing 9.6. Plik list_header.xml
Układy z plików list_item.xml (listing 9.7) i list_complex.xml (listing 9.8) są oczywiście przeznaczone na poszczególne elementy.
9.5. Stosowan e nagłówków sekcj w kontrolkach L stV ew
|
353
Listing 9.7. Plik list_item.xml
Listing 9.8. Plik list_complex.xml
Układ z pliku add_journalentry_menuitem.xml (listing 9.9) służy do dodawania nowych wpisów. Nie zaprezentowano tu jego działania. Listing 9.9. Plik add_journalentry_menuitem.xml
Na listingu 9.10 pokazano kod aktywności w Javie.
354 |
Rozdz ał 9. GU — kontrolka L stV ew
Listing 9.10. Plik ListSample.java import import import import import import import import import import
java.util.HashMap; java.util.Map; android.app.Activity; android.os.Bundle; android.view.View; android.widget.AdapterView; android.widget.ArrayAdapter; android.widget.ListView; android.widget.Toast; android.widget.AdapterView.OnItemClickListener;
public class ListSample extends Activity { public final static String ITEM_TITLE = "title"; public final static String ITEM_CAPTION = "caption"; // Nagłówki sekcji private final static String[] days = new String[]{"Pn.", "Wt.", "Śr.", "Czw.", "Pt."}; // Treść sekcji private final static String[] notes = new String[] {"Zjadłem śniadanie", "Przebiegłem maraton - pewnie", "Spałem cały dzień"}; // Menu - ListView private ListView addJournalEntryItem; // Adapter do obsługi zawartości kontrolki ListView private SeparatedListAdapter adapter; // Zawartość kontrolki ListView private ListView journalListView; public Map
9.5. Stosowan e nagłówków sekcj w kontrolkach L stV ew
|
355
});
@Override public void onItemClick(AdapterView parent, View view, int position, long duration) { String item = journalEntryAdapter.getItem(position); Toast.makeText(getApplicationContext(), item, Toast.LENGTH_SHORT).show(); }
// Tworzenie adaptera kontrolki ListView adapter = new SeparatedListAdapter(this); ArrayAdapter
}
// Odbieranie kliknięć journalListView.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView parent, View view, int position, long duration) { String item = (String) adapter.getItem(position); Toast.makeText(getApplicationContext(), item, Toast.LENGTH_SHORT).show(); } });
}
Niestety, nie udało nam się uzyskać zgody Jeffa Sharkeya na zamieszczenie jego kodu, dlatego musisz sam pobrać klasę SeparatedListAdapter, która łączy ze sobą wszystkie elementy.
Zobacz także Pierwotny artykuł Jeffa na temat nagłówków sekcji — http://jsharkey.org/blog/2008/08/18/ separating-lists-with-headers-in-android-09/.
9.6. Zachowywanie pozycji w kontrolce ListView Ian Darwin
Problem Programista nie chce rozpraszać użytkownika powracaniem na początek kontrolki ListView, z dala od miejsca operacji wykonywanych przez użytkownika.
356
|
Rozdz ał 9. GU — kontrolka L stV ew
Rozwiązanie Należy śledzić ostatnią operację wykonywaną na liście i w metodzie onCreate() przechodzić do widoku powiązanego z tą operacją.
Omówienie Jedną z rzeczy, które najbardziej mnie irytują w aplikacjach opartych na listach, jest ciągłe przenoszenie użytkownika na początek listy. Oto kilka przykładów: • Standardowy menedżer kontaktów po edycji wpisu zapomina o nim i wraca na początek
listy. • Program File Manager z serwisu OpenIntents po usunięciu elementu z listy wraca na jej
początek. Ignoruje tym samym to, że usuwanie elementów może być związane z ich porządkowaniem — w takiej sytuacji użytkownik prawdopodobnie chce wznowić pracę w tym samym miejscu. • Program pocztowy HTC SenseUI na tablety po wybraniu dużej liczby e-maili za pomocą
pól wyboru i jednoczesnym ich usunięciu zachowuje dawną pozycję listy przewijania, na której wtedy zwykle znajdują się wiadomości z poprzedniego lub jeszcze wcześniejszego dnia. Można stosunkowo łatwo ustawić dowolny element jako aktywny i sprawić, że operacje będą domyślnie wykonywane właśnie na nim. Wystarczy znaleźć indeks elementu w obiekcie Adapter (w razie potrzeby należy wywołać też metodę theList.getAdapter()), a następnie wywołać instrukcję: theList.setSelection(index);
To powoduje przejście do danego elementu, a także ustawienie go jako domyślnego przy wykonywaniu operacji. Aplikacja nie wykonuje jednak akcji powiązanej z wybranym elementem. Pozycję elementu można wyznaczyć w dowolnym miejscu kodu akcji, a następnie przekazać z powrotem do głównego widoku listy, wywołując metodę Intent.putExtra(). Można też zapisać pozycję jako pole w głównej klasie i przewijać listę w metodzie onCreate() lub w innym miejscu.
9.7. Niestandardowy adapter listy Alex Leffelman
Problem Programista chce dostosować do potrzeb zawartość kontrolki ListView.
Rozwiązanie W aktywności obejmującej kontrolkę ListView należy zdefiniować prywatną klasę pochodną od klasy BaseAdapter Androida. Następnie należy przesłonić metody klasy bazowej, tak aby wyświetlać niestandardowe widoki zdefiniowane w pliku XML z układem.
9.7. N estandardowy adapter l sty
|
357
Omówienie Nie jest tajemnicą, że najlepiej jest tłumaczyć techniki na przykładach. Przedstawiony tu kod pochodzi z napisanej przeze mnie aplikacji multimedialnej, która umożliwia użytkownikom tworzenie playlist z utworami z karty SD. Zgodnie z wcześniejszym opisem w aktywności MediaListActivity rozszerzana jest klasa BaseAdapter: private class MediaAdapter extends BaseAdapter { ... }
Omawianie wyszukiwania w telefonie plików multimedialnych wykracza poza zakres tej receptury. Warto jednak wiedzieć, że dane z listy przechowywane są w klasie MediaItem, gdzie zapisane są informacje o wykonawcy, tytuł, album, numer utworu, a także wartość logiczna określająca, czy utwór należy do obecnie odtwarzanej playlisty. W niektórych sytuacjach należy stopniowo dodawać elementy do listy (na przykład w trakcie pobierania informacji i wyświetlania ich na bieżąco), jednak tu wszystkie potrzebne dane są jednocześnie przekazywane do adaptera w konstruktorze: public MediaAdapter(ArrayList
Jeśli rozwijasz aplikację w środowisku Eclipse, zauważysz, że środowisko wymaga przesłonięcia abstrakcyjnych metod klasy BaseAdapter. Jeżeli ich nie przesłonisz, środowisko poinformuje o tym przy próbie skompilowania niepełnego kodu. Przyjrzyj się następującemu fragmentowi: public int getCount() { return mMediaList.size(); }
Platforma musi wiedzieć, ile obiektów View ma utworzyć na liście. Można to ustalić przez zażądanie od adaptera informacji o tym, iloma elementami zarządza. Tu aplikacja tworzy obiekt View dla każdego elementu z listy multimediów. public Object getItem(int position) { return mMediaList.get(position); } public long getItemId(int position) { return position; }
Metody te nie są tu potrzebne, jednak aby omówienie było kompletne, należy wspomnieć, że getItem(int) określa wartość zwracaną, gdy w kontrolce ListView powiązanej z danym adapterem wywoływana jest metoda getItemAtPosition(int) (w przykładowym kodzie metoda ta nie jest używana). Metoda getItemId(int) określa natomiast wartość przekazywaną do wywołania zwrotnego ListView.onListItemClick(ListView, View, int, int) po wybraniu elementu przez użytkownika. W ten sposób można uzyskać pozycję danego widoku na liście i jego identyfikator przypisany przez adapter. Tu obie te wartości są takie same. Główne operacje niestandardowy adapter wykonuje w metodzie getView(). Metoda ta jest wywoływana za każdym razem, gdy na ekranie pojawiają się nowe elementy z kontrolki ListView. Gdy element znika z ekranu, jest „odzyskiwany” przez system w celu późniejszego wykorzystania. Jest to cenny mechanizm, który pozwala wyświetlić w kontrolce ListView tysiące obiektów View, przy czym wystarczy zastosować tylko tyle obiektów tego typu, ile jednocześnie
358 |
Rozdz ał 9. GU — kontrolka L stV ew
mieści się na ekranie. Metoda getView() przyjmuje pozycję tworzonego elementu, czyli obiekt View (może to być niepusty obiekt „odzyskany” przez system), a także nadrzędny element ViewGroup. Zwrócić należy albo nowy obiekt View wyświetlany na liście, albo zmodyfikowaną kopię podanego parametru convertView, co pozwala oszczędzać zasoby systemowe. Potrzebny kod przedstawiono na listingu 9.11. Listing 9.11. Metoda getView public View getView(int position, View convertView, ViewGroup parent) { View V = convertView; if(V == null) { LayoutInflater vi = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE); V = vi.inflate(R.layout.media_row, null); } MediaItem mi = mMediaList.get(position); ImageView icon = (ImageView)V.findViewById(R.id.media_image); TextView title = (TextView)V.findViewById(R.id.media_title); TextView artist = (TextView)V.findViewById(R.id.media_artist); if(mi.isSelected()) { icon.setImageResource(R.drawable.item_selected); } else { icon.setImageResource(R.drawable.item_unselected); } title.setText(mi.getTitle()); artist.setText(" - " + mi.getArtist()); return V; }
Aplikacja najpierw sprawdza, czy obiekt View jest „odzyskiwany” (to dobra praktyka), czy też trzeba wygenerować nowy obiekt tego typu. Jeśli obiekt convertView nie jest dostępny, należy wywołać obiekt LayoutInflater, aby utworzyć obiekt View zdefiniowany w pliku XML z układem. Po utworzeniu obiektu View na podstawie odpowiedniego zasobu układu (lub przy użyciu „odzyskanej” kopii jednego z wcześniej wygenerowanych obiektów) wystarczy zaktualizować elementy interfejsu użytkownika. Tu aplikacja wyświetla tytuł utworu, wykonawcę oraz informację o tym, czy piosenka znajduje się na odtwarzanej playliście. W przykładowym kodzie pominięto sprawdzanie błędów, warto jednak sprawdzać, czy aktualizowane elementy interfejsu użytkownika nie mają wartości null — nie chcesz przecież, aby cała kontrolka List View przestała działać z powodu drobnego błędu w jednym elemencie. Metoda aktualizująca interfejs jest wywoływana dla każdego widocznego elementu z kontrolki ListView. W przykładowej aplikacji znajduje się więc lista identycznych obiektów View, w których wyświetlane są różne dane. Można też zastosować bardziej „twórcze” rozwiązanie i w zależności od pozycji lub zawartości elementów listy wyświetlać je za pomocą różnych układów. Wiesz już, jak przesłonić wymagane metody klasy BaseAdapter. Ponadto do adaptera można dodać dowolne mechanizmy obsługi reprezentowanych zbiorów danych. Przykładowa aplikacja ma umożliwiać użytkownikom kliknięcie elementu na liście i dodanie utworu do odtwarzanej playlisty lub usunięcie go z niej. Efekt ten można łatwo uzyskać, tworząc proste wywołanie zwrotne powiązane z kontrolką ListView oraz krótką metodę w adapterze. 9.7. N estandardowy adapter l sty
|
359
Poniższą metodę należy umieścić w klasie ListActivity: protected void onListItemClick(ListView l, View v, int position, long id) { super.onListItemClick(l, v, position, id); mAdapter.toggleItem(position); }
Poniżej pokazano metodę składową klasy MediaAdapter: public void toggleItem(int position) { MediaItem mi = mMediaList.get(position); mi.setSelected(!mi.getSelected()); mMediaList.set(position, mi); this.notifyDataSetChanged(); }
Najpierw należy zarejestrować wywołanie zwrotne, uruchamiane w momencie kliknięcia elementu na liście. Przekazywane argumenty to kontrolka ListView, obiekt View, pozycja oraz identyfikator klikniętego elementu. Potrzebna jest tylko pozycja, przekazywana do metody MediaAdapter.toggleItem(int). W tej metodzie aktualizowany jest stan odpowiedniego elementu MediaItem i wywoływana jest ważna metoda notifyDataSetChanged(). Metoda ta informuje platformę o tym, że należy ponownie wyświetlić kontrolkę ListView. Jeśli w aplikacji zabraknie wywołania wspomnianej metody, dane będzie można modyfikować w dowolny sposób, jednak zmiany pojawią się dopiero przy ponownym wyświetlaniu kontrolki (na przykład po przewinięciu listy). Po wykonaniu potrzebnych operacji trzeba poinformować nadrzędną kontrolkę ListView, że powinna za pomocą adaptera zapełnić listę. Aby uzyskać pożądany efekt, wystarczy dodać do metody onCreate(Bundle) klasy ListActivity proste wywołania: MediaAdapter mAdapter = new MediaAdapter(getSongsFromSD()); this.setListAdapter(mAdapter);
Najpierw trzeba utworzyć nowy obiekt adaptera z danymi generowanymi przez prywatną metodę, która pobiera z telefonu dane o utworze. Następnie aktywność ListActivity ma zastosować utworzony adapter do wyświetlenia listy. Gotowe — utworzyłeś własny adapter listy z niestandardowym widokiem i możliwością dodawania nowych funkcji.
9.8. Obsługa zmian orientacji — od wartości z kontrolki ListView po wykresy w orientacji poziomej Wagied Davids
Problem Aplikacja ma reagować na zmiany orientacji przez włączanie odpowiedniego układu. Na przykład w orientacji pionowej dane mają znajdować się na liście, natomiast w orientacji poziomej aplikacja ma je wyświetlać na wykresie.
360
|
Rozdz ał 9. GU — kontrolka L stV ew
Rozwiązanie Należy wykonać określone operacje w reakcji na zmianę orientacji urządzenia. Aplikacja ma wtedy tworzyć nowy obiekt View. W celu obsługi zmian orientacji można przesłonić metodę onConfigurationChanged(Configuration newConfig) aktywności.
Omówienie W tej recepturze dane umieszczone są na liście pionowej. Po obróceniu urządzenia lub emulatora zgłaszana jest nowa intencja, która prowadzi do graficznego przedstawienia danych w widoku z wykresem. Do generowania wykresu służy znakomity pakiet DroidCharts (http:// code.google.com/p/droidcharts/). Warto zauważyć, że aby przetestować aplikację w emulatorze Androida, należy zastosować kombinację Ctrl+F11. Powoduje ona zmianę orientacji z pionowej na poziomą (lub na odwrót). Najważniejszym aspektem jest zmodyfikowanie pliku AndroidManifest.xml (listing 9.12) przez umieszczenie w nim następujących wierszy: android:configChanges="orientation|keyboardHidden" android:screenOrientation="portrait"
Listing 9.12. Plik AndroidManifest.xml
Główną aktywnością w tym przykładzie jest DemoCharts, przedstawiona na listingu 9.13. Aktywność ta wykonuje w metodzie onCreate() standardowe operacje, jeśli jednak do metody tej przekazany zostanie parametr, oznacza to, że aktywność jest odtwarzana na podstawie aktywności DemoList z listingu 9.14, dlatego należy odpowiednio skonfigurować dane. Na listingach
9.8. Obsługa zm an or entacj — od wartośc z kontrolk L stV ew po wykresy w or entacj poz omej
|
361
pominięto kilka metod, które nie są związane z omawianym tu zagadnieniem, czyli obsługą zmiany konfiguracji. Cały program znajdziesz w internecie w kodzie źródłowym receptury. Listing 9.13. Plik DemoCharts.java ... import net.droidsolutions.droidcharts.core.data.XYDataset; import net.droidsolutions.droidcharts.core.data.xy.XYSeries; import net.droidsolutions.droidcharts.core.data.xy.XYSeriesCollection; public class DemoCharts extends Activity { private static final String tag = "DemoCharts"; private final String chartTitle = "Dzienne wydatki na kawę"; private final String xLabel = "Dzień tygodnia"; private final String yLabel = "Wydatki"; /** Wywoływana, gdy aktywność tworzona jest po raz pierwszy */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Dostęp do dodatkowych danych intencji Bundle params = getIntent().getExtras(); // Jeśli nie otrzymano parametrów, nie należy wykonywać żadnych operacji if (params == null) { return; } // Pobieranie wartości z przekazanego parametru String paramVals = params.getString("param"); Log.d(tag, "Dane:= " + paramVals); Toast.makeText(getApplicationContext(), "Dane:= " + paramVals, Toast.LENGTH_LONG).show(); ArrayList
362
|
Rozdz ał 9. GU — kontrolka L stV ew
double y = tuple.get(1).doubleValue(); series1.add(x, y); } // Tworzenie kolekcji na różne zbiory danych final XYSeriesCollection dataset = new XYSeriesCollection(); dataset.addSeries(series1); return dataset; } @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); Toast.makeText(this, "Zmiana orientacji", Toast.LENGTH_SHORT); // Przełączenie na widok z aktywności DemoList Intent intent = new Intent(this, DemoList.class); startActivity(intent); // Zamknięcie bieżącej aktywności this.finish(); } }
Widok z aktywności DemoList wyświetlany jest w orientacji pionowej. Metoda onConfigure() tej aktywności po zmianie konfiguracji przekazuje sterowanie z powrotem do klasy DemoCharts (obsługującej orientację poziomą). Listing 9.14. Plik DemoList.java public class DemoList extends ListActivity implements OnItemClickListener { private static final String tag = "DemoList"; private ListView listview; private ArrayAdapter
9.8. Obsługa zm an or entacj — od wartośc z kontrolk L stV ew po wykresy w or entacj poz omej
|
363
ArrayList
364 |
Rozdz ał 9. GU — kontrolka L stV ew
public void onItemClick(AdapterView parent, View view, int position, long duration) { // Po kliknięciu elementu listy wyświetlany jest komunikat toast String msg = "Element nr " + String.valueOf(position) + " - " + listAdapter.getItem(position); Toast.makeText(getApplicationContext(), msg, Toast.LENGTH_LONG).show(); } }
Klasę XYLineChartView pominięto, ponieważ służy tylko do generowania wykresu. Znajdziesz ją w dostępnej w internecie wersji kodu. Możesz go pobrać ze strony podanej w następnym punkcie.
9.8. Obsługa zm an or entacj — od wartośc z kontrolk L stV ew po wykresy w or entacj poz omej
|
365
366
|
Rozdz ał 9. GU — kontrolka L stV ew
ROZDZIAŁ 10.
Multimedia
10.1. Wprowadzenie — multimedia Ian Darwin
Omówienie Android to środowisko multimedialne. Standardowo obejmuje odtwarzacze muzyki i filmów, a większość komercyjnych urządzeń obok domyślnych narzędzi obejmuje też ich bardziej wymyślne odpowiedniki, a także odtwarzacze filmów z serwisu YouTube i inne podobne aplikacje. Z receptur z tego rozdziału dowiesz się, jak sterować wybranymi aspektami świata multimediów w Androidzie.
10.2. Odtwarzanie filmów z serwisu YouTube Marco Dinacci
Problem Programista chce w urządzeniu umożliwić odtwarzanie filmów z serwisu YouTube.
Rozwiązanie Na podstawie identyfikatora URI filmu należy utworzyć obiekt Intent z akcją ACTION_VIEW i uruchomić nową aktywność.
Omówienie Na listingu 10.1 pokazano kod potrzebny do uruchomienia filmu z serwisu YouTube za pomocą intencji.
367
Aby kod z tej receptury zadziałał, w urządzeniu użytkownika musi być zainstalowana standardowa aplikacja YouTube.
Listing 10.1. Uruchamianie filmu z serwisu YouTube za pomocą intencji public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); String video_path = "http://www.youtube.com/watch?v=opZ69P-0Jbc"; Uri uri = Uri.parse(video_path); // Ten wiersz powoduje natychmiastowe uruchomienie aplikacji YouTube (jeśli jest // zainstalowana). Jeżeli usuniesz ten wiersz, użytkownik zobaczy listę aplikacji // do wyboru. uri = Uri.parse("vnd.youtube:" + uri.getQueryParameter("v")); Intent intent = new Intent(Intent.ACTION_VIEW, uri); startActivity(intent); }
W przykładzie podano standardowy adres URL filmu z serwisu YouTube.com. Człon uri.get QueryParameter("v") służy do określania identyfikatora filmu na podstawie identyfikatora URI. W przykładzie identyfikator filmu to opZ69P-0Jbc.
10.3. Używanie obiektu Gallery wraz z kontrolką ImageSwitcher Nidhin Jose Davis
Problem Programista chce utworzyć interfejs użytkownika do przeglądania kolekcji rysunków.
Rozwiązanie Pożądany efekt można uzyskać dzięki zastosowaniu obiektu Gallery wraz z kontrolką Image
Switcher.
Omówienie Można wykorzystać obiekt Gallery (android.widget.Gallery) i kontrolkę ImageSwitcher (android. widget.ImageSwitcher) do utworzenia eleganckiej przeglądarki rysunków. Na listingu 10.2 pokazano układ dla obiektu Gallery. Listing 10.2. Układ dla obiektu Gallery
368 |
Rozdz ał 10. Mult med a
android:layout_width="fill_parent" android:layout_height="fill_parent" >
Na listingu 10.3 pokazano, jak wykorzystać ten układ. Listing 10.3. Główna aktywność ImageBrowser z przykładu z galerią public class ImageBrowser extends Activity implements AdapterView.OnItemSelectedListener, ViewSwitcher.ViewFactory { private ImageSwitcher mISwitcher; private ArrayList
10.3. Używan e ob ektu Gallery wraz z kontrolką mageSw tcher
| 369
allimages.add(this.getResources().getDrawable(R.drawable.image6)); allimages.add(this.getResources().getDrawable(R.drawable.image7)); allimages.add(this.getResources().getDrawable(R.drawable.image8)); allimages.add(this.getResources().getDrawable(R.drawable.image9)); } @Override public void onItemSelected(AdapterView arg0, View v, int position, long id) { try{ mISwitcher.setImageDrawable(allimages.get(position)); }catch(Exception e){} } @Override public void onNothingSelected(AdapterView arg0) { // Pusta } @Override public View makeView() { ImageView i = new ImageView(this); i.setBackgroundColor(0xFF000000); i.setScaleType(ImageView.ScaleType.FIT_CENTER); i.setLayoutParams(new ImageSwitcher.LayoutParams( ImageSwitcher.LayoutParams.FILL_PARENT, ImageSwitcher.LayoutParams.FILL_PARENT)); return i; } public class ImageAdapter extends BaseAdapter { private Context mContext; public ImageAdapter(Context c) { mContext = c; } public int getCount() { return allimages.size(); } public Object getItem(int position) { return position; } public long getItemId(int position) { return position; } public View getView(int position, View convertView, ViewGroup parent) { ImageView galleryview = new ImageView(mContext); galleryview.setImageDrawable(allimages.get(position)); galleryview.setAdjustViewBounds(true); galleryview.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); galleryview.setPadding(5, 0, 5, 0); galleryview.setBackgroundResource(android.R.drawable.picture_frame); return galleryview; } } }
370
|
Rozdz ał 10. Mult med a
10.4. Rejestrowanie filmów za pomocą klasy MediaRecorder Marco Dinacci
Problem Programista chce za pomocą wbudowanej kamery rejestrować filmy i zapisywać je na dysku.
Rozwiązanie Należy zarejestrować film i zapisać go w telefonie za pomocą klasy MediaRecorder z frameworku Androida.
Omówienie Do nagrywania dźwięku i filmów służy klasa MediaRecorder. Klasa ta ma prosty interfejs API, ponieważ jednak oparta jest na prostej maszynie stanowej, metody trzeba wywoływać w określonej kolejności, tak aby uniknąć wystąpienia wyjątku IllegalStateException. Utwórz nową aktywność i przesłoń metodę onCreate za pomocą kodu z listingu 10.4. Listing 10.4. Metoda onCreate() z głównej aktywności @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.media_recorder_recipe); // Film należy nagrywać w orientacji poziomej setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); mSurfaceView = (SurfaceView) findViewById(R.id.surfaceView); mHolder = mSurfaceView.getHolder(); mHolder.addCallback(this); mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); mToggleButton = (ToggleButton) findViewById(R.id.toggleRecordingButton); mToggleButton.setOnClickListener(new OnClickListener() { @Override // Rozpoczynanie i wstrzymywanie nagrywania filmu public void onClick(View v) { if (((ToggleButton)v).isChecked()) mMediaRecorder.start(); else { mMediaRecorder.stop(); mMediaRecorder.reset(); try { initRecorder(mHolder.getSurface()); } catch (IOException e) { e.printStackTrace(); } } } }); }
10.4. Rejestrowan e f lmów za pomocą klasy Med aRecorder
|
371
Klatki z podglądem filmu są wyświetlane w widoku SurfaceView. Do sterowania nagrywaniem służy przycisk, który pozwala rozpocząć i wstrzymać rejestrowanie. Po zakończeniu nagrywania należy zatrzymać pracę obiektu MediaRecorder. Ponieważ metoda stop resetuje stan wszystkich zmiennych maszyny stanowej, więc aby móc rozpocząć rejestrowanie następnego filmu, należy zresetować maszynę stanową i jeszcze raz wywołać metodę initRecorder. W metodzie initRecorder aplikacja konfiguruje obiekt MediaRecorder oraz aparat, co pokazano na listingu 10.5. Listing 10.5. Konfigurowanie obiektu MediaRecorder /* Inicjowanie obiektu MediaRecorder. Aby obiekt działał poprawnie, metody * trzeba wywoływać w odpowiedniej kolejności */ private void initRecorder(Surface surface) throws IOException { // Bardzo ważne jest, aby przed wywołaniem metody setCamera odblokować aparat. // W przeciwnym razie podgląd będzie niewidoczny if(mCamera == null) { mCamera = Camera.open(); mCamera.unlock(); } if(mMediaRecorder == null) mMediaRecorder = new MediaRecorder(); mMediaRecorder.setPreviewDisplay(surface); mMediaRecorder.setCamera(mCamera); mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA); mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.DEFAULT); File file = createFile(); mMediaRecorder.setOutputFile(file.getAbsolutePath()); // Bez ograniczeń. Nie zapomnij sprawdzić, ile wolnej pamięci jest na dysku mMediaRecorder.setMaxDuration(-1); mMediaRecorder.setVideoFrameRate(15); mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.DEFAULT); try { mMediaRecorder.prepare(); } catch (IllegalStateException e) { // Zgłaszany, jeśli poprzednie metody nie zostały wywołane w odpowiedniej // kolejności e.printStackTrace(); } }
mInitSuccesful = true;
Ważne jest, aby przed utworzeniem obiektu MediaRecorder utworzyć i odblokować obiekt Camera. Metody setPreviewDisplay i setCamera trzeba wywołać natychmiast po utworzeniu obiektu MediaRecorder. Koniecznie trzeba określić format i plik wyjściowy. Inne opcje (jeśli występują) należy ustawiać w kolejności przedstawionej na listingu 10.5. Obiekt MediaRecorder najlepiej jest inicjować po utworzeniu powierzchni. Aby otrzymywać powiadomienia o tym, że powierzchnia jest gotowa, aktywność jest rejestrowana jako odbiornik SurfaceHolder.Callback. Ponadto w kodzie przesłonięto metodę surfaceCreated. Aplikacja wywołuje w niej kod do inicjowania procesu nagrywania. 372
|
Rozdz ał 10. Mult med a
@Override public void surfaceCreated(SurfaceHolder holder) { try { if(!mInitSuccessful) initRecorder(mHolder.getSurface()); } catch (IOException e) { e.printStackTrace(); // Zastosować lepszą obsługę błędów? } }
Po zakończeniu korzystania z powierzchni należy pamiętać o zwolnieniu zasobów, ponieważ Camera to obiekt współużytkowany i inne aplikacje także mogą z niego korzystać: private void shutdown() { // Zwalnianie obiektów MediaRecorder i — przede wszystkim — Camera, // ponieważ ten drugi jest współużytkowany i mogą go potrzebować inne programy mMediaRecorder.reset(); mMediaRecorder.release(); mCamera.release(); // Po zwolnieniu obiektów nie można ich ponownie wykorzystać mMediaRecorder = null; mCamera = null; }
Aby przedstawiony wcześniej kod był wywoływany automatycznie po zakończeniu korzystania z aktywności przez użytkownika, należy przesłonić metodę surfaceDestroyed: @Override public void surfaceDestroyed(SurfaceHolder holder) { shutdown(); }
10.5. Jak wykorzystać androidowy mechanizm wykrywania twarzy? Wagied Davids
Problem Programista chce, aby aplikacja wykrywała, czy na danym zdjęciu znajdują się ludzkie twarze, a jeśli tak, to gdzie.
Rozwiązanie Należy zastosować wbudowany androidowy mechanizm wykrywania twarzy. Wykrywanie twarzy to atrakcyjna i ciekawa ukryta funkcja interfejsu API Androida. Jest dostępna od Androida 1.5. Wykrywanie twarzy polega na wskazywaniu na zdjęciach fragmentów, które przypominają ludzką twarz. Rozpoznawanie obiektów na podstawie zbioru cech to jedno z zagadnień z obszaru uczenia maszynowego. Warto zauważyć, że nie chodzi tu o funkcję rozpoznawania twarzy. Omawiany tu mechanizm jedynie wykrywa fragmenty wyglądające jak twarz, natomiast nie określa, do kogo ona należy. Dopiero w wersji Ice Cream Sandwich
10.5. Jak wykorzystać andro dowy mechan zm wykrywan a twarzy?
|
373
(Android 4.0) wprowadzono funkcję rozpoznawania twarzy, którą można wykorzystać do odblokowywania telefonu.
Omówienie Główna aktywność (przedstawiona na listingu 10.6) tworzy obiekt FaceDetectionView. W przykładowej aplikacji sprawdzany plik zapisano na stałe, jednak w produkcyjnej wersji programu zdjęcia powinny pochodzić z aparatu lub galerii. Listing 10.6. Główna aktywność import android.app.Activity; import android.os.Bundle; public class Main extends Activity { /** Wywoływana, gdy aktywność tworzona jest po raz pierwszy */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(new FaceDetectionView(this, "face5.JPG")); } }
FaceDetectionView to niestandardowa klasa, służąca do zarządzania wykrywaniem twarzy z wykorzystaniem klasy android.media.FaceDetector. Metoda init() określa grafikę używaną do oznaczania twarzy. W przykładzie wiadomo, gdzie znajdują się twarze, a Android powinien je znaleźć. Główne operacje wykonywane są w metodzie detectFaces(). Wywołuje ona metodę findFaces klasy FaceDetector. Do tej ostatniej metody należy przekazać zdjęcie i tablicę na wyniki. Następnie można przejść po znalezionych twarzach. Potrzebny kod pokazano na listingu 10.7, a na rysunku 10.1 widoczny jest efekt działania aplikacji.
Rysunek 10.1. Wykrywanie twarzy w praktyce
374
|
Rozdz ał 10. Mult med a
Listing 10.7. Plik FaceDetectionView.java ... import android.media.FaceDetector; public class FaceDetectionView extends View { private static final String tag = FaceDetectionView.class.getName(); private static final int NUM_FACES = 10; private FaceDetector arrayFaces; private final FaceDetector.Face getAllFaces[] = new FaceDetector.Face[NUM_FACES]; private FaceDetector.Face getFace = null; private final PointF eyesMidPts[] = new PointF[NUM_FACES]; private final float eyesDistance[] = new float[NUM_FACES]; private Bitmap sourceImage; private final Paint tmpPaint = new Paint(Paint.ANTI_ALIAS_FLAG); private final Paint pOuterBullsEye = new Paint(Paint.ANTI_ALIAS_FLAG); private final Paint pInnerBullsEye = new Paint(Paint.ANTI_ALIAS_FLAG); private int picWidth, picHeight; private float xRatio, yRatio; private ImageLoader mImageLoader = null; public FaceDetectionView(Context context, String imagePath) { super(context); init(); mImageLoader = ImageLoader.getInstance(context); sourceImage = mImageLoader.loadFromFile(imagePath); detectFaces(); } private void init() { Log.d(tag, "Init()..."); pInnerBullsEye.setStyle(Paint.Style.FILL); pInnerBullsEye.setColor(Color.RED); pOuterBullsEye.setStyle(Paint.Style.STROKE); pOuterBullsEye.setColor(Color.RED); tmpPaint.setStyle(Paint.Style.STROKE); tmpPaint.setTextAlign(Paint.Align.CENTER); BitmapFactory.Options bfo = new BitmapFactory.Options(); bfo.inPreferredConfig = Bitmap.Config.RGB_565; } private void loadImage(String imagePath) { sourceImage = mImageLoader.loadFromFile(imagePath); } @Override protected void onDraw(Canvas canvas) { Log.d(tag, "onDraw()..."); xRatio = getWidth() * 1.0f / picWidth; yRatio = getHeight() * 1.0f / picHeight; canvas.drawBitmap( sourceImage, null, new Rect(0, 0, getWidth(), getHeight()), tmpPaint); for (int i = 0; i < eyesMidPts.length; i++) { if (eyesMidPts[i] != null) { pOuterBullsEye.setStrokeWidth(eyesDistance[i] / 6); canvas.drawCircle(eyesMidPts[i].x * xRatio, eyesMidPts[i].y * yRatio, eyesDistance[i] / 2, pOuterBullsEye);
10.5. Jak wykorzystać andro dowy mechan zm wykrywan a twarzy?
|
375
canvas.drawCircle(eyesMidPts[i].x * xRatio, eyesMidPts[i].y * yRatio, eyesDistance[i] / 6, pInnerBullsEye); } } } private void detectFaces() { Log.d(tag, "detectFaces()..."); picWidth = sourceImage.getWidth(); picHeight = sourceImage.getHeight(); arrayFaces = new FaceDetector(picWidth, picHeight, NUM_FACES); arrayFaces.findFaces(sourceImage, getAllFaces); for (int i = 0; i < getAllFaces.length; i++) { getFace = getAllFaces[i]; try { PointF eyesMP = new PointF(); getFace.getMidPoint(eyesMP); eyesDistance[i] = getFace.eyesDistance(); eyesMidPts[i] = eyesMP; Log.i("Twarz", i + " " + getFace.confidence() + " " + getFace.eyesDistance() + " " + "Ustawienie: (" + getFace.pose(FaceDetector.Face.EULER_X) + "," + getFace.pose(FaceDetector.Face.EULER_Y) + "," + getFace.pose(FaceDetector.Face.EULER_Z) + ")" + "Punkt na wysokości oczu: (" + eyesMidPts[i].x + "," + eyesMidPts[i].y + ")"); } catch (Exception e) { Log.e("Twarz", i + " – brak twarzy"); } } } }
10.6. Odtwarzanie muzyki z pliku Marco Dinacci
Problem Programista chce odtwarzać pliki dźwiękowe przechowywane w urządzeniu.
Rozwiązanie Należy utworzyć i odpowiednio skonfigurować obiekty MediaPlayer oraz MediaController, a następnie podać ścieżkę do pliku. Potem można rozkoszować się muzyką.
Omówienie Odtwarzanie plików muzycznych jest proste — wystarczy skonfigurować obiekty MediaPlayer i MediaController. 376
|
Rozdz ał 10. Mult med a
Najpierw należy utworzyć aktywność z implementacją interfejsu MediaPlayerControl (listing 10.8). Listing 10.8. Początek klasy z implementacją interfejsu MediaPlayerControl public class PlayAudioActivity extends Activity implements MediaPlayerControl { private MediaController mMediaController; private MediaPlayer mMediaPlayer; private Handler mHandler = new Handler();
W metodzie onCreate trzeba utworzyć i skonfigurować obiekty MediaPlayer i MediaControler. Pierwszy z tych obiektów wykonuje standardowe operacje na plikach muzycznych — odtwarza je, wstrzymuje i przechodzi do wskazanego miejsca w pliku. Drugi obiekt to widok z przyciskami uruchamiającymi wspomniane operacje za pomocą metod interfejsu MediaPlayerControl. Kod metody onCreate przedstawiono na listingu 10.9. Listing 10.9. Metoda onCreate() odtwarzacza @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); mMediaPlayer = new MediaPlayer(); mMediaController = new MediaController(this); mMediaController.setMediaPlayer(PlayAudioActivity.this); mMediaController.setAnchorView(findViewById(R.id.audioView)); String audioFile = "" ; try { mMediaPlayer.setDataSource(audioFile); mMediaPlayer.prepare(); } catch (IOException e) { Log.e("PlayAudioDemo", "Nie można odtworzyć pliku " + audioFile + ".", e); }
}
mMediaPlayer.setOnPreparedListener(new OnPreparedListener() { @Override public void onPrepared(MediaPlayer mp) { mHandler.post(new Runnable() { public void run() { mMediaController.show(10000); mMediaPlayer.start(); } }); } });
Oprócz skonfigurowania obiektów MediaController i MediaPlayer w aplikacji trzeba utworzyć anonimowy odbiornik OnPreparedListener, aby uruchamiać odtwarzacz tylko wtedy, gdy plik źródłowy jest gotowy do odtworzenia. Należy też pamiętać o zwolnieniu zasobów obiektu MediaPlayer w momencie usuwania aktywności (listing 10.10). Listing 10.10. Porządkowanie zasobów odtwarzacza @Override protected void onDestroy() {
10.6. Odtwarzan e muzyk z pl ku
|
377
super.onDestroy(); mMediaPlayer.stop(); mMediaPlayer.release(); }
Trzeba też zaimplementować interfejs MediaPlayerControl. Kod implementacji jest bardzo prosty, co pokazano na listingu 10.11. Listing 10.11. Implementacja interfejsu MediaPlayerControl @Override public boolean canPause() { return true; } @Override public boolean canSeekBackward() { return false; } @Override public boolean canSeekForward() { return false; } @Override public int getBufferPercentage() { return (mMediaPlayer.getCurrentPosition() * 100) / mMediaPlayer.getDuration(); } // Pozostałe metody tylko kierują wywołania do obiektu MediaPlayer }
Na zakończenie należy przesłonić metodę onTouchEvent, aby wyświetlać przyciski z obiektu MediaController po dotknięciu ekranu przez użytkownika. Ponieważ obiekt MediaController tworzony jest programowo, układ aktywności jest bardzo prosty:
10.7. Odtwarzanie dźwięku bez interakcji z użytkownikiem Ian Darwin
Problem Programista chce, aby aplikacja odtwarzała pliki dźwiękowe bez interakcji z użytkownikiem.
378
|
Rozdz ał 10. Mult med a
Rozwiązanie Aby odtwarzać pliki dźwiękowe bez interakcji z użytkownikiem (bez kontrolek do zmiany głośności, wstrzymywania odtwarzania itd.), wystarczy utworzyć obiekt MediaPlayer dla danego pliku i wywołać metodę start().
Omówienie Jest to najprostsza technika odtwarzania plików dźwiękowych. W porównaniu z aplikacją z receptury 10.6 ten program nie udostępnia użytkownikom kontrolek do sterowania odtwarzaniem. Dlatego zwykle należy udostępnić przynajmniej przycisk Stop lub Anuluj — zwłaszcza jeśli plik jest długi. Jeżeli jednak w aplikacji chcesz tylko odtwarzać krótkie efekty dźwiękowe, nie musisz udostępniać takich kontrolek. Dla pliku trzeba utworzyć obiekt MediaPlayer. Plik dźwiękowy może znajdować się na karcie SD lub w katalogu res/raw aplikacji. Jeśli plik dźwiękowy jest elementem programu, należy umieścić go we wspomnianym katalogu. Tu używany jest plik res/raw/alarm_sound.3gp. Referencja do niego to R.raw.alarm_sound, a do odtwarzania pliku służy następujący kod: MediaPlayer player = MediaPlayer.create(this, R.raw.alarm_sound); player.start();
Jeśli plik znajduje się na karcie SD, można odtworzyć go w następujący sposób: MediaPlayer player = new MediaPlayer(); player.setDataSource(fileName); player.prepare(); player.start();
Istnieje też metoda pomocnicza, MediaPlayer.create(Context, URI). Metoda ta automatycznie wywołuje metodę prepare(). Aby sterować odtwarzaczem w aplikacji, można wywoływać odpowiednie metody, np. player. stop(), player.pause() itd. Jeśli chcesz ponownie uruchomić odtwarzacz po jego zatrzymaniu, wywołaj znów metodę prepare(). Do odbierania powiadomień o zakończeniu odtwarzania służy odbiornik OnCompletionListener: player.setOnCompletionListener(new OnCompletionListener() { @Override public void onCompletion(MediaPlayer mp) { Toast.makeText(Main.this, "Zakończono odtwarzanie", Toast.LENGTH_SHORT).show(); } });
Po ostatecznym zakończeniu korzystania z obiektu MediaPlayer należy wywołać metodę release() tego obiektu, aby zwolnić pamięć. Jeśli aplikacja tworzy dużo obiektów MediaPlayer i nie zwalnia pamięci, może nastąpić wyczerpanie zasobów.
Zobacz także Aby wykorzystać wszystkie możliwości obiektu MediaPlayer, należy poznać jego różne stany i przejścia między nimi. Pomaga to zrozumieć, które metody można wywoływać. Na stronie http://developer.android.com/reference/android/media/MediaPlayer.html znajdziesz kompletny diagram stanów obiektu MediaPlayer. 10.7. Odtwarzan e dźw ęku bez nterakcj z użytkown k em
|
379
10.8. Konwersja mowy na tekst Corey Sunwold
Problem Programista chce, aby aplikacja rejestrowała mowę i przetwarzała ją jako tekst.
Rozwiązanie Jedną z wyjątkowych cech Androida jest wbudowane przetwarzanie mowy na tekst. Zapewnia to alternatywę dla wprowadzania tekstu. Jest to przydatne, ponieważ użytkownicy mają czasem zajęte ręce i nie mogą wpisywać informacji.
Omówienie Android udostępnia wygodny interfejs API do korzystania z wbudowanej funkcji rozpoznawania mowy. Interfejs ten oparty jest na intencji RecognizerIntent. Przykładowy układ jest bardzo prosty (przedstawiono go na listingu 10.12). W układzie znajduje się tylko kontrolka TextView o nazwie speechText i kontrolka Button o nazwie getSpeechButton. Ta ostatnia kontrolka służy do uruchamiania mechanizmu rozpoznawania mowy, a zwracane wyniki pojawiają się w kontrolce TextView. Listing 10.12. Program ilustrujący rozpoznawanie mowy public class Main extends Activity { private static final int RECOGNIZER_RESULT = 1234; /** Wywoływana, gdy aktywność tworzona jest po raz pierwszy */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); Button startSpeech = (Button)findViewById(R.id.getSpeechButton); startSpeech.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH); intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM); intent.putExtra(RecognizerIntent.EXTRA_PROMPT, "Mowa na tekst"); startActivityForResult(intent, RECOGNIZER_RESULT); } }); } /** * Obsługa wyników zwróconych przez aktywność przetwarzającą mowę
380 |
Rozdz ał 10. Mult med a
*/ @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == RECOGNIZER_RESULT && resultCode == RESULT_OK) { ArrayList
Zobacz także http://developer.android.com/reference/android/speech/RecognizerIntent.html.
10.9. Konwersja tekstu na mowę Ian Darwin
Problem Programista chce, aby aplikacja „wypowiadała” napisane słowa, tak aby użytkownik mógł się z nimi zapoznać bez spoglądania na ekran (np. w trakcie prowadzenia samochodu).
Rozwiązanie Należy zastosować interfejs API TextToSpeech.
Omówienie Interfejs API TextToSpeech wbudowany jest w Android, choć w niektórych wersjach platformy trzeba doinstalować pliki dźwiękowe. Aby rozpocząć korzystanie z omawianego mechanizmu, potrzebny jest tylko obiekt TextTo Speech. Teoretycznie wystarczy napisać poniższy kod: private TextToSpeech myTTS = new TextToSpeech(this, this); myTTS.setLanguage(Locale.US); myTTS.speak(textToBeSpoken, TextToSpeech.QUEUE_FLUSH, null); myTTS.shutdown();
Aby jednak zapewnić poprawne działanie aplikacji, trzeba zastosować kilka intencji. Jedna powinna sprawdzać, czy dane mechanizmu TTS są dostępne (i instalować je, jeśli jest inaczej). Druga jest potrzebna do uruchamiania mechanizmu TTS. Dlatego w praktyce kod powinien wyglądać tak jak na listingu 10.13. Ta ciekawa prosta aplikacja po każdym wciśnięciu przycisku Mów wygłasza jedno z kilku banalnych zdań.
10.9. Konwersja tekstu na mowę
|
381
Listing 10.13. Program ilustrujący konwersję tekstu na mowę public class Main extends Activity implements OnInitListener { private TextToSpeech myTTS; private List
n Pierwszy argument to kontekst (aktywność), a drugi — odbiornik OnInitListener, tu także zaimplementowany w głównej aktywności. Po zainicjowaniu obiektu TextToSpeech wywoływany jest wspomniany odbiornik. Metoda onInit() tego odbiornika ma powiadamiać o tym, że mechanizm TTS jest gotowy. Przedstawiona tu prosta aplikacja Speaker tylko generuje słowa. W bardziej rozbudowanym programie warto uruchomić wątek lub usługę do obsługi mowy.
382
|
Rozdz ał 10. Mult med a
ROZDZIAŁ 11.
Utrwalanie danych
11.1. Wprowadzenie — utrwalanie danych Ian Darwin
Omówienie Utrwalanie danych to obszerny temat. W tym rozdziale skoncentrowano się na wybranych zagadnieniach, takich jak: • dostępne w aplikacjach części systemu plików (katalog /sdcard i podobne lokalizacje); w re-
cepturach założono, że wiesz już, jak zapisywać i wczytywać pliki tekstowe; • utrwalanie danych w bazach — głównie (choć nie tylko) w bazie SQLite; • odczyt i zapis danych w bazie kontaktów; • konwersja między wybranymi formatami danych (np. JSON i XML).
11.2. Pobieranie informacji o plikach Ian Darwin
Problem Programista chce pobrać z dysku (zwykle z pamięci wewnętrznej lub karty SD) wszystkie informacje o danym pliku.
Rozwiązanie Należy zastosować obiekt java.io.File.
383
Omówienie Klasa File udostępnia wiele metod zwracających informacje. Aby wywołać jedną z nich, trzeba utworzyć obiekt File na podstawie nazwy odpowiedniego pliku. Warto od razu zaznaczyć, że utworzenie obiektu File nie wpływa na trwałe systemy plików — taki obiekt znajduje się tylko w pamięci Javy. Aby zmodyfikować system plików, trzeba wywołać odpowiednie metody obiektu File. Istnieje wiele metod do modyfikowania danych. Umożliwiają one utworzenie nowego (ale pustego) pliku, zmianę nazwy pliku itd. W tabeli 11.1 wymieniono wybrane metody zwracające informacje. Tabela 11.1. Metody klasy File zwracające informacje Typ zwracanej wartośc
Nazwa metody
Znaczen e
boolean
exists()
Zw aca true jeśli istnieje element o danej nazwie
String
getCanonicalPath()
Pełna nazwa (ze ścieżką)
String
getName()
Tylko nazwa pliku
String
getParent()
Katalog nad zędny
boolean
canRead()
Zw aca true jeśli możliwy jest odczyt pliku
boolean
canWrite()
Zw aca true jeśli możliwy jest zapis do pliku
long
lastModified()
Czas ostatniej modyfikacji pliku
long
length()
Wielkość pliku
boolean
isFile()
Zw aca true jeśli dany element to plik
boolean
isDirectory()
Zw aca true jeśli dany element to katalog (uwaga nie być ani plikiem ani katalogiem)
element może
Nie można zmienić nazwy w obiekcie File, tak aby reprezentował inny plik. Jeśli chcesz manipulować innym plikiem, utwórz nowy obiekt File. Kod z listingu 11.1 pochodzi z programu napisanego w standardowej Javie (na komputery stacjonarne), jednak obiekt File w Androidzie działa tak samo jak w Javie SE. Listing 11.1. Program wyświetlający informacje o pliku import java.io.*; import java.util.*; /** * Wyświetlanie informacji o pliku za pomocą Javy */ public class FileStatus { public static void main(String[] argv) throws IOException { // Sprawdzanie, czy argv[0] ma określoną wartość if (argv.length == 0) { System.err.println("Stosowanie: FileStatus nazwa_pliku"); System.exit(1); } for (int i = 0; i< argv.length; i++) { status(argv[i]); } }
384 |
Rozdz ał 11. Utrwalan e danych
public static void status(String fileName) throws IOException { System.out.println("---" + fileName + "---"); // Tworzenie obiektu File dla danego pliku File f = new File(fileName); // Sprawdzanie, czy plik istnieje if (!f.exists()) { System.out.println("Pliku nie znaleziono"); System.out.println(); // Pusty wiersz return; } // Wyświetlanie pełnej nazwy System.out.println("Pełna nazwa: " + f.getCanonicalPath()); // Wyświetlanie nazwy katalogu nadrzędnego (jeśli to możliwe) String p = f.getParent(); if (p != null) { System.out.println("Katalog nadrzędny: " + p); } // Sprawdzanie uprawnień do pliku if (f.canRead()) { System.out.println("Odczyt pliku jest możliwy"); } // Sprawdzanie, czy możliwy jest zapis do pliku if (f.canWrite()) { System.out.println("Zapis do pliku jest możliwy"); } // Wyświetlanie daty ostatniej modyfikacji Date d = new Date(); d.setTime(f.lastModified()); System.out.println("Data ostatniej modyfikacji: " + d); // Sprawdzanie, czy nazwa dotyczy pliku, katalogu czy czegoś innego. // Jeśli podano plik, program wyświetla jego długość if (f.isFile()) { // Podawanie wielkości pliku System.out.println("Wielkość pliku to " + f.length() + " bajtów."); } else if (f.isDirectory()) { System.out.println("To katalog"); } else { System.out.println("Dziwne, to ani plik, ani katalog!"); } System.out.println(); // Pusty wiersz między wpisami } }
Po uruchomieniu programu w wierszu poleceń z trzema przedstawionymi argumentami otrzymasz dane wyjściowe pokazane na listingu 11.2. Listing 11.2. Efekt uruchomienia programu zwracającego informacje o plikach w systemie Microsoft Windows C:\javasrc\dir_file>java FileStatus / /tmp/id /autoexec.bat ---/--Pełna nazwa: C:\ Odczyt pliku jest możliwy Zapis do pliku jest możliwy Data ostatniej modyfikacji: Thu Jan 01 00:00:00 GMT 1970
11.2. Pob eran e nformacj o pl kach
| 385
To katalog ---/tmp/id--Pliku nie znaleziono ---/autoexec.bat--Pełna nazwa: C:\AUTOEXEC.BAT Katalog nadrzędny: \ Odczyt pliku jest możliwy Zapis do pliku jest możliwy Data ostatniej modyfikacji: Fri Sep 10 15:40:32 GMT 1999 Wielkość pliku to 308 bajtów
Jak widać, pełna nazwa nie tylko obejmuje katalog nadrzędny C:\, ale jest też podawana przy użyciu wielkich liter. Na tej podstawie można wywnioskować, że program uruchomiono w jednej ze starszych wersji systemu Microsoft Windows. W Uniksie aplikacja działa inaczej, co pokazano na listingu 11.3. Listing 11.3. Uruchomiony w Uniksie program zwracający informacje o plikach $ java FileStatus / /tmp/id /autoexec.bat ---/--Pełna nazwa: / Odczyt pliku jest możliwy Data ostatniej modyfikacji: October 4, 1999 6:29:14 AM PDT To katalog ---/tmp/id--Pełna nazwa: /tmp/id Katalog nadrzędny: /tmp Odczyt pliku jest możliwy Zapis do pliku jest możliwy Data ostatniej modyfikacji: October 8, 1999 1:01:54 PM PDT Wielkość pliku to 0 bajtów ---/autoexec.bat--Pliku nie znaleziono $
Taki efekt wynika z tego, że w Uniksie standardowo nie ma pliku autoexec.bat. Ponadto nazwy plików w Uniksie (podobnie jak w systemie plików w Androidzie i w komputerach Mac) mogą obejmować małe i wielkie litery. Jaką nazwę wpiszesz, taka zostanie ustawiona.
11.3. Wczytywanie plików z aplikacji, a nie z systemu plików Rachee Singh
Problem Potrzebny jest dostęp do danych z pliku z katalogu /res/raw, a nie z systemu plików (/data, /sdcard lub /mnt). Standardowe klasy Javy przeznaczone do obsługi wejścia – wyjścia umożliwiają tylko otwieranie plików zapisanych na dysku (np. w katalogach /data lub /sdcard).
386 |
Rozdz ał 11. Utrwalan e danych
Rozwiązanie Należy zastosować metody getResources() i openRawResource() do otwarcia przykładowego pliku. Następnie można wczytać go w standardowy sposób.
Omówienie Programista chce wczytywać informacje z pliku dołączonego do aplikacji na Android. Wymaga to umieszczenia odpowiedniego pliku w katalogu res/raw (katalog ten trzeba utworzyć, ponieważ domyślnie nie istnieje). Ponieważ plik znajduje się w katalogu res/, identyfikator tego pliku dostępny jest w wygenerowanej klasie R. Identyfikator należy przekazać do metody openRawResource(). Następnie można wczytać plik za pomocą zwróconego obiektu Input StreamReader, umieszczonego w obiekcie BufferedReader. W ostatnim kroku za pomocą metody readLine wystarczy z obiektu BufferedReader wyodrębnić łańcuch znaków. Środowisko Eclipse zaleca umieszczenie wywołania metody readLine w bloku try-catch, ponieważ może ona zgłosić wyjątek IOException. Przykładowy plik z katalogu /res/raw to samplefile. Kod służący do odczytu tego pliku pokazano na listingu 11.4. Listing 11.4. Kod do wczytywania pliku InputStreamReader is = new InputStreamReader(this.getResources().openRawResource(R.raw.samplefile)); BufferedReader reader = new BufferedReader(is); StringBuilder finalText = new StringBuilder(); String line; try { while ((line = reader.readLine()) != null) { finalText.append(line); } } catch (IOException e) { e.printStackTrace(); } fileTextView = (TextView)findViewById(R.id.fileText); fileTextView.setText(finalText.toString());
Po wczytaniu całego łańcucha znaków program przypisuje go do kontrolki TextView w aktywności. Efekt przedstawiono na rysunku 11.1.
11.4. Wyświetlanie zawartości katalogu Ian Darwin
Problem Programista chce wyświetlić zawartość katalogu.
Rozwiązanie Należy zastosować metody list() i listFiles() obiektu java.io.File. 11.4. Wyśw etlan e zawartośc katalogu
|
387
Rysunek 11.1. Plik wczytany z zasobów aplikacji
Omówienie Klasa java.io.File obejmuje kilka metod do obsługi katalogów. Aby zapisać zawartość bieżącego katalogu, wystarczy wywołać instrukcję: String[] list = new File(".").list()
Jeśli chcesz utworzyć tablicę obiektów File, a nie łańcuchów znaków, wywołaj poniższe polecenie: File[] list = new File(".").listFiles();
Wynik można wyświetlić w kontrolce ListView (zobacz recepturę 9.2). Oczywiście można stosować wiele dodatkowych opcji, np. wyświetlać nazwy w wielu kolumnach lub jedna pod drugą, używając kontrolki TextView i czcionki o stałej szerokości znaków, ponieważ z góry znana jest liczba elementów na liście. Można też pominąć nazwy plików zaczynające się od kropek, jak robi to instrukcja ls w Uniksie. Jeszcze inna możliwość to wyświetlanie najpierw nazw katalogów — tak działają niektóre eksploratory plików. Za pomocą metody listFiles(), która tworzy nowy obiekt File na podstawie każdej nazwy pliku, można wyświetlić wielkość poszczególnych plików, tak jak robi to polecenie dir z systemu MS-DOS lub instrukcja ls –l w Uniksie (zobacz recepturę 11.2, dostępną w witrynie książki na stronie http://androidcookbook.com/r/3220). Można też ustalić, czy dana nazwa określa plik czy katalog, czy też nie dotyczy żadnego z tych elementów. Na podstawie tych informacji można przekazać wszystkie katalogi do metody najwyższego poziomu i rekurencyjnie wyświetlić zawartość wszystkich katalogów (tak działa polecenie wyszukiwania i instrukcja ls –R w Uniksie, a także polecenie DIR /S w systemie DOS). Można w ten sposób samodzielnie napisać eksplorator plików.
388 |
Rozdz ał 11. Utrwalan e danych
Większą swobodę w wyświetlaniu zawartości systemu plików daje instrukcja list(Filename Filter ff). FilenameFilter to prosty interfejs o tylko jednej metodzie — boolean accept(File inDir, String fileName). Jeśli programista chce wyświetlić tylko pliki Javy (*.java, *.class, *.jar itd.), powinien napisać metodę accept(), która zwraca wartość true wyłącznie dla takich plików. Na listingu 11.5 pokazano program do wyświetlania zawartości katalogów, w którym zastosowano interfejs FilenameFilter.
Listing 11.5. Program do wyświetlania katalogów, w którym zastosowano interfejs FilenameFilter import java.io.*; /** * FNFilter – program do wyświetlania zawartości katalogów, w którym * zastosowano interfejs FilenameFilter */ public class FNFilter { public static String[] getListing(String startingDir) { // Generowanie listy wybranych plików za pomocą obiektu File do jednorazowego użytku String[] dir = new java.io.File(startingDir).list(new OnlyJava()); java.util.Arrays.sort(dir); // Sortowanie według nazw return dir; } /** Implementacja interfejsu FilenameFilter * Metoda Accept zwraca true tylko dla plików .java , .jar i .dex */ class OnlyJava implements FilenameFilter { public boolean accept(File dir, String s) { if (s.endsWith(".java") || s.endsWith(".jar") || s.endsWith(".dex")) return true; // Inne projekty itp. return false; } }
Interfejs FilenameFilter może działać w bardziej elastyczny sposób. W produkcyjnej wersji aplikacji lista plików zwracanych przez ten interfejs jest zwykle określana dynamicznie (możliwe, że automatycznie) na podstawie operacji wykonywanych przez użytkownika. Podobny mechanizm zaimplementowany jest w oknach dialogowych do wyboru plików, gdzie użytkownik może wskazać jeden ze zbiorów typów plików. Jest to duże udogodnienie przy wyszukiwaniu, ponieważ — podobnie jak w przykładzie — zmniejsza liczbę plików, które trzeba sprawdzić. Jedna z przeciążonych wersji metody listFiles() przyjmuje argument typu FileFilter. Jedyna różnica w porównaniu z opisaną tu wersją polega na tym, że metoda accept() klasy File Filter przyjmuje obiekt File, natomiast ta sama metoda interfejsu FilenameFilter przyjmuje łańcuch znaków z nazwą pliku.
Zobacz także Z receptury 9.2 dowiesz się, jak wyświetlać wyniki w interfejsie GUI. W rozdziale 11. książki Java Cookbook1, napisanej przeze mnie i opublikowanej przez wydawnictwo O’Reilly, znajdziesz więcej informacji na temat operacji na plikach i katalogach. 1
Wydanie polskie: Ian Darwin, Java. Receptury, Helion, Gliwice 2003. — przyp. tłum.
11.4. Wyśw etlan e zawartośc katalogu
| 389
11.5. Określanie łącznej ilości pamięci oraz ilości wolnego miejsca na karcie SD Amir Alagic
Problem Programista chce ustalić łączną ilość pamięci oraz ilość wolnego miejsca na karcie SD.
Rozwiązanie Aby ustalić pojemność karty SD oraz ilość wolnego miejsca na niej, należy zastosować klasy StatFs i Environment z pakietu android.os.
Omówienie Oto kod do pobierania potrzebnych informacji: StatFs statFs = new StatFs(Environment.getExternalStorageDirectory().getPath()); double bytesTotal = (long) statFs.getBlockSize() * (long) statFs.getBlockCount(); double megTotal = bytesTotal / 1048576;
Aby ustalić łączną pojemność karty SD, należy zastosować klasę StatFs z pakietu android.os. Jako parametr konstruktora trzeba podać instrukcję Environment.getExternalStorageDirectory(). getPath(). Następnie należy pomnożyć wielkość bloku przez liczbę bloków karty SD: (long) statFs.getBlockSize() * (long) statFs.getBlockCount();
Aby uzyskać pojemność w megabajtach, należy podzielić wynik przez 1048576. W celu określenia ilości wolnej pamięci na karcie SD wystarczy zastąpić instrukcję statFs.getBlockCount() poleceniem statFs.getAvailableBlocks(): (long) statFs.getBlockSize() * (long) statFs.getAvailableBlocks();
Jeśli chcesz wyświetlić wartość z dokładnością do dwóch miejsc po przecinku, zastosuj obiekt DecimalFormat z pakietu java.text: DecimalFormat twoDecimalForm = new DecimalFormat("#.##");
11.6. Prosty sposób tworzenia aktywności do ustawiania preferencji użytkownika Ian Darwin
Problem Programista chce umożliwić użytkownikom ustawianie preferencji i zachowywać je w programie.
390
|
Rozdz ał 11. Utrwalan e danych
Rozwiązanie Do uruchamiania aktywności pochodnej od klasy PreferenceActivity można zastosować opcję albo przycisk Preferencje lub Ustawienia. W metodzie onCreate() nowej aktywności należy wczytywać określony w XML-u ekran PreferenceScreen.
Omówienie Android zachowuje obiekty SharedPreferences w półtrwałej pamięci. Aby pobrać ustawienia z tego obiektu, należy wywołać następującą instrukcję: sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
Należy ją umieścić w metodzie onCreate() głównej aktywności lub w takiej metodzie w aktywności, w której potrzebne są ustawienia użytkownika. Android trzeba poinformować o tym, jakie wartości użytkownik ma ustawiać — nazwę, konto na Twitterze, ulubiony kolor itd. Nie należy przy tym stosować tradycyjnych kontrolek, takich jak ListView lub Spinner. Zamiast nich używa się specjalnych elementów z rodziny Preference. Dostępnych jest szereg takich elementów, np. listy (ListPreference), pola tekstowe (EditText Preference), pola wyboru (CheckBoxPreference) itd. Na listingu 11.6 zastosowano wymienione tu typy elementów. Listing 11.6. Element PreferenceScreen w XML-u
XML-owy element PreferenceCategory umożliwia podział panelu na sekcje o określonych etykietach. Można też utworzyć kilka takich elementów, jeśli liczba ustawień jest duża i chcesz podzielić je na strony. W elemencie PreferenceScreen stosować można różne elementy interfejsu użytkownika. Szczegółowe informacje znajdziesz w oficjalnej dokumentacji.
11.6. Prosty sposób tworzen a aktywnośc do ustaw an a preferencj użytkown ka
|
391
Podklasa PreferenceActivity obejmuje tylko przedstawioną poniżej metodę onCreate(): @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); addPreferencesFromResource(R.layout.prefs); }
Wygląd uruchomionej aktywności PreferenceActivity przedstawiono na rysunku 11.2.
Rysunek 11.2. Element PreferenceScreen
Kliknięcie opcji Nazwisko powoduje otwarcie okna dialogowego z możliwością edycji, co pokazano na rysunku 11.3. W XML-owym układzie ekranu preferencji każde ustawienie ma określoną nazwę (klucz), tak jak w odwzorowaniach lub właściwościach Javy. Obsługiwane typy wartości to łańcuchy znaków, liczby całkowite, liczby zmiennoprzecinkowe i wartości logiczne. Klucz służy do pobierania wartości wprowadzonej przez użytkownika. Można też określić wartość domyślną, która jest stosowana wtedy, gdy użytkownik nie otworzył jeszcze ekranu z preferencjami lub nie określił danego ustawienia. String preferredName = sharedPreferences.getString("nameChoice", "Brak nazwiska");
W tym programie, podobnie jak w wielu innych przykładowych aplikacjach na Android, na stronie z preferencjami nie ma przycisku Wstecz, dlatego użytkownik musi posłużyć się systemowym przyciskiem Back. W standardowych aplikacjach po powrocie do głównej aktywności uwzględniane są opcje ustawione przez użytkownika. Przedstawiona tu przykładowa aplikacja jedynie wyświetla ustawione wartości, co pokazano na rysunku 11.4. To już wszystkie potrzebne elementy. W XML-owym elemencie PreferenceScreen należy zdefiniować właściwości i sposób ich ustawiania. Następnie potrzebne są wywołania metody
392
|
Rozdz ał 11. Utrwalan e danych
Rysunek 11.3. Okno dialogowe do edycji łańcucha znaków
Rysunek 11.4. Wartości używane w głównej aktywności getDefaultSharedPreferences() oraz metod getString(), getBoolean() i innych zwróconego obiektu SharedPreferences. Obsługa preferencji w ten sposób jest łatwa i zapewnia jednolitość, spój-
ność oraz przewidywalność w Androidzie, co jest ważne ze względu na ogólny komfort pracy użytkowników.
11.6. Prosty sposób tworzen a aktywnośc do ustaw an a preferencj użytkown ka
|
393
11.7. Sprawdzanie poprawności ustawień Federico Paolinelli
Problem Android umożliwia bardzo łatwe ustawianie domyślnych preferencji. Wystarczy zdefiniować aktywność PreferenceActivity i przekazać do niej plik zasobów, co opisano w recepturze 11.6. Jak jednak sprawdzić ustawienia podane przez użytkownika?
Rozwiązanie Można w aktywności PreferenceActivity zaimplementować metodę onSharedPreferenceChanged: public void onSharedPreferenceChanged(SharedPreferences prefs, String key)
Ustawienia sprawdzane są w ciele tej metody. Jeśli sprawdzanie kończy się niepowodzeniem, należy przywrócić wartość domyślną. Warto przy tym wiedzieć, że nawet jeśli w obiekcie SharedPreferences ustawiona zostanie w tym momencie poprawna wartość, nie będzie ona automatycznie wyświetlana. Dlatego trzeba ponownie uruchomić aktywność do obsługi preferencji.
Omówienie W aktywności do ustawiania domyślnych preferencji, obejmującej implementację interfejsu OnSharedPreferenceChangeListener (listing 11.7), można zaimplementować metodę onShared PreferenceChanged. Listing 11.7. Klasa pochodna od PreferenceActivity public class MyPreferenceActivity extends PreferenceActivity implements OnSharedPreferenceChangeListener { public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Context context = getApplicationContext(); prefs = PreferenceManager.getDefaultSharedPreferences(context); addPreferencesFromResource(R.xml.userprefs); }
Metoda onSharedPreferenceChanged() wywoływana jest po zatwierdzeniu zmiany, dlatego modyfikacje są wprowadzane na stałe. Pomysł polega na tym, aby sprawdzać, czy wartość jest właściwa. Jeśli nie jest, należy ustawić wartość domyślną lub wyłączyć opcję. Aby metoda była uruchamiana, trzeba zarejestrować odbiornik dla aktywności. Dobrym rozwiązaniem jest rejestrowanie go w metodzie onResume i wyrejestrowywanie w metodzie onPause: @Override protected void onResume() { super.onResume(); prefs.registerOnSharedPreferenceChangeListener(this); }
394 |
Rozdz ał 11. Utrwalan e danych
@Override protected void onPause() { super.onPause(); prefs.unregisterOnSharedPreferenceChangeListener(this); }
Na tym etapie można sprawdzić poprawność. Jeśli kluczem opcji jest MY_OPTION_KEY, do sprawdzania wartości tej opcji i jej zaakceptowania lub odrzucenia można zastosować kod z listingu 11.8. Listing 11.8. Sprawdzanie i akceptowanie lub odrzucanie wartości public void onSharedPreferenceChanged(SharedPreferences prefs, String key) { SharedPreferences.Editor prefEditor = prefs.edit(); if(key.equals(MY_OPTION_KEY)){ String optionValue = prefs.getString(MY_OPTION_KEY, ""); if(dontLikeTheValue(optionValue)){ prefEditor.putString(MY_OPTION_KEY, "Wartość domyślna"); prefEditor.commit(); reload(); } } return; }
Jeśli zastosujesz to podejście, użytkownik poczuje się zaskoczony i nie będzie wiedział, dlaczego aplikacja odrzuciła wartość. Dlatego warto wyświetlić okno dialogowe z komunikatem o błędzie i ponownie wczytać aktywność po zamknięciu tego okna (listing 11.9). Listing 11.9. Wyjaśnianie powodów odrzucenia wartości private void showErrorDialog(String errorString){ String okButtonString = context.getString(R.string.ok_name); AlertDialog.Builder ad = new AlertDialog.Builder(context); ad.setTitle(context.getString(R.string.error_name)); ad.setMessage(errorString); ad.setPositiveButton(okButtonString,new OnClickListener() { public void onClick(DialogInterface dialog, int arg1) { reload(); } } ); ad.show(); return; }
W tym rozwiązaniu instrukcja if z wywołaniem dontLikeTheValue wygląda tak: if(dontLikeTheValue(optionValue)){ if(!GeneralUtils.isPhoneNumber(smsNumber)){ showErrorDialog("Wartość opcji jest nieprawidłowa"); prefEditor.putString(MY_OPTION_KEY, "Wartość domyślna"); prefEditor.commit(); } }
Nadal brakuje funkcji reload(), jednak jej kod jest oczywisty. Funkcja powinna ponownie włączać aktywność, wykorzystując intencję, która posłużyła do pierwszego uruchomienia danej aktywności: private void reload(){ startActivity(getIntent()); finish(); }
11.7. Sprawdzan e poprawnośc ustaw eń
|
395
11.8. Zaawansowane wyszukiwanie tekstu Claudio Esperanca
Problem Programista zamierza zaimplementować zaawansowane wyszukiwanie i chce wiedzieć, jak za pomocą mechanizmu wyszukiwania pełnotekstowego z bazy SQLite utworzyć warstwę danych do przechowywania i przeszukiwania tekstu.
Rozwiązanie Potrzebne rozwiązanie można zbudować za pomocą tabeli wirtualnej FTS3 (ang. Full Text Search 3) z bazy SQLite i funkcji dopasowywania (także udostępnianej przez tę bazę).
Omówienie Przedstawione poniżej kroki pozwalają utworzyć przykładowy projekt na Android, obejmujący warstwę danych, która umożliwia przechowywanie i pobieranie danych za pomocą bazy SQLite.
1. Utwórz nowy projekt na Android (AdvancedSearchProject). 2. Wersję interfejsu API ustaw na 8. lub wyższą. 3. Aplikację nazwij AdvancedSearch. 4. Jako nazwę pakietu ustaw com.androidcookbook.example.advancedsearch. 5. Utwórz aktywność o nazwie AdvancedSearchActivity. 6. Wartość w polu Min SDK ustaw na 8 (odpowiada ona Androidowi 2.2, czyli Froyo). 7. W katalogu src utwórz nową klasę Javy, DbAdapter, w pakiecie com.androidcookbook.example. advancedsearch.
W celu utworzenia warstwy danych przykładowej aplikacji umieść w utworzonym pliku kod z listingu 11.10. Listing 11.10. Klasa DbAdapter package com.androidcookbook.example.advancedsearch; import import import import import import import import
java.util.LinkedList; android.content.ContentValues; android.content.Context; android.database.Cursor; android.database.SQLException; android.database.sqlite.SQLiteDatabase; android.database.sqlite.SQLiteOpenHelper; android.util.Log;
public class DbAdapter { public static final String APP_NAME = "AdvancedSearch"; private static final String DATABASE_NAME = "AdvancedSearch_db"; private static final int DATABASE_VERSION = 1;
396
|
Rozdz ał 11. Utrwalan e danych
// Wersja bazy danych (przydatna na przykład do kontrolowania aktualizacji) private static final String TABLE_NAME = "example_tbl"; public static final String KEY_USERNAME = "username"; public static final String KEY_FULLNAME = "fullname"; public static final String KEY_EMAIL = "email"; public static long GENERIC_ERROR = -1; public static long GENERIC_NO_RESULTS = -2; public static long ROW_INSERT_FAILED = -3; private final Context context; private DbHelper dbHelper; private SQLiteDatabase sqlDatabase; public DbAdapter(Context context) { this.context = context; } private static class DbHelper extends SQLiteOpenHelper { private boolean databaseCreated=false; DbHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); } @Override public void onCreate(SQLiteDatabase db) { Log.d(APP_NAME, "Tworzenie bazy danych dla aplikacji"); try{ // Tworzenie tabeli wirtualnej FTS3 db.execSQL( "CREATE VIRTUAL TABLE ["+TABLE_NAME+"] USING FTS3 (" + "["+KEY_USERNAME+"] TEXT," + "["+KEY_FULLNAME+"] TEXT," + "["+KEY_EMAIL+"] TEXT" + ");" ); this.databaseCreated = true; } catch (Exception e) { Log.e(APP_NAME, "Błąd w czasie tworzenia bazy danych: " + e.toString(), e); this.deleteDatabaseStructure(db); } } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { Log.d(APP_NAME, "Aktualizacja bazy z wersji " + oldVersion + " do wersji " + newVersion + "..."); // To tylko przykład — aplikacja usuwa dane w trakcie aktualizacji bazy this.deleteDatabaseStructure(db); this.onCreate(db); } public boolean databaseCreated(){ return this.databaseCreated; } private boolean deleteDatabaseStructure(SQLiteDatabase db){ try{ db.execSQL("DROP TABLE IF EXISTS ["+TABLE_NAME+"];"); return true; }catch (Exception e) { Log.e(APP_NAME, "Błąd w czasie usuwania bazy: " + e.toString(), e); } return false;
11.8. Zaawansowane wyszuk wan e tekstu
|
397
} } /** * Otwieranie bazy. Jeśli nie można tego zrobić, aplikacja próbuje utworzyć bazę * * @return {@link Boolean} true, jeśli bazę otwarto lub utworzono (w przeciwnym razie false) * @throws {@link SQLException} jeśli wystąpił błąd */ public boolean open() throws SQLException { try{ this.dbHelper = new DbHelper(this.context); this.sqlDatabase = this.dbHelper.getWritableDatabase(); return this.sqlDatabase.isOpen(); }catch (SQLException e) { throw e; } } /** * Zamykanie połączenia z bazą * @return {@link Boolean} true, jeśli zamknięto połączenie (w przeciwnym razie false) */ public boolean close() { this.dbHelper.close(); return !this.sqlDatabase.isOpen(); } /** * Sprawdzanie, czy baza jest otwarta * * @return {@link Boolean} true, jeśli baza jest otwarta (w przeciwnym razie false) */ public boolean isOpen(){ return this.sqlDatabase.isOpen(); } /** * Sprawdzanie, czy utworzono bazę * * @return {@link Boolean} true, jeśli bazę utworzono (w przeciwnym razie false) */ public boolean databaseCreated(){ return this.dbHelper.databaseCreated(); } /** * Wstawianie nowego wiersza do tablicy * * @param username {@link String} z nazwą użytkownika * @param fullname {@link String} z nazwiskiem * @param email {@link String} z adresem e-mail * @return {@link Long} z identyfikatorem wiersza lub, przy wystąpieniu błędu * (wartość poniżej 0), z wartością ROW_INSERT_FAILED */ public long insertRow(String username, String fullname, String email) { try{ // Przygotowywanie wartości ContentValues values = new ContentValues(); values.put(KEY_USERNAME, username); values.put(KEY_FULLNAME, fullname); values.put(KEY_EMAIL, email); // Próba wstawiania wiersza
398 |
Rozdz ał 11. Utrwalan e danych
return this.sqlDatabase.insert(TABLE_NAME, null, values); }catch (Exception e) { Log.e(APP_NAME, "Błąd w trakcie wstawiania wiersza: "+e.toString(), e); } return ROW_INSERT_FAILED; } /** * W metodzie search zastosowano tabelę wirtualną FTS3 i funkcję * MATCH z bazy SQLite do wyszukiwania danych. * @see http //www.sqlite.org/fts3.html, aby dowiedzieć się więcej o składni * @param search {@link String} z szukanym wyrażeniem * @return {@link LinkedList} z {@link String} z wynikami wyszukiwania */ public LinkedList
} }catch(Exception e){ Log.e(APP_NAME, "Błąd w czasie wyszukiwania tekstu "+search+": "+e.toString(), e); }finally{ if(cursor!=null && !cursor.isClosed()){ cursor.close(); } } return results; }
}
Gdy warstwa danych jest już gotowa, można przetestować ją za pomocą aktywności Advanced
SearchActivity.
Aby zdefiniować łańcuchy znaków potrzebne w aplikacji, zmień zawartość pliku res/values/ strings.xml na:
11.8. Zaawansowane wyszuk wan e tekstu
| 399
Układ aplikacji zapisany jest w pliku res/layout/main.xml. Plik ten obejmuje kontrolkę EditText (o nazwie etSearch), kontrolkę Button (o nazwie btnSearch) i kontrolkę TextView (o nazwie tvResults), w której wyświetlane są wyniki. Wszystkie te elementy znajdują się w układzie LinearLayout. Na listingu 11.11 przedstawiono kod z pliku AdvancedSearchActivity.java. Listing 11.11. Aktywność AdvancedSearchActivity package com.androidcookbook.example.advancedsearch; import java.util.Iterator; import java.util.LinkedList; import import import import import import
android.app.Activity; android.os.Bundle; android.view.View; android.widget.Button; android.widget.EditText; android.widget.TextView;
public class AdvancedSearchActivity extends Activity { private DbAdapter dbAdapter; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); dbAdapter = new DbAdapter(this); dbAdapter.open(); if(dbAdapter.databaseCreated()){ dbAdapter.insertRow("test", "przyklad", "[email protected]"); dbAdapter.insertRow("lorem", "lorem ipsum", "[email protected]"); dbAdapter.insertRow("jkowalski", "Jan Kowalski", "[email protected]"); } Button button = (Button) findViewById(R.id.btnSearch); final EditText etSearch = (EditText) findViewById(R.id.etSearch); final TextView tvResults = (TextView) findViewById(R.id.tvResults); button.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { LinkedList
400 |
Rozdz ał 11. Utrwalan e danych
Zobacz także Na stronie http://www.sqlite.org/fts3.html znajdziesz więcej informacji na temat możliwości mechanizmu FTS3, a także poznasz składnię służącą do wyszukiwania. Ze strony http://code.google. com/p/localizeandroid/ dowiesz się więcej o projekcie z implementacją wspomnianego mechanizmu.
11.9. Tworzenie bazy SQLite w aplikacji na Android Rachee Singh
Problem Programista chce, aby zapisane dane były zachowywane między uruchomieniami aplikacji, i potrzebuje wygodnego dostępu do danych.
Rozwiązanie SQLite to popularna relacyjna baza SQL, którą można wykorzystać do przechowywania danych aplikacji. Aby wykorzystać tę bazę, zwykle tworzy się klasę pochodną od SQLiteOpenHelper.
Omówienie Aby wykorzystać bazę SQLite w aplikacji na Android, należy utworzyć klasę pochodną od SQLiteOpenHelper. Jest to standardowa klasa Androida, która służy do otwierania plików bazy. Klasa ta sprawdza, czy plik bazy istnieje. Jeśli tak, otwiera go, a w przeciwnym razie tworzy nowy. public class SqlOpenHelper extends SQLiteOpenHelper {
Konstruktor klasy SQLiteOpenHelper przyjmuje kilka argumentów: kontekst, nazwę bazy, obiekt CursorFactory i numer wersji. public public public public public
static static static static static
final final final final final
String DBNAME = "tasksdb.sqlite"; int VERSION =1; String TABLE_NAME = "tasks"; String ID= "id"; String NAME="name";
public SqlOpenHelper(Context context) { super(context, DBNAME, null, VERSION); }
Aby utworzyć bazę danych w SQL-u, należy wywołać instrukcję CREATE: CREATE TABLE
Metoda onCreate() klasy SQLiteOpenHelper służy do tworzenia (i często także zapełniania) bazy. public void onCreate(SQLiteDatabase db) { createDatabase(db); }
11.9. Tworzen e bazy SQL te w apl kacj na Andro d
|
401
private void createDatabase(SQLiteDatabase db) { db.execSQL("create table " + TABLE_NAME + "(" + ID + " integer primary key autoincrement not null, " + NAME + " text " + ");" ); }
Aby uzyskać uchwyt do utworzonej bazy SQL, należy utworzyć obiekt klasy pochodnej od SQLiteOpenHelper: SqlOpenHelper helper = new SqlOpenHelper(this); SQLiteDatabase database= helper.getWritableDatabase();
Teraz obiekt SQLiteDatabase można wykorzystać do wczytywania elementów zapisanych w bazie, a także do jej aktualizowania i wstawiania elementów.
11.10. Wstawianie danych do bazy SQLite Rachee Singh
Problem Programista chce zapisywać dane w bazie SQLite.
Rozwiązanie Należy zastosować metodę insert() i przekazać do niej obiekt typu ContentValues.
Omówienie Klasa ContentValues udostępnia pary klucz – wartość. W podanym przykładzie NAME to łańcuch znaków z kluczem, a Mango to wartość. Wartości po utworzeniu należy wstawić do tabeli, wywołując metodę insert(). Baza SQLite zwraca identyfikator docelowego wiersza bazy danych. Long id = (database.insert(TABLE_NAME, null, values)); tasks.add(t);
Obiekt id to identyfikator wiersza wstawionego do bazy.
11.11. Wczytywanie wartości z istniejącej bazy SQLite Rachee Singh
Problem Przy wcześniejszych uruchomieniach aplikacja utworzyła i zapełniła bazę SQLite. Teraz ma pobrać dane z istniejącej bazy.
402 |
Rozdz ał 11. Utrwalan e danych
Rozwiązanie Należy wywołać metodę query() bazy, a następnie wykorzystać zwrócony obiekt Cursor do poruszania się po bazie i do przetwarzania danych.
Omówienie Do poruszania się po elementach bazy potrzebny jest obiekt Cursor. Do kierowania zapytań do bazy służy metoda query, do której trzeba przekazać odpowiednie argumenty — przede wszystkim nazwę tabeli i nazwy kolumn obejmujących potrzebne wartości (listing 11.12). Listing 11.12. Kierowanie zapytania do bazy i poruszanie się po wynikach ArrayList
Metoda moveToFirst() zaczyna przechodzenie od pierwszego elementu bazy, a metoda move ToNext() przenosi kursor do następnego elementu. Za każdym razem należy sprawdzać, czy aplikacja nie pobrała już wszystkich elementów z bazy. Każdy element bazy dodawany jest do obiektu ArrayList.
11.12. Praca z datami w bazie SQLite Jonathan Fuerth
Problem Wbudowana w Android baza SQLite3 obsługuje bezpośredni dostęp do dat i czasu — między innymi udostępnia przydatne operacje arytmetyczne na wartościach tego typu. Jednak pobieranie dat z bazy jest dość kłopotliwe. W interfejsie API Androida nie ma metody Cursor.getDate().
Rozwiązanie Należy zastosować funkcję strftime() bazy SQLite do przekształcania między znacznikami czasu z bazy SQLite a reprezentacją używaną w interfejsie API Javy (czyli liczbą milisekund od początku “epoki”).
11.12. Praca z datam w baz e SQL te
| 403
Omówienie W tej recepturze przedstawiono zalety korzystania z dostępnych w bazie SQLite znaczników czasu w porównaniu z przechowywaniem w bazie wartości w milisekundach. Dowiesz się też, jak pobierać z bazy znaczniki czasu jako obiekty java.util.Date.
Wprowadzenie W Uniksie czas standardowo reprezentuje się za pomocą typu time_t, który historycznie był aliasem dla 32-bitowych liczb całkowitych. Ta liczba całkowita reprezentuje datę jako liczbę sekund od godziny 00:00 czasu UTC 1 stycznia 1970 roku (od początku epoki Uniksa). W systemach, gdzie typ time_t nadal jest 32-bitową liczbą całkowitą, pojemność tego typu wyczerpie się w 2038 roku. W Javie zastosowano podobne podejście, jednak z kilkoma modyfikacjami. “Epoka” pozostała taka sama, jednak czas jest zapisywany jako 64-bitowa liczba całkowita ze znakiem (jako natywny typ long Javy), a jednostkami są milisekundy zamiast sekund. Ta metoda zapisywania czasu będzie działać przez najbliższe 292 miliony lat. W przykładowym kodzie na Android przeznaczonym do utrwalania dat i czasu zwykle po prostu zapisuje się i pobiera liczbę milisekund od początku epoki. Nie pozwala to jednak wykorzystać pewnych przydatnych funkcji wbudowanych w bazę SQLite.
Zalety Przechowywanie czasu w formie znaczników czasu bazy SQLite ma kilka zalet. Można na przykład bez korzystania z Javy utworzyć kolumnę z domyślnymi znacznikami czasu wskazującymi bieżący czas, wykonywać operacje arytmetyczne na datach kalendarzowych, np. pobrać pierwszy dzień tygodnia lub miesiąca albo dodać tydzień do wartości zapisanej w bazie, a także wyodrębnić samą datę lub czas i zwrócić odpowiednie dane za pomocą dostawcy. Oprócz korzyści płynących z tego, że nie trzeba pisać kodu, występują też dwie dodatkowe zalety. Po pierwsze, w interfejsie API dostawcy danych można zastosować standardowe rozwiązanie z Androida i przekazywać znaczniki czasu jako wartości typu long. Po drugie, manipulować danymi można w kompilowanym kodzie bazy SQLite. Pozwala to uniknąć kosztów związanych z przywracaniem pamięci, które trzeba ponieść przy tworzeniu szeregu obiektów java.util.Date lub java.util.Calendar.
Kod Pora przejść do części praktycznej. Najpierw należy utworzyć tabelę z kolumną typu timestamp. CREATE TABLE current_list ( item_id INTEGER NOT NULL, added_on TIMESTAMP NOT NULL DEFAULT current_timestamp, added_by VARCHAR(50) NOT NULL, quantity INTEGER NOT NULL, units VARCHAR(50) NOT NULL, CONSTRAINT current_list_pk PRIMARY KEY (item_id) );
404 |
Rozdz ał 11. Utrwalan e danych
Warto zwrócić uwagę na wartość domyślną ustawioną w kolumnie added_on. Po wstawieniu wiersza do tabeli baza SQLite automatycznie zapisuje bieżący czas (z dokładnością do sekundy) dla nowego wiersza. Pokazano to na przykładzie uruchamianego z wiersza poleceń programu z bazą SQLite. W dalszej części receptury zobaczysz, jak uzyskać taki efekt w Androidzie. sqlite> insert into current_list (item_id, added_by, quantity, units) ...> values (1, 'fuerth', 1, 'EA'); sqlite> select * from current_list where item_id = 1; 1|2010-05-14 23:10:26|fuerth|1|EA sqlite>
W jaki sposób aplikacja automatycznie wstawiła datę? Jest to jedna z zalet płynących z korzystania ze znaczników czasu bazy SQLite. A co z innymi zaletami? Można na przykład pobrać tylko część daty i wymusić ustawienie czasu na północ: sqlite> select item_id, date(added_on,'start of day') ...> from current_list where item_id = 1; 1|2010-05-14 sqlite>
Inna możliwość to ustawienie daty na poniedziałek następnego tygodnia: sqlite> select item_id, date(added_on,'weekday 1') ...> from current_list where item_id = 1; 1|2010-05-17 sqlite>
Można też wybrać ostatni poniedziałek: sqlite> select item_id, date(added_on,'weekday 1','-7 days') ...> from current_list where item_id = 1; 1|2010-05-10 sqlite>
Te przykłady ilustrują tylko mały wycinek możliwości. Za pomocą znaczników czasu bazy SQLite można wykonać wiele przydatnych operacji. Możliwe, że się zastanawiasz, jak przenieść takie daty z powrotem do kodu w Javie. Sztuczka polega na zastosowaniu następnej dostępnej w bazie SQLite funkcji do obsługi dat — strftime(). Oto metoda Javy, która pobiera wiersz z używanej już wcześniej tabeli current_list: Cursor cursor = database.rawQuery( "SELECT item_id AS _id," + " (strftime('%s', added_on) * 1000) AS added_on," + " added_by, quantity, units" + " FROM current_list", new String[0]); long millis = cursor.getLong(cursor.getColumnIndexOrThrow("added_on")); Date addedOn = new Date(millis);
I to wystarczy. Formatująca sekwencja %s w instrukcji strftime pozwala umieścić znaczniki czasu bezpośrednio w obiekcie Cursor jako używaną w Javie liczbę milisekund od początku epoki. Kod klienta działa wtedy w standardowy sposób, jednak w dostawcy treści można bez problemów wykonywać na danych operacje, które wymagałyby znacznej ilości kodu w Javie i alokowania dodatkowych obiektów.
11.12. Praca z datam w baz e SQL te
| 405
Zobacz także Dokumentacja funkcji bazy SQLite przeznaczonych do obsługi dat i czasu (http://www.sqlite. org/lang_datefunc.html).
11.13. Przetwarzanie danych w formacie JSON za pomocą klasy JSONObject Rachee Singh
Problem JSON to akronim od nazwy JavaScript Object Notation (czyli javascriptowy zapis obiektów). JSON jest prostszym od XML-a formatem wymiany danych. W wielu witrynach dane udostępnia się właśnie w formacie JSON. Także liczne aplikacje przetwarzają takie dane i je udostępniają.
Rozwiązanie Zastosowanie wbudowanych klas (takich jak JSONObject) upraszcza proces przetwarzania danych w formacie JSON oraz pobierania zapisanych w nich wartości.
Omówienie W tej recepturze do generowania danych w formacie JSON służy metoda. W zwykłych aplikacjach takie dane najczęściej pochodzą z usług sieciowych. We wspomnianej metodzie wykorzystano obiekt JSONObject, który zapisuje wartości, a następnie zwraca odpowiedni łańcuch znaków (przez wywołanie metody toString()). W trakcie tworzenia obiektu JSONObject może wystąpić wyjątek JSONException, dlatego kod znajduje się w bloku try-catch (listing 11.13). Listing 11.13. Generowanie przykładowych danych w formacie JSON private String getJsonString() { JSONObject string = new JSONObject(); try { string.put("name", "Jan Kowalski"); string.put("age", new Integer(25)); string.put("address", "ul. Malinowa 123, 45-000 Pcim"); string.put("phone", "8367667829"); } catch (JSONException e) { e.printStackTrace(); } return string.toString(); }
Następnie trzeba utworzyć obiekt JSONObject, który jako argument przyjmuje łańcuch znaków w formacie JSON. Tu jest to łańcuch znaków zwracany przez metodę getJsonString. Potem aplikacja wyodrębnia informacje z obiektu JSONObject i wyświetla je w kontrolce TextView.
406 |
Rozdz ał 11. Utrwalan e danych
Listing 11.14. Przetwarzanie łańcucha znaków w formacie JSON i pobieranie wartości try { String jsonString = getJsonString(); JSONObject jsonObject = new JSONObject(jsonString); String name = jsonObject.getString("name"); String age = jsonObject.getString("age"); String address = jsonObject.getString("address"); String phone = jsonObject.getString("phone"); String jsonText=name + "\n" + age + "\n" + address + "\n" + phone; json= (TextView)findViewById(R.id.json); json.setText(jsonText); } catch (JSONException e) { // Wyświetlanie informacji o wyjątku }
11.14. Przetwarzanie dokumentów XML za pomocą interfejsu DOM API Ian Darwin
Problem Programista ma dane w formacie XML i chce za pomocą aplikacji przekształcać je na użyteczną postać.
Rozwiązanie Android udostępnia całkiem dobrą wersję standardowego interfejsu DOM API z Javy SE. Warto wykorzystać ten interfejs, zamiast pisać własny kod.
Omówienie Tu przedstawiono kod przetwarzający dokument XML z listą receptur z tej książki (listę tę omówiono w recepturze 13.2). Plik wejściowy obejmuje jeden element główny, recipes, w skład którego wchodzi zestaw elementów recipe — każdy z elementami id i title z zawartością tekstową. Kod tworzy obiekt DocumentBuilderFactory dla modelu DOM. Obiekt ten można zmodyfikować, aby na przykład utworzyć parser rozpoznający schematy. W kodzie produkcyjnym można utworzyć obiekt w statycznym bloku inicjującym, zamiast generować go za każdym razem od nowa. Obiekt DocumentBuilderFactory służy do tworzenia mechanizmu przetwarzania dokumentów, czyli parsera. Parser wczytuje dane z obiektu InputStream, dlatego dane z łańcucha znaków należy przekształcić na tablicę bajtów i utworzyć obiekt ByteArrayInputStream. W produkcyjnej wersji aplikacji ten kod warto połączyć z odbiorcą usługi sieciowej, co pozwala na wczytywanie wejściowego strumienia przez sieć oraz odczyt dokumentu XML bezpośrednio w parserze. Nie trzeba wtedy zapisywać strumienia do łańcucha znaków, a następnie obsługiwać go za pomocą konwertera.
11.14. Przetwarzan e dokumentów XML za pomocą nterfejsu DOM AP
|
407
Po wczytaniu elementów dokument jest przetwarzany na tablicę z danymi (z obiektami Datum; w języku angielskim datum to liczba pojedyncza od słowa data, czyli dane). Wykorzystywane są do tego metody interfejsu DOM API, np. getDocumentElement(), getChildNodes() i getNode Value(). Ponieważ interfejs DOM API nie został wymyślony przez twórców Javy, nie używa się w nim standardowego interfejsu Collections API. W zamian stosuje się specjalne kolekcje, np. NodeList. W obronie modelu DOM warto wspomnieć, że takie same lub podobne interfejsy występują w wielu językach programowania, dlatego interfejsy te można uznać za równie standardowe jak kolekcje Javy. Opisany kod przedstawiono na listingu 11.15. Listing 11.15. Przetwarzanie kodu w formacie XML /** Przekształcanie listy receptur z łańcucha znaków z usługi sieciowej * na kolekcję ArrayList z elementami typu Datum * @throws ParserConfigurationException * @throws IOException * @throws SAXException */ public static ArrayList
Aby przekształcić kod z wersji na Javę SE na wersję na Android, wystarczy do pobierania elementów id i title zastosować metodę getNodeValue() zamiast metody getTextContent() z Javy SE. Interfejsy API są tu więc bardzo podobne.
Zobacz także Usługa sieciowa opisana jest w recepturze 13.2. Znacznie więcej informacji znajdziesz w rozdziale poświęconym XML-owi w napisanej przeze mnie książce Java Cookbook2.
2
Wydanie polskie: Ian Darwin, Java. Receptury, Helion, Gliwice 2003. — przyp. tłum.
408 |
Rozdz ał 11. Utrwalan e danych
11.15. Przetwarzanie dokumentów w formacie XML z wykorzystaniem interfejsu XmlPullParser Johan Pelgrim
Problem Programista ma dane w formacie XML i chce za pomocą aplikacji przekształcać je na użyteczną postać.
Rozwiązanie Android nie tylko umożliwia przetwarzanie danych XML-owych za pomocą parserów DOM i SAX, ale też udostępnia implementację interfejsu XmlPullParser, opisanego w interfejsie API XmlPull v1.
Omówienie Interfejs API XmlPull v1 to łatwy w użyciu interfejs do przetwarzania typu pull, zaprojektowany pod kątem prostoty i wysokiej wydajności zarówno w środowiskach z ograniczonymi zasobami (na przykład opartych na Javie Micro Edition), jak i po stronie serwera (gdzie jest używany w serwerach aplikacji J2EE). Przetwarzanie typu pull umożliwia stopniowe (strumieniowe) przetwarzanie danych w formacie XML, kontrolowane przez aplikację. Przetwarzanie można wstrzymać w dowolnym momencie i wznowić, gdy aplikacja jest gotowa na obsługę dalszych danych wejściowych.
Przetwarzanie danych w formacie XML za pomocą interfejsu XmlPullParser Kod z listingu 11.16 przetwarza dokument XML obejmujący listę receptur z tej książki, opisaną w recepturach 13.2 i 11.14. Plik z danymi wejściowymi zawiera jeden element główny, recipes, który obejmuje zestaw elementów recipe z elementami id oraz title z zawartością tekstową. Listing 11.16. Stosowanie parsera typu pull public static ArrayList
11.15. Przetwarzan e dokumentów w formac e XML z wykorzystan em nterfejsu XmlPullParser
| 409
while (eventType != XmlPullParser.END_DOCUMENT) { if (eventType == XmlPullParser.START_TAG) { currentTag = xpp.getName(); } else if (eventType == XmlPullParser.TEXT) { if ("id".equals(currentTag)) { id = Integer.valueOf(xpp.getText()); } if ("title".equals(currentTag)) { title = xpp.getText(); } } else if (eventType == XmlPullParser.END_TAG) { if ("recipe".equals(xpp.getName())) { results.add(new Datum(id, title)); } } eventType = xpp.next(); } return results; }
Najpierw aplikacja tworzy obiekt XmlPullParserFactory przez wywołanie jego statycznej metody newInstance(). Metoda ta sprawdza, czy w ścieżce klas nie znajdują się gotowe obiekty XmlPullParserFactory i XmlPullParser. Jeśli takie obiekty nie istnieją, metoda zgłasza wyjątek XmlPullParserException. Obiekt XmlPullParser można utworzyć przez wywołanie metody fabrycznej newPullParser(). Następnie, w metodzie setInput(InputStream inputStream, String input Encoding), należy przekazać adres URL listy receptur. Wywołanie metody setInput powoduje przywrócenie początkowego stanu parsera i ustawienie typu zdarzenia na pierwotną wartość (START_DOCUMENT). Warto zauważyć, że — inaczej niż w recepturach 13.2 i 11.14 — nie trzeba najpierw pobierać zawartości dokumentu o danym adresie URL za pomocą metody converse. Przetwarzanie danych wejściowych w formacie XML za pomocą interfejsu XmlPullParser polega na przetwarzaniu zdarzeń parsera. Oto typy prostych zdarzeń: START_DOCUMENT, END_DOCUMENT, START_TAG, END_TAG i TEXT (warto zauważyć, że odpowiadają one wywoływanym zwrotnie metodom do obsługi zdarzeń w parserach SAX). Po przekazaniu adresu URL do metody setInput() można rozpocząć przetwarzanie zdarzeń. Typ pierwszego zdarzenia to START_DOCUMENT. Dane wejściowe są przetwarzane do momentu napotkania znacznika END_DOCUMENT. Aby przejść do następnego zdarzenia, należy wywołać metodę next(). Za pomocą metody nextToken() można przetworzyć większą liczbę zdarzeń, jednak technika ta wykracza poza zakres tej receptury. Kod przechodzi między kolejnymi zdarzeniami aż do napotkania znacznika START_TAG. Wtedy za pomocą metody getName() pobierana jest nazwa elementu. Jeśli przetwarzanie przestrzeni nazw jest wyłączone, metoda ta zwraca samą nazwę. Jest ona zapisywana w zmiennej lokalnej currentTag jako część ścieżki. Jeżeli element początkowy obejmuje atrybuty, można je wyodrębnić za pomocą metody getAttributeValue(String namespace, String name), jednak także to zagadnienie wykracza poza zakres receptury. Po zapisaniu nazwy aplikacja przechodzi w pętli do następnego zdarzenia. Po wystąpieniu zdarzenia TEXT aplikacja sprawdza, czy zmienna currentTag ma wartość id lub title. Jeśli tak jest, należy pobrać tekst przez wywołanie metody getText() i przypisać go do odpowiedniej zmiennej lokalnej. Cały proces jest powtarzany do momentu napotkania zdarzenia END_TAG dla elementu recipe. Wtedy aplikacja tworzy nowy obiekt Datum z wcześniej utworzonymi zmiennymi id i title.
410
|
Rozdz ał 11. Utrwalan e danych
Wersja ze sprawdzaniem poprawności Można zmodyfikować metodę przetwarzającą dokument, aby sprawdzała poprawność kodu. Na listingu 11.17 zastosowano metodę require() do sprawdzania, czy struktura dokumentu XML jest zgodna z oczekiwaniami. Po dojściu do zdarzeń START_TAG dla elementów id i title aplikacja wywołuje metodę nextText(), aby pobrać tekst odpowiedniego elementu, i przeskakuje do następnego zdarzenia END_TAG. Listing 11.17. Przetwarzanie ze sprawdzaniem poprawności public static ArrayList
Obie wersje zwracają te same dane. W kodzie źródłowym receptury pobrana lista obiektów Datum służy do zapełnienia listy w aktywności ListActivity. Kliknięcie elementu na liście pozwala przejść do strony z wybraną recepturą.
Przetwarzanie statycznych zasobów XML-owych Za pomocą interfejsu XmlPullParser można łatwo przetwarzać statyczne zasoby w formacie XML. Wystarczy za pomocą metody getResources() klasy Context wywołać metodę getXml(), aby otrzymać obiekt XmlResourceParser. Obiekt ten obejmuje implementację interfejsu XmlPull Parser, a także metodę pomocniczą ułatwiającą zamykanie zasobów z danymi wejściowymi. Techniki opisane w tej recepturze można więc zastosować także do przetwarzania statycznych zasobów w formacie XML!
11.15. Przetwarzan e dokumentów w formac e XML z wykorzystan em nterfejsu XmlPullParser
|
411
Podsumowanie XmlPullParser to parser wybierany przez wielu programistów — przede wszystkim z uwagi na jego prostotę. Jeśli chcesz przyspieszyć pracę aplikacji, wybierz parser SAX. Parsery oparte na modelu DOM są mniej więcej dwukrotnie wolniejsze od parserów SAX. Parser XmlPullParser jest mniej więcej w połowie między tymi skrajnościami. Pamiętaj, aby umieścić uprawnienia android.permission.INTERNET w pliku Android Manifest.xml. Jeśli go nie dodasz, aplikacja nie będzie mogła łączyć się z internetem.
Zobacz także Receptura 13.2, receptura 11.14, receptura 4.11, http://developer.android.com/reference/org/xmlpull/ v1/XmlPullParser.html, http://developer.android.com/reference/org/xmlpull/v1/XmlPullParserFactory. html, http://developer.android.com/reference/android/content/res/XmlResourceParser.html.
11.16. Dodawanie danych kontaktowych Ian Darwin
Problem Programista chce zapisywać dane kontaktowe, aby były dostępne dla aplikacji Contacts i innych programów z urządzenia.
Rozwiązanie Należy przygotować listę operacji wsadowego wstawiania danych i nakazać wykonanie tych zadań menedżerowi utrwalania danych.
Omówienie Baza danych aplikacji Contacts bez wątpienia jest “elastyczna”. Musi współdziałać z różnymi rodzajami kont, sposobami zarządzania danymi kontaktowymi i typami danych. Z tego powodu jest stosunkowo skomplikowana. W obecnej wersji klasy z rodziny Contacts (a tym samym i wszystkie ich klasy wewnętrzne oraz interfejsy) są uznawane za przestarzałe, co oznacza, że nie należy ich stosować w nowych programach. Zalecane klasy i interfejsy mają nazwy rozpoczynające się od (nieco dziwnego i trudnego do wymówienia) członu ContactsContract.
Na początku omówiono najprostszą sytuację — dodawanie danych kontaktowych dotyczących pewnej osoby. Aplikacja ma umożliwiać wstawianie następujących informacji (pobieranych od użytkownika lub z sieci):
412
|
Rozdz ał 11. Utrwalan e danych
Name
Jan Kowalski
Home Phone
416-555-5555
Work Phone
416-555-6666
[email protected]
Najpierw trzeba ustalić, z którym kontem Androida aplikacja ma powiązać dane. Tu użyto fikcyjnej nazwy konta, darwinian (jest to moje imię i nazwisko, a także słowo oznaczające “darwinowski” w języku angielskim). Dla każdego z czterech pól potrzebna jest operacja na koncie. Wszystkie pięć operacji należy zapisać w obiekcie List, który następnie przekazywany jest do metody getContentResolver().applyBatch(). Na listingu 11.18 pokazano kod metody addContact(). Listing 11.18. Metoda addContact() private void addContact() { final String ACCOUNT_NAME = "darwinian"; String name = "Jan Kowalski"; String homePhone = "416-555-5555"; String workPhone = "416-555-6666"; String email = "[email protected]"; // Operacje wsadowe w nowym stylu — tworzenie listy operacji i wywoływanie metody applyBatch try { ArrayList
11.16. Dodawan e danych kontaktowych
|
413
ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE) .withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, workPhone).withValue( ContactsContract.CommonDataKinds.Phone.TYPE, ContactsContract.CommonDataKinds.Phone.TYPE_WORK) .build()); ops.add(ContentProviderOperation.newInsert( ContactsContract.Data.CONTENT_URI).withValueBackReference( ContactsContract.Data.RAW_CONTACT_ID, 0).withValue( ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE) .withValue(ContactsContract.CommonDataKinds.Email.DATA, email) .withValue(ContactsContract.CommonDataKinds.Email.TYPE, ContactsContract.CommonDataKinds.Email.TYPE_HOME) .build()); getContentResolver().applyBatch(ContactsContract.AUTHORITY, ops); Toast.makeText(this, getString(R.string.addContactSuccess), Toast.LENGTH_LONG).show(); } catch (Exception e) { Toast.makeText(this, getString(R.string.addContactFailure), Toast.LENGTH_LONG).show(); Log.e(LOG_TAG, getString(R.string.addContactFailure), e); } }
Utworzone dane kontaktowe pojawiają się w aplikacjach Contacts (rysunek 11.5) i People. Jeśli są niewidoczne, przejdź do głównej strony aplikacji Contacts, wciśnij Menu, wybierz opcję Display options, a następnie znajdź grupę z nowymi danymi. Inna możliwość to wyszukiwanie danych wśród wszystkich danych kontaktowych.
Rysunek 11.5. Nowe dane kontaktowe
414
|
Rozdz ał 11. Utrwalan e danych
11.17. Wczytywanie danych kontaktowych Ian Darwin
Problem Programista chce pobrać szczegółowe informacje (np. numer telefonu lub adres e-mail) z bazy aplikacji Contacts.
Rozwiązanie Aby umożliwić użytkownikowi wybranie jednej osoby z listy kontaktów, należy zastosować intencję. Za pomocą obiektu ContentResolver trzeba utworzyć zapytanie do bazy SQLite dotyczące wybranej osoby. Następnie przy użyciu bazy SQLite i predefiniowanych stałych z klasy o dziwnej nazwie, ContactContract, można pobrać potrzebne informacje. Należy pamiętać, że bazę aplikacji Contacts zaprojektowano nie pod kątem łatwości używania, ale tak, aby była uniwersalna.
Omówienie Kod z listingu 11.19 pochodzi z napisanej przeze mnie aplikacji TabbyText na tablety (służy ona do wysyłania wiadomości SMS). Kod uruchamiany jest po tym, gdy użytkownik wybierze już osobę z listy kontaktów (za pomocą aplikacji Contacts; zobacz recepturę 5.2). Przedstawiony fragment pobiera numer telefonu komórkowego i wyświetla go w polu tekstowym bieżącej aktywności. Użytkownik przed wysłaniem wiadomości SMS może w razie potrzeby zmienić numer lub nawet go odrzucić. Dlatego po znalezieniu numeru aplikacja wyświetla go w kontrolce EditText. Listing 11.19. Pobieranie danych kontaktowych z obiektu ContentResolver z zapytania z intencji @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == REQ_GET_CONTACT) { switch(resultCode) { case Activity.RESULT_OK: // Interfejs API aplikacji Contacts jest trudny w użyciu. // Najpierw trzeba uzyskać dane kontaktowe, ponieważ // intencja zwraca tylko identyfikator URI Uri resultUri = data.getData(); // Na przykład content //contacts/people/123 Cursor cont = getContentResolver().query(resultUri, null, null, null, null); if (!cont.moveToNext()) { // Oczekiwany jest jeden wiersz Toast.makeText(this, "Brak danych w kursorze", Toast.LENGTH_LONG).show(); return; } int columnIndexForId = cont.getColumnIndex(ContactsContract.Contacts._ID); String contactId = cont.getString(columnIndexForId); int columnIndexForHasPhone = cont.getColumnIndex(ContactsContract.Contacts.HAS_PHONE_NUMBER);
11.17. Wczytywan e danych kontaktowych
|
415
boolean hasAnyPhone = Boolean.parseBoolean(cont.getString(columnIndexForHasPhone)); if (!hasAnyPhone) { Toast.makeText(this, "Brak numeru telefonu w danych wybranej osoby ", Toast.LENGTH_LONG).show(); } // Teraz trzeba przesłać następne zapytanie, aby pobrać numery Cursor numbers = getContentResolver().query( ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null, ContactsContract.CommonDataKinds.Phone.CONTACT_ID + "=" + contactId, // "selection", null, null); // Można też pobierać tylko numer telefonu komórkowego while (numbers.moveToNext()) { String aNumber = numbers.getString(numbers.getColumnIndex( ContactsContract.CommonDataKinds.Phone.NUMBER)); System.out.println(aNumber); number.setText(aNumber); } if (cont.moveToNext()) { System.out.println("UWAGA: Zwrócono dane więcej niż jednej osoby!"); } numbers.close(); cont.close(); break; case Activity.RESULT_CANCELED: // Nie trzeba wykonywać żadnych operacji break; default: Toast.makeText(this, "Nieoczekiwana wartość resultCode: " + resultCode, Toast.LENGTH_LONG).show(); break; } } super.onActivityResult(requestCode, resultCode, data); }
Wyszukiwanie numeru to stosunkowo skomplikowana operacja. Najpierw trzeba pobrać zapytanie od dostawcy treści, aby wyodrębnić pole z identyfikatorem danej osoby. Informacje w rodzaju numeru telefonu i adresu e-mail znajdują się w określonych tabelach, dlatego potrzebne jest też drugie zapytanie, w którym identyfikator jest używany w członie SELECT. Zapytanie to zwraca numery telefonu danej osoby. Należy przejść po uzyskanych wartościach, pobrać każdy poprawny numer telefonu i zapisać go w kontrolce EditText. W bardziej dopracowanej wersji można pobierać tylko numer telefonu komórkowego. Aplikacja Contacts umożliwia wprowadzenie zarówno domowego, jak i firmowego numeru faksu, ale tylko jednego numeru telefonu komórkowego — przynajmniej w wersji Honeycomb (Android 3.2).
416
|
Rozdz ał 11. Utrwalan e danych
ROZDZIAŁ 12.
Aplikacje do obsługi połączeń telefonicznych
12.1. Wprowadzenie — aplikacje do obsługi połączeń telefonicznych Ian Darwin
Omówienie Android powstał jako platforma dla telefonów komórkowych, dlatego nie jest zaskoczeniem, że aplikacje na Android doskonale potrafią obsługiwać połączenia telefoniczne. Można pisać aplikacje, które nawiązują połączenia telefoniczne lub sugerują użytkownikowi, aby on to zrobił. Aplikacje mogą sprawdzać lub modyfikować numer wybierany przez użytkownika (na przykład przez dodanie numeru kierunkowego). Ponadto można wysyłać i odbierać wiadomości SMS — oczywiście jeśli urządzenie jest wyposażone w funkcje telefonu. Obecnie wiele tabletów z Androidem obsługuje tylko sieci Wi-Fi i nie współdziała z sieciami 3G (a nawet 2G), przez co nie umożliwia nawiązywania połączeń telefonicznych i wysyłania wiadomości SMS. W takich urządzeniach trzeba korzystać z innych rozwiązań, na przykład z przesyłania wiadomości przez internet oraz telefonii VoIP (ang. Voice over IP; zwykle wykorzystuje się tu protokół SIP). W tym rozdziale omówiono większość zagadnień z tego obszaru. Niektóre z pozostałych kwestii opisano w innych miejscach książki.
417
12.2. Wykonywanie operacji w momencie, gdy dzwoni telefon Johan Pelgrim
Problem Aplikacja ma wykonywać operacje na numerze osoby, która wykonała odbierane połączenie.
Rozwiązanie Można zaimplementować odbiornik rozgłoszeniowy i odbierać akcję TelephonyManager.ACTION_
PHONE_STATE_CHANGED.
Omówienie Jeśli aplikacja ma wykonywać określone operacje w momencie, gdy dzwoni telefon, należy zaimplementować odbiornik rozgłoszeniowy (ang. broadcast receiver), który odbiera akcję TelephonyManager.ACTION_PHONE_STATE_CHANGED intencji. Jest to rozgłoszeniowa akcja intencji oznaczająca zmianę stanu połączenia telefonicznego w urządzeniu. Na listingu 12.1 przedstawiono kod do przechwytywania odbieranych połączeń, a na listingu 12.2 znajduje się kod manifestu aplikacji. Listing 12.1. Kod przechwytujący przychodzące połączenia package nl.codestone.cookbook.incomingcallinterceptor; import import import import import
android.content.BroadcastReceiver; android.content.Context; android.content.Intent; android.telephony.TelephonyManager; android.widget.Toast;
public class IncomingCallInterceptor extends BroadcastReceiver {
n
@Override public void onReceive(Context context, Intent intent) { o String state = intent.getStringExtra(TelephonyManager.EXTRA_STATE);p String msg = "Zmiana stanu połączenia na: " + state; if (TelephonyManager.EXTRA_STATE_RINGING.equals(state)) { String incomingNumber = intent.getStringExtra (TelephonyManager.EXTRA_INCOMING_NUMBER);r msg += ". Numer rozmówcy: " + incomingNumber;
q
// Tu należy umieścić operacje, które aplikacja ma wykonywać w momencie, gdy dzwoni telefon } Toast.makeText(context, msg, Toast.LENGTH_LONG).show(); } }
418
|
Rozdz ał 12. Apl kacje do obsług połączeń telefon cznych
n Tworzenie klasy IncomingCallInterceptor, pochodnej od BroadcastReceiver. o Przesłanianie metody onReceive, tak aby obsługiwała odbierane komunikaty rozgłoszeniowe. p Dodatkowe dane intencji, czyli EXTRA_STATE, tu określają nowy stan połączenia. q Tylko w przypadku, gdy nowy stan to RINGING, inne dodatkowe dane intencji, czyli EXTRA_ INCOMING_NUMBER, udostępniają w formie łańcucha znaków numer, z którego wykonane zostało połączenie. r Aplikacja wyodrębnia numer z dodatkowych danych intencji, czyli EXTRA_INCOMING_NUMBER. Ponadto można wykonywać operacje przy zmianie stanu na OFFHOOK lub IDLE, kiedy to użytkownik odbiera telefon lub kończy (albo odrzuca) połączenie.
Listing 12.2. Plik manifestu aplikacji do przechwytywania przychodzących połączeń
q
n W elemencie
12.2. Wykonywan e operacj w momenc e, gdy dzwon telefon
|
419
Rysunek 12.1. Przechwytywanie połączeń przychodzących
Co się dzieje, jeśli dwa odbiorniki rejestrują zmiany stanu telefonu? Ogólnie komunikat rozgłoszeniowy jest, jak wskazuje na to nazwa, rozsyłany jednocześnie do wielu odbiorników. Jest tak przy standardowym rozgłaszaniu, stosowanym także dla intencji z akcją ACTION_PHONE_STATE_CHANGED. Wszystkie odbiorniki komunikatów rozgłoszeniowych są uruchamiane w nieokreślonej kolejności (często jednocześnie), dlatego nie można wyznaczyć ich porządku. Inna możliwość to rozgłaszanie uporządkowane, opisane szczegółowo w recepturze 12.3.
Końcowe uwagi Jeśli odbiornik BroadcastReceiver nie zakończy pracy w ciągu 10 sekund, Android wyświetli niesławne okno dialogowe ANR (ang. Application Not Responding) i umożliwi użytkownikowi zamknięcie programu. Jeżeli aplikacja ma przetwarzać dane przez dłużej niż 10 sekund, należy zaimplementować usługę (obiekt Service) i wywołać jej metodę. Nie zaleca się też uruchamiania w odbiornikach BroadcastReceiver aktywności, ponieważ prowadzi to do wyświetlenia nowego ekranu, zasłaniającego aktywność, z której akurat użytkownik korzysta. Jeśli aplikacja w reakcji na odebranie intencji rozgłoszeniowej ma wyświetlać informacje użytkownikowi, należy zrobić to za pomocą menedżera powiadomień.
Zobacz także Receptura 12.3, http://developer.android.com/reference/android/content/BroadcastReceiver.html, http:// developer.android.com/reference/android/telephony/TelephonyManager.html#ACTION_PHONE_ STATE_CHANGED.
420 |
Rozdz ał 12. Apl kacje do obsług połączeń telefon cznych
12.3. Przetwarzanie wychodzących połączeń telefonicznych Johan Pelgrim
Problem Aplikacja ma blokować niektóre połączenia lub zmieniać wybierany numer.
Rozwiązanie Należy odbierać akcję Intent.ACTION_NEW_OUTGOING_CALL i ustawiać dane wynikowe w odbiorniku rozgłoszeniowym na nowy numer.
Omówienie Aby przechwytywać połączenia przed ich nawiązaniem, należy zaimplementować odbiornik rozgłoszeniowy i odbierać akcję Intent.ACTION_NEW_OUTGOING_CALL. Ta receptura przypomina recepturę 12.2, jest jednak ciekawsza, ponieważ pozwala manipulować numerem telefonu! Oto potrzebne kroki. Kod przedstawiono na listingu 12.3. n Należy utworzyć klasę OutgoingCallInterceptor, pochodną od BroadcastReceiver. o Trzeba przesłonić metodę onReceive. p Z dodatkowych danych intencji, czyli Intent.EXTRA_PHONE_NUMBER, należy pobrać numer telefonu wybrany przez użytkownika. q Numer należy zastąpić przez wywołanie metody setResultData. Nowy numer trzeba podać jako argument typu String. Po zakończeniu obsługi intencji dane wynikowe są używane jako wybierany numer. Jeśli dane wynikowe to null, urządzenie w ogóle nie nawiąże połączenia! Listing 12.3. Klasa przechwytująca połączenia wychodzące (odbiornik typu BroadcastReceiver) package nl.codestone.cookbook.outgoingcallinterceptor; import import import import
android.content.BroadcastReceiver; android.content.Context; android.content.Intent; android.widget.Toast;
public class OutgoingCallInterceptor extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { final String oldNumber = intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER); this.setResultData("0123456789"); final String newNumber = this.getResultData(); String msg = "Przechwycony numer wychodzący. Dawny numer: " +
n o p q
12.3. Przetwarzan e wychodzących połączeń telefon cznych
|
421
oldNumber + ", nowy numer: " + newNumber; Toast.makeText(context, msg, Toast.LENGTH_LONG).show(); } }
Na listingu 12.4 pokazano plik AndroidManifest.xml aplikacji do przechwytywania połączeń wychodzących. n Rejestrowanie klasy OutgoingCallInterceptor jako odbiornika (element
n o p
q
Teraz próba wybrania numeru 11111 jest przekierowywana pod numer 0123456789 (rysunek 12.2).
Co się dzieje, jeśli połączenia wychodzące są przetwarzane przez dwa odbiorniki? Dla intencji Intent.ACTION_NEW_OUTGOING_CALL stosowane jest rozgłaszanie uporządkowane. Jest to intencja chroniona, wysyłana tylko przez system. Komunikaty rozsyłane za pomocą rozgłaszania uporządkowanego w porównaniu z komunikatami rozgłaszanymi standardowo mają trzy dodatkowe cechy. Opisano je poniżej. • Za pomocą atrybutu android:priority elementu
danego odbiornika w mechanizmie obsługi komunikatów. Atrybut ten to liczba całkowita określająca, który odbiornik ma wyższy priorytet przy przetwarzaniu odbieranych komunikatów rozgłoszeniowych. Im wyższa liczba, tym wyższy priorytet i tym wcześniej odbiornik przetwarza komunikat.
422 |
Rozdz ał 12. Apl kacje do obsług połączeń telefon cznych
Rysunek 12.2. Przechwycone połączenie wychodzące • Za pomocą metody setResultData można przekazać wynik do następnego odbiornika. • Można całkowicie anulować rozsyłanie intencji, tak aby nie była ona przekazywana do
innych odbiorników. Efekt ten można uzyskać przez wywołanie metody abortBroadcast(). Warto zauważyć, że zgodnie z interfejsem API odbiornik BroadcastReceiver otrzymujący intencję Intent.ACTION_NEW_OUTGOING_CALLE nie może anulować rozsyłania intencji za pomocą wywołania metody abortBroadcast(). Anulowanie intencji nie prowadzi do zgłaszania błędów, jednak niektóre odbiorniki systemowe nadal będą oczekiwały na określony komunikat rozgłoszeniowy. Za pomocą opisanego mechanizmu nie można przechwytywać połączeń z numerami alarmowymi ani zmieniać innych numerów na numery alarmowe. Dopuszczalne jest natomiast, aby różne odbiorniki przetwarzały po kolei połączenia wychodzące. Na przykład w aplikacji do kontrolowania użytkowników można sprawdzać, czy dana osoba może wykonać telefon w danym momencie, a następnie aplikacja do zmiany numerów może dodać numer kierunkowy, jeśli go nie podano. Jeżeli dwa odbiorniki mają tę samą wartość atrybutu android:priority, są (według interfejsu API) uruchamiane w nieokreślonej kolejności. Jednak wydaje się, że jeżeli oba są wymienione w jednym pliku AndroidManifest.xml, kolejność ich definicji wpływa na porządek, w jakim odbierają komunikaty rozgłoszeniowe. Natomiast jeśli dwa odbiorniki o tej samej wartości atrybutu android:priority są zdefiniowane w różnych plikach AndroidManifest.xml (czyli pochodzą z różnych aplikacji), wydaje się, że odbiornik z aplikacji instalowanej jako pierwsza jest też rejestrowany jako pierwszy i wcześniej przetwarza komunikat. Nie należy jednak zakładać, że zawsze tak będzie! Jeśli chcesz mieć pewność, że odbiornik jako pierwszy będzie przetwarzał komunikat, możesz zastosować maksymalną wartość całkowitoliczbową (2147483647). Choć według interfejsu API
12.3. Przetwarzan e wychodzących połączeń telefon cznych
| 423
nawet to nie gwarantuje, że dany odbiornik zostanie uruchomiony jako pierwszy, znacznie zwiększa jednak prawdopodobieństwo wystąpienia takiego efektu. Możliwe, że inne aplikacje przechwycą numer telefonu przed zadziałaniem danego odbiornika. Jeśli chcesz wykonywać operacje na pierwotnym numerze telefonu, możesz w opisany wcześniej sposób wykorzystać dodatkowe dane intencji, czyli EXTRA_PHONE_NUMBER, i pominąć wartości zwrócone przez poprzednie odbiorniki. Jeżeli natomiast aplikacja ma kontynuować przetwarzanie w miejscu, w którym zakończył je poprzedni odbiornik, należy pobrać pośredni numer telefonu za pomocą metody getResultData(). Aby aplikacja działała poprawnie, każdy odbiornik, który ma blokować połączenia, powinien mieć priorytet 0. Dzięki temu taki odbiornik będzie miał dostęp do ostatecznego numeru telefonu. Odbiorniki modyfikujące numer telefonu powinny mieć dodatni priorytet. Ujemne priorytety są w tym przypadku zarezerwowane dla systemu. Korzystanie z nich może prowadzić do problemów.
Zobacz także Receptura 12.2, http://developer.android.com/reference/android/content/Intent.html#ACTION_NEW_ OUTGOING_CALL.
12.4. Wybieranie numeru telefonu Ian Darwin
Problem Programista chce wybierać numer telefonu z poziomu aplikacji bez konieczności obsługi nawiązywania połączenia.
Rozwiązanie Należy uruchomić intencję, aby wybrać numer.
Omówienie Jedną z zalet Androida jest łatwość wykorzystywania w programach innych aplikacji za pomocą intencji. Nie trzeba przy tym znać szczegółów działania owych innych aplikacji (nie trzeba nawet znać ich nazwy). Na przykład aby wybrać numer telefonu, wystarczy utworzyć i uruchomić intencję z akcją DIAL i identyfikatorem URI obejmującym człon tel oraz pożądany numer. Dlatego podstawowy kod do wybierania połączeń jest bardzo prosty, co pokazano na listingu 12.5. Listing 12.5. Prosta aktywność do wybierania numeru telefonu public class Main extends Activity { String phoneNumber = "555-1212"; String intentStr = "tel:" + phoneNumber;
424 |
Rozdz ał 12. Apl kacje do obsług połączeń telefon cznych
/** Standardowe wywołanie zwrotne onCreate. * Tu tylko wybierany jest numer telefonu */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); Intent intent = new Intent("android.intent.action.DIAL", Uri.parse(intentStr)); startActivity(intent); } }
Aby móc uruchomić ten kod, trzeba uzyskać uprawnienia android.permission.CALL_PHONE. Aplikacja wyświetla ekran widoczny na rysunku 12.3. Użytkownicy wiedzą, że aby nawiązać połączenie, muszą wcisnąć zielony przycisk.
Rysunek 12.3. Prosta aplikacja do wybierania numeru
W praktyce zwykle numer nie jest zapisany na stałe — użytkownik wybiera go na przykład z listy z aplikacji Contacts.
12.5. Wysyłanie jedno- lub wieloczęściowych wiadomości SMS Colin Wilcox
Problem Programista szuka prostego sposobu na wysyłanie jedno- lub wieloczęściowych wiadomości SMS z jednego miejsca.
12.5. Wysyłan e jedno- lub w eloczęśc owych w adomośc SMS
| 425
Rozwiązanie Należy zastosować obiekt SmsManager.
Omówienie Wiadomości SMS (ang. Short Message Service), nazywane też wiadomościami tekstowymi, od lat są częścią świata telefonii komórkowej. Interfejs API Androida umożliwia wysyłanie takich wiadomości za pomocą intencji lub w kodzie. Tu opisano tylko tę drugą możliwość. Wiadomości SMS mają do około 160 znaków (dokładna wartość zależy od operatora), co może wyjaśniać, skąd wziął się limit 140 znaków w wiadomościach na Twitterze. Dłuższe wiadomości tekstowe trzeba dzielić na części. Można to kontrolować za pomocą klasy SmsManager, która pozwala podzielić tekst na fragmenty i zwraca ich listę. Jeśli część jest tylko jedna, oznacza to, że wiadomość jest wystarczająco krótka, aby wysłać ją bezpośrednio. Dlatego aplikacja wywołuje wtedy metodę sendTextMessage(). Dłuższe wiadomości wymagają przesłania listy fragmentów, dlatego listę tę należy przekazać do metody sendMultipartTextMessage(). Kod do wysyłania wiadomości przedstawiono na listingu 12.6. W pakiecie z kodem źródłowym znajdziesz ponadto prostą aktywność, która wywołuje wspomniany kod. Choć wiadomość jest wysyłana w trzech częściach, trafia do odbiorcy jako jedna całość, co pokazano na rysunku 12.4.
Rysunek 12.4. Odebrana wieloczęściowa wiadomość Listing 12.6. Kod do wysyłania wiadomości SMS package com.example.sendsms; import java.util.ArrayList; import android.telephony.SmsManager;
426 |
Rozdz ał 12. Apl kacje do obsług połączeń telefon cznych
import android.util.Log; /** Kod do obsługi menedżera wiadomości SMS * wywoływany z poziomu interfejsu GUI */ public class SendSMS { static String TAG = "SendSMS"; SmsManager mSMSManager = null; /* Lista fragmentów, na które wiadomość * jest dzielona przez obiekt SmsManager */ ArrayList
Nie jest zaskoczeniem, że opisywana aplikacja wymaga ustawienia uprawnień android.permis sion.SEND_SMS w pliku AndroidManifest.xml.
Zobacz także Informacje na temat klasy SmsManager znajdziesz na stronie http://developer.android.com/reference/ android/telephony/SmsManager.html. Aby dowiedzieć się, jak „na zapleczu” przebiega podział dłuższych wiadomości na części, zajrzyj na stronę http://en.wikipedia.org/wiki/Concatenated_SMS.
12.5. Wysyłan e jedno- lub w eloczęśc owych w adomośc SMS
|
427
12.6. Odbieranie wiadomości SMS w aplikacjach na Android Rachee Singh
Problem Programista chce umożliwić odbieranie wiadomości SMS w aplikacji.
Rozwiązanie Należy zastosować odbiornik rozgłoszeniowy do odbierania przychodzących wiadomości SMS, a następnie wyodrębniać takie wiadomości.
Omówienie W momencie odebrania wiadomości przez urządzenie z Androidem zgłaszana jest intencja rozgłoszeniowa (obejmuje ona także samą wiadomość SMS). W aplikacji można zarejestrować odbiornik takich intencji. Akcja w omawianych intencjach to android.provider.Telephony.SMS_ RECEIVED. W manifeście aplikacji, która ma odbierać wiadomości SMS, należy zażądać uprawnień RECEIVE_SMS:
Po odebraniu wiadomości wywoływana jest (przesłaniana) metoda onReceive(). W metodzie tej można przetworzyć wiadomość. Aby wyodrębnić wiadomość SMS z odebranej intencji, należy wywołać metodę get(). Na listingu 12.7 przedstawiono odbiornik typu BroadcastReceiver z kodem służącym do wyodrębniania wiadomości. Listing 12.7. Odbiornik BroadcastReceiver do odbierania wiadomości SMS public class InvitationSmsReceiver extends BroadcastReceiver { public void onReceive(Context context, Intent intent) { Bundle bundle = intent.getExtras(); SmsMessage[] msgs = null; String message = ""; if(bundle != null) { Object[] pdus = (Object[]) bundle.get("pdus"); msgs = new SmsMessage[pdus.length]; for(int i=0; i
428 |
Rozdz ał 12. Apl kacje do obsług połączeń telefon cznych
Kod odbiornika generuje komunikat toast z zawartością odebranej wiadomości SMS. Aby zarejestrować, że klasa InvitationSmsReceiver ma odbierać wiadomości SMS, należy umieścić w manifeście następujący kod:
12.7. Wysyłanie wiadomości SMS do emulatora za pomocą okna Emulator Control Rachee Singh
Problem Aby możliwe było interaktywne przetestowanie aplikacji odbierającej wiadomości SMS przed jej zainstalowaniem, konieczna jest możliwość wysyłania takich wiadomości do emulatora.
Rozwiązanie Wysyłanie wiadomości SMS do emulatora umożliwia okno Emulator Control w perspektywie DDMS środowiska Eclipse.
Omówienie Aby sprawdzić, czy aplikacja reaguje na przychodzące wiadomości SMS, trzeba przesłać taką wiadomość do emulatora. Umożliwia to perspektywa DDMS środowiska Eclipse. Warto powiększyć okno Emulator Control, ponieważ w przeciwnym razie jego ważne elementy mogą być ukryte (dostęp do nich wymaga przewinięcia okna w pionie i poziomie). W zakładce Emulator Control znajdź pole Telephony actions i wprowadź numer telefonu. Może to być dowolny numer. Będzie on wyświetlany jako numer nadawcy wiadomości. Następnie zaznacz przycisk opcji SMS i w polu Message wprowadź wiadomość, którą chcesz wysłać. W ostatnim kroku kliknij przycisk Send pod tekstem wiadomości (rysunek 12.5).
12.7. Wysyłan e w adomośc SMS do emulatora za pomocą okna Emulator Control
| 429
Rysunek 12.5. Wysyłanie wiadomości SMS za pomocą okna Emulator Control
12.8. Korzystanie z androidowej klasy TelephonyManager do pobierania informacji o urządzeniu Pratik Rupwal
Problem Programista chce uzyskać informacje związane z siecią i połączeniem telefonicznym.
Rozwiązanie Należy zastosować standardową klasę TelephonyManager Androida do pobrania różnych danych na temat stanu sieci i połączenia telefonicznego.
Omówienie Klasa TelephonyManager Androida udostępnia informacje na temat połączeń telefonicznych Androida. Pomaga uzyskać dane na temat lokalizacji komórki, numeru IMEI (ang. International Mobile Equipment Identity), dostawcy sieci itd. Program z listingu 12.8 jest dość długi, ale przedstawiono w nim większość możliwości androidowej klasy TelephonyManager. We własnych aplikacjach prawdopodobnie nie będziesz potrzebował wszystkich tych funkcji, jednak tu połączono je w jeden kompletny program. 430 |
Rozdz ał 12. Apl kacje do obsług połączeń telefon cznych
Listing 12.8. Przykładowa aktywność do określania stanu telefonu ... import import import import import import
android.telephony.CellLocation; android.telephony.NeighboringCellInfo; android.telephony.PhoneStateListener; android.telephony.ServiceState; android.telephony.TelephonyManager; android.telephony.gsm.GsmCellLocation;
public class PhoneStateSample extends Activity { private private private private private
static static static static static
final final final final final
String APP_NAME = "SignalLevelSample"; int EXCELLENT_LEVEL = 75; int GOOD_LEVEL = 50; int MODERATE_LEVEL = 25; int WEAK_LEVEL = 0;
// Te stałe służą do zapisywania w tablicy wyświetlanych łańcuchów znaków private static final int INFO_SERVICE_STATE_INDEX = 0; private static final int INFO_CELL_LOCATION_INDEX = 1; private static final int INFO_CALL_STATE_INDEX = 2; private static final int INFO_CONNECTION_STATE_INDEX = 3; private static final int INFO_SIGNAL_LEVEL_INDEX = 4; private static final int INFO_SIGNAL_LEVEL_INFO_INDEX = 5; private static final int INFO_DATA_DIRECTION_INDEX = 6; private static final int INFO_DEVICE_INFO_INDEX = 7; // To identyfikatory wyświetlanych łańcuchów znaków; muszą być zgodne z // przedstawionymi wcześniej stałymi private static final int[] info_ids= { R.id.serviceState_info, R.id.cellLocation_info, R.id.callState_info, R.id.connectionState_info, R.id.signalLevel, R.id.signalLevelInfo, R.id.dataDirection, R.id.device_info }; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); startSignalLevelListener(); displayTelephonyInfo(); } @Override protected void onPause() { super.onPause(); stopListening(); } @Override protected void onResume() { super.onResume(); startSignalLevelListener(); }
12.8. Korzystan e z andro dowej klasy TelephonyManager do pob eran a nformacj o urządzen u
|
431
@Override protected void onDestroy() { stopListening(); super.onDestroy(); } private void setTextViewText(int id,String text) { ((TextView)findViewById(id)).setText(text); } private void setSignalLevel(int id,int infoid,int level){ int progress = (int) ((((float)level)/31.0) * 100); String signalLevelString =getSignalLevelString(progress); ((ProgressBar)findViewById(id)).setProgress(progress); ((TextView)findViewById(infoid)).setText(signalLevelString); Log.i("signalLevel ","" + progress); } private String getSignalLevelString(int level) { String signalLevelString = "Niska"; if(level > EXCELLENT_LEVEL) signalLevelString = "Doskonała"; else if(level > GOOD_LEVEL) signalLevelString = "Dobra"; else if(level > MODERATE_LEVEL) signalLevelString = "Przeciętna"; else if(level > WEAK_LEVEL) signalLevelString= "Niska"; return signalLevelString; } private void stopListening(){ TelephonyManager tm = (TelephonyManager) getSystemService(TELEPHONY_SERVICE); tm.listen(phoneStateListener, PhoneStateListener.LISTEN_NONE); } private void setDataDirection(int id, int direction){ int resid = getDataDirectionRes(direction); ((ImageView)findViewById(id)).setImageResource(resid); } private int getDataDirectionRes(int direction){ int resid = R.drawable.data_none; switch(direction) { case TelephonyManager.DATA_ACTIVITY_IN: resid = R.drawable.data_in; break; case TelephonyManager.DATA_ACTIVITY_OUT: resid = R.drawable.data_out; break; case TelephonyManager.DATA_ACTIVITY_INOUT: resid = R.drawable.data_both; break; case TelephonyManager.DATA_ACTIVITY_NONE: resid = R.drawable.data_none; break; default: resid = R.drawable.data_none; break; } return resid; } private void startSignalLevelListener() { TelephonyManager tm = (TelephonyManager) getSystemService(TELEPHONY_SERVICE); int events = PhoneStateListener.LISTEN_SIGNAL_STRENGTH | PhoneStateListener.LISTEN_DATA_ACTIVITY | PhoneStateListener.LISTEN_CELL_LOCATION| PhoneStateListener.LISTEN_CALL_STATE |
432 |
Rozdz ał 12. Apl kacje do obsług połączeń telefon cznych
PhoneStateListener.LISTEN_CALL_FORWARDING_INDICATOR | PhoneStateListener.LISTEN_DATA_CONNECTION_STATE | PhoneStateListener.LISTEN_MESSAGE_WAITING_INDICATOR | PhoneStateListener.LISTEN_SERVICE_STATE; tm.listen(phoneStateListener, events); } ...
Za dużą część zbierania informacji odpowiadają w tym programie różne odbiorniki. Wyjątkiem jest metoda displayTelephonyInfo() (listing 12.9), która bezpośrednio pobiera liczne informacje z obiektu TelephonyManager i dodaje je do długiego łańcucha znaków, wyświetlanego następnie w kontrolce TextView. Listing 12.9. Aktywność do określania stanu telefonu (ciąg dalszy) ... private void displayTelephonyInfo(){ TelephonyManager tm = (TelephonyManager)getSystemService(TELEPHONY_SERVICE); GsmCellLocation loc = (GsmCellLocation)tm.getCellLocation(); int cellid = loc.getCid(); int lac = loc.getLac(); String deviceid = tm.getDeviceId(); String phonenumber = tm.getLine1Number(); String softwareversion = tm.getDeviceSoftwareVersion(); String operatorname = tm.getNetworkOperatorName(); String simcountrycode = tm.getSimCountryIso(); String simoperator = tm.getSimOperatorName(); String simserialno = tm.getSimSerialNumber(); String subscriberid = tm.getSubscriberId(); String networktype = getNetworkTypeString(tm.getNetworkType()); String phonetype = getPhoneTypeString(tm.getPhoneType()); logString("CID: " + cellid); logString("LAC: " + lac); logString("ID urządzenia: " + deviceid); logString("Numer telefonu: " + phonenumber); logString("Wersja oprogramowania: " + softwareversion); logString("Nazwa operatora: " + operatorname); logString("Kod kraju karty SIM: " + simcountrycode); logString("Operator karty SIM: " + simoperator); logString("Numer karty SIM: " + simserialno); logString("ID karty: " + subscriberid); String deviceinfo = ""; deviceinfo += ("CID: " + cellid + "\n"); deviceinfo += ("LAC: " + lac + "\n"); deviceinfo += ("ID urządzenia: " + deviceid + "\n"); deviceinfo += ("Numer telefonu: " + phonenumber + "\n"); deviceinfo += ("Wersja oprogramowania: " + softwareversion + "\n"); deviceinfo += ("Nazwa operatora: " + operatorname + "\n"); deviceinfo += ("Kod kraju karty SIM: " + simcountrycode + "\n"); deviceinfo += ("Operator karty SIM: " + simoperator + "\n"); deviceinfo += ("Numer karty SIM: " + simserialno + "\n"); deviceinfo += ("ID karty: " + subscriberid + "\n"); deviceinfo += ("Typ sieci: " + networktype + "\n"); deviceinfo += ("Typ telefonu: " + phonetype + "\n"); List
12.8. Korzystan e z andro dowej klasy TelephonyManager do pob eran a nformacj o urządzen u
| 433
} setTextViewText(info_ids[INFO_DEVICE_INFO_INDEX],deviceinfo); } private String getNetworkTypeString(int type) { String typeString = "BRAK DANYCH"; switch(type) { case TelephonyManager.NETWORK_TYPE_EDGE: typeString = "EDGE"; break; case TelephonyManager.NETWORK_TYPE_GPRS: typeString = "GPRS"; break; case TelephonyManager.NETWORK_TYPE_UMTS: typeString = "UMTS"; break; default: typeString = "BRAK DANYCH"; break; } return typeString; } private String getPhoneTypeString(int type){ String typeString = "BRAK DANYCH"; switch(type) { case TelephonyManager.PHONE_TYPE_GSM: typeString = "GSM"; break; case TelephonyManager.PHONE_TYPE_NONE: typeString = "BRAK DANYCH"; break; default:typeString = "BRAK DANYCH"; break; } return typeString; } private int logString(String message) { return Log.i(APP_NAME,message); } private final PhoneStateListener phoneStateListener = new PhoneStateListener(){ @Override public void onCallForwardingIndicatorChanged(boolean cfi) { Log.i(APP_NAME, "onCallForwardingIndicatorChanged " +cfi); super.onCallForwardingIndicatorChanged(cfi); } @Override public void onCallStateChanged(int state, String incomingNumber) { String callState = "BRAK DANYCH"; switch(state) { case TelephonyManager.CALL_STATE_IDLE: callState = "BEZCZYNNOŚĆ"; break; case TelephonyManager.CALL_STATE_RINGING: callState = "DZWONI NUMER " + incomingNumber; break; case TelephonyManager.CALL_STATE_OFFHOOK: callState = "ZAJĘTY"; break; } setTextViewText(info_ids[INFO_CALL_STATE_INDEX],callState); Log.i(APP_NAME, "onCallStateChanged " + callState); super.onCallStateChanged(state, incomingNumber);
434 |
Rozdz ał 12. Apl kacje do obsług połączeń telefon cznych
} @Override public void onCellLocationChanged(CellLocation location) { String locationString = location.toString(); setTextViewText( info_ids[INFO_CELL_LOCATION_INDEX],locationString); Log.i(APP_NAME, "onCellLocationChanged " + locationString); super.onCellLocationChanged(location); } @Override public void onDataActivity(int direction) { String directionString = "BRAK"; switch (direction) { case TelephonyManager.DATA_ACTIVITY_IN: directionString = "ODBIERANIE"; break; case TelephonyManager.DATA_ACTIVITY_OUT: directionString = "WYSYŁANIE"; break; case TelephonyManager.DATA_ACTIVITY_INOUT: directionString = "OBA KIERUNKI"; break; case TelephonyManager.DATA_ACTIVITY_NONE: directionString = "BRAK"; break; default: directionString = "BRAK DANYCH: " + direction; break; } setDataDirection(info_ids[INFO_DATA_DIRECTION_INDEX],direction); Log.i(APP_NAME, "onDataActivity " + directionString); super.onDataActivity(direction); } @Override public void onDataConnectionStateChanged(int state) { String connectionState = "BRAK DANYCH"; switch(state) { case TelephonyManager.DATA_CONNECTED: connectionState = "POŁĄCZONO"; break; case TelephonyManager.DATA_CONNECTING: connectionState = "ŁĄCZENIE"; break; case TelephonyManager.DATA_DISCONNECTED: connectionState = "ROZŁĄCZONO"; break; case TelephonyManager.DATA_SUSPENDED: connectionState = "WSTRZYMANO"; break; default: connectionState = "BRAK DANYCH: " + state; break; } setTextViewText( info_ids[INFO_CONNECTION_STATE_INDEX], connectionState); Log.i(APP_NAME, "onDataConnectionStateChanged " + connectionState); super.onDataConnectionStateChanged(state); } @Override public void onMessageWaitingIndicatorChanged(boolean mwi) { Log.i(APP_NAME, "onMessageWaitingIndicatorChanged " + mwi);
12.8. Korzystan e z andro dowej klasy TelephonyManager do pob eran a nformacj o urządzen u
| 435
super.onMessageWaitingIndicatorChanged(mwi); } @Override public void onServiceStateChanged(ServiceState serviceState) { String serviceStateString = "BRAK DANYCH"; switch(serviceState.getState()) { case ServiceState.STATE_IN_SERVICE: serviceStateString = "W SIECI"; break; case ServiceState.STATE_EMERGENCY_ONLY: serviceStateString = "TYLKO NUMERY ALARMOWE"; break; case ServiceState.STATE_OUT_OF_SERVICE: serviceStateString = "POZA SIECIĄ"; break; case ServiceState.STATE_POWER_OFF: serviceStateString = "WYŁĄCZONE ZASILANIE"; break; default: serviceStateString = "BRAK DANYCH"; break; } setTextViewText( info_ids[INFO_SERVICE_STATE_INDEX], serviceStateString); Log.i(APP_NAME, "onServiceStateChanged " + serviceStateString); super.onServiceStateChanged(serviceState); } @Override public void onSignalStrengthChanged(int asu) { Log.i(APP_NAME, "onSignalStrengthChanged " + asu); setSignalLevel(info_ids[INFO_SIGNAL_LEVEL_INDEX], info_ids[INFO_SIGNAL_LEVEL_INFO_INDEX],asu); super.onSignalStrengthChanged(asu); } }; }
Pokazany poniżej układ z pliku main.xml obejmuje kilka zagnieżdżonych układów LinearLayout i pozwala na eleganckie wyświetlenie wszystkich informacji zebranych przez przedstawiony wcześniej kod.
436 |
Rozdz ał 12. Apl kacje do obsług połączeń telefon cznych
W kodzie wykorzystano style dla interfejsu użytkownika. Style te zdefiniowano w pliku styles.xml:
12.8. Korzystan e z andro dowej klasy TelephonyManager do pob eran a nformacj o urządzen u
|
437
Omawiana aplikacja wymaga uprawnień do przybliżonego określania lokalizacji (na podstawie komórkowej sieci radiowej). Trzeba ich zażądać w pliku AndroidManifest.xml projektu:
Aplikacja ponadto wyświetla rysunki określające stan połączenia do transferu danych (brak połączenia, odbieranie danych, wysyłanie danych i komunikację obustronną). Nazwy plików z tymi rysunkami to data_none.png, data_in.png, data_out.png i data_both.png. Ikony o takich nazwach należy umieścić w katalogu res/drawable projektu.
12.8. Korzystan e z andro dowej klasy TelephonyManager do pob eran a nformacj o urządzen u
| 439
440 |
Rozdz ał 12. Apl kacje do obsług połączeń telefon cznych
ROZDZIAŁ 13.
Aplikacje sieciowe
13.1. Wprowadzenie — sieć Ian Darwin
Omówienie Sieci — można mówić o nich godzinami. W kontekście Androida najważniejsze w tym obszarze są usługi sieciowe, z których inne programy (aplikacje na Android) korzystają przez protokół HTTP. Są dwa rodzaje usług sieciowych — XML/SOAP i RESTful. Usługi sieciowe XML/ SOAP są bardziej „formalne”, dlatego wymagają więcej pracy (zarówno na etapie programowania, jak i w trakcie wykonywania programu). Oferują jednak większe możliwości. Usługi typu RESTful są znacznie prostsze i nie są powiązane z XML-em. W rozdziale przedstawiono receptury pokazujące, jak w usługach sieciowych korzystać z JSON-a (ang. JavaScript Object Notation) i innych formatów.
Wybór właściwego protokołu Choć Java pozwala na łatwe tworzenie połączeń sieciowych z wykorzystaniem dowolnego protokołu, z praktyki wynika, że najbardziej uniwersalnym z nich jest HTTP (i pokrewny protokół HTTPS). Jeśli korzystasz z niestandardowego protokołu do komunikacji z własnym serwerem, pamiętaj, że niektórzy użytkownicy nie będą mieli do niego dostępu. W niektórych krajach szybkie połączenia z transferem danych (sieci 3G) są albo niedostępne, albo bardzo drogie. Sieci GPRS i EDGE są tańsze i dostępne na większych obszarach. Większość dostawców usług GPRS obsługuje tylko połączenia HTTP i HTTPS, często z pośrednikiem WAP. Jednak niektórych operacji nie da się wykonać za pomocą protokołu HTTP — na przykład z uwagi na to, że protokół wymaga innego numeru portu (przykładowo w protokole SIP używa się portu 5000). Tam, gdzie to możliwe, należy jednak starać się stosować protokół HTTP, ponieważ pozwala to dotrzeć do większego grona użytkowników.
441
13.2. Stosowanie usług sieciowych typu RESTful Ian Darwin
Problem Programista potrzebuje usług sieciowych typu RESTful.
Rozwiązanie Można wykorzystać albo obiekty URL i URLConnection ze „standardowej” Javy, albo dostępną w Androidzie bibliotekę HttpClient Apache’a (pozwala ona pisać kod na wyższym poziomie ogólności), albo inne niż GET i POST metody protokołu HTTP.
Omówienie Architekturę REST opracowano z myślą o opisie architektury wczesnej sieci WWW, w której korzystano z żądań GET, a stan żądania w pełni określały (reprezentowały) adresy URL. Obecnie stosowane usługi sieciowe typu RESTful pozwalają uniknąć kosztów związanych z używaniem technologii XML SOAP, WSDL i XML Schema. Są one oparte na przesyłaniu adresów URL obejmujących wszystkie (lub prawie wszystkie — dla niektórych typów żądań przesyłane jest też ciało żądania POST) informacje potrzebne do obsługi żądania. Na przykład na potrzeby androidowego klienta umożliwiającego edycję receptur z tej książki powstała (niedopracowana) usługa sieciowa, która pozwala wyświetlić listę receptur (za pomocą żądania HTTP GET z końcówką /recipe/list), wyświetlić szczegółowe informacje o wybranej recepturze (za pomocą żądania HTTP GET z końcówką /recipe/NNN, gdzie NNN to klucz główny wpisu, pobrany z listy receptur), a także przesłać poprawioną wersję receptury. To ostatnie zadanie wykonywane jest za pomocą żądania HTTP POST z członem /recipe/NNN. Ciało żądania POST obejmuje tu poprawioną wersję receptury w dokumencie XML w tym samym formacie, w jakim receptury są pobierane. Przy okazji warto wspomnieć, że usługi typu RESTful stosowane w przykładowych aplikacjach są napisane w Javie za pomocą RestEasy (http://www.jboss.org/resteasy) (implementacji interfejsu API JAX-RS) i frameworku JBoss Seam (http://seamframework.org/).
Stosowanie klas URL i URLConnection Programiści Androida rozsądnie zachowali dużą część standardowego interfejsu API Javy, w tym często używane klasy do obsługi sieci. Ułatwia to przenoszenie gotowego kodu na Android. W przedstawionej na listingu 13.1 metodzie converse()do zgłaszania żądań GET wykorzystano klasy URL i URLConnection z pakietu java.net. Metoda ta pochodzi z przykładowego kodu z rozdziału poświęconego sieciom z napisanej przeze mnie książki Java Cookbook1 (wydawnictwo O’Reilly). W komentarzach wyjaśniono, co trzeba zrobić, aby zastosować żądanie POST.
1
Wydanie polskie: Ian Darwin, Java. Receptury, Helion, Gliwice 2003. — przyp. tłum.
442 |
Rozdz ał 13. Apl kacje s ec owe
Listing 13.1. Klient usługi sieciowej typu RESTful — wersja z klasą URLConnection public static String converse(String host, int port, String path) throws IOException { URL url = new URL("http", host, port, path); URLConnection conn = url.openConnection(); // Wykonuje żądanie GET. Aby wykonać żądanie POST, dodaj conn.setDoOutput(true); conn.setDoInput(true); conn.setAllowUserInteraction(true); // Niepotrzebne, ale nieszkodliwe conn.connect(); // W żądaniu POST należy zapisać dane do conn.getOutputStream()); StringBuilder sb = new StringBuilder(); BufferedReader in = new BufferedReader( new InputStreamReader(conn.getInputStream())); String line; while ((line = in.readLine()) != null) { sb.append(line); } in.close(); return sb.toString(); }
Wywołanie tej metody na przykład w metodzie onResume() lub onCreate() może być bardzo proste — tak jak w poniższym kodzie, który pobiera listę receptur z tej książki: String host = "androidcookbook.net"; String path = "/seam/resource/rest/recipe/list"; String ret = converse(host, 80, path);
Korzystanie z biblioteki HttpClient Android udostępnia bibliotekę HttpClient Apache’a, powszechnie stosowaną do komunikowania się na nieco wyższym poziomie ogólności niż przy korzystaniu z klasy URLConnection. Bibliotekę tę zastosowałem w rozwijanym przeze mnie frameworku do testowania aplikacji sieciowych, PageUnit (http://www.darwinsys.com/pageunit/). Biblioteka HttpClient umożliwia też stosowanie innych metod protokołu HTTP (takich jak PUT i DELETE), często używanych w usługach typu RESTful. Opisany wcześniej obiekt URLConnection obsługuje tylko metody GET i POST. Na listingu 13.2 przedstawiono wcześniejszą metodę converse (opartą na metodzie GET), napisaną jednak za pomocą biblioteki HttpClient. Listing 13.2. Klient usługi sieciowej typu RESTful — wersja z biblioteką HttpClient public static String converse(String host, int port, String path, String postBody) throws IOException { HttpHost target = new HttpHost(host, port); HttpClient client = new DefaultHttpClient(); HttpGet get = new HttpGet(path); HttpEntity results = null; try { HttpResponse response=client.execute(target, get); results = response.getEntity(); return EntityUtils.toString(results); } catch (Exception e) { throw new RuntimeException("Awaria usługi sieciowej"); } finally { if (results!=null) try {
13.2. Stosowan e usług s ec owych typu RESTful
| 443
results.consumeContent(); } catch (IOException e) { // Puste. Wystąpił wyjątek sprawdzany, ale aplikacja nic z nim nie robi } } }
Tę metodę można stosować w taki sam sposób jak wersję opartą na klasie URLConnection.
Zwracane dane W obecnej wersji usługa sieciowa zwraca dane w formie dokumentu XML. Trzeba go przetworzyć, aby wyświetlić dane na liście. Jeśli zainteresowanie będzie wystarczająco duże, możliwe, że w przyszłości dodana zostanie wersja zwracająca dane w formacie JSON. Pamiętaj, aby do pliku AndroidManifest.xml dodać żądania uprawnień android.per mission.INTERNET. Jeśli tego nie zrobisz, aplikacja nie będzie mogła nawiązać połączenia z internetem.
Zobacz także Receptura 11.14, receptura 9.1.
13.3. Używanie wyrażeń regularnych do wyodrębniania informacji z nieustrukturyzowanego tekstu Ian Darwin
Problem Programista chce pobierać informacje generowane przez inną organizację, która jednak nie udostępnia danych w określonym formacie, a tylko wyświetla je na stronie internetowej.
Rozwiązanie Należy zastosować pakiet java.net do pobrania strony HTML i użyć wyrażeń regularnych do wyodrębnienia informacji ze strony.
Omówienie Jeśli jeszcze nie jesteś miłośnikiem wyrażeń regularnych, to najwyższa pora, abyś się nim stał. Możliwe, że ta receptura zachęci Cię do poznania tego zagadnienia. Załóżmy, że opublikowałem książkę i chcę śledzić jej sprzedaż w porównaniu z innymi pozycjami. Potrzebne informacje można bezpłatnie uzyskać w większości witryn dużych księgarni na stronach poświęconych danej książce. Wymaga to odczytania z ekranu miejsca na liście 444 |
Rozdz ał 13. Apl kacje s ec owe
najpopularniejszych książek i zapisania odpowiedniej liczby w pliku. Jednak to rozwiązanie jest zbyt żmudne. W jednej z napisanych przeze mnie książek stwierdziłem, że „po to kupuje się komputery, aby wyodrębniały ważne informacje z plików — ludzie nie powinni musieć wykonywać tak żmudnych zadań”. W przedstawionym tu programie wykorzystano interfejs API wyrażeń regularnych, w tym dopasowywanie sekwencji nowego wiersza, co jest potrzebne do wyodrębnienia wartości ze strony HTML z witryny Amazon.com. Program wczytuje dane za pomocą obiektu URL (zobacz recepturę 13.2). Szukany wzorzec wygląda mniej więcej tak (warto pamiętać, że kod w HTML-u może się zmienić w dowolnym momencie, dlatego wzorzec powinien być jak najogólniejszy): (nazwa księgarni) Sales Rank: # 26,252
Ponieważ wzorzec może obejmować kilka wierszy, program wczytuje całą stronę o podanym adresie URL do jednego długiego łańcucha znaków (służy do tego prywatna metoda pomocnicza readerToString()), zamiast w tradycyjny sposób odczytywać dane wiersz po wierszu. Wartość jest wyodrębniana na podstawie wyrażenia regularnego, przekształcana na liczbę całkowitą i zwracana. Dłuższa wersja aplikacji (z książki Java Cookbook), generuje też wykres za pomocą zewnętrznego programu. Na listingu 13.3 przedstawiono fragment kodu omawianego tu programu. Listing 13.3. Fragment klasy BookRank public static int getBookRank(String isbn) throws IOException { // Wzorzec wyrażenia regularnego — dozwolone są cyfry i przecinki final String pattern = "Rank: #([\\d,]+)"; final Pattern r = Pattern.compile(pattern); // Adres url musi obejmować człon "isbn=" na końcu lub // umożliwiać dołączanie informacji w inny sposób final String url = "http://www.amazon.com/exec/obidos/ASIN/" + isbn; // Otwieranie strony o danym adresie URL i tworzenie dla niej obiektu Reader final BufferedReader is = new BufferedReader(new InputStreamReader( new URL(url).openStream())); // Wczytywanie strony i wyszukiwanie informacji o miejscu książki na liście. Strona // wczytywana jest jako jeden długi łańcuch znaków, co pozwala dopasować wyrażenie // regularne do wielu wierszy final String input = readerToString(is); // Jeśli znaleziono informacje, należy zapisać je w pliku z danymi o sprzedaży Matcher m = r.matcher(input); if (m.find()) { // Grupa 1. to dopasowane cyfry (i ewentualnie przecinki). Przecinki należy usunąć return Integer.parseInt(m.group(1).replace(",","")); } else { throw new RuntimeException( "Nie znaleziono wzorca na stronie '" + url + "'!"); } }
Zobacz także Jak wspomniano, stosowanie interfejsu API wyrażeń regularnych jest niezwykle istotne przy korzystaniu z częściowo ustrukturyzowanych danych, z którymi będziesz stykał się w praktyce. W rozdziale 4. książki Java Cookbook, napisanej przeze mnie i wydanej przez wydawnictwo 13.3. Używan e wyrażeń regularnych do wyodrębn an a nformacj z n eustrukturyzowanego tekstu
| 445
O’Reilly, znajdziesz wszystkie potrzebne informacje o wyrażeniach regularnych. Kompletne omówienie tego zagadnienia zawiera też książka Mastering Regular Expressions2 Jeffreya Friedla (też wydana przez wydawnictwo O’Reilly).
13.4. Przetwarzanie danych z kanałów RSS i Atom za pomocą parsera ROME Wagied Davids
Problem Programista chce przetwarzać dane z kanałów RSS i Atom. Dane tego rodzaju służą do udostępniania aktualnych list nowych artykułów z witryny i często towarzyszy im ikona informacji: .
Rozwiązanie W tej recepturze opisano parser danych z kanałów RSS i Atom, wykorzystujący oparty na Javie parser ROME (https://rometools.jira.com/wiki/display/ROME/Home). Omawiany parser udostępnia kilka ciekawych funkcji, np. warunkowe żądania HTTP GET, znaczniki ETag i kompresję w formacie Gzip. Ponadto obsługuje różnorodne formaty, w tym RSS 0.90, RSS 2.0 i Atom 0.3 oraz 1.0.
Omówienie Oto podstawowe kroki, które należy wykonać:
1. Dodaj poniższy kod do pliku AndroidManifest.xml, aby umożliwić przeglądanie zasobów internetu:
2. Pobierz odpowiednie pliki JAR — rome-0.9.jar i jdom-1.0.jar. 3. Utwórz projekt na Android. W pliku układu zapisz kod z listingu 13.4. Listing 13.4. Plik main.xml
Wydanie polskie: Jeffrey Friedl, Wyrażenia regularne, Helion, Gliwice 2001. — przyp. tłum.
446 |
Rozdz ał 13. Apl kacje s ec owe
android:stretchColumns="0">
4. Utwórz aktywność z kodem z listingu 13.5. W metodzie getRSS() pokazano, jak za pomocą interfejsu API parsera ROME przetwarzać XML-owe dane z kanału RSS i wyświetlać wyniki.
Listing 13.5. Plik AndroidRss.java import import import import import import
java.io.IOException; java.net.MalformedURLException; java.net.URL; java.util.ArrayList; java.util.Iterator; java.util.List;
import import import import import import import import import
android.app.Activity; android.os.Bundle; android.util.Log; android.view.View; android.view.View.OnClickListener; android.widget.AdapterView; android.widget.ArrayAdapter; android.widget.Button; android.widget.EditText;
13.4. Przetwarzan e danych z kanałów RSS Atom za pomocą parsera ROME
|
447
import android.widget.ListView; import android.widget.Toast; import android.widget.AdapterView.OnItemClickListener; import import import import import
com.sun.syndication.feed.synd.SyndEntry; com.sun.syndication.feed.synd.SyndFeed; com.sun.syndication.io.FeedException; com.sun.syndication.io.SyndFeedInput; com.sun.syndication.io.XmlReader;
public class AndroidRss extends Activity { private static final String tag="AndroidRss "; private int selectedItemIndex = 0; private final ArrayList list = new ArrayList(); private EditText text; private ListView listView; private Button goButton; private Button clearButton; private ArrayAdapter adapter = null; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); text = (EditText) this.findViewById(R.id.rssURL); goButton = (Button) this.findViewById(R.id.goButton); goButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { String rss = text.getText().toString().trim(); getRSS(rss); } }); clearButton = (Button) this.findViewById(R.id.clearButton); clearButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { adapter.clear(); adapter.notifyDataSetChanged(); } }); listView = (ListView) this.findViewById(R.id.ListView); listView.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView parent, View view, int position, long duration) { selectedItemIndex = position; Toast.makeText(getApplicationContext(), "Wybrano " + adapter.getItem(position) + " @ " + position, Toast.LENGTH_SHORT).show(); } });
448 |
Rozdz ał 13. Apl kacje s ec owe
adapter = new ArrayAdapter(this, R.layout.dataview, R.id.ListItemView); listView.setAdapter(adapter); } private void getRSS(String rss) { URL feedUrl; try { Log.d("DEBUG", "Adres: " + rss); feedUrl = new URL(rss); SyndFeedInput input = new SyndFeedInput(); SyndFeed feed = input.build(new XmlReader(feedUrl)); List entries = feed.getEntries(); Toast.makeText(this, "Liczba wiadomości: " + entries.size(), Toast.LENGTH_SHORT).show(); Iterator iterator = entries.listIterator(); while (iterator.hasNext()) { SyndEntry ent = (SyndEntry) iterator.next(); String title = ent.getTitle(); adapter.add(title); } adapter.notifyDataSetChanged(); } catch (MalformedURLException e) { e.printStackTrace(); } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (FeedException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } private void clearTextFields() { Log.d(tag, "clearTextFields()"); this.text.setText(""); } }
Jeśli uruchomisz program dla adresu URL http://rss.cbc.ca/lineup/topstories.xml, dane wyjściowe powinny wyglądać tak jak na rysunku 13.1 (choć wiadomości będą oczywiście nowsze).
13.4. Przetwarzan e danych z kanałów RSS Atom za pomocą parsera ROME
| 449
Rysunek 13.1. Wiadomości z kanału RSS w kontrolce ListView
13.5. Korzystanie ze skrótów MD5 do przetwarzania zwykłego tekstu Colin Wilcox
Problem Czasem zwykły tekst przed zapisaniem lub przesłaniem trzeba przekształcić na postać nieczytelną.
Rozwiązanie Android udostępnia standardową klasę MD5 Javy, która umożliwia zastąpienie zwykłego tekstu jego skrótem MD5. Skrót ten powstaje przy użyciu jednostronnego szyfru, uznawanego za trudny do odwrócenia (jeśli chcesz uzyskać taki efekt, zastosuj pakiet Javy przeznaczony do kryptografii).
Omówienie Na listingu 13.6 przedstawiono prostą funkcję, która przyjmuje zwykły tekst, przetwarza go za pomocą algorytmu MD5, a następnie zwraca zaszyfrowany łańcuch znaków.
450 |
Rozdz ał 13. Apl kacje s ec owe
Listing 13.6. Tworzenie skrótu MD5 public static String md5(String s) { try { // Tworzenie obiektu do generowania skrótów MD5 MessageDigest digest = java.security.MessageDigest.getInstance("MD5"); digest.update(s.getBytes()); byte messageDigest[] = digest.digest(); // Tworzenie szesnastkowego łańcucha znaków StringBuffer hexString = new StringBuffer(); for (int i = 0; i < messageDigest.length; i++) { hexString.append(Integer.toHexString(0xFF & messageDigest[i])); } return hexString.toString(); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } return ""; // Lub zgłaszanie wyjątku }
13.6. Przekształcanie tekstu na odnośniki Rachee Singh
Problem Programista chce przekształcić adresy URL stron internetowych na odnośniki w kontrolce TextView w aplikacji na Android.
Rozwiązanie Zastosuj właściwość autoLink kontrolki TextView.
Omówienie Programista umieścił adres URL www.google.com w tekście wyświetlanym w kontrolce TextView i chce przy tym, aby adres miał postać odnośnika, którego kliknięcie pozwoli użytkownikowi otworzyć stronę w przeglądarce. Aby uzyskać taki efekt, należy dodać do kontrolki TextView właściwość autoLink: android:autoLink = "all"
Teraz w kodzie aktywności można do kontrolki TextView przypisać dowolny tekst, a wszystkie adresy URL zostaną przekształcone na odnośniki. linkText = (TextView)findViewById(R.id.link); linkText.setText("Odnośnik to www.google.com");
13.6. Przekształcan e tekstu na odnośn k
|
451
13.7. Dostęp do stron internetowych za pomocą kontrolki WebView Rachee Singh
Problem Programista chce umożliwić pobieranie i wyświetlanie stron internetowych w aplikacji.
Rozwiązanie Aby wczytać i wyświetlić stronę internetową, należy umieścić w układzie standardową kontrolkę WebView oraz wywołać jej metodę loadUrl().
Omówienie WebView to kontrolka pochodna od View. Można ją stosować w aktywnościach. Jej głównym przeznaczeniem jest (jak wskazuje nazwa, czyli „widok internetowy”) obsługa stron internetowych.
Ponieważ kontrolka WebView zwykle wymaga dostępu do stron internetowych z zewnętrznych serwerów, należy pamiętać o dodaniu do pliku manifestu żądania uprawnień dostępu do internetu:
Następnie kontrolkę WebView można dodać do układu XML:
452 |
Rozdz ał 13. Apl kacje s ec owe
android:layout_height="fill_parent" android:layout_width="fill_parent"/>
W napisanym w Javie kodzie aktywności, który wyświetla daną stronę internetową, należy pobrać uchwyt do kontrolki WebView. Służy do tego metoda findViewById(). Następnie można wywołać metodę loadUrl() kontrolki WebView, aby określić adres URL strony, którą aplikacja ma otworzyć. WebView webview = (WebView)findViewById(R.id.webview); webview.loadUrl("http://google.com");
13.8. Modyfikowanie wyglądu kontrolki WebView Rachee Singh
Problem Programista chce zmodyfikować wygląd kontrolki WebView używanej w aplikacji.
Rozwiązanie Należy użyć klasy WebSettings, tak aby uzyskać dostęp do wbudowanych funkcji służących do zmieniania wyglądu przeglądarki.
Omówienie W recepturze 13.7 opisano, że do otwierania stron internetowych w aplikacjach na Android służy kontrolka WebView. Aby w kontrolce WebView wczytać stronę o danym adresie URL, należy zastosować następującą instrukcję: webview.loadUrl("http://www.google.com/");
Przeglądarkę można na wiele sposobów dostosować do potrzeb użytkowników. Aby zmodyfikować kontrolkę, należy utworzyć obiekt WebSettings, który można pobrać z kontrolki WebView: WebSettings webSettings = webView.getSettings();
Oto niektóre operacje, które można wykonać za pomocą obiektu WebSettings: • Można nakazać kontrolce WebView blokowanie grafiki z internetu: webSettings.setBlockNetworkImage(true);
• Można ustawić domyślną czcionkę przeglądarki: webSettings.setDefaultFontSize(25);
• Można określić, czy kontrolka WebView ma obsługiwać przybliżanie: webSettings.setSupportZoom(true);
• W kontrolce WebView można włączyć obsługę JavaScriptu: webSettings.setJavaScriptEnabled(true);
• Można określić, czy kontrolka WebView ma zapisywać hasła: webSettings.setSavePassword(false);
13.8. Modyf kowan e wyglądu kontrolk WebV ew
| 453
• Można określić, czy kontrolka WebView ma zachowywać dane z formularzy: webSettings.setSaveFormData(false);
Dostępnych jest też wiele innych podobnych metod. Więcej informacji znajdziesz w serwisie Android Developers na stronie poświęconej temu zagadnieniu.
454 |
Rozdz ał 13. Apl kacje s ec owe
ROZDZIAŁ 14.
Gry i animacje
14.1. Wprowadzenie — gry i animacje Ian Darwin
Omówienie Gry są ważnym powodem, dla którego ludzie korzystają z komputerów, a obecnie także z urządzeń przenośnych. Android zapewnia duże możliwości w zakresie grafiki, ponieważ obsługuje specyfikację OpenGL ES. Jeśli zamierzasz wykorzystać zaawansowane funkcje z obszaru gier, a przy tym nie chcesz pisać dużych ilości kodu, masz szczęście — obecnie dostępnych jest wiele frameworków do rozwijania gier. Większość z nich przeznaczona jest głównie lub wyłącznie dla komputerów stacjonarnych. Frameworki wymienione w tabeli 14.1 działają także w Androidzie. Jeśli znasz inne narzędzia tego rodzaju, dodaj, proszę, komentarz do internetowej wersji tej procedury (http://androidcookbook.com/r/1816). Dodamy to narzędzie w internetowej wersji książki, a także do jej przyszłych papierowych wydań. Tabela 14.1. Frameworki do tworzenia gier na Android Nazwa
Otwarty dostęp do kodu źródłowego?
Cena
Adres URL
AndEngine
T
Bezpłatny
http://www andengine org
Box2D
T
Bezpłatny
http://code google com/p/box2d/
Co ona SDK
?
Od 199 dola ów ocznie
http://www anscamobile com/corona/
Flixel
T
Bezpłatny
http://flixel org/index html
libgdx
T
Bezpłatny
http://code google com/p/libgdx/
PlayN
T
Bezpłatny
http://code google com/p/playn
okon
T
Bezpłatny
http://code google com/p/rokon/
ShiVa 3D
N
Od 169 eu o za edyto i se we
http://www stonetrip com/
Unity
N
Od 400 dola ów
http://unity3d com/unity/publishing/android html
455
Przed podjęciem decyzji o zastosowaniu w projekcie określonego frameworku warto porównać funkcje poszczególnych narzędzi.
14.2. Tworzenie gier na Android za pomocą frameworku flixel-android Wagied Davids
Problem Programista chce utworzyć grę na Android za pomocą wysokopoziomowego frameworku.
Rozwiązanie Można zastosować Flixel (http://flixel.org/index.html), czyli framework do tworzenia gier oparty na ActionScripcie, opracowany przez Adama „Atomica” Saltsmana.
Omówienie Dzięki olbrzymiej pracy wykonanej przez Wing Erasera powstała oparta na Javie wersja narzędzia (http://code.google.com/p/flixel-android/), która jest bardzo podobna w użyciu do napisanego w AS3 frameworku Flixel. W tej recepturze zobaczysz, jak utworzyć prostą grę platformową. Obejmuje ona kilka obiektów, droida i kilka wind. Każdy obiekt jest zadeklarowany jako odrębna klasa z własnymi zasobami i odbiornikami zdarzeń związanych z cyfrowym touchpadem. Na listingu 14.1 pokazano kod aktywności z gry opartej na Flixelu. Listing 14.1. Aktywność z gry opartej na Flixelu import import import import import
android.app.Activity; android.content.pm.ActivityInfo; android.os.Bundle; android.view.Window; android.view.WindowManager;
public class Main extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_NO_TITLE); getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); // Orientacja // setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
456 |
Rozdz ał 14. Gry an macje
setContentView(new GameView(this, R.class)); } }
Na listingu 14.2 znajduje się kod widoku z gry opartej na Flixelu. Listing 14.2. Widok z gry opartej na Flixelu import org.flixel.FlxGame; import org.flixel.FlxGameView; import android.content.Context; public class GameView extends FlxGameView { public GameView(Context context, Class resource) { super(new FlxGame(400, 240, SimpleJumper.class, context, resource), context); } }
Sprite to mały obiekt graficzny (np. postać gracza w grze komputerowej), przenoszony po ekranie w aplikacjach z grafiką. Na listingu 14.3 pokazano kod klasy Sprite, utworzonej z wykorzystaniem Flixela. Listing 14.3. Plik Droid.java z implementacją interfejsu FlxSprite import org.flixel.FlxG; import org.flixel.FlxSound; import org.flixel.FlxSprite; public class Droid extends FlxSprite { private final FlxSound sound = new FlxSound(); public Droid(int X, int Y) { super(X, Y); loadGraphic(R.drawable.player, true, true); maxVelocity.x = 100; // Szybkość poruszania się acceleration.y = 400; // Grawitacja drag.x = maxVelocity.x * 4; // Zwalnianie (przy zatrzymywaniu się) // Modyfikowanie obramowania, tak aby zwiększyć atrakcyjność gry width = 8; height = 10; offset.x = 3; offset.y = 3; addAnimation("idle", new int[] { 0 }, 0, false); addAnimation("walk", new int[] { 1, 2, 3, 0 }, 12); addAnimation("walk_back", new int[] { 3, 2, 1, 0 }, 10, true); addAnimation("flail", new int[] { 1, 2, 3, 0 }, 18, true); addAnimation("jump", new int[] { 4 }, 0, false); } @Override public void update() { // Płynne sterowanie chodzeniem acceleration.x = 0; if (FlxG.dpad.pressed("LEFT")) acceleration.x -= drag.x;
14.2. Tworzen e g er na Andro d za pomocą frameworku fl xel-andro d
|
457
if (FlxG.dpad.pressed("RIGHT")) acceleration.x += drag.x; if (onFloor) { // Sterowanie skokiem if (FlxG.dpad.justTouched("UP")) { sound.loadEmbedded(R.raw.jump); sound.play(); velocity.y = -acceleration.y * 0.51f; play("jump"); }// Animacje else if (velocity.x > 0) { play("walk"); } else if (velocity.x < 0) { play("walk_back"); } else play("idle"); } else if (velocity.y < 0) play("jump"); else play("flail"); // Domyślne aktualizowanie fizycznych aspektów obiektów super.update(); } }
14.3. Tworzenie gry na Android za pomocą narzędzia AndEngine (Android-Engine) Wagied Davids
Problem Programista chce zaprojektować grę na Android za pomocą frameworku AndEngine.
Rozwiązanie AndEngine (http://www.andengine.org/) to framework do tworzenia silników gier, przeznaczony do rozwijania gier na Android. Pierwotną wersję narzędzia opracował Nicholas Gramlich. Obecnie framework obejmuje wiele zaawansowanych funkcji, pozwalających tworzyć fantastyczne gry.
Omówienie Na potrzeby tej receptury napisałem prostą grę w bilard z obsługą praw fizyki (uwzględniane są na przykład wskazania akcelometru) i zdarzeń dotykowych. Dotknięcie konkretnej bili i prze-
458 |
Rozdz ał 14. Gry an macje
ciągnięcie palcem powoduje uderzenie kuli w inne bile. Gra odpowiednio obsługuje zderzenia kul. Na listingu 14.4 pokazano kod aktywności z gry opartej na frameworku AndEngine. Listing 14.4. Aktywność z gry opartej na frameworku AndEngine import import import import import import import import import import import import import import import import import import import import import import import import import import import import
org.anddev.andengine.engine.Engine; org.anddev.andengine.engine.camera.Camera; org.anddev.andengine.engine.options.EngineOptions; org.anddev.andengine.engine.options.EngineOptions.ScreenOrientation; org.anddev.andengine.engine.options.resolutionpolicy.RatioResolutionPolicy; org.anddev.andengine.entity.Entity; org.anddev.andengine.entity.primitive.Rectangle; org.anddev.andengine.entity.scene.Scene; org.anddev.andengine.entity.scene.Scene.IOnAreaTouchListener; org.anddev.andengine.entity.scene.Scene.IOnSceneTouchListener; org.anddev.andengine.entity.scene.Scene.ITouchArea; org.anddev.andengine.entity.shape.Shape; org.anddev.andengine.entity.sprite.AnimatedSprite; org.anddev.andengine.entity.sprite.Sprite; org.anddev.andengine.entity.util.FPSLogger; org.anddev.andengine.extension.physics.box2d.PhysicsConnector; org.anddev.andengine.extension.physics.box2d.PhysicsFactory; org.anddev.andengine.extension.physics.box2d.PhysicsWorld; org.anddev.andengine.extension.physics.box2d.util.Vector2Pool; org.anddev.andengine.input.touch.TouchEvent; org.anddev.andengine.opengl.texture.Texture; org.anddev.andengine.opengl.texture.TextureOptions; org.anddev.andengine.opengl.texture.region.TextureRegion; org.anddev.andengine.opengl.texture.region.TextureRegionFactory; org.anddev.andengine.opengl.texture.region.TiledTextureRegion; org.anddev.andengine.sensor.accelerometer.AccelerometerData; org.anddev.andengine.sensor.accelerometer.IAccelerometerListener; org.anddev.andengine.ui.activity.BaseGameActivity;
import android.hardware.SensorManager; import android.util.DisplayMetrics; import import import import
com.badlogic.gdx.math.Vector2; com.badlogic.gdx.physics.box2d.Body; com.badlogic.gdx.physics.box2d.BodyDef.BodyType; com.badlogic.gdx.physics.box2d.FixtureDef;
public class SimplePool extends BaseGameActivity implements IAccelerometerListener, IOnSceneTouchListener, IOnAreaTouchListener { private private private private private private private private private private private
Camera mCamera; Texture mTexture; Texture mBallYellowTexture; Texture mBallRedTexture; Texture mBallBlackTexture; Texture mBallBlueTexture; Texture mBallGreenTexture; Texture mBallOrangeTexture; Texture mBallPinkTexture; Texture mBallPurpleTexture; Texture mBallWhiteTexture;
private TiledTextureRegion mBallYellowTextureRegion; private TiledTextureRegion mBallRedTextureRegion; private TiledTextureRegion mBallBlackTextureRegion;
14.3. Tworzen e gry na Andro d za pomocą narzędz a AndEng ne (Andro d-Eng ne)
| 459
private private private private private private
TiledTextureRegion TiledTextureRegion TiledTextureRegion TiledTextureRegion TiledTextureRegion TiledTextureRegion
mBallBlueTextureRegion; mBallGreenTextureRegion; mBallOrangeTextureRegion; mBallPinkTextureRegion; mBallPurpleTextureRegion; mBallWhiteTextureRegion;
private Texture mBackgroundTexture; private TextureRegion mBackgroundTextureRegion; private PhysicsWorld mPhysicsWorld; private float mGravityX; private float mGravityY; private Scene mScene; private final int mFaceCount = 0; private final int CAMERA_WIDTH = 720; private final int CAMERA_HEIGHT = 480; @Override public Engine onLoadEngine() { DisplayMetrics dm = new DisplayMetrics(); getWindowManager().getDefaultDisplay().getMetrics(dm); this.mCamera = new Camera(0, 0, CAMERA_WIDTH, CAMERA_HEIGHT); return new Engine(new EngineOptions(true, ScreenOrientation.LANDSCAPE, new RatioResolutionPolicy(CAMERA_WIDTH, CAMERA_HEIGHT), this.mCamera)); } @Override public void onLoadResources() { this.mTexture = new Texture(64, 64, TextureOptions.BILINEAR_PREMULTIPLYALPHA); this.mBallBlackTexture = new Texture(64, 64, TextureOptions.BILINEAR_PREMULTIPLYALPHA); this.mBallBlueTexture = new Texture(64, 64, TextureOptions.BILINEAR_PREMULTIPLYALPHA); this.mBallGreenTexture = new Texture(64, 64, TextureOptions.BILINEAR_PREMULTIPLYALPHA); this.mBallOrangeTexture = new Texture(64, 64, TextureOptions.BILINEAR_PREMULTIPLYALPHA); this.mBallPinkTexture = new Texture(64, 64, TextureOptions.BILINEAR_PREMULTIPLYALPHA); this.mBallPurpleTexture = new Texture(64, 64, TextureOptions.BILINEAR_PREMULTIPLYALPHA); this.mBallYellowTexture = new Texture(64, 64, TextureOptions.BILINEAR_PREMULTIPLYALPHA); this.mBallRedTexture = new Texture(64, 64, TextureOptions.BILINEAR_PREMULTIPLYALPHA); this.mBallWhiteTexture = new Texture(64, 64, TextureOptions.BILINEAR_PREMULTIPLYALPHA); TextureRegionFactory.setAssetBasePath("gfx/"); mBallYellowTextureRegion = TextureRegionFactory.createTiledFromAsset(this.mBallYellowTexture, this, "ball_yellow.png", 0, 0, 1, 1); // 64x32 mBallRedTextureRegion =
460 |
Rozdz ał 14. Gry an macje
TextureRegionFactory.createTiledFromAsset(this.mBallRedTexture, this, "ball_red.png", 0, 0, 1, 1); // 64x32 mBallBlackTextureRegion = TextureRegionFactory.createTiledFromAsset(this.mBallBlackTexture, this, "ball_black.png", 0, 0, 1, 1); // 64x32 mBallBlueTextureRegion = TextureRegionFactory.createTiledFromAsset(this.mBallBlueTexture, this, "ball_blue.png", 0, 0, 1, 1); // 64x32 mBallGreenTextureRegion = TextureRegionFactory.createTiledFromAsset(this.mBallGreenTexture, this, "ball_green.png", 0, 0, 1, 1); // 64x32 mBallOrangeTextureRegion = TextureRegionFactory.createTiledFromAsset(this.mBallOrangeTexture, this, "ball_orange.png", 0, 0, 1, 1); // 64x32 mBallPinkTextureRegion = TextureRegionFactory.createTiledFromAsset(this.mBallPinkTexture, this, "ball_pink.png", 0, 0, 1, 1); // 64x32 mBallPurpleTextureRegion = TextureRegionFactory.createTiledFromAsset(this.mBallPurpleTexture, this, "ball_purple.png", 0, 0, 1, 1); // 64x32 mBallWhiteTextureRegion = TextureRegionFactory.createTiledFromAsset(this.mBallWhiteTexture, this, "ball_white.png", 0, 0, 1, 1); // 64x32 this.mBackgroundTexture = new Texture(512, 1024, TextureOptions.BILINEAR_PREMULTIPLYALPHA); this.mBackgroundTextureRegion = TextureRegionFactory.createFromAsset(this.mBackgroundTexture, this, "table_bkg.png", 0, 0); this.enableAccelerometerSensor(this); mEngine.getTextureManager().loadTextures(mBackgroundTexture, mBallYellowTexture, mBallRedTexture, mBallBlackTexture, mBallBlueTexture, mBallGreenTexture, mBallOrangeTexture, mBallPinkTexture, mBallPurpleTexture); } @Override public Scene onLoadScene() { this.mEngine.registerUpdateHandler(new FPSLogger()); this.mPhysicsWorld = new PhysicsWorld( new Vector2(0, SensorManager.GRAVITY_EARTH), false); this.mScene = new Scene(); this.mScene.attachChild(new Entity()); this.mScene.setBackgroundEnabled(false); this.mScene.setOnSceneTouchListener(this); Sprite background = new Sprite(0, 0, this.mBackgroundTextureRegion); background.setWidth(CAMERA_WIDTH); background.setHeight(CAMERA_HEIGHT); background.setPosition(0, 0); this.mScene.getChild(0).attachChild(background); final final final final
Shape Shape Shape Shape
ground = new Rectangle(0, CAMERA_HEIGHT, CAMERA_WIDTH, 0); roof = new Rectangle(0, 0, CAMERA_WIDTH, 0); left = new Rectangle(0, 0, 0, CAMERA_HEIGHT); right = new Rectangle(CAMERA_WIDTH, 0, 0, CAMERA_HEIGHT);
14.3. Tworzen e gry na Andro d za pomocą narzędz a AndEng ne (Andro d-Eng ne)
|
461
final FixtureDef wallFixtureDef = PhysicsFactory.createFixtureDef(0, 0.5f, 0.5f); PhysicsFactory.createBoxBody( mPhysicsWorld, ground, BodyType.StaticBody, wallFixtureDef); PhysicsFactory.createBoxBody( mPhysicsWorld, roof, BodyType.StaticBody, wallFixtureDef); PhysicsFactory.createBoxBody( mPhysicsWorld, left, BodyType.StaticBody, wallFixtureDef); PhysicsFactory.createBoxBody( mPhysicsWorld, right, BodyType.StaticBody, wallFixtureDef); this.mScene.attachChild(ground); this.mScene.attachChild(roof); this.mScene.attachChild(left); this.mScene.attachChild(right); this.mScene.registerUpdateHandler(this.mPhysicsWorld); this.mScene.setOnAreaTouchListener(this); return this.mScene; } @Override public void onLoadComplete() { setupBalls(); } @Override public boolean onAreaTouched( final TouchEvent pSceneTouchEvent, final ITouchArea pTouchArea, final float pTouchAreaLocalX, final float pTouchAreaLocalY) { if (pSceneTouchEvent.isActionDown()) { final AnimatedSprite face = (AnimatedSprite) pTouchArea; this.jumpFace(face); return true; } return false; } @Override public boolean onSceneTouchEvent( final Scene pScene, final TouchEvent pSceneTouchEvent) { if (this.mPhysicsWorld != null) { if (pSceneTouchEvent.isActionDown()) { // this.addFace(pSceneTouchEvent.getX(), // pSceneTouchEvent.getY()); return true; } } return false; } @Override public void onAccelerometerChanged(final AccelerometerData pAccelerometerData) {
462 |
Rozdz ał 14. Gry an macje
this.mGravityX = pAccelerometerData.getX(); this.mGravityY = pAccelerometerData.getY(); final Vector2 gravity = Vector2Pool.obtain(this.mGravityX, this.mGravityY); this.mPhysicsWorld.setGravity(gravity); Vector2Pool.recycle(gravity); } private void setupBalls() { final AnimatedSprite[] balls = new AnimatedSprite[9]; final FixtureDef objectFixtureDef = PhysicsFactory.createFixtureDef(1, 0.5f, 0.5f); AnimatedSprite redBall = new AnimatedSprite(10, 10, AnimatedSprite yellowBall = new AnimatedSprite(20, 20, AnimatedSprite blueBall = new AnimatedSprite(30, 30, AnimatedSprite greenBall = new AnimatedSprite(40, 40, AnimatedSprite orangeBall = new AnimatedSprite(50, 50, AnimatedSprite pinkBall = new AnimatedSprite(60, 60, AnimatedSprite purpleBall = new AnimatedSprite(70, 70, AnimatedSprite blackBall = new AnimatedSprite(70, 70, AnimatedSprite whiteBall = new AnimatedSprite(70, 70, balls[0] balls[1] balls[2] balls[3] balls[4] balls[5] balls[6] balls[7] balls[8]
= = = = = = = = =
this.mBallRedTextureRegion); this.mBallYellowTextureRegion); this.mBallBlueTextureRegion); this.mBallGreenTextureRegion); this.mBallOrangeTextureRegion); this.mBallPinkTextureRegion); this.mBallPurpleTextureRegion); this.mBallBlackTextureRegion); this.mBallWhiteTextureRegion);
redBall; yellowBall; blueBall; greenBall; orangeBall; pinkBall; purpleBall; blackBall; whiteBall;
for (int i = 0; i < 9; i++) { Body body = PhysicsFactory.createBoxBody(this.mPhysicsWorld, balls[i], BodyType.DynamicBody, objectFixtureDef); this.mPhysicsWorld.registerPhysicsConnector(new PhysicsConnector(balls[i], body, true, true)); balls[i].animate(new long[] { 200, 200 }, 0, 1, true); balls[i].setUserData(body); this.mScene.registerTouchArea(balls[i]); this.mScene.attachChild(balls[i]); } } private void jumpFace(final AnimatedSprite face) { final Body faceBody = (Body) face.getUserData(); final Vector2 velocity =
14.3. Tworzen e gry na Andro d za pomocą narzędz a AndEng ne (Andro d-Eng ne)
| 463
Vector2Pool.obtain(this.mGravityX * -50, this.mGravityY * -50); faceBody.setLinearVelocity(velocity); Vector2Pool.recycle(velocity); } }
14.4. Przetwarzanie danych wejściowych wprowadzonych w określonym czasie Kurosh Fallahzadeh
Problem Programista chce ustalić, czy w określonym przedziale czasu użytkownik wykonał odpowiednie działania, np. wcisnął lub zwolnił klawisz. Jest to przydatne na przykład przy obsłudze danych wejściowych w grach.
Rozwiązanie Należy uśpić wątek na określony czas i zastosować komponent obsługi do sprawdzenia, czy klawisz został wciśnięty lub zwolniony.
Omówienie Przedział jest zapisywany za pomocą liczby typu long, która reprezentuje czas w milisekundach. Na listingu 14.5 przesłonięto metodę onKeyUp w taki sposób, aby Android po zwolnieniu klawisza przez użytkownika uruchamiał metodę taskHandler. Metoda ta wykonuje zadanie A, dopóki użytkownik wciska i zwalnia klawisze w jednosekundowych przedziałach czasu. W przeciwnym razie metoda wykonuje zadanie B. Listing 14.5. Kod do obsługi klawiszy wciśniętych w określonym czasie // W głównej klasie private long interval = 1000; // Przedział to jedna sekunda private taskHandler myTaskHandler = new TaskHandler(); class TaskHandler extends Handler { @Override public void handleMessage(Message msg) { MyMainClass.this.executeTaskB(); } public void sleep(long timeInterval) { // Usuwanie z kolejki wcześniejszych komunikatów związanych z klawiaturą this.removeMessages(0); // Umieszczanie w kolejce obecnego komunikatu związanego z klawiaturą, // obsługiwanego po upływie czasu zapisanego w timeInterval sendMessageDelayed(obtainMessage(0), timeInterval);
464 |
Rozdz ał 14. Gry an macje
} } @Override public boolean onKeyUp(int keyCode, KeyEvent event) { // Wykonywanie zadania A i wywoływanie komponentu z zadaniem B, jeśli komunikat o // zwolnieniu klawisza nadszedł po upływie czasu zapisanego w interval executeTaskA(); myTaskHandler.sleep(interval); return true; } public void executeTaskA() { ... } public void executeTaskB() { ... }
14.4. Przetwarzan e danych wejśc owych wprowadzonych w określonym czas e
| 465
466 |
Rozdz ał 14. Gry an macje
ROZDZIAŁ 15.
Sieci społecznościowe
15.1. Wprowadzenie — sieci społecznościowe Ian Darwin
Omówienie Pisząc o internecie w drugim dziesięcioleciu obecnego wieku, nie można nie uwzględniać znaczenia sieci społecznościowych. W obszarze tym dominującą pozycję zajmuje obecnie kilka dużych serwisów (największe z nich to Facebook i Twitter). Sieci społecznościowe dają programistom duże możliwości, natomiast społeczność programistów jako całość nadal nie potrafi ich w pełni wykorzystać. Oczywiście nadal można w pomysłowy sposób wykorzystać sieci społecznościowe. Brakuje jednak, mimo podejmowanych prób, jednego otwartego interfejsu API dla sieci społecznościowych, który obsługiwałby uwierzytelnianie, wymianę wiadomości i wymianę plików multimedialnych. W tym rozdziale opisano kilka sposobów na dostęp do Facebooka i Twittera za pomocą zwykłego protokołu HTTP (zanim popularność zyskały aplikacje mobilne, sieci społecznościowe były zwykłymi witrynami internetowymi) oraz przy użyciu bardziej zaawansowanych interfejsów API, przeznaczonych do wykonywania określonych zadań.
15.2. Integrowanie aplikacji z sieciami społecznościowymi za pomocą protokołu HTTP Shraddha Shravagi
Problem Programista chce zapewnić w aplikacji prostą obsługę sieci społecznościowych.
467
Rozwiązanie Zamiast korzystać z interfejsów API, można dodać obsługę sieci społecznościowych. Aby zintegrować aplikację z Facebookiem, Twitterem i LinkedInem, wystarczy wykonać trzy proste kroki:
1. Pobrać logo Facebooka, Twittera i LinkedIna. 2. Utworzyć przyciski graficzne z każdym z tych logo. 3. Zaimplementować komponent obsługi zdarzeń, który w reakcji na kliknięcie przycisku przechodzi do odpowiedniej witryny i wyświetla ją w oknie przeglądarki.
Omówienie Oto nieskomplikowany sposób na dodanie prostej obsługi sieci społecznościowych.
Krok 1. Pobieranie logo Pobierz logo z witryn serwisów lub użyj wyszukiwarki.
Krok 2. Tworzenie przycisków graficznych z logo Układ przedstawiony na listingu 15.1 obejmuje przyciski graficzne dla każdej z uwzględnianych sieci społecznościowych. Przyciski te przedstawiono na rysunku 15.1. Listing 15.1. Główny układ
Krok 3. Obsługa kliknięcia W kodzie z listingu 15.2 pokazano kilka odbiorników. Każdy z nich uruchamia obiekt Intent powiązany z witryną określonej sieci społecznościowej. Odbiorniki są dodawane jako odbiorniki
468 |
Rozdz ał 15. S ec społecznośc owe
Rysunek 15.1. Przyciski prowadzące do sieci społecznościowych
typu OnClickListener za pomocą atrybutów android:onClick w układzie z listingu 15.1, dlatego kod głównej aktywności jest stosunkowo krótki. Listing 15.2. Kod do otwierania witryn sieci społecznościowych /* Podany adres URL prowadzi do strony aplikacji, do której chcę kierować użytkowników. * Adres ten to http //goo.gl/eRAD9. Możesz też podać adres własnej aplikacji. * W tym celu skróć adres URL aplikacji z serwisu Google Play za pomocą bit.ly * lub mechanizmu google’owego * */ public void facebookBtnClicked(View v) { Toast.makeText(this, "Otwieranie Facebooka...\n Sprawdź, czy telefon ma połączenie z internetem", Toast.LENGTH_SHORT).show(); String url="http://m.facebook.com/sharer.php?u=http%3A%2F%2Fgoo.gl%2FeRAD9"; Intent i = new Intent(Intent.ACTION_VIEW); i.setData(Uri.parse(url)); startActivity(i); } public void twitterBtnClicked(View v) { Toast.makeText(this, "Otwieranie Twittera... \n Sprawdź, czy telefon ma połączenie z internetem", Toast.LENGTH_SHORT).show(); /**/ String url = "http://www.twitter.com/share?text= Checkout+This+Demo+http://goo.gl/eRAD9+"; Intent i = new Intent(Intent.ACTION_VIEW); i.setData(Uri.parse(url)); startActivity(i); }
15.2. ntegrowan e apl kacj z s ec am społecznośc owym za pomocą protokołu HTTP
| 469
public void linkedinBtnClicked(View v) { Toast.makeText(this, "Otwieranie LinkedIna... \n Sprawdź, czy telefon ma połączenie z internetem", Toast.LENGTH_SHORT).show(); String url="http://www.linkedin.com/shareArticle?url= http%3A%2F%2Fgoo.gl%2FeRAD9&mini= true&source=SampleApp&title=App+on+your+mobile"; Intent intent=new Intent(Intent.ACTION_VIEW); intent.setData(Uri.parse(url)); startActivity(intent); }
W ten sposób w trzech prostych krokach możesz dodać do aplikacji obsługę sieci społecznościowych. Tu do otwierania witryny w przeglądarce użyto intencji. Można też wykorzystać kontrolkę WebView w sposób pokazany w recepturze 13.7.
15.3. Wczytywanie chronologicznych list tweetów za pomocą formatu JSON Rachee Singh
Problem Programista chce umożliwić wczytywanie chronologicznej listy tweetów z Twittera do aplikacji na Android.
Rozwiązanie Ponieważ chronologiczne listy wpisów są publicznie dostępne, w Twitterze nie trzeba uwierzytelniać użytkownika. Wystarczy zastosować żądanie HttpGet, aby pobrać dane w formacie JSON z twitterowej strony użytkownika. Następnie można przetwarzać te dane w celu uzyskania tweetów.
Omówienie Na listingu 15.3 zastosowano żądanie HttpGet do pobrania danych ze strony twitterowej (tu jest to strona dziennika „Times of India”). Odpowiedź zwrócona po zgłoszeniu żądania powinna obejmować dane z tej strony zapisane w formacie JSON. Aplikacja sprawdza kod stanu. Jeśli jest on różny od 200, pobieranie danych kończy się niepowodzeniem. Z odpowiedzi można pobrać dane w formacie JSON i umieścić je w obiekcie StringBuilder. Metoda getTwitter Timeline() zwraca łańcuch znaków z danymi w formacie JSON. Listing 15.3. Metoda getTwitterTimeline() public String getTwitterTimeline() { StringBuilder builder = new StringBuilder(); HttpClient client = new DefaultHttpClient(); HttpGet httpGet = new HttpGet( "http://twitter.com/statuses/user_timeline/timesofindia.json");
470
|
Rozdz ał 15. S ec społecznośc owe
try { HttpResponse response = client.execute(httpGet); 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); } } else { // Nie można pobrać danych } } catch (ClientProtocolException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return builder.toString(); } }
Następnie aplikacja w zwykły sposób (za pomocą metody getString()) przetwarza dane w formacie JSON zwrócone przez metodę getTwitterTimeline(). Pobrane tweety wyświetlane są w kontrolce TextView, co pokazano na listingu 15.4. Efekt końcowy przedstawiono na rysunku 15.2.
Rysunek 15.2. Przetworzone dane z Twittera w formacie JSON Listing 15.4. Zapisywanie w kontrolce ListView chronologicznej listy tweetów z danych w formacie JSON String twitterTimeline = getTwitterTimeline(); try { String tweets = "";
15.3. Wczytywan e chronolog cznych l st tweetów za pomocą formatu JSON
|
471
JSONArray jsonArray = new JSONArray(twitterTimeline); for (int i = 0; i < jsonArray.length(); i++) { JSONObject jsonObject = jsonArray.getJSONObject(i); int j = i+1; tweets +="*** " + j + " ***\n"; tweets += "Data:" + jsonObject.getString("created_at") + "\n"; tweets += "Wpis:" + jsonObject.getString("text") + "\n\n"; } json= (TextView)findViewById(R.id.json); json.setText(tweets); } catch (JSONException e) { e.printStackTrace(); }
472
|
Rozdz ał 15. S ec społecznośc owe
ROZDZIAŁ 16.
Lokalizacja i mapy
16.1. Wprowadzenie — aplikacje wykorzystujące lokalizację Ian Darwin
Omówienie Jeszcze nie tak dawno urządzenia GPS były niedostępne, drogie lub nieporęczne. Obecnie prawie każdy smartfon i wiele aparatów cyfrowych jest wyposażonych w odbiornik GPS. Takie odbiorniki stają się niemal wszechobecne. Firmy udostępniające mapy zdają sobie z tego sprawę. Projekt OpenStreetMap (http://openstreetmap.org/) powstał po części z uwagi na wzrost liczby użytkowników urządzeń GPS i zapewnia dostęp do „bezpłatnej i edytowalnej mapy świata”. Większość map wykonanych w ramach projektu to efekt pracy entuzjastów. Google dużą część danych pozyskuje od komercyjnych firm kartograficznych. Przy rozwijaniu Androida Google starał się dobrze wykorzystać dostępne w urządzeniach odbiorniki GPS. W tym rozdziale poznasz tajniki korzystania w urządzeniach z Androidem z map udostępnianych przez Google’a i w ramach projektu OpenStreetMap.
16.2. Pobieranie danych o lokalizacji Ian Darwin
Problem Programista chce umożliwić określanie położenia użytkownika.
Rozwiązanie Należy użyć wbudowanych androidowych dostawców lokalizacji.
473
Android określa lokalizację w dwóch trybach. Jeśli ma precyzyjnie ustalić położenie użytkownika, należy zastosować tryb FINE, oparty na GPS-ie. Jeżeli aplikacja ma wskazać lokalizację tylko w przybliżeniu, można włączyć tryb COARSE, oparty na nadajnikach sieci komórkowej, w których zasięgu znajduje się telefon. Tryb FINE pozwala określić pozycję z dokładnością do kilku metrów. W trybie COARSE lokalizacja w dużych miastach podawana jest z dokładnością do budynku, jednak na obszarach o małej liczbie mieszkańców, gdzie nadajniki sieci komórkowej są znacznie oddalone od siebie, dane podawane są z małą dokładnością — do 5 lub 10 kilometrów.
Omówienie Na listingu 16.1 pokazano kod konfiguracyjny. Pochodzi on z aplikacji JPStrack (http://www. darwinsys.com/jpstrack/), w której wykorzystywane są mapy z projektu OpenStreetMap (http:// openstreetmap.org/). Aby móc korzystać z map, potrzebny jest GPS, dlatego określanie lokalizacji odbywa się w trybie FINE. Listing 16.1. Pobieranie danych o lokalizacji // Kod z aplikacji jpstrack Main.java LocationManager mgr = (LocationManager) getSystemService(LOCATION_SERVICE); for (String prov : mgr.getAllProviders()) { Log.i(LOG_TAG, getString(R.string.provider_found) + prov); } // Konfigurowanie GPS-u Criteria criteria = new Criteria(); criteria.setAccuracy(Criteria.ACCURACY_FINE); List
Gdy po zakończeniu konfigurowania aplikacja ma zacząć odbierać z odbiornika GPS dane o lokalizacji, należy wywołać metodę LocationManager.requestLocationUpdates i przekazać do niej nazwę znalezionego dostawcy, minimalny czas między aktualizacjami (w milisekundach), minimalną odległość między aktualizacjami (w metrach) oraz obiekt z implementacją interfejsu LocationListener. Aktualizowanie można zatrzymać przez wywołanie metody removeUpdates dla wcześniej przekazanego obiektu z implementacją interfejsu LocationListener. Pozwala to zmniejszyć ilość wykorzystywanych zasobów i wydłużyć czas pracy urządzenia na baterii. Na listingu 16.2 pokazano dalszą część kodu z aplikacji JPStrack. Listing 16.2. Wstrzymywanie i wznawianie aktualizowania lokalizacji @Override protected void onResume() { super.onResume(); if (preferred != null) { mgr.requestLocationUpdates(preferred, MIN_SECONDS * 1000, MIN_METRES, this); }
474
|
Rozdz ał 16. Lokal zacja mapy
} @Override protected void onPause() { super.onPause(); if (preferred != null) { mgr.removeUpdates(this); } }
Następnie w momencie zmiany lokalizacji wywoływana jest metoda onLocationChanged() interfejsu LocationListener. To w tej metodzie wykorzystywane są informacje o lokalizacji. @Override public void onLocationChanged(Location location) { long time = location.getTime(); double latitude = location.getLatitude(); double longitude = location.getLongitude(); // Wykorzystywanie szerokości i długości geograficznej (oraz czasu) }
Pozostałe metody interfejsu LocationListener mogą być namiastkami. Sposób wykorzystania danych o lokalizacji zależy oczywiście od programu. Aplikacja JPStrack za pomocą niestandardowego kodu zapisuje dane w pliku XML. Dane tego rodzaju często służą do aktualizowania pozycji na mapie lub są przesyłane do usług opartych na lokalizacji. Możliwości przy korzystaniu z takich informacji są niemal nieograniczone.
16.3. Dostęp do danych z GPS-a w aplikacjach Pratik Rupwal
Problem W klasie aplikacji potrzebny jest dostęp do lokalizacji ustalonej za pomocą GPS-a.
Rozwiązanie Należy dodać klasę z implementacją interfejsu LocationListener. W miejscu, w którym aplikacja potrzebuje dostępu do informacji z GPS-a, należy utworzyć egzemplarz tej klasy i pobrać dane.
Omówienie Na listingu 16.3 przedstawiono klasę MyLocationListener z implementacją interfejsu Location Listener.
Listing 16.3. Implementacja interfejsu LocationListener public class MyLocationListener implements LocationListener { @Override public void onLocationChanged(Location loc) {
16.3. Dostęp do danych z GPS-a w apl kacjach
|
475
loc.getLatitude(); loc.getLongitude(); } @Override public void onProviderDisabled(String provider) { } @Override public void onProviderEnabled(String provider) { } @Override public void onStatusChanged(String provider, int status, Bundle extras) { } }// Koniec klasy MyLocationListener
Plik z klasą z listingu 16.3 należy dołączyć do pakietu aplikacji. Egzemplarz tej klasy można wykorzystać w sposób przedstawiony na listingu 16.4, aby uzyskać dostęp do danych z GPS-a w dowolnej innej klasie. Listing 16.4. Klasa korzystająca z implementacji interfejsu LocationListener public class AccessGPS extends Activity { // Deklaracje potrzebnych obiektów LocationManager mlocManager; LocationListener mlocListener; Location lastKnownLocation; Double latitude,longitude; ... ... protected void onCreate(Bundle savedInstanceState) { ... ... // Tworzenie obiektów potrzebnych do pobrania danych z GPS-a mlocListener = new MyLocationListener(); // Żądanie zaktualizowania lokalizacji mlocManager.requestLocationUpdates( LocationManager.GPS_PROVIDER, 0, 0, mlocListener); locationProvider=LocationManager.GPS_PROVIDER; ... ... // Dostęp do ostatniej zidentyfikowanej lokalizacji lastKnownLocation = mlocManager.getLastKnownLocation(locationProvider); // Za pomocą tego obiektu można pobrać dane z GPS-a w następujący sposób latitude=lastKnownLocation.getLatitude(); longitude=lastKnownLocation.getLongitude();
476
|
Rozdz ał 16. Lokal zacja mapy
// Na podstawie danych z GPS-a można wykonać operacje związane z lokalizacją ... ... } }
Obiekt loc typu Location można wykorzystać w metodzie onLocationChanged, aby pobrać informacje z GPS-a. Jednak aplikacja nie zawsze wszystkie operacje związane z informacjami z GPS-a może wykonać w tej przesłanianej metodzie (wynika to między innymi z poziomu dostępu do innych danych). Wyobraź sobie aplikację, która pobiera listę nazw centrów handlowych zlokalizowanych w pobliżu użytkownika, a następnie ją wyświetla. Gdy użytkownik wybiera dane centrum handlowe, program wyświetla listę sklepów z tego centrum. Aplikacja wykorzystuje tu informacje o lokalizacji użytkownika do ustalenia, które nazwy ma pobrać z bazy. Potrzebny jest do tego komponent obsługi bazy, który jest prywatną składową klasy obejmującej widok, gdzie aplikacja wyświetla listę centrów handlowych. Komponent ten jest więc niedostępny w przesłanianej metodzie, dlatego nie można wykonać potrzebnych operacji.
16.4. Podawanie fikcyjnych współrzędnych GPS w urządzeniu Emaad Manzoor
Problem Programista chce zademonstrować działanie aplikacji, ale obawia się tego, że program może mieć trudności z ustaleniem współrzędnych GPS. Możliwe też, że programista chce podać współrzędne konkretnego miejsca.
Rozwiązanie Należy podłączyć dostawcę fikcyjnych współrzędnych do obiektu LocationManager, a następnie przypisać takie współrzędne do tego dostawcy.
Omówienie Tworzenie metody setMockLocation Metoda z listingu 16.5 pozwala ustawić w urządzeniu fikcyjne współrzędne GPS. Listing 16.5. Ustawianie fikcyjnych współrzędnych GPS private void setMockLocation(double latitude, double longitude, float accuracy) { lm.addTestProvider (LocationManager.GPS_PROVIDER, "requiresNetwork" == "", "requiresSatellite" == "", "requiresCell" == "", "hasMonetaryCost" == "", "supportsAltitude" == "", "supportsSpeed" == "",
16.4. Podawan e f kcyjnych współrzędnych GPS w urządzen u
|
477
"supportsBearing" == "", android.location.Criteria.POWER_LOW, android.location.Criteria.ACCURACY_FINE); Location newLocation = new Location(LocationManager.GPS_PROVIDER); newLocation.setLatitude(latitude); newLocation.setLongitude(longitude); newLocation.setAccuracy(accuracy); lm.setTestProviderEnabled(LocationManager.GPS_PROVIDER, true); lm.setTestProviderStatus(LocationManager.GPS_PROVIDER, LocationProvider.AVAILABLE, null,System.currentTimeMillis()); lm.setTestProviderLocation(LocationManager.GPS_PROVIDER, newLocation); }
Co dzieje się w kodzie? Na listingu 16.5 za pomocą metody addTestProvider klasy LocationManager aplikacja dodaje dostawcę fikcyjnych współrzędnych. Następnie tworzy nową lokalizację, używając obiektu Location. Obiekt ten pozwala ustawić szerokość geograficzną, długość geograficzną oraz precyzję. Aby aktywować dostawcę fikcyjnych danych, najpierw za pomocą metody setTestProvider Enabled() klasy LocationManager należy ustawić wartość określającą, czy dostawca ma zwracać fikcyjne dane. Następnie trzeba ustawić status i lokalizację dostawcy fikcyjnych danych.
Korzystanie z metody setMockLocation Aby zastosować tę metodę, najpierw należy w standardowy sposób utworzyć obiekt Location Manager. Następnie można wywołać wspomnianą metodę, podając współrzędne (listing 16.6). Listing 16.6. Podawanie fikcyjnej lokalizacji LocationManager lm = (LocationManager)getSystemService(Context.LOCATION_SERVICE); lm.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, new LocationListener() { @Override public void onStatusChanged(String provider, int status, Bundle extras) {} @Override public void onProviderEnabled(String provider) {} @Override public void onProviderDisabled(String provider) {} @Override public void onLocationChanged(Location location) {} }); /* Ustawianie fikcyjnej lokalizacji w celach diagnostycznych */ setMockLocation(15.387653, 73.872585, 500);
Po podaniu fikcyjnych współrzędnych GPS konieczne może być ponowne uruchomienie urządzenia, aby znów zacząć odbierać rzeczywiste dane.
478
|
Rozdz ał 16. Lokal zacja mapy
Jak korzystać z przykładowej aplikacji? Find Me X (https://github.com/emaadmanzoor/findmex) to aplikacja na Android, która przyjmuje zapytanie w postaci rodzaj_miejsca w okolica, miasto, a następnie zwraca listę miejsc z informacjami o odległości od użytkownika. W aplikacji ustawiana jest fikcyjna lokalizacja, odpowiadająca położeniu kampusu BITS-Pilani Goa w Goa w Indiach.
Zobacz także Receptura 16.2, http://developer.android.com/reference/android/location/LocationManager.html, http:// developer.android.com/reference/android/location/Location.html.
16.5. Geokodowanie i geokodowanie odwrotne Nidhin Jose Davis
Problem Programista chce zastosować geokodowanie (przekształcanie adresu na współrzędne) i geokodowanie odwrotne (przekształcanie współrzędnych na adres).
Rozwiązanie Należy zastosować wbudowaną klasę Geocoder.
Omówienie Geokodowanie to proces ustalania współrzędnych geograficznych (szerokości i długości) dla danego adresu lub miejsca. Geokodowanie odwrotne, jak łatwo się domyślić, to odwrotna operacja. Polega na przekształcaniu pary szerokość geograficzna – długość geograficzna na adres lub miejsce. Aby przeprowadzić geokodowanie lub geokodowanie odwrotne, należy najpierw zaimportować klasę Geocoder: import android.location.Geocoder;
Geokodowania i geokodowania odwrotnego nie należy przeprowadzać w wątku interfejsu użytkownika, ponieważ obie te operacje mogą wymagać dostępu do serwera oraz prowadzić przez to do pojawienia się okna dialogowego Application Not Responding. Zadania te należy wykonywać w odrębnym wątku. Na listingu 16.7 pokazano kod służący do geokodowania, a na listingu 16.8 — operacje potrzebne przy geokodowaniu odwrotnym. Listing 16.7. Geokodowanie Geocoder gc = new Geocoder(context); if(gc.isPresent()){ List list = gc.getFromLocationName("1600 Amphitheatre Parkway, Mountain View, CA", 1);
16.5. Geokodowan e geokodowan e odwrotne
|
479
Address address = list.get(0); double lat = address.getLatitude(); double lng = address.getLongitude(); }
Listing 16.8. Geokodowanie odwrotne Geocoder gc = new Geocoder(context); if(gc.isPresent()){ List list = gc.getFromLocation(37.42279, -122.08506,1); Address address = list.get(0); StringBuffer str = new StringBuffer(); str.append("Nazwa: " + address.getLocality() + "\n"); str.append("Podległy obszar administracyjny: " + address.getSubAdminArea() + "\n"); str.append("Główny obszar administracyjny: " + address.getAdminArea() + "\n"); str.append("Państwo: " + address.getCountryName() + "\n"); str.append("Kod państwa: " + address.getCountryCode() + "\n"); String strAddress = str.toString(); }
16.6. Przygotowania do korzystania z map Google’a Johan Pelgrim
Problem Programista chce skonfigurować aplikację na Android, aby móc używać w niej google’owych elementów układu MapView.
Rozwiązanie Należy wykorzystać biblioteki Google Maps API, element układu MapView oraz aktywność MapActivity.
Omówienie Należy zacząć od utworzenia projektu na Android, który wyświetla domyślną mapę.
Konfigurowanie urządzenia AVD korzystającego z bibliotek pakietu SDK z Google API W czasie tworzenia nowego projektu na Android trzeba określić najstarszą wersję pakietu SDK współdziałającą z aplikacją, a także docelową wersję pakietu SDK. Ponieważ program ma korzystać z bibliotek Google Maps API, trzeba utworzyć urządzenie AVD, na którym są one zainstalowane. Jako docelowy pakiet SDK dla urządzenia AVD wybierz Google APIs – 1.5 – API level 3 lub nowszą wersję (rysunek 16.1).
480 |
Rozdz ał 16. Lokal zacja mapy
Rysunek 16.1. Tworzenie urządzenia AVD z obsługą Google API
Tworzenie nowego projektu z docelowym pakietem Google APIs – 1.5 – API level 3 Teraz trzeba utworzyć projekt MapTest przeznaczony dla pakietu Google APIs – 1.5 – API level 3 lub jego nowszej wersji. W polu Min SDK version należy wpisać 3 (rysunek 16.2). Teraz można pozwolić kreatorowi nowych projektów utworzyć aktywność MapTest, a następnie kliknąć przycisk Finish. Element MapView może znajdować się tylko w aktywności MapActivity, dlatego aktywność MapTest powinna dziedziczyć właśnie po MapActivity. W nowej aktywności trzeba zaimplementować metodę isRouteDisplayed(). Metoda ta jest potrzebna do współdziałania z usługami zwracającymi mapy i pozwala stwierdzić, czy aplikacja obecnie wyświetla informacje o trasie. Przykładowy program nie wyświetla takich informacji. Trzeba zaimplementować tę metodę, jednak tu może ona nie wykonywać żadnych operacji oprócz zwracania wartości false. Aby umożliwić przybliżanie wybranych obszarów mapy, należy ustawić wyświetlanie wbudowanych kontrolek na true. Służy do tego metoda setBuiltInZoomControls klasy MapView. Potrzebny kod pokazano na listingu 16.9. Listing 16.9. Klasa MapTest package nl.codestone.cookbook.maptest; import android.os.Bundle; import com.google.android.maps.MapActivity; public class MapTest extends MapActivity {
16.6. Przygotowan a do korzystan a z map Google’a
|
481
Rysunek 16.2. Tworzenie projektu z obsługą interfejsów Google API @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); MapView mapview = (MapView) findViewById(R.id.mapview); mapview.setBuiltInZoomControls(true); } @Override protected boolean isRouteDisplayed() { return false; } }
Dodawanie elementu MapView do pliku układu Otwórz plik res/layout/main.xml, usuń element TextView i w jego miejsce wstaw element MapView:
Oto kilka ważnych informacji: • MapView nie należy do standardowego pakietu com.android.view, dlatego w elemencie trze-
ba podać pełną nazwę pakietu.
482 |
Rozdz ał 16. Lokal zacja mapy
• Atrybut android:clickable trzeba ustawić na true, aby możliwe było przeciąganie mapy,
a także jej przybliżanie i oddalanie. • W konfiguracji obiektu MapView trzeba podać osobisty klucz Google Maps API. Należy po-
dać go w specjalnym atrybucie android:apiKey w definicji elementu MapView. Klucz można otrzymać przez podanie skrótu MD5 z pliku keystore, używanego do podpisywania aplikacji. W trakcie rozwijania aplikacji można wykorzystać skrót z pliku debug.keystore.
Rejestrowanie klucza Google Maps API Pełny opis rejestrowania klucza Google Maps API znajdziesz na stronie http://code.google.com/ android/add-ons/google-apis/mapkey.html. W tym podpunkcie opisano najprostsze operacje potrzebne do uzyskania takiego klucza. Jeśli natrafisz na problemy, zajrzyj do kompletnego opisu udostępnianego przez Google’a. Aplikacje na Android muszą być podpisane za pomocą certyfikatu. Certyfikaty są przechowywane w plikach keystore. W komercyjnych aplikacjach potrzebny jest prywatny (samodzielnie podpisany) certyfikat, importowany do pliku keystore. Na etapie tworzenia i instalowania aplikacji na Android w środowisku programistycznym do jej podpisywania służy certyfikat z pliku debug.keystore. Plik ten znajduje się w katalogu .android w folderze użytkownika. Aby zarejestrować się w celu otrzymania klucza Google Maps API, należy podać prywatny skrót MD5 klucza androiddebugkey. Otwórz wiersz poleceń i przejdź do katalogu .android w folderze użytkownika (w środowiskach opartych na Uniksie należy wpisać instrukcje cd ~/.android). Wprowadź następujące polecenie: keytool -list -alias androiddebugkey -keystore debug.keystore -storepass android
Otrzymasz informacje podobne do poniższych: androiddebugkey, 29-mrt-2011, PrivateKeyEntry, Certificate fingerprint (MD5): 2E:54:39:DB:33:E7:D6:3A:9E:18:3D:7F:FB:6D:BC:8D
Wartość podaną po tekście Certificate fingerprint (MD5): skopiuj do schowka i otwórz stronę http://code.google.com/android/maps-api-signup.html, gdzie na podstawie tej wartości można wygenerować klucz Google Maps API. Uzyskany klucz wygląda mniej więcej tak: 18Qcs3h-Sq5l8A7L56bjLwY1gwxgeMYF9Rp_0Cg
Przeklej ten klucz do atrybutu android:apiKey w elemencie MapView w pliku res/main.xml. Jeśli tworzysz widok MapView bezpośrednio w kodzie, powinieneś przekazać klucz Google Maps API do konstruktora klasy MapView. Klucz zawsze można odtworzyć w opisany wcześniej sposób, dlatego nie trzeba zapisywać go w bezpiecznym miejscu. Warto natomiast utworzyć kopię pliku keystore używanego do podpisywania własnych aplikacji.
16.6. Przygotowan a do korzystan a z map Google’a
| 483
W pliku AndroidManifest.xml wprowadź następujące zmiany (gotowy plik pokazano na listingu 16.10): • Dodaj żądanie uprawnień
aby aplikacja mogła pobierać fragmenty map z internetu. Fragmenty map są automatycznie zapisywane w pamięci podręcznej w katalogu apps-data, dlatego nie trzeba wykonywać dodatkowych operacji, aby uzyskać ten efekt. • Klasy do obsługi map Google’a nie znajdują się w standardowych pakietach, dlatego
w pliku AndroidManifest.xml trzeba określić, że aplikacja korzysta z biblioteki com.google.
android.maps.
Listing 16.10. Plik AndroidManifest.xml z przykładowej aplikacji
Istnieje też inny plik, a mianowicie default.properties (lub project.properties — zależy to od używanej wersji pakietu SDK Androida), obejmujący docelową wersję systemu, dla której budowana jest aplikacja. W trakcie tworzenia projektu plik ten jest generowany automatycznie, dlatego nie trzeba zmieniać w nim żadnych danych. Jeśli jednak w przyszłości zechcesz zmienić docelową wersję na nowszą lub starszą, warto wiedzieć, że wersja zapisana jest właśnie we wspomnianym pliku. Zmienić ją można bezpośrednio w pliku lub w oknie dialogowym z właściwościami projektu w środowisku Eclipse. target=Google Inc.:Google APIs:3
I to już wszystko! Uruchom urządzenie AVD i aplikację na Android. Jeśli wszystko przebiegło prawidłowo, powinieneś zobaczyć mapę Ameryki Północnej i Południowej (rysunek 16.3). Mapę tę można przesuwać i przybliżać.
Lista kontrolna Receptura kończy się listą kontrolną, która pozwala szybko skonfigurować projekty z innych receptur dotyczących map Google’a.
484 |
Rozdz ał 16. Lokal zacja mapy
Rysunek 16.3. Mapa Ameryk • Używaj urządzenia AVD z bibliotekami pakietu SDK z Google API. • Aktywność powinna dziedziczyć po klasie MapActivity. • Trzeba zaimplementować metodę isRouteDisplayed(). W większości sytuacji wystarczają-
ca jest implementacja domyślna, która zwraca wartość false. • Ustaw wbudowane kontrolki do obsługi przybliżania na true. W tym celu wywołaj meto-
dę setBuiltInZoomControls obiektu MapView. • W elemencie MapView w pliku układu podaj pełną nazwę pakietu (czyli com.google.android.
maps.MapView). • W atrybucie android:apiKey elementu MapView podaj klucz Google Maps API. • Jeśli widok MapView tworzysz bezpośrednio w kodzie, klucz Google Maps API powinieneś
przekazać do konstruktora klasy MapView. • Atrybut android:clickable w elemencie MapView ustaw na true, aby móc przeciągać mapę
oraz przybliżać i oddalać widok. • W pliku AndroidManifest.xml w elemencie manifest dodaj element
name="android.permission.INTERNET "/>. • W pliku AndroidManifest.xml w elemencie application dodaj element
name="com.google.android.maps" />.
Zobacz także Projekt Google APIs w serwisie Google Code (https://developers.google.com/android/add-ons/ google-apis/?hl=pl-PL). Strona przeznaczona do generowania kluczy Google Maps API (https:// developers.google.com/android/maps-api-signup?hl=pl-PL).
16.6. Przygotowan a do korzystan a z map Google’a
| 485
16.7. Wyświetlanie aktualnej lokalizacji urządzenia na mapach Google’a Rachee Singh
Problem Programista chce wyświetlać aktualną lokalizację urządzenia na mapach Google’a.
Rozwiązanie Obecną lokalizację urządzenia można wyświetlić na mapie za pomocą klasy MyLocationOverlay. MyLocationOverlay to standardowa klasa Androida, dostępna w pakiecie com.google.android.maps, choć nazwa tej klasy może sugerować, że utworzono ją na potrzeby tego przykładu.
Omówienie Do pliku AndroidManifest.xml dodaj następujące żądania uprawnień:
Jeśli do aplikacji dodajesz widok MapView, w pliku XML z układem powinieneś umieścić przedstawione poniżej wiersze kodu (identyfikatorem widoku MapView jest tu map):
W napisanej w Javie klasie aktywności, która wyświetla widok MapView, dodaj następujące pole: private MyLocationOverlay myLocationOverlay;
Ponadto należy zapisać uchwyt do zdefiniowanego w pliku XML widoku MapView i dodać obiekt MyLocationOverlay. Następnie trzeba wywołać metodę invalidate(). mapView = (MapView)findViewById(R.id.map); myLocationOverlay = new MyLocationOverlay(this, mapView); mapView.getOverlays().add(myLocationOverlay); mapView.invalidate();
Aby zapobiec wyczerpaniu się baterii, w metodzie onPause rozwijanej klasy należy wywołać metodę disableMyLocation(), co pokazano na listingu 16.11 (podobną technikę zastosowano na listingu 16.2). Listing 16.11. Udostępnianie metod onPause i onResume w celu wydłużenia czasu pracy urządzenia na baterii @Override protected void onPause() { super.onPause();
486 |
Rozdz ał 16. Lokal zacja mapy
myLocationOverlay.disableMyLocation(); } @Override protected void onResume() { super.onResume(); myLocationOverlay.enableMyLocation(); }
16.8. Wyświetlanie znacznika lokalizacji w widoku MapView Johan Pelgrim
Problem Programista stosuje geolokalizację i chce wyświetlać znacznik w google’owym znaczniku MapView.
Rozwiązanie Należy utworzyć obiekt Overlay, umieścić na nim znacznik i dodać obiekt do warstw widoku MapView. Można też za pomocą animacji przejść do danego punktu.
Omówienie Utwórz nowy projekt, Location on Map, i na podstawie receptury 16.6 odpowiednio go skonfiguruj. Możesz też wykorzystać kod klasy MapTest ze wspomnianej receptury. Jeśli wszystko przebiegnie zgodnie z planem, metoda onCreate powinna wyglądać tak: @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); MapView mapView = (MapView) findViewById(R.id.mapview); mapView.setBuiltInZoomControls(true); }
Dalej zobaczysz, jak uatrakcyjnić aplikację. Najpierw typ widoku jest ustawiany na satellite, co prowadzi do wyświetlenia informacji o ukształtowaniu terenu: mapView.setSatellite(true);
Uruchom aplikację, aby zobaczyć efekt ustawienia tego trybu. Aby dodać informacje o ruchu drogowym, należy wywołać metodę setTraffic. Dane tego rodzaju najprzydatniejsze są jednak na zwykłej mapie, a nie na mapie z ukształtowaniem terenu. Mapę można już przeciągać — teraz zobaczysz, jak dodać animowane przechodzenie do danego punktu. Najpierw należy utworzyć prywatne pole geoPoint i przypisać do niego współrzędne. Warto zauważyć, że argumenty konstruktora klasy GeoPoint, określające szerokość
16.8. Wyśw etlan e znaczn ka lokal zacj w w doku MapV ew
|
487
i długość geograficzną, to liczby całkowite, a nie zmiennoprzecinkowe. Aby przekształcić parę szerokość geograficzna – długość geograficzna z liczby zmiennoprzecinkowych na całkowite, należy pomnożyć wartości przez milion (w Javie można też użyć zapisu 1E6): GeoPoint geoPoint = new GeoPoint( (int) (52.334822 * 1E6), (int) (4.668907 * 1E6));
Potrzebny jest też uchwyt do obiektu MapController z widoku MapView. Obiekt ten pozwala ustawić poziom przybliżenia i uruchomić animację przy przechodzeniu do danego punktu GeoPoint: MapController mc = mapView.getController(); mc.setZoom(18); mc.animateTo(geoPoint);
Jest to dość proste. Uruchom aplikację, aby zobaczyć, jak działa. Spróbuj ustawić różne poziomy przybliżenia. Jaka jest minimalna wartość, którą możesz wybrać? Jaką maksymalną wartość możesz ustawić? Do wyświetlania na mapie znaczników na trasie, obecnej lokalizacji i innych ważnych punktów służą nakładane warstwy (rysunek 16.4). Przypomnij sobie tradycyjne rzutniki, używane często w dawnych czasach. Warstwy są jak przezroczyste plastikowe kartki, na których czasem znajdują się rysunki lub tekst. Na jeden widok MapView można nałożyć kilka warstw.
Rysunek 16.4. Warstwy na mapie
Utwórz prywatną klasę wewnętrzną pochodną od Overlay i przesłoń metodę draw. Ta klasa wewnętrzna nosi nazwę MyOverlay (listing 16.12) i — w odróżnieniu od klasy z receptury 16.7 — jest zwykłą klasą przykładową. Listing 16.12. Klasa MyOverlay private class MyOverlay extends com.google.android.maps.Overlay { @Override public void draw(Canvas canvas, MapView mapView, boolean shadow) { n super.draw(canvas, mapView, shadow); if (!shadow) { o Point point = new Point(); mapView.getProjection().toPixels(geoPoint, point); p Bitmap bmp = BitmapFactory.decodeResource(getResources(), R.drawable.marker_default); q
488 |
Rozdz ał 16. Lokal zacja mapy
int x = point.x - bmp.getWidth() / 2; r int y = point.y - bmp.getHeight(); s canvas.drawBitmap(bmp, x, y, null); t }
}
}
Oto opis przedstawionego kodu: n Metoda draw ma kilka argumentów. Pierwszy to uchwyt do obiektu Canvas, używanego w trakcie rysowania znacznika. Drugi to obiekt MapView, nad którym wyświetlana jest dana warstwa. Trzeci argument to wartość logiczna określająca, czy aplikacja ma wyświetlić rysunek, czy cień. Metoda draw jest wywoływana dwukrotnie — po raz pierwszy, aby narysować cień, i po raz drugi w celu narysowania głównego elementu. o Aplikacja nie ma za zadanie rysować cienia. p Współrzędne geograficzne są przekształcane na współrzędne w pikselach, a efekt tej operacji aplikacja zapisuje w zmiennej point. q Aplikacja przekształca zasób (określony przez identyfikator) na obiekt Bitmap, aby móc zapisać go w obiekcie Canvas. r Aplikacja wyznacza współrzędną x punktu, w którym ma umieścić znacznik. Punkt przesuwany jest nieco w lewo, tak aby środek rysunku pokrywał się ze współrzędną x odpowiadającą współrzędnym geograficznym. s Aplikacja wyznacza współrzędną y punktu, w którym ma umieścić znacznik. Punkt przesuwany jest nieco w górę, tak aby dół rysunku pokrywał się ze współrzędną y odpowiadającą współrzędnym geograficznym. t Aplikacja wyświetla bitmapę na podstawie wyznaczonych współrzędnych x i y. Jako plik marker_default.png możesz wykorzystać poniższy rysunek. Umieść go w katalogu ./res/drawable.
Do manipulowania warstwami służy metoda getOverlays() klasy MapView: List
Aby wymusić wyświetlenie widoku, należy wywołać metodę invalidate() klasy View. I to już wszystko. Gdy uruchomisz aplikację, powinieneś zobaczyć obraz podobny do tego z rysunku 16.5.
Zobacz także Receptura 16.6.
16.8. Wyśw etlan e znaczn ka lokal zacj w w doku MapV ew
| 489
Rysunek 16.5. Znacznik na mapie Google’a
16.9. Wyświetlanie kilku znaczników w widoku MapView Johan Pelgrim
Problem Programista chce w google’owym widoku MapView wyświetlić kilka punktów geograficznych.
Rozwiązanie Należy zaimplementować abstrakcyjną klasę ItemizedOverlay i dodać do niej różne obiekty OverlayItem.
Omówienie Wprowadzenie Jeśli chcesz w widoku MapView wyświetlić kilka znaczników, możesz oczywiście zaimplementować interfejs Overlay, aby obsługiwać pobieranie zasobów i wyświetlanie obiektów za pomocą przesłoniętej metody draw() (tę technikę zastosowano w recepturze 16.8). To podejście bywa jednak niewygodne, a napisany w ten sposób kod — trudny w konserwacji. Jeśli zamierzasz wykonywać podstawowe operacje związane z rysowaniem linii i kształtów, musisz niestety przesłonić metodę draw(). Jeżeli jednak chcesz tylko wyświetlić kilka prostych znaczników i obsługiwać ich kliknięcia, możesz wykorzystać klasę ItemizedOverlay z bibliotek Google Maps API. Ta abstrakcyjna klasa służy do przechowywania listy obiektów Overlay i wyświetla
490 |
Rozdz ał 16. Lokal zacja mapy
je w widoku MapView jako połączony obiekt Overlay. Klasa ItemizedOverlay obejmuje implementację interfejsu Overlay. Obsługuje ponadto sortowanie „od północy do południa” na potrzeby wyświetlania, określanie ograniczeń, w ramach których umieszczane są elementy, wyświetlanie znaczników dla wszystkich punktów i utrzymywanie aktywnego elementu. Klasa łączy też dotknięcia ekranu z elementami i kieruje do opcjonalnego odbiornika zdarzenia związane ze zmianą aktywnego obiektu. Wygląda więc na to, że klasa ta dobrze nadaje się do wyświetlania kilku znaczników w widokach MapView.
Dodawanie obiektu ItemizedOverlay do widoku MapView Zacznij od szkieletu projektu korzystającego z map Google’a, opisanego w recepturze 16.6. Inna możliwość to utworzenie własnego projektu i przejrzenie listy kontrolnej z końcowej części wspomnianej receptury (pozwala to zagwarantować, że projekt będzie działał). Do aktywności MapActivity dodaj klasę wewnętrzną pochodną od ItemizedOverlay. Zaimplementuj w niej metody abstrakcyjne i konstruktor domyślny. Metody createItem() i size() z tej klasy posłużą do uzyskiwania dostępu do wszystkich warstw oraz do łączenia ich ze sobą (listing 16.13). Listing 16.13. Implementacja klasy ItemizedOverlay private class MyItemizedOverlay extends ItemizedOverlay
Argument defaultMarker to obiekt graficzny wyświetlany dla każdego obiektu OverlayItem dodawanego do obiektu ItemizedOverlay. Przy dodawaniu obiektu graficznego do obiektu OverlayItem trzeba ustawić ograniczający prostokąt, do czego służy metoda setBounds. Można też wykorzystać jedną z dwóch metod pomocniczych — boundCenterBottom lub boundCenter — które ustawiają ograniczający prostokąt według środkowego punktu dolnej krawędzi albo środkowego punktu obiektu graficznego. Uwaga — wywołanie metody boundCenterBottom jest odpowiednikiem wywołania marker.setBounds(-marker.getIntrinsicWidth()/2, -marker.getIn trinsicHeight(), marker.getIntrinsicWidth() /2, 0); (marker to obiekt typu Drawable). Zwykle konstruktor ma następującą postać: public MyItemizedOverlay(Drawable defaultMarker) { super(boundCenterBottom(defaultMarker)); }
W aplikacji powinno znajdować się kilka obiektów OverlayItem, dlatego należy utworzyć obiekt List z obiektami typu wewnętrznego i zmodyfikować metody createItem(int i) oraz size() w taki sposób, aby korzystały z nowej listy (listing 16.14).
16.9. Wyśw etlan e k lku znaczn ków w w doku MapV ew
|
491
Listing 16.14. Korzystanie z kilku obiektów OverlayItem private List
Do tej pory wszystko działa poprawnie. Teraz trzeba utworzyć metodę pomocniczą dodającą obiekty OverlayItem do wewnętrznej listy. public void addOverlayItem(OverlayItem overlayItem) { mOverlays.add(overlayItem); populate(); }
Metoda populate() to metoda narzędziowa, która odpowiada za przetwarzanie nowych obiektów ItemizedOverlay. Elementy są udostępniane za pomocą metody createItem(int). Dobrą praktyczną regułą jest wywoływanie tej metody zaraz po zapisaniu danych w obiekcie Itemized Overlay — przed wywołaniem innych metod. Klasa wewnętrzna jest już gotowa. Teraz należy dodać kilka instrukcji do metody onCreate nadrzędnej aktywności MapActivity, aby zapisać kilka obiektów OverlayItem w nowej klasie pochodnej od ItemizedOverlay.
Używanie klasy MyItemizedOverlay w metodzie onCreate Poniżej pokazano, jak rozbudować metodę onCreate i utworzyć w niej obiekt typu wewnętrznego MyItemizedOverlay. Drawable markerDefault = this.getResources().getDrawable(R.drawable.marker_default); MyItemizedOverlay itemizedOverlay = new MyItemizedOverlay(markerDefault);
Teraz należy dodać kilka warstw. W czasie tworzenia obiektu OverlayItem trzeba do konstruktora przekazać trzy argumenty — jeden obiekt GeoPoint i dwa obiekty String (jeden z nagłówkiem, a drugi z dodatkowym tekstem). Poniższy kod dodaje obiekt OverlayItem dla Amsterdamu: GeoPoint point = new GeoPoint(52372991, 4892655); OverlayItem overlayItem = new OverlayItem(point, "Amsterdam", null); itemizedOverlay.addOverlayItem(overlayItem);
Do typu wewnętrznego MyItemizedOverlay należy teraz dodać następną metodę pomocniczą. Pobiera ona dwie wartości typu int (określające szerokość i długość geograficzną) i obiekt String (z nagłówkiem). public void addOverlayItem(int lat, int lon, String title) { GeoPoint point = new GeoPoint(lat, lon); OverlayItem overlayItem = new OverlayItem(point, title, null); addOverlayItem(overlayItem); }
Teraz można zmodyfikować kod dodający obiekt OverlayItem dla Amsterdamu, a także dodać dwa nowe obiekty tego typu — dla Londynu i dla Paryża.
492 |
Rozdz ał 16. Lokal zacja mapy
itemizedOverlay.addOverlayItem(52372991, 4892655, "Amsterdam"); itemizedOverlay.addOverlayItem(51501851, -140623, "Londyn"); itemizedOverlay.addOverlayItem(48857522, 2294496, "Paryż");
Następny krok polega na dodaniu warstwy z elementami do warstw dla widoku MapView. Uchwyt do listy warstw można uzyskać za pomocą metody getOverlays(). mapView.getOverlays().add(itemizedOverlay);
Należy też ustawić obiekt MapController widoku MapView, aby z właściwym przybliżeniem wyświetlić odpowiedni obszar widoku. Środek widoku jest ustawiany na obiekt GeoPoint odpowiadający Dunkierce, który dobrze się sprawdza dla dodanych miast. W klasie ItemizedOverlay nie istnieje metoda pomocnicza getCenter(), jednak jeśli chcesz, możesz ją łatwo zaimplementować. Poziom przybliżenia można ustawić na stałą wartość, choć klasa ItemizedOverlay udostępnia wygodne metody do wyznaczania poziomu, przy którym widoczne są wszystkie elementy z warstwy. Metody te wykorzystano w wywołaniu metody zoomToSpan obiektu MapController. MapController mc = mapView.getController(); mc.setCenter(new GeoPoint(51035349, 2370987)); // Dunkierka w Belgii mc.zoomToSpan(itemizedOverlay.getLatSpanE6(), itemizedOverlay.getLonSpanE6());
Gotowe! Po uruchomieniu aplikacji powinieneś zobaczyć obraz podobny do tego z rysunku 16.6.
Rysunek 16.6. Kilka znaczników na jednej mapie
Dodatkowe ćwiczenie — wyświetlanie znacznika zastępczego Wyszukaj w wyszukiwarce Google ciekawe znaczniki o wymiarach 100 × 100 pikseli i umieść je w katalogu ./res/drawable. Dodaj nowe obiekty graficzne jako dodatkowy argument do metody pomocniczej addOverlayItem. W czasie tworzenia obiektu OverlayItem wywołaj metodę setMarker(Drawable drawable) do ustawienia innego znacznika. Pamiętaj, aby określić ograniczenie przez wywołanie metody pomocniczej boundCenterBottom lub boundCenter. Możesz też
16.9. Wyśw etlan e k lku znaczn ków w w doku MapV ew
| 493
samodzielnie przeprowadzić obliczenia i wywołać metodę setBounds. Powodzenia! Jeśli te wskazówki nie wystarczą, rozwiązanie znajdziesz w kodzie źródłowym.
Wykonywanie operacji w reakcji na kliknięcie znacznika Klasa ItemizedOverlay udostępnia wygodne mechanizmy do obsługi zmian aktywnego elementu i dotknięć elementów z warstw. W tym końcowym podpunkcie zobaczysz, jak zaimplementować metodę onTap(int index), aby wyświetlała komunikat toast z nagłówkiem elementu z warstwy. Oczywiście w aplikacji w reakcji na kliknięcie znacznika można wykonywać dowolne operacje — wyświetlić okno dialogowe, uruchomić inną aktywność, za pomocą metody addView wyświetlić widok na mapie itd. Przekonasz się, że jest to bardzo proste! @Override protected boolean onTap(int index) { Toast.makeText(MainActivity.this, getItem(index).getTitle(), Toast.LENGTH_LONG).show(); return true; }
Metoda zwraca wartość true, aby poinformować, że obsłużyła dotknięcie. Po zwróceniu wartości false metoda onTap jest wywoływana dla wszystkich elementów z obiektu ItemizedOverlay. Jeśli ponownie uruchomisz aplikację i dotkniesz ekranu w pobliżu znacznika reprezentującego Paryż, zobaczysz obraz widoczny na rysunku 16.7.
Rysunek 16.7. Kilka różnych znaczników na jednej mapie
Zobacz także Receptura 16.6.
494 |
Rozdz ał 16. Lokal zacja mapy
16.10. Tworzenie warstw dla widoku MapView Rachee Singh
Problem Programista chce za pomocą grafiki wyróżnić punkt na google’owej mapie.
Rozwiązanie Należy wykorzystać warstwy mapy.
Omówienie Tworzenie własnej warstwy obejmuje dwa etapy:
1. Należy utworzyć klasę pochodną od Overlay i określić w niej potrzebne elementy (typ warstwy oraz jej cechy). Przeznaczony do tego kod pokazano na listingu 16.15.
2. W innej klasie, która kontroluje google’ową mapę na ekranie, trzeba utworzyć obiekt klasy pochodnej od Overlay. public class AddressOverlay extends Overlay
Listing 16.15. Konstruktor klasy AddressOverlay public AddressOverlay(Context context, Address address, int drawable) { super(); this.context=context; this.drawable=drawable; assert(null != address); this.setAddress(address); Double convertedLongitude = address.getLongitude() * 1E6; Double convertedLatitude = address.getLatitude() * 1E6; setGeopoint(new GeoPoint( convertedLatitude.intValue(), convertedLongitude.intValue())); }
Teraz w sposób pokazany na listingu 16.16 przesłoń metodę draw() klasy Overlay. Listing 16.16. Wyświetlanie warstwy @Override public boolean draw(Canvas canvas, MapView mapView, boolean shadow, long when) { super.draw(canvas, mapView, shadow); Point locationPoint = new Point(); Projection projection = mapView.getProjection(); projection.toPixels(getGeopoint(), locationPoint); // Wczytywanie rysunku
16.10. Tworzen e warstw dla w doku MapV ew
| 495
Bitmap markerImage = BitmapFactory.decodeResource(context.getResources(), drawable); // Wyświetlanie grafiki. Jej środek powinien znajdować się w miejscu // odpowiadającym adresowi canvas.drawBitmap(markerImage,locationPoint.x - markerImage.getWidth() / 2, locationPoint.y - markerImage.getHeight() / 2, null); return true; }
W klasie z implementacją funkcji obiektu MapView umieść kod z listingu 16.17. Dodaje on warstwę do mapy. Listing 16.17. Tworzenie obiektu z implementacją obsługi warstw List
16.11. Zmienianie trybów widoku MapView Rachee Singh
Problem Programista chce, aby w zależności od kontekstu pracy aplikacji ustawiany był odpowiedni tryb — widoku mapy, widoku ulic lub widoku satelitarnego.
Rozwiązanie Klasa MapView udostępnia metody do zmiany trybu mapy z domyślnego (standardowej mapy) na widok satelitarny lub z ulicami.
Omówienie Jeśli aplikacja ma wyświetlać informacje o odległości między dwoma miejscami, warto wyświetlać mapę z ulicami. Z kolei w innych aplikacjach potrzebny może być widok satelitarny z mapami Google’a. Zmiany można wprowadzać programowo za pomocą następujących instrukcji: // Widok z ulicami mapView.setStreetView(true); // Widok satelitarny mapView.setSatellite(true);
496 |
Rozdz ał 16. Lokal zacja mapy
16.12. Wyświetlanie ikony na warstwie bez korzystania z obiektów Drawable Keith Mendoza
Problem Programista chce wyświetlać warstwę w widoku MapView bez stosowania obiektów Drawable.
Rozwiązanie Należy przesłonić funkcję draw() klasy ItemizedOverlay().
Omówienie Aby zrozumieć tę recepturę, powinieneś wcześniej utworzyć przynajmniej przykładową aplikację „Witaj, MapView” z receptury 16.1. Tu nie znajdziesz informacji o tym, które metody klasy ItemizedOverlay trzeba zaimplementować. Możesz pobrać kompletny kod źródłowy przykładowej aplikacji Nearby Metars 01.01.0.2, dlatego niektóre fragmenty kodu omawianych klas zostały pominięte.
Wprowadzenie Aplikacja Nearby Metars w warstwie nad widokiem MapView wyświetla ikonę określającą zachmurzenie i kierunek wiatru w pobliżu lotniska. Ikona jest wyświetlana w taki sposób, że zajmuje obszar odpowiadający około mili wokół lotniska. Zainteresowani znajdą poniżej opis formatu METAR pochodzący z systemu pomocy ośrodka NOAA’s Aviation Weather Center (http://aviationweather.gov/adds/metars/description.php): Stacje meteorologiczne z całego świata co godzinę nadsyłają informacje o pogodzie, zapisując je w formacie METAR (jest to akronim od francuskiego zwrotu oznaczającego „rutynowe obserwacje pogody dla lotnictwa”). Dane są zbierane i rozpowszechniane przez ośrodek U.S. National Weather Service (oraz analogiczne jednostki z innych państw). Na stronie 4. systemu pomocy (http://aviationweather.gov/adds/metars/description/page_no/4) znajdują się ikony reprezentujące zachmurzenie. Aplikacja ma wyświetlać takie ikony nad lotniskami, aby informować o zachmurzeniu. Kreska obok ikony określa kierunek, z którego wieje wiatr.
Przesłanianie metody ItemizedOverlay::draw() Metoda ItemizedOverlay::draw() jest wywoływana zawsze wtedy, gdy z dowolnego powodu trzeba ponownie wyświetlić widok MapView. Oto sygnatura metody draw(): public void draw(android.graphics.Canvas canvas, MapView mapView, boolean shadow)
16.12. Wyśw etlan e kony na warstw e bez korzystan a z ob ektów Drawable
|
497
Poniżej znajdziesz opisy parametrów pochodzące z dokumentacji interfejsu API: canvas
Jest to obiekt Canvas, na którym wyświetlane są elementy. Zauważ, że aplikacja mogła już odpowiednio przekształcić ten obiekt, dlatego nie należy go modyfikować.
mapView
Jest to obiekt MapView, który zażądał wyświetlenia. Za pomocą metody MapView.getProjec tion() można przekształcać wartości między widocznymi na ekranie pikselami a parami
szerokość geograficzna – długość geograficzna. shadow
Jeśli jego wartość to true, należy wyświetlić warstwę z cieniem. Jeżeli wartość to false, wyświetlana jest zawartość danej warstwy.
Przy każdym ponownym wyświetlaniu elementów metoda draw() jest wywoływana dwukrotnie — pierwszy raz z parametrem shadow ustawionym na true i drugi raz dla tego parametru z wartością false. W aplikacji Nearby Metars nie trzeba wyświetlać cieni dla elementów z warstwy. MetarList z aplikacji NearbyMetars to implementacja klasy ItemizedOverlay dostosowana do obiektów MetarItem. W klasie MetarList przesłonięte są metody abstrakcyjne i metoda draw(). Poniżej pokazano kod metody MetarList::draw(): public void draw(android.graphics.Canvas canvas, MapView mapView, boolean shadow) { if(!shadow) { Log.v("NearbyMetars", "Wyświetlanie elementów"); MetarItem item; for(int i=0; i
W metodzie tej mOverlays to obiekt typu ArrayList
Omówienie klasy MetarItem Klasa ta dziedziczy po klasie OverlayItem. W polach mTitle i mSnippet (odziedziczonych po klasie OverlayItem) zapisywane są kody lotnisk organizacji ICAO (ang. International Civil Aviation Organization) oraz nieprzetworzony łańcuch znaków z danymi w formacie METAR. W klasie MetarItem znajdują się też dwa nowe pola: skyCond
Jest to egzemplarz typu wyliczeniowego SkyConds, zdefiniowanego w klasie MetarItem.
windDir
Jest to wartość typu float, w której przechowywany jest kierunek wiatru.
Omówienie metody MetarItem::draw() To w tej metodzie aplikacja dodaje ikonę do obiektu Canvas. Na diagramach z danymi METAR z usługi ADDS ikony reprezentujące zachmurzenie są wyświetlane za pomocą kolorów ozna-
498 |
Rozdz ał 16. Lokal zacja mapy
czających aktualną kategorię lotów, która jest obowiązująca dla danego lotniska. Jednak w wersji 01.01.0.2 aplikacja Nearby Metars nie określa kategorii lotów, dlatego wszystkie ikony są czarne. Aby zwiększyć przejrzystość, kod podzielono na fragmenty. Po każdym fragmencie znajduje się wyjaśnienie. public void draw(Canvas canvas, MapView mapView) {
Metoda przyjmuje dwa parametry — canvas i mapView. Mają one te same typy co dwa pierwsze parametry metody ItemizedOverlay::draw(). // Określanie miejsca wyświetlania ikony Point point = new Point(); Projection projection = mapView.getProjection(); projection.toPixels(mPoint, point);
Najpierw współrzędne geograficzne lotniska są przekształcane na współrzędne (x, y). Metoda Projection::toPixels() jako pierwszy parametr przyjmuje obiekt GeoPoint (obejmujący szerokość geograficzną i długość geograficzną miejsca, dla którego wyświetlany jest znacznik), a jako drugi parametr — obiekt Point (zapisywane są w nim współrzędne (x, y) określające położenie tego miejsca w widoku MapView). final float project = (float)((projection.metersToEquatorPixels((float)1609.344) > 10) ? projection.metersToEquatorPixels((float)1609.344) : 10.0); Log.d("NearbyMetars", "Uzyskane wartości: " + Float.toString(project)); final RectF drawPos = new RectF(point.x-project, point.y-project, point.x+project, point.y+project);
Następnie należy wyznaczyć, ilu pikselom odpowiada jedna mila przy ustawionym poziomie przybliżenia. Potem aplikacja oblicza współrzędne obszaru, w którym znaleźć się ma ikona, i zapisuje je w obiekcie RectF (http://developer.android.com/reference/android/graphics/RectF.html). // Tworzenie obiektu Paint Paint paint = new Paint(); paint.setStyle(Paint.Style.STROKE); paint.setARGB(179, 0, 0, 0); paint.setStrokeWidth(2.0f); paint.setStrokeCap(Paint.Cap.BUTT);
Aplikacja tworzy obiekt Paint (http://developer.android.com/reference/android/graphics/Paint.html), a następnie ustawia grubość linii (2 piksele) oraz jej kolor (czarny z około 70% przezroczystości). Ikony określające zachmurzenie są częściowo przezroczyste, aby użytkownik mógł przeczytać etykiety na mapie. Warto pamiętać, że ikony są wyświetlane w warstwie nakładanej na mapę. Dalszy kod pokazano na listingu 16.18. Listing 16.18. Wyświetlanie ikony switch(skyCond) { case CLR: canvas.drawRect(drawPos, paint); break; case SKC: canvas.drawCircle(point.x, point.y, project, paint); break; case FEW: canvas.drawCircle(point.x, point.y, project, paint); canvas.drawLine(point.x, drawPos.top, point.x, drawPos.bottom, paint); break; case SCT: canvas.drawArc(drawPos, 0, 270, false, paint);
16.12. Wyśw etlan e kony na warstw e bez korzystan a z ob ektów Drawable
| 499
paint.setStyle(Paint.Style.FILL_AND_STROKE); canvas.drawArc(drawPos, 270, 90, true, paint); break; case BKN: canvas.drawArc(drawPos, 180, 90, false, paint); paint.setStyle(Paint.Style.FILL_AND_STROKE); canvas.drawArc(drawPos, 270, 270, true, paint); break; case OVC: paint.setStyle(Paint.Style.FILL_AND_STROKE); canvas.drawCircle(point.x, point.y, project, paint); break; case OVX: canvas.drawArc(drawPos, 45, 180, true, paint); canvas.drawArc(drawPos, 135, 180, true, paint); canvas.drawArc(drawPos, 315, 90, true, paint); break; }
Kod z listingu 16.18 na podstawie wartości zmiennej skyCond wyświetla odpowiednią ikonę reprezentującą zachmurzenie. Omówienie metod draw*() znajdziesz w dokumentacji klasy Canvas (http://developer.android.com/reference/android/graphics/Canvas.html). Wyświetlanie ikon dla warunków CLR i SKC jest proste — wystarczy wywołać odpowiednią metodę z rodziny draw*(). Warunek FEW wymaga wywołania metody drawCircle(), aby narysować okrąg, i metody drawLine(), która tu wyświetla pionową linię. W tym miejscu nie ma znaczenia, że wywołanie metody drawLine() znajduje się przed wywołaniem drawCircle(). Warto jednak pamiętać, że kolejne wywołania metod draw*() dla tego samego obszaru powodują rysowanie kształtów jeden na drugim. W warunkach SKT, BKN i OVC najpierw należy wywołać metodę drawArc(), aby narysować niewypełniony fragment ikony. Następnie trzeba zmienić tryb pędzla na FILL_AND_STROKE i ponownie wywołać metodę drawArc(), żeby domknąć okrąg jego wypełnioną częścią. Zastosowanie metody drawArc() jest tu optymalizacją. Metoda Canvas::drawCircle() także wywołuje na zapleczu metodę Canvas::drawCircle(). Po co rysować obraz, który zostanie zasłonięty przez inną grafikę, rysowaną w tym samym miejscu? // Wyświetlanie kreski określającej kierunek wiatru (jeśli wiatr NIE jest zmienny) if(windDir > 0) { final float barLen = project * 3; // Kod zmodyfikowano, aby uzyskać współrzędne kartezjańskie // (przeciwne względem standardowych współrzędnych biegunowych) canvas.drawLine(point.x, point.y, (float)(point.x + barLen * Math.sin(windDir)), (float)(point.y - barLen * Math.cos(windDir)), paint); } }
Ostatni fragment kodu wyświetla kreskę określającą kierunek wiatru (choć nie wyświetla linii oznaczających siłę wiatru). Jak wspomniano w komentarzu, funkcja wyznacza współrzędne kartezjańskie — z kątem ustawianym zgodnie z ruchem wskazówek zegara (w ten sposób kierunek określony jest w kompasie). W matematyce standardowe współrzędne biegunowe wyznaczają kod rysowany przeciwnie do ruchu wskazówek zegara. Warto też zauważyć, że wartość project to wyrażony w radianach odpowiednik kierunku wiatru pokazywanego w kompasie.
500 |
Rozdz ał 16. Lokal zacja mapy
Uwagi końcowe Korzystanie z metod Canvas::draw*() nie zawsze jest najlepszym sposobem na rysowanie ikon na nakładanej warstwie. W Androidzie renderowanie zasobów graficznych (http://developer. android.com/guide/topics/resources/drawable-resource.html) jest bardziej zoptymalizowane niż wywoływanie metod Canvas::draw*(). Ponadto łatwiej jest przygotować atrakcyjne rysunki za pomocą programu graficznego niż przy użyciu instrukcji. Gdyby w warstwach w aplikacji Nearby Metars wykorzystano zasoby graficzne, edycja plików XML byłaby niewygodna. W tym przypadku zastosowanie bitmap prowadziłoby tylko do niepotrzebnego wzrostu złożoności kodu. To, czy lepiej jest zastosować obiekty graficzne, czy rysować ikony programowo, zależy w dużym stopniu od wymagań projektowych.
Zobacz także Samouczek tworzenia aplikacji „Witaj, MapView” (https://developers.google.com/maps/documenta tion/android/hello-mapview), dokumentacja klasy Canvas (http://developer.android.com/reference/ android/graphics/Canvas.html), dokumentacja klasy ItemizedOverlay (https://developers.google.com/ maps/documentation/android/reference/com/google/android/maps/ItemizedOverlay?hl=pl-PL), dokumentacja klasy OverlayItem (https://developers.google.com/maps/documentation/android/reference/ com/google/android/maps/OverlayItem?hl=pl-PL), dokumentacja rozszerzenia Google Add-On API (https://developers.google.com/maps/documentation/android/reference/?hl=pl-PL).
16.13. Implementowanie wyszukiwania lokalizacji na mapach Google’a Rachee Singh
Problem Programista chce umożliwić użytkownikowi wprowadzenie nazwy miejsca i wyszukiwanie go na mapach Google’a. Aplikacja ma udostępniać użytkownikowi listę wszystkich wyników i wyświetlać najodpowiedniejszą lokalizację.
Rozwiązanie Aplikacja powinna najpierw pobierać tekst wprowadzony przez użytkownika w polu EditText. Następnie powinna uruchamiać wyszukiwanie lokalizacji i pobierać wyniki. W ostatnim kroku należy wyświetlić najlepszy wynik (w przykładowym programie jest on pokazywany w komunikacie toast; w produkcyjnej aplikacji dane można wykorzystać w znacznie ciekawszy sposób).
Omówienie Pokazana poniżej metoda pobiera tekst z kontrolki EditText o nazwie addressText. Następnie aplikacja wyszukuje podaną nazwę za pomocą metody getFromLocationName() klasy Geocoder.
16.13. mplementowan e wyszuk wan a lokal zacj na mapach Google’a
|
501
Z uzyskanych wyników wyszukiwania należy pobrać pierwszą pozycję i wyświetlić w komunikacie toast. Jeśli zwrócony łańcuch znaków ma długość 0, aplikacja wyświetla odpowiedni komunikat. Na listingu 16.19 pokazano kod metody, a na rysunku 16.8 — efekt uruchomienia aplikacji. Listing 16.19. Wyszukiwanie lokalizacji za pomocą map Google’a protected void mapCurrentAddress() { String addressString = addressText.getText().toString(); Geocoder g = new Geocoder(this); List addresses; try { addresses = g.getFromLocationName(addressString, 1); String add = ""; if (addresses.size() > 0) { address = addresses.get(0); for (int i=0; i < address.getMaxAddressLineIndex();i++) { add += address.getAddressLine(i) + "\n"; } Toast.makeText(getBaseContext(), add, Toast.LENGTH_SHORT).show(); } else { Toast.makeText(getBaseContext(), "Nieudana próba zlokalizowania adresu.", Toast.LENGTH_SHORT).show(); } } catch (IOException e) { e.printStackTrace(); } }
Rysunek 16.8. Mapa widoczna na pierwszej zakładce
502
|
Rozdz ał 16. Lokal zacja mapy
16.14. Wyświetlanie widoku MapView w kontrolce TabView Vladimir Kroz
Problem Programista chce wyświetlać widok MapView w kontrolce TabView.
Rozwiązanie Należy utworzyć widok MapView i powiązany z nim układ w formacie XML. Widok ten powinien być wyświetlany jako samodzielny. Następnie należy dodać kontrolkę TabView i odpowiadający jej układ w XML-u. W ostatnim kroku aktywność wyświetlającą widok MapView trzeba powiązać z jedną z zakładek. Służy do tego metoda TabSpec.setContent(). I to już wszystko!
Omówienie Aby uruchomić program przedstawiony w tej recepturze, trzeba uzyskać klucz Google Maps API. Generowanie takiego klucza opisano w recepturze 16.6. W typowym układzie TabLayout (rysunek 16.9) znajduje się kontener TabHost, kontrolka Tab Widget, w której wyświetlane są zakładki, oraz układ FrameLayout o predefiniowanym identyfikatorze @android:id/tabcontent (w układzie tym umieszczana jest zmienna zawartość zakładek). Na listingu 16.20 przedstawiono kod układu w XML-u.
Rysunek 16.9. Układ TabLayout Listing 16.20. XML-owy układ zakładek
16.14. Wyśw etlan e w doku MapV ew w kontrolce TabV ew
| 503
android:layout_width="fill_parent" android:layout_height="wrap_content"/>
Kod układu widoku MapView pokazano na listingu 16.21. Listing 16.21. XML-owy układ widoku MapView
Na listingu 16.22 pokazano kod, który stanowi punkt wejścia do aplikacji. Listing 16.22. Plik AppMain.java package org.kroztech.cookbook; import import import import import import import
android.app.TabActivity; android.content.Context; android.content.Intent; android.os.Bundle; android.widget.FrameLayout; android.widget.TabHost; android.widget.TabHost.TabSpec;
public class AppMain extends TabActivity { TabHost mTabHost; FrameLayout mFrameLayout; /** Wywoływana, gdy aktywność tworzona jest po raz pierwszy */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); mTabHost = getTabHost(); TabSpec tabSpec = mTabHost.newTabSpec("tab_test1"); tabSpec.setIndicator("Map"); Context ctx = this.getApplicationContext(); Intent i = new Intent(ctx, MapTabView.class); tabSpec.setContent(i); mTabHost.addTab(tabSpec); mTabHost.addTab( mTabHost.newTabSpec("tab_test2").setIndicator("Szczegóły").setContent(R.id.textview2)); mTabHost.setCurrentTab(0); } }
504 |
Rozdz ał 16. Lokal zacja mapy
Na listingu 16.23 znajduje się kod aktywności MapActivity. Listing 16.23. Aktywność MapActivity package org.kroztech.cookbook; import android.os.Bundle; import com.google.android.maps.MapActivity; public class MapTabView extends MapActivity { @Override protected void onCreate(Bundle icicle) { super.onCreate(icicle); setContentView(R.layout.maptabview); } @Override protected boolean isRouteDisplayed() { return false; } }
Ostatni z prezentowanych listingów, 16.24, pokazuje plik manifestu. Listing 16.24. Plik AndroidManifest.xml
16.15. Obsługa długich kliknięć w widokach MapView Roger Kind Kristiansen
Problem W niektórych aplikacjach z mapami warto umożliwić użytkownikom wykonywanie operacji związanych z dowolnym punktem na mapie — na przykład poprzez menu kontekstowe. 16.15. Obsługa dług ch kl kn ęć w w dokach MapV ew
| 505
Umożliwienie użytkownikom długiego kliknięcia punktu jest jednym z najbardziej intuicyjnych sposobów na uzyskanie tego efektu, jednak Android nie oferuje wbudowanego rozwiązania tego problemu.
Rozwiązanie Trzeba samodzielnie dodać potrzebny mechanizm. Najpierw utwórz klasę pochodną od MapView i zdefiniuj w niej własny interfejs OnLongpressListener, a także przesłoń metodę MapView.onTouch Event() (należy umieścić w niej kod wykonywany po wykryciu długiego kliknięcia). Metoda onTouchEvent()wywoływana jest za każdym razem, gdy użytkownik położy palec na mapie, przesunie nim po niej lub podniesie go, dlatego doskonale nadaje się do uzyskania pożądanego efektu. Po zmodyfikowaniu pliku układu z mapą i powiązaniu mapy z aktywnością MapActivity można utworzyć obiekt OnLongPressListener i dodać go do egzemplarza klasy pochodnej od MapView. Następnie można już obserwować efekty długiego kliknięcia.
Omówienie Najpierw opisano istotę rozwiązania — klasę pochodną od MapView, definicję interfejsu OnLong pressListener oraz kod uruchamiany w odpowiedzi na długie kliknięcie (listing 16.25). Listing 16.25. Widok MapView z obsługą długiego kliknięcia public class MyCustomMapView extends MapView { // Definicja interfejsu odbiornika, używanego później w aktywności MapActivity public interface OnLongpressListener { public void onLongpress(MapView view, GeoPoint longpressLocation); } // Czas w milisekundach, po którym uruchamiany jest odbiornik OnLongpressListener static final int LONGPRESS_THRESHOLD = 500; /* * Obiekt Timer jest bardzo ważny przy wykrywaniu długich kliknięć. Wykonuje on * zadanie po upływie określonego czasu */ private Timer longpressTimer = new Timer(); /* * Obiekt OnLongPressListener. Po wykryciu długiego kliknięcia wywoływana jest * metoda onLongPress() */ private MyCustomMapView.OnLongpressListener longpressListener; /* * Przechowywanie współrzędnych środka mapy, co pozwala ustalić, czy mapę przesunięto */ private GeoPoint lastMapCenter; public MyCustomMapView(Context context, String apiKey) { super(context, apiKey); }
506
|
Rozdz ał 16. Lokal zacja mapy
public MyCustomMapView(Context context, AttributeSet attrs) { super(context, attrs); } public MyCustomMapView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } public void setOnLongpressListener(MyCustomMapView.OnLongpressListener listener) { longpressListener = listener; } /* * Android wywołuje tę metodę za każdym razem, gdy użytkownik dotknie mapy, * przesunie po niej palcem lub oderwie palec od mapy */ @Override public boolean onTouchEvent(MotionEvent event) { // Wykonywanie niestandardowego kodu handleLongpress(event); return super.onTouchEvent(event); } /* * Ta metoda przyjmuje argument typu MotionEvent i określa, * czy nastąpiło długie kliknięcie * * Po określonym czasie klasa Timer uruchamia zadanie TimerTask. * Odliczanie jest uruchamiane po dotknięciu ekranu * * Następnie aplikacja oczekuje na poruszenie palcem lub jego oderwanie od * ekranu. Jeśli któreś z tych zdarzeń nastąpi przed wykonaniem zadania * TimerTask, należy je anulować. W przeciwnym razie aplikacja uruchamia * metodę OnLongPressListener.onLongpress() */ private void handleLongpress(final MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_DOWN) { // Palec dotknął ekranu longpressTimer = new Timer(); longpressTimer.schedule(new TimerTask() { @Override public void run() { GeoPoint longpressLocation = getProjection().fromPixels((int)event.getX(), (int)event.getY()); /* * Uruchamianie odbiornika. Należy przekazać także miejsce * długiego kliknięcia (może być potrzebne w jednostce * wywołującej) */ longpressListener.onLongpress( MyCustomMapView.this, longpressLocation); } }, LONGPRESS_THRESHOLD); lastMapCenter = getMapCenter(); }
16.15. Obsługa dług ch kl kn ęć w w dokach MapV ew
|
507
if (event.getAction() == MotionEvent.ACTION_MOVE) { if (!getMapCenter().equals(lastMapCenter)) { // Użytkownik przesuwa mapę — nie jest to długie kliknięcie longpressTimer.cancel(); } lastMapCenter = getMapCenter(); } if (event.getAction() == MotionEvent.ACTION_UP) { // Użytkownik oderwał palec od mapy longpressTimer.cancel(); } if (event.getPointerCount() > 1) { // Zdarzenie z kilkoma dotknięciami, prawdopodobnie przybliżanie longpressTimer.cancel(); } } }
Trzeba zmodyfikować plik układu z mapą i zastosować w nim zdefiniowany wcześniej widok MyCustomMapView:
Zwróć uwagę na atrybut android:clickable. Jak może wiesz, trzeba go tak ustawić, aby możliwe było przesuwanie mapy, przybliżanie jej i wchodzenie z nią w interakcje na inne sposoby. Ostatnią rzeczą, jaką trzeba zrobić, jest dodanie obiektu OnLongpressListener do widoku MapView w aktywności MapActivity. Na potrzeby przykładu przyjmij, że nazwa wcześniejszego pliku układu to res/layout/map.xml. Kod potrzebny do dodania odbiornika OnLongpressListener pokazano na listingu 16.26. Listing 16.26. Aktywność Map public class Map extends MapActivity { private MyCustomMapView mapView; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Dodawanie układu z mapą do danej aktywności MapActivity setContentView(R.layout.map); // Dodawanie odbiornika OnLongpressListener do niestandardowego widoku MapView mapView = (MyCustomMapView)findViewById(R.id.mapview); mapView.setOnLongpressListener(new MyCustomMapView.OnLongpressListener() { public void onLongpress(final MapView view, final GeoPoint longpressLocation) {
508 |
Rozdz ał 16. Lokal zacja mapy
runOnUiThread(new Runnable() { public void run() { /* * Tu umieść operacje wykonywane w reakcji na długie kliknięcie */ } }); } }); }
Aby długie kliknięcie prowadziło do otwarcia menu kontekstowego, trzeba dodatkowo skonfigurować takie menu. Tu pominięto ten krok, aby przykład był krótszy i bardziej przejrzysty. Aby przetestować działanie kodu, możesz na przykład dodać instrukcję zapisującą wpis w dzienniku.
16.16. Korzystanie z map OpenStreetMap Rachee Singh
Problem Programista chce w aplikacji zastosować mapy OpenStreetMap (OSM) zamiast map Google’a.
Rozwiązanie Należy zastosować niezależną bibliotekę osmdroid do interakcji z mapami OSM.
Omówienie OSM to bezpłatna, możliwa do edycji mapa świata, natomiast OpenStreetMapView jest bezpłatnym i (prawie) kompletnym zastępnikiem androidowej klasy MapView. Szczegółowe informacje znajdziesz na stronie biblioteki osmdroid w serwisie Google Code (http://code.google.com/p/osmdroid/). Aby w aplikacji na Android wykorzystać mapy OSM, trzeba utworzyć projekt z interfejsem API w wersji 3. (wersja 1.5 Androida). Do projektu należy dodać dwa archiwa JAR — osmdroid-android-x.xx.jar i slf4j-android-1.x.x.jar. Biblioteka osmdroid obejmuje zestaw narzędzi do obsługi map OSM. SLF4J to uproszczony fronton do rejestrowania wpisów w dzienniku. Archiwa można pobrać z następujących stron: • osmdroid (http://code.google.com/p/osmdroid/downloads/detail?name=osmdroid-android-3.0.5.jar); • slf4j (http://www.slf4j.org/android/slf4j-android-1.5.8.jar).
Z receptury 1.10 dowiesz się, jak stosować zewnętrzne biblioteki w projektach na Android. Po dodaniu do projektu archiwów JAR można rozpocząć pisanie kodu. Najpierw do XML-owego układu dodaj widok MapView dla map OSM:
16.16. Korzystan e z map OpenStreetMap
| 509
android:layout_height="fill_parent">
Warto pamiętać, że w pliku AndroidManifest.xml należy zażądać uprawnień INTERNET, aby aplikacja mogła pobierać dane z internetu. Kod biblioteki osmdroid wymaga też uprawnień ACCESS_NETWORK_STATE:
Teraz widok MapView należy zastosować w kodzie aktywności. Odbywa się to podobnie jak przy korzystaniu z map Google’a. Potrzebny kod pokazano na listingu 16.27. Listing 16.27. Zastosowanie widoku MapView w aplikacji private MapView mapView; private MapController mapController; mapView = (MapView) this.findViewById(R.id.mapview); mapView.setBuiltInZoomControls(true); mapView.setMultiTouchControls(true); mapController = this.mapView.getController(); mapController.setZoom(2);
Na rysunku 16.10 pokazano, jak aplikacja powinna wyglądać po uruchomieniu. Na rysunku 16.11 przedstawiono program po dotknięciu przez użytkownika kontrolek przeznaczonych do przybliżania mapy.
Rysunek 16.10. Mapa OSM
510
|
Rozdz ał 16. Lokal zacja mapy
Rysunek 16.11. Mapa OSM po przybliżeniu
16.17. Tworzenie warstw dla map OSM Rachee Singh
Problem Programista chce na mapach OSM wyświetlać grafikę (np. znaczniki). Większość narzędzi do obsługi map udostępnia warstwy, co pozwala umieszczać grafikę nad głównym obrazem lub mapą (zobacz rysunek 16.4).
Rozwiązanie Utwórz obiekt Overlay i dodaj warstwę w odpowiednim miejscu mapy.
Omówienie Wprowadzenie do korzystania z map OSM znajdziesz w recepturze 16.16. Aby dodać warstwy, najpierw trzeba pobrać uchwyt do widoku MapView zdefiniowanego w XML-owym układzie aktywności: mapView = (MapView) this.findViewById(R.id.mapview);
Następnie w widoku MapView należy włączyć kontrolki przybliżania, używając metody set BuildInZoomControls. Warto też ustawić poziom przybliżenia na rozsądną wartość.
16.17. Tworzen e warstw dla map OSM
|
511
mapView.setBuiltInZoomControls(true); mapController = this.mapView.getController(); mapController.setZoom(12);
Teraz należy utworzyć dwa obiekty GeoPoint. Pierwszy (mapCenter) służy do wypośrodkowania mapy OSM na podstawie określonego punktu w momencie uruchamiania aplikacji. Drugi (overlayPoint) określa, gdzie należy umieścić nakładaną warstwę. GeoPoint mapCenter = new GeoPoint(53554070, -2959520); GeoPoint overlayPoint = new GeoPoint(53554070 + 1000, -2959520 + 1000); mapController.setCenter(mapCenter);
Aby dodać warstwę, należy utworzyć listę ArrayList obiektów OverlayItem. Do listy trzeba dodać warstwy, które mają znaleźć się nad mapą OSM. ArrayList
W celu przygotowania warstwy należy utworzyć obiekt ItemizedIconOverlay i za pomocą argumentów określić punkt, w którym warstwa ma być wyświetlana, a także pośrednika zasobów itd. Następnie można warstwę dodać do mapy OSM. resourceProxy = new DefaultResourceProxyImpl(getApplicationContext()); this.myLocationOverlay = new ItemizedIconOverlay
Teraz należy wywołać metodę invalidate. Jest to potrzebne do aktualizowania widoku MapView, aby użytkownik zobaczył wprowadzone zmiany. mapView.invalidate();
Efekt końcowy pokazano na rysunkach 16.12 i 16.13.
Rysunek 16.12. Mapa OSM z warstwą ze znacznikiem
512
|
Rozdz ał 16. Lokal zacja mapy
Rysunek 16.13. Przybliżona mapa OSM z warstwą ze znacznikiem
16.18. Stosowanie skali w mapach OSM Rachee Singh
Problem Programista chce na mapie OSM wyświetlać skalę, aby informować użytkowników o poziomie przybliżenia widoku MapView.
Rozwiązanie Skalę do mapy OSM można dodać jako warstwę za pomocą klasy ScaleBarOverlay z biblioteki osmdroid.
Omówienie Umieszczenie skali w widoku MapView pomaga użytkownikom ustalić poziom przybliżenia mapy (a także oszacować odległości na mapie). Aby nałożyć skalę na widok MapView z mapą OSM, należy utworzyć obiekt ScaleBarOverlay i dodać go do listy warstw widoku MapView za pomocą metody add(). Kod powinien wyglądać tak: ScaleBarOverlay myScaleBarOverlay = new ScaleBarOverlay(this); this.mapView.getOverlays().add(this.myScaleBarOverlay);
16.18. Stosowan e skal w mapach OSM
|
513
Warstwę ze skalą pokazano na rysunku 16.14.
Rysunek 16.14. Mapa OSM ze skalą
16.19. Obsługa dotknięć warstwy mapy OSM Rachee Singh
Problem Programista chce wykonywać operacje w reakcji na dotknięcie warstwy mapy OSM.
Rozwiązanie Należy przesłonić metody interfejsu OnItemGestureListener przeznaczone do obsługi zwykłego dotknięcia oraz długiego kliknięcia.
Omówienie Aby dodać obsługę dotknięć warstwy mapy, należy zmienić sposób tworzenia elementów warstw (więcej informacji o stosowaniu warstw w mapach OSM znajdziesz w recepturze 16.17). W czasie tworzenia obiektu OverlayItem można podać jako argument anonimowy obiekt typu OnItemGestureListener i udostępnić własną implementację metod onItemSingleTapUp oraz onItem LongPress. Tu metody te wyświetlają komunikat toast z informacją o zgłoszonym zdarzeniu (zwykłe dotknięcie lub długie kliknięcie), a także z nagłówkiem i opisem dotkniętej warstwy. Potrzebny kod przedstawiono na listingu 16.28.
514
|
Rozdz ał 16. Lokal zacja mapy
Listing 16.28. Kod do obsługi dotknięć mapy OSM ArrayList
Po pojedynczym dotknięciu warstwy aplikacja powinna wyglądać tak jak na rysunku 16.15.
Rysunek 16.15. Mapa OSM z obsługą dotknięcia
16.19. Obsługa dotkn ęć warstwy mapy OSM
|
515
Na rysunku 16.16 pokazano, jak aplikacja działa po długim kliknięciu warstwy.
Rysunek 16.16. Reakcja na długie kliknięcie warstwy
16.20. Aktualizowanie lokalizacji na mapach OSM Rachee Singh
Problem Aplikacja ma reagować na zmiany lokalizacji urządzenia i przesuwać mapę, tak aby widoczne było aktualne położenie użytkownika.
Rozwiązanie Za pomocą odbiornika LocationListener aplikacja może żądać aktualizowania lokalizacji (zobacz recepturę 16.2), a następnie reagować na zmiany położenia przez przesunięcie mapy.
Omówienie • W aktywności obejmującej widok MapView z mapą OSM trzeba zaimplementować interfejs
LocationListener, aby można było żądać zmian w lokalizacji urządzenia. W tej aktywności trzeba też umieścić niezaimplementowane (abstrakcyjne) metody interfejsu Location Listener (środowisko Eclipse robi to automatycznie). Środek mapy jest tu ustawiany na obiekt GeoPoint o nazwie mapCenter, dlatego aplikacja początkowo wyśrodkowana jest na
tym punkcie.
516
|
Rozdz ał 16. Lokal zacja mapy
• Następnie trzeba pobrać obiekt LocationManager i wykorzystać go do zażądania aktualizo-
wania lokalizacji, wywołując metodę requestLocationUpdates. • W jednej z przeciążonych metod abstrakcyjnych z interfejsu LocationListener, a mianowi-
cie onLocationChanged, można umieścić kod wykonywany w reakcji na zmianę lokalizacji urządzenia. • W metodzie onLocationChanged aplikacja pobiera szerokość geograficzną i długość geogra-
ficzną nowej lokalizacji, a następnie ustawia środek mapy na obiekt GeoPoint utworzony na podstawie uzyskanych współrzędnych. Potrzebny kod znajdziesz na listingu 16.29. Listing 16.29. Zarządzanie zmianami lokalizacji na mapach OSM public class LocationChange extends Activity implements LocationListener { private LocationManager myLocationManager; private MapView mapView; private MapController mapController; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); mapView = (MapView)findViewById(R.id.mapview); mapController = this.mapView.getController(); mapController.setZoom(15); GeoPoint mapCenter = new GeoPoint(53554070, -2959520); mapController.setCenter(mapCenter); myLocationManager = (LocationManager) getSystemService(LOCATION_SERVICE); myLocationManager.requestLocationUpdates( LocationManager.GPS_PROVIDER, 1000, 100, this); } @Override public void onLocationChanged(Location location) { int latitude = (int) (location.getLatitude() * 1E6); int longitude = (int) (location.getLongitude() * 1E6); GeoPoint geopoint = new GeoPoint(latitude, longitude); mapController.setCenter(geopoint); mapView.invalidate(); } @Override public void onProviderDisabled(String arg0) { } @Override public void onProviderEnabled(String arg0) { } @Override public void onStatusChanged(String arg0, int arg1, Bundle arg2) { } }
Po uruchomieniu aplikacji mapa jest wyśrodkowana na obiekt GeoPoint o nazwie mapCenter. Ponieważ aplikacja odbiera zmiany lokalizacji, na górnym pasku telefonu widoczna jest specjalna ikona (rysunek 16.17).
16.20. Aktual zowan e lokal zacj na mapach OSM
|
517
Rysunek 16.17. Przesuwanie mapy — początkowy etap
Następnie za pomocą okna Emulator Control można przesłać do emulatora nowe współrzędne GPS (–122,094095, 37,422006). Aplikacja reaguje na to przez wyśrodkowanie mapy na podstawie nowych współrzędnych (rysunek 16.18).
Rysunek 16.18. Przesuwanie mapy — koniec procesu
Po podaniu w oknie Emulator Control innych współrzędnych GPS aplikacja wyśrodkuje mapę w innym miejscu (rysunek 16.19).
518
|
Rozdz ał 16. Lokal zacja mapy
Rysunek 16.19. Zmiana lokalizacji w emulatorze
Aby umożliwić aplikacji odbieranie zmian lokalizacji, należy zażądać w pliku AndroidManifest. xml następujących uprawnień:
android:name="android.permission.ACCESS_COARSE_LOCATION"/> android:name="android.permission.ACCESS_FINE_LOCATION"/> android:name="android.permission.ACCESS_NETWORK_STATE" /> android:name="android.permission.INTERNET" />
16.20. Aktual zowan e lokal zacj na mapach OSM
|
519
520
|
Rozdz ał 16. Lokal zacja mapy
ROZDZIAŁ 17.
Akcelerometr
17.1. Wprowadzenie — czujniki Ian Darwin
Omówienie Akcelerometry to jedne z ciekawszych mechanizmów wbudowanych w smartfony. Znajdowały się już w starszych telefonach, takich jak Neo z systemem OpenMoko czy iPhone Apple’a. Zanim na rynku pojawił się Android, na konferencjach na temat oprogramowania o otwartym dostępie do kodu źródłowego zachęcałem do korzystania z systemu OpenMoko. Jedną z moich ulubionych wymarzonych aplikacji było narzędzie do generowania prywatnych kluczy. Byłaby ona oparta na zasadzie, że gdy prywatność jest „nielegalna”, tylko przestępcy mogą mieć prywatność. W kilka osób rozmawialiśmy o tym już w 2008 roku, gdy zaprezentowałem ten pomysł (spotkał się on z bardzo ciepłym przyjęciem) na konferencji Ontario Linux Fest. Rozwiązanie polega na tym, że jeśli użytkownicy nie mogą lub nie chcą wymieniać kluczy prywatnych przez publicznie dostępny kanał, to wystarczy, iż spotkają się osobiście i wymienią uścisk dłoni, trzymając w nich telefony. Telefony stykają się wtedy ze sobą, dlatego ich czujniki powinny zarejestrować te same losowe ruchy. Za pomocą obliczeń matematycznych można odfiltrować ruchy sprzed uścisku (i po nim), po czym w obu urządzeniach powinny znaleźć się identyczne, losowe dane, niedostępne nikomu innemu. Właśnie to jest potrzebne do wymiany kluczy szyfrujących. Nie spotkałem się jeszcze z implementacją takiego rozwiązania. Wciąż mam nadzieję, że ktoś kiedyś napisze podobny program. Zanim jednak się to stanie, w tym rozdziale możesz zapoznać się z wieloma innymi recepturami dotyczącymi akcelerometrów i innych czujników.
17.2. Wykrywanie obecności lub braku czujnika Rachee Singh
Problem Programista chce wykorzystać określony czujnik. Przed uruchomieniem w urządzeniu z Androidem aplikacji opartej na czujniku należy się upewnić, że jest on dostępny. 521
Rozwiązanie Należy sprawdzić dostępność czujnika w urządzeniu z Androidem.
Omówienie Do zarządzania czujnikami dostępnymi w urządzeniach z Androidem służy klasa SensorManager. Potrzebny jest więc obiekt tej klasy: SensorManager deviceSensorManager = (SensorManager) getSystemService(SOME_SENSOR_SERVICE);
Następnie za pomocą metody getSensorList() należy sprawdzić dostępność czujników określonego typu (akcelerometru, żyroskopu, czujnika ciśnienia itd.). Jeśli na zwróconej liście znajduje się dany element, oznacza to, że odpowiadający mu czujnik jest dostępny. Do wyświetlania wyników wykorzystano tu kontrolkę TextView. Wyświetla ona informację Czujnik jest dostępny lub Czujnik jest niedostępny. Potrzebny kod pokazano na listingu 17.1. Listing 17.1. Sprawdzanie dostępności akcelerometru List
17.3. Wykorzystywanie akcelerometru do wykrywania potrząsania urządzeniem Thomas Manthey
Problem Czasem przydatne jest wykrywanie nie tylko informacji wprowadzanych poprzez ekran, ale też gestów fizycznych, takich jak zmiana ułożenia telefonu lub potrząsanie nim. Do wykrywania potrząsania urządzeniem służy akcelerometr.
522
|
Rozdz ał 17. Akcelerometr
Rozwiązanie Należy zarejestrować odbiornik wskazań akcelerometru i porównywać obecne wartości przyspieszenia we wszystkich trzech wymiarach z poprzednimi wskazaniami. Jeśli wartości nagle zmieniły się dla przynajmniej dwóch wymiarów, a wielkość zmian przekracza ustawiony próg, oznacza to, że użytkownik potrząsa telefonem.
Omówienie Potrząsanie oznacza tu stosunkowo szybki ruch urządzenia w jednym kierunku, po czym następuje podobny ruch w innym kierunku (zwykle przeciwnym, choć nie jest to konieczne). Jeśli aplikacja ma wykrywać tego rodzaju ruch w aktywności, musi korzystać z czujników sprzętowych. Są one dostępne poprzez klasę SensorManager. Ponadto trzeba zdefiniować odbiornik SensorEventListener i zarejestrować go w obiekcie SensorManager. Na listingu 17.2 pokazano początkowy fragment kodu aktywności. Listing 17.2. Aktywność ShakeActivity służy do pobierania danych z akcelerometru public class ShakeActivity extends Activity { /* Zapewnia aplikacji dostęp do sprzętu */ private SensorManager mySensorManager; /* Odbiornik SensorEventListener pozwala uzyskać dostęp do zdarzeń dotyczących sprzętu */ private final SensorEventListener mySensorEventListener = new SensorEventListener() { public void onSensorChanged(SensorEvent se) { /* Zostanie uzupełniona później */ } public void onAccuracyChanged(Sensor sensor, int accuracy) { /* W tym przykładzie tę metodę można pominąć */ } }; ...
Aby zaimplementować odbiornik SensorEventListener, trzeba dodać metody onSensorChanged (SensorEvent se) i onAccuracyChanged(Sensor sensor, int accuracy). Pierwsza z tych metod jest wywoływana za każdym razem, gdy pojawiają się nowe dane z czujnika. Wywołanie drugiej ma miejsce, gdy dokładność pomiarów się zmienia — na przykład gdy urządzenie zaczyna określać lokalizację na podstawie sieci zamiast za pomocą GPS-a. W tym przykładzie potrzebna jest tylko metoda onSensorChanged. Przed przejściem do dalszego kodu należy zdefiniować kilka zmiennych. Posłużą one do przechowywania informacji na temat wartości przyspieszenia i stanu telefonu (listing 17.3). Listing 17.3. Zmienne do przechowywania wartości przyspieszenia /* Tu przechowywana jest wartość przyspieszenia (dla każdej osi) */ private float xAccel; private float yAccel; private float zAccel;
17.3. Wykorzystywan e akcelerometru do wykrywan a potrząsan a urządzen em
|
523
/* Tu zapisane są wcześniejsze wartości */ private float xPreviousAccel; private float yPreviousAccel; private float zPreviousAccel; /* Wartość true oznacza pierwszą zmianę przyspieszenia */ private boolean firstUpdate = true; /* Jaka wartość przyspieszenia to już "szybki ruch"? */ private final float shakeThreshold = 1.5f; /* Czy potrząsanie zostało rozpoczęte (ruch w jednym kierunku)? */ private boolean shakeInitiated = false;
Mam nadzieję, że nazwy i komentarze wystarczą, aby zrozumieć, jakie dane przechowywane są w poszczególnych zmiennych. Jeśli jednak i to za mało, wszystko powinno stać się oczywiste po zapoznaniu się z następnymi fragmentami kodu. Aplikacja uzyskuje w nich dostęp do czujników sprzętowych i ich zdarzeń. Idealnym miejscem na wykonanie potrzebnych operacji jest metoda onCreate (listing 17.4). Listing 17.4. Inicjowanie kodu do pobierania danych z akcelerometru @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); mySensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE); n mySensorManager.registerListener(mySensorEventListener, mySensorManager .getDefaultSensor(Sensor.TYPE_ACCELEROMETER), SensorManager.SENSOR_DELAY_NORMAL); o }
n Aplikacja pobiera referencję do usługi zwracającej dane z czujnika. o Aplikacja rejestruje wcześniej zdefiniowany odbiornik SensorEventListener dla wspomnianej usługi — a dokładniej, rejestruje odbiornik w taki sposób, aby odbierał zdarzenia z akcelerometru ze standardową częstotliwością (ustawienie to można potem zmienić, aby otrzymywać bardziej precyzyjne wskazania). Teraz należy określić, co aplikacja ma robić z otrzymanymi nowymi danymi z czujników. Metoda onSensorChanged odbiornika SensorEventListener to na razie tylko namiastka, dlatego należy uzupełnić ją właściwym kodem (listing 17.5). Listing 17.5. Wykorzystywanie danych z czujników public void onSensorChanged(SensorEvent se) { updateAccelParameters(se.values[0], se.values[1], se.values[2]); n if ((!shakeInitiated) && isAccelerationChanged()) { o shakeInitiated = true; } else if ((shakeInitiated) && isAccelerationChanged()) { p executeShakeAction(); } else if ((shakeInitiated) && (!isAccelerationChanged())) { q shakeInitiated = false; } }
n Aplikacja kopiuje wartości przyspieszenia ze zdarzenia SensorEvent do zmiennych określających stan. Służy do tego następująca metoda:
524 |
Rozdz ał 17. Akcelerometr
/* Zapisywanie wartości przyspieszenia z czujnika */ private void updateAccelParameters(float xNewAccel, float yNewAccel, float zNewAccel) { /* Należy rozpoznawać pierwszą zmianę przyspieszenia; * gdy zachodzi, zmienne są zainicjowane wartością 0 */ if (firstUpdate) { xPreviousAccel = xNewAccel; yPreviousAccel = yNewAccel; zPreviousAccel = zNewAccel; firstUpdate = false; } else { xPreviousAccel = xAccel; yPreviousAccel = yAccel; zPreviousAccel = zAccel; } xAccel = xNewAccel; yAccel = yNewAccel; zAccel = zNewAccel; }
o Aplikacja sprawdza, czy nastąpiła nagła zmiana przyspieszenia i czy zdarzyło się to po raz pierwszy. Jeśli jest to pierwsza zmiana, należy zapisać, że nastąpiła. p Aplikacja ponownie sprawdza, czy miała miejsce nagła zmiana przyspieszenia, ale tym razem wykorzystuje do tego zapisane wcześniej informacje. Jeżeli nastąpiła kolejna zmiana, zgodnie z definicją oznacza to potrząśnięcie, dlatego należy podjąć odpowiednie działania. q W ostatnim kroku zmienna oznaczająca potrząśnięcie jest zerowana, jeśli wcześniej wykryto takie zdarzenie, natomiast użytkownik przestał już potrząsać telefonem. Aby kod był kompletny, trzeba dodać jeszcze dwie metody. Pierwsza z nich to isAcceleration Changed() (listing 17.6). Listing 17.6. Metoda isAccelerationChanged() /* Jeśli wartość przyspieszenia zmieniła się dla przynajmniej dwóch osi, użytkownik prawdopodobnie potrząsa telefonem */ private boolean isAccelerationChanged() { float deltaX = Math.abs(xPreviousAccel - xAccel); float deltaY = Math.abs(yPreviousAccel - yAccel); float deltaZ = Math.abs(zPreviousAccel - zAccel); return (deltaX > shakeThreshold && deltaY > shakeThreshold) || (deltaX > shakeThreshold && deltaZ > shakeThreshold) || (deltaY > shakeThreshold && deltaZ > shakeThreshold); }
W tym miejscu aplikacja porównuje obecne wartości przyspieszenia z poprzednimi. Jeśli przynajmniej dwie zmiany przekraczają poziom progowy, metoda zwraca true. Ostatnią metodą jest executeShakeAction(). Znajdują się w niej operacje wykonywane, gdy użytkownik potrząsa telefonem. private void executeShakeAction() { /* Tu można uratować księżniczkę, zbawić świat lub wykonać inne, bardziej sensowne operacje */ }
17.3. Wykorzystywan e akcelerometru do wykrywan a potrząsan a urządzen em
|
525
17.4. Używanie akcelerometru do sprawdzania, czy ekran skierowany jest w dół, czy w górę Rachee Singh
Problem Programista chce sprawdzać, czy urządzenie z Androidem skierowane jest w dół, czy w górę.
Rozwiązanie Należy wykorzystać odbiornik SensorEventListener do sprawdzania wskazań akcelerometru.
Omówienie Aby zaimplementować odbiornik SensorEventListener, należy napisać metodę onSensorChanged. Jest ona wywoływana przy każdej zmianie wskazań czujnika. W metodzie tej należy sprawdzać, czy wartości znajdują się w określonych przedziałach, oznaczających, że ekran urządzenia jest skierowany w dół lub w górę. Oto kod do pobierania obiektu reprezentującego akcelerometr: List
Na listingu 17.7 znajduje się implementacja odbiornika SensorEventListener. Listing 17.7. Implementacja odbiornika SensorEventListener private SensorEventListener accelerometerListener = new SensorEventListener() { @Override public void onSensorChanged(SensorEvent event) { float z = event.values[2]; if (z >9 && z < 10) face.setText("W GÓRĘ"); else if (z > -10 && z < -9) face.setText("W DÓŁ"); } @Override public void onAccuracyChanged(Sensor arg0, int arg1) { } };
Po zaimplementowaniu odbiornika i potrzebnych metod należy zarejestrować odbiornik dla konkretnego czujnika (tu jest nim akcelerometr). W kodzie sensor jest to obiekt klasy Sensor, który reprezentuje czujnik (akcelerometr) używany przez aplikację. deviceSensorManager.registerListener(accelerometerListener, sensor, 0, null);
526
|
Rozdz ał 17. Akcelerometr
17.5. Określanie ułożenia telefonu z Androidem za pomocą czujnika orientacji Rachee Singh
Problem Programista chce wykrywać, która krawędź urządzenia z Androidem (górna, dolna, prawa lub lewa) jest skierowana w górę.
Rozwiązanie Dzięki ustaleniu, czy wartości wyznaczające obrót wokół osi w czujniku orientacji urządzenia z Androidem znajdują się w określonych przedziałach, można stwierdzić, która krawędź skierowana jest w górę.
Omówienie Podobnie jak przy korzystaniu z każdego czujnika obsługiwanego przez Android, tak i tu najpierw trzeba utworzyć obiekt SensorManager. SensorManager sensorManager = (SensorManager)getSystemService(SENSOR_SERVICE);
Za pomocą tego obiektu można uzyskać dostęp do czujników urządzenia. Metoda getSensor List() zwraca listę wszystkich czujników określonego rodzaju (tu są to czujniki orientacji). Trzeba sprawdzić, czy dane urządzenie jest wyposażone w czujnik orientacji. Jeśli tak, należy pobrać z listy pierwszy czujnik. Jeżeli potrzebny czujnik jest niedostępny, aplikacja wyświetla odpowiedni komunikat (zobacz listing 17.8). Listing 17.8. Znajdowanie czujnika orientacji List
Aby zarejestrować odbiornik SensorEventListener dla wybranego czujnika, należy uruchomić poniższy kod: sensorManager.registerListener(orientationListener,sensor, 0, null);
Teraz należy zdefiniować odbiornik SensorEventListener. Trzeba zaimplementować jego dwie metody — onAccuracyChanged() i onSensorChanged(). Metoda onSensorChanged()wywoływana jest w momencie zmiany wskazań czujnika. Tu jest to czujnik orientacji, którego wskazania się zmieniają, gdy użytkownik porusza urządzeniem. Czujnik orientacji zwraca trzy wartości — azymut, kąt nachylenia względem osi wzdłużnej i kąt nachylenia względem osi poprzecznej.
17.5. Określan e ułożen a telefonu z Andro dem za pomocą czujn ka or entacj
|
527
Następnie należy sprawdzić uzyskane wartości. Jeśli znajdują się w określonych przedziałach, aplikacja wyświetla odpowiedni tekst, co pokazano na listingu 17.9. Listing 17.9. Implementacja odbiornika SensorEventListener private SensorEventListener orientationListener = new SensorEventListener() { @Override public void onAccuracyChanged(Sensor arg0, int arg1) { } @Override public void onSensorChanged(SensorEvent sensorEvent) { if (sensorEvent.sensor.getType() == Sensor.TYPE_ORIENTATION) { float azimuth = sensorEvent.values[0]; float pitch = sensorEvent.values[1]; float roll = sensorEvent.values[2]; if (pitch < -45 && pitch > -135) { orient.setText("Górna krawędź telefonu jest skierowana w górę"); } else if (pitch > 45 && pitch < 135) { orient.setText ("Dolna krawędź telefonu jest skierowana w górę"); } else if (roll > 45) { orient.setText("Prawa strona telefonu jest skierowana w górę"); } else if (roll < -45) { orient.setText ("Lewa strona telefonu jest skierowana w górę "); } } } };
17.6. Odczyt wskazań czujnika temperatury Rachee Singh
Problem Programista chce sprawdzać temperaturę za pomocą odpowiedniego czujnika.
Rozwiązanie Należy zastosować obiekty SensorManager i SensorEventListener do śledzenia zmian w poziomie temperatury wykrywanych przez odpowiedni czujnik.
Omówienie Aby korzystać z czujników aplikacji, należy utworzyć obiekt SensorManager. Następnie trzeba zarejestrować odbiornik dla potrzebnego typu czujnika. W celu zarejestrowania odbiornika należy do metody registerListener przekazać jego nazwę, obiekt Sensor oraz rodzaj opóźnie-
528
|
Rozdz ał 17. Akcelerometr
nia (tu jest to SENSOR_DELAY_FASTEST). W odbiorniku, w przesłoniętej metodzie onSensorChanged, aplikacja wyświetla liczbę stopni w kontrolce TextView o nazwie tempVal. SensorManager sensorManager = (SensorManager)getSystemService(SENSOR_SERVICE); sensorManager.registerListener(temperatureListener, sensorManager.getDefaultSensor(Sensor.TYPE_TEMPERATURE), SensorManager.SENSOR_DELAY_FASTEST);
Na listingu 17.10 przedstawiono implementację odbiornika SensorEventListener. Listing 17.10. Implementacja odbiornika SensorEventListener private final SensorEventListener temperatureListener = new SensorEventListener(){ @Override public void onAccuracyChanged(Sensor sensor, int accuracy) {} @Override public void onSensorChanged(SensorEvent event) { tempVal.setText("Temperatura wynosi:"+event.values[0]); } };
Zobacz także Receptura 17.2.
17.6. Odczyt wskazań czujn ka temperatury
|
529
530
|
Rozdz ał 17. Akcelerometr
ROZDZIAŁ 18.
Bluetooth
18.1. Wprowadzenie — Bluetooth Ian Darwin
Omówienie Technologia Bluetooth umożliwia użytkownikom podłączanie różnych urządzeń peryferyjnych do komputerów, tabletów i telefonów. Słuchawki, głośniki, klawiatury, drukarki, urządzenia medyczne (np. glukometry i maszyny do pomiaru EKG) to tylko niektóre sprzęty, które można podłączyć przez Bluetooth. Niektóre z tych urządzeń, np. słuchawki, Android obsługuje automatycznie. Mniej popularne sprzęty wymagają dodania specjalnego kodu. Niektóre z tego rodzaju urządzeń korzystają z protokołu SPP (ang. Serial Port Protocol) — jest to nieustrukturyzowany protokół, dla którego programista sam musi napisać kod formatujący dane. W tym rozdziale znajdziesz receptury, z których dowiesz się, jak sprawdzić, czy Bluetooth jest włączony, jak umożliwić wykrywanie telefonu, jak wykrywać inne sprzęty, a także jak wymieniać dane przez połączenie Bluetooth1. W przyszłych wydaniach tej książki znajdzie się omówienie standardu HDP (ang. Bluetooth Health Device Profile), rozwijanego przez organizację Continua Health Alliance.
1
Bluetooth jest znakiem zastrzeżonym organizacji The Bluetooth Special Interest Group (https://www.bluetooth. org/apps/content/).
531
18.2. Włączanie Bluetootha i umożliwianie wykrywania urządzenia Rachee Singh
Problem Aplikacja wymaga, aby moduł Bluetooth był włączony, trzeba więc to sprawdzić. Jeśli moduł nie jest włączony, aplikacja powinna wyświetlić prośbę o jego włączenie. Aby umożliwić zdalnym sprzętom znajdowanie głównego urządzenia, trzeba włączyć opcję wykrywania.
Rozwiązanie Należy zastosować intencje, aby wyświetlić prośbę o włączenie Bluetootha oraz umożliwienie wykrywania urządzenia.
Omówienie Przed wykonaniem jakichkolwiek operacji na obiekcie BluetoothAdapter należy za pomocą metody isEnabled() sprawdzić, czy w urządzeniu włączony jest moduł Bluetooth. Jeśli wspomniana metoda zwraca wartość false, aplikacja powinna wyświetlić prośbę o włączenie Bluetootha. BluetoothAdapter BT = BluetoothAdapter.getDefaultAdapter(); if (!BT.isEnabled()) { // Uzyskiwanie od użytkownika zgody na włączenie Bluetootha Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); startActivityForResult(enableIntent, REQUEST_ENABLE_BT); }
Powyższy kod wyświetla okno AlertDialog z prośbą o włączenie Bluetootha (rysunek 18.1).
Rysunek 18.1. Prośba o włączenie Bluetootha
Po powrocie do aktywności, która uruchomiła potrzebną intencję, aplikacja wywołuje metodę onActivityResult(). W metodzie tej można pobrać nazwę głównego urządzenia i jego adres MAC (listing 18.1). Listing 18.1. Pobieranie nazwy urządzenia i adresu MAC modułu Bluetooth protected void onActivityResult(int requestCode, int resultCode, Intent data) { if(requestCode==REQUEST_ENABLE_BT && resultCode==Activity.RESULT_OK) {
532
|
Rozdz ał 18. Bluetooth
BluetoothAdapter BT = BluetoothAdapter.getDefaultAdapter(); String address = BT.getAddress(); String name = BT.getName(); String toastText = name + " : " + address; Toast.makeText(this, toastText, Toast.LENGTH_LONG).show(); }
Aby zażądać od użytkownika umożliwienia wykrywania urządzenia przez inne sprzęty wyposażone w moduł Bluetooth, należy zastosować następujący kod: // Żądanie od użytkownika włączenia wykrywania urządzenia na 120 sekund Intent discoverableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE); startActivity(discoverableIntent);
Kod ten wyświetla użytkownikowi okno AlertDialog z prośbą o włączenie na 120 sekund wykrywania urządzenia (rysunek 18.2).
Rysunek 18.2. Konfigurowanie Bluetootha
18.3. Podłączanie urządzenia z Bluetoothem Ashwini Shahapurkar
Problem Programista chce podłączyć do telefonu inny sprzęt z Bluetoothem i nawiązać komunikację między urządzeniami.
Rozwiązanie Do podłączenia urządzenia za pomocą gniazd należy zastosować androidowy interfejs Bluetooth API. Komunikacja odbywa się wtedy za pomocą strumieni przesyłanych przez gniazda.
Omówienie W pliku AndroidManifest.xml w każdej aplikacji korzystającej z Bluetootha należy zażądać dwóch uprawnień:
18.3. Podłączan e urządzen a z Bluetoothem
|
533
W aplikacji trzeba utworzyć oparte na gniazdach połączenie z innym urządzeniem z Bluetoothem. Następnie wystarczy w wątku odbierać dane ze strumienia przesyłanego przez gniazda. Dane do podłączonego strumienia można wysyłać poza tym wątkiem. Obsługa połączenia blokuje pracę kodu. Ponieważ wykrywanie urządzeń z Bluetoothem jest zasobochłonne, może obniżyć wydajność połączenia. Dlatego dobrą praktyką jest wyłączanie wykrywania urządzeń do czasu próby nawiązania połączenia z innym urządzeniem. Połączenie przez gniazda Bluetootha blokuje pracę kodu i zwraca sterowanie tylko po udanym nawiązaniu połączenia lub po wystąpieniu wyjątku w trakcie jego nawiązywania. Obiekt BluetoothConnection tworzy oparte na gniazdach połączenie z innym urządzeniem i zaczyna odbierać dane z podłączonego sprzętu. Listing 18.2. Wymiana danych z urządzeniem z Bluetoothem private class BluetoothConnection extends Thread { private final BluetoothSocket mmSocket; private final InputStream mmInStream; private final OutputStream mmOutStream; byte[] buffer; // Unikatowy identyfikator UUID aplikacji (powinieneś zastosować inny) private static final UUID MY_UUID = UUID .fromString("fa87c0d0-afac-11de-8a39-0800200c9a66"); public BluetoothConnection(BluetoothDevice device) { BluetoothSocket tmp = null; // Tworzenie obiektu BluetoothSocket dla połączenia z urządzeniem // reprezentowanym przez obiekt BluetoothDevice try { tmp = device.createRfcommSocketToServiceRecord(MY_UUID); } catch (IOException e) { e.printStackTrace(); } mmSocket = tmp; // Połączenie z gniazdem należy utworzyć w odrębnym wątku, aby uniknąć wymuszonego zamknięcia aplikacji Thread connectionThread = new Thread(new Runnable() { @Override public void run() { // Zawsze należy wyłączać wykrywanie, ponieważ zmniejsza ono wydajność połączenia mAdapter.cancelDiscovery(); // Nawiązywanie połączenia za pomocą obiektu BluetoothSocket try { // To wywołanie blokuje pracę kodu i zwraca sterowanie dopiero // po udanym nawiązaniu połączenia lub wystąpieniu wyjątku mmSocket.connect(); } catch (IOException e) { // Próba połączenia zakończyła się niepowodzeniem, dlatego należy zamknąć gniazdo try { mmSocket.close(); } catch (IOException e2) { e2.printStackTrace(); } } }
534 |
Rozdz ał 18. Bluetooth
}); connectionThread.start(); InputStream tmpIn = null; OutputStream tmpOut = null; // Pobieranie strumieni wejścia i wyjścia obiektu BluetoothSocket try { tmpIn = mmSocket.getInputStream(); tmpOut = mmSocket.getOutputStream(); buffer = new byte[1024]; } catch (IOException e) { e.printStackTrace(); } mmInStream = tmpIn; mmOutStream = tmpOut; } public void run() { // Dopóki połączenie jest aktywne, należy odbierać dane ze strumienia InputStream while (true) { try { // Odczyt danych ze strumienia z gniazda mmInStream.read(buffer); // Odebrane dane należy przekazać do aktywności interfejsu użytkownika } catch (IOException e) { // Wyjątek w tym miejscu oznacza zerwanie połączenia. // Należy wysłać odpowiedni komunikat do aktywności interfejsu użytkownika break; } } } public void write(byte[] buffer) { try { // Zapis danych do strumienia powiązanego z gniazdem mmOutStream.write(buffer); } catch (IOException e) { e.printStackTrace(); } } public void cancel() { try { mmSocket.close(); } catch (IOException e) { e.printStackTrace(); } } }
Zobacz także Receptura 18.5.
18.3. Podłączan e urządzen a z Bluetoothem
|
535
18.4. Oczekiwanie na żądania połączenia Bluetooth oraz ich akceptowanie Rachee Singh
Problem Programista chce utworzyć serwer oczekujący na żądania połączeń Bluetooth.
Rozwiązanie Zanim dwa urządzenia wyposażone w moduł Bluetooth zaczną interakcję, jedno z nich musi działać jak serwer. Wymaga to utworzenia obiektu BluetoothServerSocket oraz oczekiwania na nadchodzące żądania. Wspomniany obiekt można utworzyć przez wywołanie metody listen UsingRfcommWithServiceRecord() obiektu BluetoothAdapter.
Omówienie Po utworzeniu obiektu BluetoothServerSocket można za pomocą metody start() rozpocząć oczekiwanie na przychodzące żądania od zdalnych urządzeń. Oczekiwanie blokuje pracę kodu, dlatego trzeba utworzyć nowy wątek i wywołać wspomnianą metodę właśnie w nim. W przeciwnym razie interfejs użytkownika przestanie reagować. Potrzebny kod pokazano na listingu 18.3. Listing 18.3. Tworzenie serwera Bluetooth i akceptowanie połączeń // Umożliwianie wykrywania głównego urządzenia startActivityForResult(new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE), DISCOVERY_REQUEST_BLUETOOTH); @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == DISCOVERY_REQUEST_BLUETOOTH) { boolean isDiscoverable = resultCode > 0; if (isDiscoverable) { UUID uuid = UUID.fromString("a60f35f0-b93a-11de-8a39-08002009c666"); String serverName = "BTserver"; final BluetoothServerSocket bluetoothServer = bluetoothAdapter.listenUsingRfcommWithServiceRecord(serverName, uuid); Thread listenThread = new Thread(new Runnable() { public void run() { try { BluetoothSocket serverSocket = bluetoothServer.accept(); myHandleConnectionWith(serverSocket); } catch (IOException e) { Log.d("BLUETOOTH", e.getMessage()); } } }); listenThread.start(); } } }
536
|
Rozdz ał 18. Bluetooth
18.5. Implementowanie wykrywania urządzeń z Bluetoothem Shraddha Shravagi
Problem Programista chce wyświetlać listę urządzeń z Bluetoothem, które znajdują się w zasięgu głównego urządzenia.
Rozwiązanie Należy przygotować plik XML z wyświetlaną listą, utworzyć plik z klasą wczytującą listę, a następnie zmodyfikować plik manifestu. Zadanie jest proste. Warto zauważyć, że z przyczyn bezpieczeństwa wykrywane urządzenia muszą pracować w trybie umożliwiającym ich znalezienie (w trybie parowania). Urządzenia z Androidem można przełączyć w ten tryb za pomocą opcji Discoverable na karcie Bluetooth Settings. Aby włączyć potrzebny tryb w innych sprzętach z Bluetoothem, należy zapoznać się z instrukcją.
Omówienie W pliku XML służącym do wyświetlania listy umieść następujący kod:
Kod z listingu 18.4 pochodzi z pliku klasy wczytującej listę. Listing 18.4. Aktywność z odbiornikiem BroadcastReceiver obsługującym połączenia // Filtr IntentFilter określający potrzebne akcje IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND); // Rejestrowanie odbiornika BroadcastReceiver dla ustawionego filtra this.registerReceiver(mReceiver, filter); // Podłączanie adaptera ListView newDevicesListView = (ListView)findViewById(R.id.pairedBtDevices); newDevicesListView.setAdapter(mNewDevicesArrayAdapter); filter = new IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_FINISHED); this.registerReceiver(mReceiver, filter); // Tworzenie odbiornika dla intencji private final BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) {
18.5. mplementowan e wykrywan a urządzeń z Bluetoothem
|
537
String action = intent.getAction(); if(BluetoothDevice.ACTION_FOUND.equals(action)){ BluetoothDevice btDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); if(btDevice.getBondState() != BluetoothDevice.BOND_BONDED){ mNewDevicesArrayAdapter.add(btDevice.getName()+"\n"+ btDevice.getAddress()); } } else if(BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)){ setProgressBarIndeterminateVisibility(false); setTitle(R.string.select_device); if(mNewDevicesArrayAdapter.getCount() == 0){ String noDevice = getResources().getText(R.string.none_paired).toString(); mNewDevicesArrayAdapter.add(noDevice); } } } };
W pliku AndroidManifest.xml trzeba określić, że potrzebne są następujące uprawnienia: • android.permission.BLUETOOTH, • android.permission.BLUETOOTH_ADMIN.
538 |
Rozdz ał 18. Bluetooth
ROZDZIAŁ 19.
Sterowanie systemem i urządzeniem
19.1. Wprowadzenie — sterowanie systemem i urządzeniem Ian Darwin
Omówienie W Androidzie wypracowano dobry kompromis między potrzebą kontroli ze strony operatorów a potrzebą dostępu do urządzeń ze strony programistów. W tym rozdziale poznasz publicznie dostępne dla programistów interfejsy API do pobierania informacji i sterowania urządzeniem. Umożliwiają one poznawanie rozmaitych mechanizmów sprzętowych dostępnych w systemie oraz zarządzanie nimi. Pomagają też radzić sobie z różnorodnym sprzętem — od 2-calowych telefonów komórkowych po 10-calowe tablety i netbooki.
19.2. Dostęp do informacji o sieci i połączeniu Amir Alagic
Problem Programista chce wyszukiwać informacje na temat połączenia urządzenia z siecią.
Rozwiązanie Za pomocą obiektów ConnectivityManager i NetworkInfo można ustalić, czy telefon jest podłączony do sieci, jaki jest rodzaj połączenia i czy włączona jest usługa roamingu.
Omówienie Często potrzebne są informacje, czy urządzenie może w danym momencie nawiązać połączenie z internetem. Ponadto ponieważ roaming bywa drogi, bardzo przydatna jest możliwość
539
ustalenia, czy usługa ta jest włączona (użytkownik, który zdecydowanie chce uniknąć kosztów, powinien za pomocą aplikacji Settings wyłączyć transfer danych z wykorzystaniem roamingu). Te i inne ustawienia można sprawdzić za pomocą klasy NetworkInfo z pakietu android.net, co pokazano na listingu 19.1. Listing 19.1. Pobieranie informacji o sieci ConnectivityManager connManager = (ConnectivityManager)this.getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo ni = connManager.getActiveNetworkInfo(); /* Określa, czy można nawiązać połączenie z siecią. Sieć jest niedostępna, jeśli występują trwałe lub półtrwałe warunki uniemożliwiające nawiązanie połączenia z nią */ boolean available = ni.isAvailable(); boolean connected = ni.isConnected(); boolean roaming = ni.isRoaming(); /* Określa typ sieci (mobilna lub Wi-Fi), której dotyczą informacje w danym obiekcie */ int networkType = ni.getType();
19.3. Pobieranie informacji z pliku manifestu Colin Wilcox
Problem W trakcie wykonywania programu programista chce pobierać z pliku AndroidManifest.xml ustawienia projektu (na przykład wersję aplikacji).
Rozwiązanie Należy wykorzystać obiekt PackageManager. Zamiast na stałe zapisywać w aplikacji wartości i modyfikować je przy każdej zmianie w programie, łatwiej jest wczytywać numer wersji z pliku manifestu. W ten sam sposób można też wczytywać inne ustawienia.
Omówienie Korzystanie z obiektu PackageManager jest stosunkowo łatwe. Najpierw do aktywności trzeba dodać dwie poniższe instrukcje import: import android.content.pm.PackageInfo; import android.content.pm.PackageManager;
Główny kod przedstawiono na listingu 19.2. Listing 19.2. Kod pobierający informacje z manifestu // W głównej aktywności public String readVersionNameFromManifest() { PackageInfo packageInfo = null; // Wczytywanie z manifestu nazwy pakietu i numeru wersji try { // Wczytywanie obiektu PackageManager z bieżącego kontekstu
540 |
Rozdz ał 19. Sterowan e systemem urządzen em
PackageManager packageManager = this.getPackageManager(); // Pobieranie struktury z informacjami o pakiecie oraz potrzebnych pól packageInfo = packageManager.getPackageInfo(this.getPackageName(), 0); } catch (Exception e) { Log.e(TAG, "W trakcie wczytywania wersji z manifestu wystąpił wyjątek " + e); } return (packageInfo.versionName); }
19.4. Zmienianie trybu dzwonka telefonu na cichy, wibracje lub normalny Rachee Singh
Problem Programista chce ustawić tryb dzwonka telefonu z Androidem na cichy, wibracje lub normalny.
Rozwiązanie Aby przestawić tryb dzwonka telefonu na normalny, cichy lub wibracje, należy zastosować androidową usługę systemową AudioManager.
Omówienie W tej recepturze przedstawiono prostą aplikację z trzema przyciskami, które pozwalają zmienić tryb dzwonka telefonu na cichy, wibracje lub normalny (rysunek 19.1).
Rysunek 19.1. Ustawianie trybu dzwonka telefonu 19.4. Zm en an e trybu dzwonka telefonu na c chy, w bracje lub normalny
|
541
Aplikacja tworzy obiekt AudioManager, który umożliwia zastosowanie metody setRingerMode. Dla każdego z przycisków (silentButton, normalButton i vibrateButton) zdefiniowany jest odbiornik OnClickListener, w którym aplikacja używa obiektu AudioManager do ustawienia trybu dzwonka. Ponadto wyświetlany jest komunikat toast z informacją o zmianie trybu. Kod przedstawiono na listingu 19.3. Listing 19.3. Ustawianie trybu dzwonka am= (AudioManager) getBaseContext().getSystemService(Context.AUDIO_SERVICE); silentButton = (Button)findViewById(R.id.silent); normalButton = (Button)findViewById(R.id.normal); vibrateButton = (Button)findViewById(R.id.vibrate); // Tryb cichy silentButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View arg0) { am.setRingerMode(AudioManager.RINGER_MODE_SILENT); Toast.makeText(getApplicationContext(), "Włączono tryb cichy", Toast.LENGTH_LONG).show(); } }); // Tryb normalny normalButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View arg0) { am.setRingerMode(AudioManager.RINGER_MODE_NORMAL); Toast.makeText(getApplicationContext(), "Włączono tryb normalny", Toast.LENGTH_LONG).show(); } }); // Tryb wibracji vibrateButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View arg0) { am.setRingerMode(AudioManager.RINGER_MODE_VIBRATE); Toast.makeText(getApplicationContext(), "Włączono tryb wibracji", Toast.LENGTH_LONG).show(); } });
Na rysunku 19.2 pokazano wygląd aplikacji po kliknięciu przycisku Cichy (zwróć uwagę na ikonę trybu cichego na pasku stanu).
19.5. Kopiowanie tekstu i pobieranie go ze schowka Rachee Singh
Problem Programista chce skopiować tekst do schowka i uzyskać dostęp do zapisanego tam tekstu. Umożliwia to udostępnienie kompletnego mechanizmu kopiowania i wklejania tekstu.
542 |
Rozdz ał 19. Sterowan e systemem urządzen em
Rysunek 19.2. Po włączeniu trybu cichego
Rozwiązanie Za pomocą klasy ClipboardManager można uzyskać dostęp do danych zapisanych w schowku urządzenia z Androidem.
Omówienie Klasa ClipboardManager umożliwia kopiowanie tekstu do schowka (za pomocą metody setText()) i pobieranie zapisanego w schowku tekstu (za pomocą metody getText()). Metoda getText() zwraca obiekt charSequence, który aplikacja za pomocą metody toString() przekształca na łańcuch znaków. Z przykładowego kodu z listingu 19.4 dowiesz się, jak utworzyć obiekt ClipboardManager i jak wykorzystać go do kopiowania tekstu do schowka. Następnie można pobrać tekst ze schowka (za pomocą metody getText()) i przypisać go do kontrolki TextView. Listing 19.4. Kopiowanie tekstu do schowka ClipboardManager clipboard = (ClipboardManager)getSystemService(CLIPBOARD_SERVICE); clipboard.setText("Using the clipboard for the first time!"); String clip = clipboard.getText().toString(); clipTextView = (TextView) findViewById(R.id.clipText); clipTextView.setText(clip);
19.5. Kop owan e tekstu pob eran e go ze schowka
| 543
19.6. Powiadomienia oparte na diodach LED Rachee Singh
Problem Większość urządzeń z Androidem wyposażona jest w diody LED, które można wykorzystać do wyświetlania powiadomień. Za pomocą takich diod programista chce wyświetlać kolorowe sygnały.
Rozwiązanie Do wyświetlania powiadomień za pomocą diod LED można wykorzystać klasy Notification Manager i Notification.
Omówienie Jak zawsze przy korzystaniu z powiadomień, tak i tu trzeba najpierw utworzyć obiekt Noti ficationManager. Następnie należy utworzyć obiekt Notification. Za pomocą metody ledARGB() można określić kolor diody LED. Stała ledOnMS służy do określania w milisekundach czasu, przez który dioda ma świecić. Metoda ledOffMS pozwala podać czas, przez który dioda ma być wyłączona. Metoda notify() rozpoczyna proces generowania powiadomienia. Na listingu 19.5 przedstawiono kod potrzebny do wykonania opisanych zadań. Listing 19.5. Włączanie diody LED świecącej na niebiesko NotificationManager notificationManager = (NotificationManager)getSystemService(NOTIFICATION_SERVICE); Notification notification = new Notification(); notification.ledARGB = 0xff0000ff; // Niebieskie światło notification.ledOnMS = 1000; // Dioda LED jest włączana na jedną sekundę notification.ledOffMS = 1000; // Dioda LED jest wyłączana na jedną sekundę notification.flags = Notification.FLAG_SHOW_LIGHTS; notificationManager.notify(0, notification);
19.7. Włączanie wibracji w urządzeniu Rachee Singh
Problem Programista chce powiadamiać użytkownika o pewnym zdarzeniu przez włączanie wibracji.
Rozwiązanie Należy wykorzystać powiadomienia do ustawienia wzorca wibracji.
544 |
Rozdz ał 19. Sterowan e systemem urządzen em
Omówienie Aby umożliwić włączanie wibracji, należy w pliku AndroidManifest.xml zażądać następujących uprawnień:
W kodzie Javy trzeba utworzyć obiekty NotificationManager i Notification: NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); Notification notification = new Notification();
Aby ustawić wzorzec wibracji, należy do właściwości vibrate obiektu Notification przypisać ciąg wartości typu long (czas w milisekundach). Ciąg ten reprezentuje czas, przez który wibracje są włączane i wstrzymywane. Przykładowy wzorzec przedstawiony poniżej powoduje włączenie wibracji na sekundę, wstrzymanie ich na sekundę, ponowne włączenie na sekundę itd. notification.vibrate = new long[]{1000, 1000, 1000, 1000, 1000}; notificationManager.notify(0, notification);
19.8. Uruchamianie poleceń powłoki z poziomu aplikacji Rachee Singh
Problem Programista chce z poziomu aplikacji uruchamiać uniksowe i linuksowe polecenia powłoki (pwd, ls itd.).
Rozwiązanie Należy zastosować metodę exec() klasy Runtime i przekazać do niej pożądane polecenia powłoki jako argumenty.
Omówienie W Androidzie, podobnie jak w standardowej Javie, w aplikacji nie można utworzyć obiektu Runtime. Zamiast tego trzeba go pobrać przez wywołanie statycznej metody getRuntime(). Za pomocą obiektu Runtime można wywołać metodę exec(), która wykonuje określony program w odrębnym procesie natywnym. Wspomniana metoda przyjmuje jako argument nazwę wykonywanego programu, a następnie zwraca nowy obiekt Process, reprezentujący proces natywny. W ramach przykładu uruchomiono tu polecenie ps, które wyświetla wszystkie procesy uruchomione w systemie. Jako argument metody exec() podano pełną ścieżkę do katalogu z tym poleceniem (/system/bin/ps). Aplikacja pobiera dane wyjściowe od polecenia i zwraca łańcuch znaków. Wywołuje też metodę process.waitFor(), aby odczekać na zakończenie wykonywania polecenia (listing 19.6).
19.8. Urucham an e poleceń powłok z poz omu apl kacj
| 545
Listing 19.6. Uruchamianie polecenia powłoki try { Process process = Runtime.getRuntime().exec("/system/bin/ps"); InputStreamReader reader = new InputStreamReader(process.getInputStream()); BufferedReader bufferedReader = new BufferedReader(reader); int numRead; char[] buffer = new char[5000]; StringBuffer commandOutput = new StringBuffer(); while ((numRead = bufferedReader.read(buffer)) > 0) { commandOutput.append(buffer, 0, numRead); } bufferedReader.close(); process.waitFor(); return commandOutput.toString(); } catch (IOException e) { throw new RuntimeException(e); } catch (InterruptedException e) { throw new RuntimeException(e); }
Na rysunku 19.3 pokazano dane wyjściowe otrzymane z polecenia ps.
Rysunek 19.3. Dane wyjściowe z polecenia ps() wywołanego w Androidzie
19.9. Określanie, czy dana aplikacja jest uruchomiona Colin Wilcox
Problem Programista chce wiedzieć, czy napisana przez niego aplikacja (lub inny program) działa w danym momencie.
546 |
Rozdz ał 19. Sterowan e systemem urządzen em
Rozwiązanie Systemowy menedżer aktywności utrzymuje listę wszystkich aktywnych zadań. Obejmuje ona nazwy zadań i pozwala pobrać różne informacje systemowe.
Omówienie Kod z listingu 19.7 przyjmuje nazwę aplikacji i zwraca true, jeśli według informacji z obiektu ActivityManager dana aplikacja obecnie jest uruchomiona. Listing 19.7. Sprawdzanie, czy aplikacja działa import android.app.ActivityManager; import android.app.ActivityManager.RunningAppProcessInfo; public boolean isAppRunning (String aApplicationPackageName) { ActivityManager activityManager = (ActivityManager) this.getSystemService(ACTIVITY_SERVICE); if (activityManager == null) { return false; // Kod powinien zgłaszać problem – nie można pobrać obiektu ActivityManager } List
19.9. Określan e, czy dana apl kacja jest uruchom ona
|
547
548 |
Rozdz ał 19. Sterowan e systemem urządzen em
ROZDZIAŁ 20.
Inne języki programowania i frameworki
20.1. Wprowadzenie — inne języki programowania Ian Darwin
Omówienie W branży informatycznej nieustannie powstają nowe języki programowania. Niedawno popularność zyskało kilka nowych (i trochę starszych) języków — Scheme, Erlang, Scala, Clojure, Groovy, C#, F# i inne. Twórcy Androida zachęcają do korzystania z różnych języków, choć firma Apple przez pewien czas wymagała od programistów aplikacji na iPhone’y stosowania języka Objective-C i nie umożliwiała używania innych języków (przynajmniej początkowo — ostatnio ograniczenia zostały nieco rozluźnione), a zwłaszcza języków interpretowanych, wymagających maszyn podobnych do JVM. Programiści oczywiście mogą pisać aplikacje w samej Javie, korzystając z pakietu SDK. To jest właśnie tematem większości rozdziałów tej książki. Można też za pomocą kodu natywnego łączyć z Javą kod w językach C i C++ (receptura 20.3). Umożliwia to androidowy pakiet NDK. Programistom udaje się uruchamiać w Androidzie większość popularnych języków kompilowanych, zwłaszcza (choć nie tylko) tych opartych na maszynie JVM. Można też pisać kod w różnych językach skryptowych, takich jak Perl, Python i Ruby (receptura 20.4). A to jeszcze nie wszystko! Jeśli chcesz rozwijać aplikacje na bardzo wysokim poziomie ogólności (za pomocą przeciągania komponentów), zapoznaj się ze środowiskiem Android App Inventor. Jest to wymyślone przez Google’a środowisko do łatwego rozwijania aplikacji za pomocą przeciągania elementów i łączenia bloków. Trwają prace nad recepturą wykorzystującą to środowisko (http://andro idcookbook.com/Recipe.seam?recipeId=3224). Obecnie rozwijaniem środowiska zajmuje się MIT. Więcej informacji znajdziesz na stronie (http://appinventor.mit.edu/). Jeżeli piszesz aplikacje sieciowe i jesteś przyzwyczajony do korzystania z języków HTML, JavaScript i CSS, możesz tworzyć programy na Android za pomocą znanych sobie narzędzi. Istnieje pięć lub sześć frameworków, które to umożliwiają, np. AppCelerator Titanium i PhoneGap (receptura 20.9). W tym podejściu zwykle wykorzystuje się CSS (do opracowania
549
stylu podobnego do natywnych aplikacji z danego urządzenia), JavaScript (do obsługi operacji) i standardy organizacji W3 (do uzyskiwania dostępu do mechanizmów urządzenia, np. do GPS-a). W większości rozwiązań z tego obszaru w pliku APK umieszczany jest interpreter JavaScriptu wraz z kodem w HTML-u i CSS-ie. Dodatkową zaletą tego podejścia jest to, że pozwala rozwijać aplikacje działające na iPhone’ach, telefonach BlackBerry i w innych urządzeniach przenośnych. Dla mnie wadą jest to, że ponieważ omawiane frameworki nie wykorzystują elementów natywnych, interfejs użytkownika aplikacji opartych na tych frameworkach może wyglądać nietypowo oraz być niezgodny z wytycznymi projektowania rozwiązań na Android i oczekiwaniami użytkowników. Warto o tym pamiętać przy korzystaniu z omawianych narzędzi. W trakcie rozwijania Androida jednym z bardzo ważnych celów było zachowanie otwartego charakteru tego systemu. Liczba języków, w których można pisać aplikacje na Android, jest dowodem na to, że cel ten udało się osiągnąć.
20.2. Uruchamianie zewnętrznych, natywnych instrukcji Uniksa i Linuksa Amir Alagic
Problem Czasem wygodnie jest uruchomić jedno z dostępnych w telefonie poleceń linuksowych (np. rm, sync, top lub uptime).
Rozwiązanie Do uruchamiania w Androidzie poleceń linuksowych służą klasy ze standardowej Javy. Przy ich użyciu można uruchamiać procesy zewnętrzne. Najpierw należy ustalić, jaką instrukcję aplikacja ma wywołać, a następnie pobrać obiekt Runtime i wykonać polecenie natywne w odrębnym procesie natywnym. Często trzeba też wczytać wyniki. Służą do tego strumienie.
Omówienie Java (zarówno wersja tradycyjna, jak i z Androida) pozwala na stosunkowo łatwe uruchamianie procesów zewnętrznych. Polecenia linuksowe można znaleźć w katalogu ./system/bin za pomocą menedżera plików, np. aplikacji AndroZip File Manager. Jednym z tych poleceń jest ls, które wyświetla pliki (i podkatalogi) z danego folderu. Aby uruchomić to polecenie, należy ścieżkę do niego przekazać w metodzie Runtime.exec(). Obiektu Runtime nie można utworzyć bezpośrednio, ponieważ jest singletonem. Aby otrzymać taki obiekt, trzeba wywołać statyczną metodę getRuntime(), a następnie przekazać ścieżkę do polecenia linuksowego, które aplikacja ma uruchomić. Process process = Runtime.getRuntime().exec("/system/bin/ls");
550
|
Rozdz ał 20. nne język programowan a framework
W kodzie tym klasa Process służy do utworzenia procesu. Ta sama klasa pomaga też wczytywać dane z procesu. W tym celu należy utworzyć obiekt InputStream, powiązany ze standardowym strumieniem wyjścia natywnego procesu, który jest reprezentowany przez obiekt Process. DataInputStream osRes = new DataInputStream(process.getInputStream());
Następnie aplikacja tworzy obiekt BufferedReader, który pomaga wczytywać dane wiersz po wierszu. BufferedReader reader = new BufferedReader(new InputStreamReader(osRes)); String line; while ((line = reader.readLine()) != null || reader.read() !=-1) { Log.i("Wczytywanie wyniku wywołania polecenia", line); }
Kod ten wczytuje wszystkie wiersze i wyświetla je w konsoli LogCat. Dane wyjściowe z przykładowego kodu można zobaczyć w środowisku Eclipse. Oczywiście można też przechwytywać dane wyjściowe z poleceń systemowych w programie i przetwarzać je w celu wyświetlenia na przykład w kontrolce ListView lub TextView.
20.3. Uruchamianie kodu natywnego w języku C lub C++ za pomocą interfejsu JNI z pakietu NDK Ian Darwin
Problem Programista chce uruchamiać fragmenty aplikacji za pomocą kodu natywnego — na przykład w celu wykorzystania istniejącego kodu w języku C lub C++ albo poprawienia wydajności kodu wymagającego dużej ilości czasu procesora.
Rozwiązanie Należy wykorzystać interfejs JNI (ang. Java Native Interface) z androidowego pakietu NDK (ang. Native Development Kit) (http://developer.android.com/tools/sdk/ndk/1.6_r1/index.html).
Omówienie Standardowa Java zawsze umożliwiała wczytywanie natywnego lub skompilowanego kodu w programach Javy. Środowisko uruchomieniowe Dalvik w Androidzie obsługuje ten mechanizm w niemal identyczny sposób. Dlaczego programiści mieliby korzystać z tej możliwości? Jedną z przyczyn jest dostęp do funkcji systemu operacyjnego. Innym powodem jest szybkość. Kod natywny często działa szybciej od kodu w Javie (przynajmniej obecnie), choć trwają dyskusje na temat tego, jak istotna jest ta różnica w praktyce. W internecie znajdziesz sprzeczne opinie na ten temat. Mechanizmy do uruchamiania kodu natywnego są opracowane dla języków C i C++. Jeśli chcesz zastosować inny język, możesz napisać fragment w języku C lub C++ i przekazywać
20.3. Urucham an e kodu natywnego w języku C lub C++ za pomocą nterfejsu JN z pak etu NDK
|
551
w nim sterowanie do innych funkcji lub aplikacji. Możesz też wykorzystać środowisko Android Script Environment (opisane w recepturze 20.4). Przykładowa aplikacja wykonuje tu proste operacje matematyczne — oblicza pierwiastek kwadratowy z wartości typu double za pomocą iteracyjnej metody Newtona-Raphsona. Kod jest dostępny w dwóch wersjach, w Javie i w C, co pozwala porównać szybkość obu podejść.
Wywoływanie kodu natywnego w Javie — podstawowe kroki zalecane przez Iana: Aby wywoływać kod natywny w Javie, wykonaj następujące kroki:
1. Oprócz pakietu ADK (ang. Android Development Kit) zainstaluj też pakiet NDK. 2. Napisz kod w Javie z deklaracjami i wywołaniami metod natywnych. 3. Skompiluj kod w Javie. 4. Utwórz plik nagłówkowy .h za pomocą instrukcji javah. 5. Napisz w języku C funkcję, która dołącza wspomniany plik nagłówkowy, i zaimplementuj metodę natywną wykonującą potrzebne zadania.
6. Przygotuj plik konfiguracyjny Android.mk (i opcjonalnie także Application.mk). 7. Skompiluj kod w języku C do ładowalnego obiektu, używając instrukcji $NDK/ndk-build. 8. Utwórz pakiet z aplikacją i ją zainstaluj. Następnie ją przetestuj. Wstępny krok wymaga pobrania pakietu NDK jako pliku TAR lub ZIP, wypakowania go w wygodnym miejscu i odpowiedniego ustawienia na to miejsce zmiennej środowiskowej (np. NDK), co pozwoli łatwo wskazywać zainstalowany pakiet. Będzie to potrzebne przy zaglądaniu do dokumentacji oraz uruchamianiu narzędzi. Pierwszy właściwy krok polega na napisaniu w Javie kodu z deklaracją i wywołaniami metody natywnej (listing 20.1). Do deklarowania takich metod służy słowo kluczowe native, które określa, że dana metoda jest natywna. Wywoływanie metod natywnych nie wymaga stosowania specjalnej składni, jednak w aplikacji (zwykle w głównej aktywności) trzeba umieścić statyczny blok kodu wczytujący metodę natywną za pomocą instrukcji System.loadLibrary(), co pokazano na listingu 20.2 (dynamicznie ładowany moduł utworzysz w kroku 6.). Bloki statyczne są uruchamiane w momencie wczytywania zawierającej je klasy. Wczytanie kodu natywnego jest gwarancją, że kod ten będzie znajdował się w pamięci, gdy będzie potrzebny. Listing 20.1. Kod w Javie public class SqrtDemo { public static final double EPSILON = 0.05d; public static native double sqrtC(double d); public static double sqrtJava(double d) { double x0 = 10.0, x1 = d, diff; do { x1 = x0 - (((x0 * x0) - d) / (x0 * 2)); diff = x1 - x0; x0 = x1; } while (Math.abs(diff) > EPSILON); return x1; } }
552
|
Rozdz ał 20. nne język programowan a framework
Listing 20.2. W klasie aktywności z pliku Main.java używany jest kod natywny // W klasie aktywności, poza metodami static { System.loadLibrary("sqrt-demo"); } // W klasie aktywności w metodzie, gdzie potrzebny jest kod natywny double d = SqrtDemo.sqrtC(123456789.0);
Obiekty, które można modyfikować w kodzie natywnym, powinny mieć modyfikator volatile. W przykładowym kodzie plik SqrtDemo.java obejmuje deklarację metody natywnej (a także implementację algorytmu napisaną w Javie). Następny krok jest prosty — wystarczy w standardowy sposób zbudować projekt, używając Anta lub wtyczki ADK dla środowiska Eclipse. Potem trzeba utworzyć plik nagłówkowy .h w języku C. Plik ten posłuży za interfejs między maszyną JVM a kodem natywnym. Do wygenerowania tego pliku zastosuj instrukcję javah. Należy przekazać do niej klasę z deklaracjami metod natywnych, tak aby uzyskać plik .h dla odpowiedniej klasy z określonego pakietu. mkdir jni // W tym katalogu należy przechowywać cały kod związany z interfejsem JNI javah -d jni -classpath bin foo.ndkdemo.SqrtDemo // Tworzy plik foo_ndkdemo_SqrtDemo.h
Wygenerowany plik .h jest plikiem spajającym rozwiązanie. Nie jest przeznaczony do używania przez ludzi, a zwłaszcza do edytowania. Jeśli jednak zajrzysz do tego pliku, zobaczysz, że nazwa metody w języku C składa się ze słowa „Java”, nazwy pakietu, nazwy klasy i nazwy metody: JNIEXPORT jdouble JNICALL Java_foo_ndkdemo_SqrtDemo_sqrtC (JNIEnv *, jclass, jdouble);
Teraz należy utworzyć funkcję w języku C, która będzie wykonywać potrzebne zadania. Trzeba zaimportować plik .h i dostosować się do sygnatury funkcji z tego pliku. W funkcji można wykonywać dowolne potrzebne operacje. Warto zauważyć, że przed zadeklarowanymi argumentami do funkcji przekazywane są dwa domyślne argumenty — środowisko JVM i uchwyt „this”, dający dostęp do obiektu kontekstu wywołania. W tabeli 20.1 pokazano zależności między typami z Javy a typami z języka C (typami z interfejsu JNI), które są używane w kodzie pisanym w tym języku. Na listingu 20.3 przedstawiono kompletną implementację natywną w języku C. Kod ten oblicza pierwiastek kwadratowy z podanej liczby i zwraca uzyskany wynik. Metoda jest statyczna, dlatego wskaźnik this nie jest tu używany. Listing 20.3. Kod w języku C // jni/sqrt-demo.c #include
20.3. Urucham an e kodu natywnego w języku C lub C++ za pomocą nterfejsu JN z pak etu NDK
|
553
Tabela 20.1. Typy z Javy i interfejsu JNI Typ z Javy
Typ z nterfejsu JN
Typ tabl cowy z Javy
Typ tabl cy z nterfejsu JN
byte
jbyte
byte[]
jbyteArray
short
jshort
short[]
jshortArray
int
jint
int[]
jintArray
long
jlong
long[]
jlongArray
float
jfloat
float[]
jfloatArray
double
jdouble
double[]
jdoubleArray
char
jchar
char[]
jcharArray
boolean
jboolean
boolean[]
jbooleanArray
void
jvoid
Object
jobject
Object[]
jobjectArray
Class
jclass
String
jstring
array
jarray
Throwable
jthrowable
do { x1 = x0 - (((x0 * x0) - d) / (x0 * 2)); diff = x1 - x0; x0 = x1; } while (labs(diff) > foo_ndkdemo_SqrtDemo_EPSILON); return x1; }
Ten kod jest bardzo podobny do wersji napisanej w Javie. Warto zauważyć, że polecenie javah odwzorowuje nawet wartość EPSILON typu double z klasy SqrtDemo Javy w dyrektywie #define w wersji opartej na języku C. Następny krok polega na przygotowaniu pliku Android.mk, który także należy umieścić w katalogu jni. Na potrzeby kompilowania prostej biblioteki współużytkowanej wystarczą instrukcje z listingu 20.4. Listing 20.4. Przykładowy plik Android.mk # Android.mk LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := sqrt-demo LOCAL_SRC_FILES := sqrt-demo.c include $(BUILD_SHARED_LIBRARY)
W ostatnim kroku można kod w języku C skompilować do obiektu ładowalnego. W tradycyjnej Javie przebieg tej operacji zależy od platformy, kompilatora itd. Jednak pakiet NDK udostępnia skrypt budowania, który pozwala zautomatyzować kompilację. Jeśli zmienną NDK ustawiłeś na główny katalog z instalacją pakietu NDK pobranego w kroku 1., wystarczy wprowadzić następujące instrukcje:
554 |
Rozdz ał 20. nne język programowan a framework
$ $NDK/ndk-build # Dla systemów Linux, Unix, OS-X > %NDK%/ndk-build # Dla systemów MS-Windows Compile thumb SharedLibrary Install
: sqrt-demo <= sqrt-demo.c : libsqrt-demo.so : libsqrt-demo.so => libs/armeabi/libsqrt-demo.so
Gotowe! Można utworzyć pakiet z aplikacją i uruchomić ją w standardowy sposób. Dane wyjściowe powinny wyglądać podobnie jak na rysunku 20.1. W kompletnej przykładowej aplikacji znajdziesz przyciski do uruchamiania obu wersji funkcji sqrt (napisanych w Javie i w C) większą liczbę razy, co pozwala porównać ich wydajność. Warto zauważyć, że w tej wersji aplikacja wykonuje zadania w wątku głównym, dlatego duża liczba powtórzeń prowadzi do błędu Application Not Responding, co zaburza pomiar czasu.
Rysunek 20.1. Przykładowe dane wyjściowe z aplikacji opartej na pakiecie NDK
Gratulacje! Wywołałeś metodę natywną. Kod powinien działać teraz nieco szybciej, jednak jego przenoszenie jest trudniejsze. Android działa na coraz większej liczbie platform sprzętowych, co trzeba uwzględnić przynajmniej w pliku Application.mk. Przy stosowaniu kodu asemblerowego problem jest znacznie poważniejszy. Warto wiedzieć, że kod natywny może powodować awarię procesu w maszynie JVM. Maszyny tego typu nie chronią użytkowników przed źle napisanym kodem w językach C i C++. To programista musi zarządzać pamięcią w takim kodzie. Systemowy alokator nie zarządza automatycznie przywracaniem pamięci w aplikacjach napisanych w tych językach. Oznacza to, że programista bezpośrednio komunikuje się z systemem operacyjnym, a czasem nawet ze sprzętem, dlatego musisz być ostrożny — bardzo ostrożny.
20.3. Urucham an e kodu natywnego w języku C lub C++ za pomocą nterfejsu JN z pak etu NDK
|
555
Zobacz także W rozdziale 26. napisanej przeze mnie książki Java Cookbook1 (wydawnictwo O’Reilly) znajduje się receptura, w której zmienne z klasy napisanej w Javie są używane w kodzie natywnym. Oficjalną dokumentację pakietu NDK dla Androida zawiera witryna z informacjami o androidowym pakiecie SDK (http://developer.android.com/tools/sdk/ndk/1.6_r1/index.html). Jeśli szukasz dodatkowych informacji na temat natywnych metod Javy, wyczerpujące omówienie tego tematu znajdziesz w książce Essential JNI: Java Native Interface Roba Gordona (wydawnictwo Prentice Hall), napisanej pierwotnie w kontekście tradycyjnej wersji Javy.
20.4. Wprowadzenie do aplikacji Scripting Layer for Android (SL4A; wcześniej Android Scripting Environment) Ian Darwin
Problem Programista chce napisać aplikację w jednym z kilku popularnych języków skryptowych lub chce pisać kod w telefonie w interaktywny sposób.
Rozwiązanie Jednym z najlepszych rozwiązań jest zastosowanie aplikacji SL4A (ang. Scripting Layer for Android). Współdziała ona z kilkoma popularnymi językami skryptowymi, takimi jak Python, Perl, Lua i BeanShell. Omawiana biblioteka obejmuje obiekt Android, który zapewnia dostęp do większości androidowych interfejsów API w poszczególnych językach. W tej recepturze pokazano, jak rozpocząć korzystanie z SL4A. W kilku innych recepturach omówiono konkretne aspekty tej biblioteki. Na początku należy wykonać następujące operacje:
1. Pobrać aplikację SL4A (jej poprzednia nazwa to Android Scripting Environment) ze strony http://code.google.com/p/android-scripting/.
2. Dodać potrzebne interpretery. 3. Wprowadzić kod programu. 4. Uruchomić go — nie trzeba kompilować kodu ani tworzyć pakietu z aplikacją!
Omówienie W czasie powstawania tej książki aplikacja do zarządzania skryptami SL4A nie była dostępna w sklepie Google Play, dlatego trzeba odwiedzić poświęconą jej witrynę i pobrać potrzebny 1
Wydanie polskie: Ian Darwin, Java. Receptury, Helion, Gliwice 2003. — przyp. tłum.
556
|
Rozdz ał 20. nne język programowan a framework
plik (w witrynie dostępny jest kod QR, dlatego należy otworzyć ją w laptopie lub w komputerze stacjonarnym). Ponieważ aplikacja nie pochodzi ze sklepu Google Play, przed jej pobraniem należy przejść do opcji Settings/Applications/Unknown Sources i umożliwić instalowanie aplikacji z nieznanych źródeł. Ponadto ponieważ pliku nie pobrano ze wspomnianego sklepu, nie będziesz otrzymywał powiadomień o pojawieniu się nowej wersji w google’owej witrynie projektu. Po zainstalowaniu aplikacji do zarządzania skryptami SL4A trzeba uruchomić ten program i pobrać potrzebne interpretery. W trakcie powstawania książki dostępne były interpretery dla następujących języków: • Python, • Perl, • JRuby, • Lua, • BeanShell, • JavaScript, • Tcl, • powłoka Uniksa.
Niektóre z interpreterów (na przykład dla języka JRuby) działają w maszynie wirtualnej Dalvik. Inne (na przykład dla Pythona) powodują uruchomienie natywnej wersji języka w linuksowym systemie telefonu. Komunikacja odbywa się poprzez prosty serwer, uruchamiany w razie potrzeby automatycznie lub za pomocą menu Interpreters. Pobieranie nowych interpreterów odbywa się w nieoczywisty sposób. Aplikacja do zarządzania skryptami SL4A po uruchomieniu wyświetla listę skryptów (jeśli są dostępne). Należy kliknąć przycisk Menu, a następnie wybrać opcję View/Interpreters (warto zauważyć, że można też wyświetlić konsolę LogCat z dziennikiem wyjątków systemowych). Gdy zobaczysz listę Interpreters, ponownie kliknij opcję Menu, a pojawi się pasek menu z przyciskiem Add. Pozwala to dodać nowy interpreter.
Wybieranie języka (tu jest to Python) Programista uważa, że Python jest doskonałym językiem (rzeczywiście tak jest). Po zainstalowaniu interpretera należy wrócić do głównego ekranu aplikacji do zarządzania skryptami SL4A, kliknąć przycisk Menu, a następnie wybrać opcję Add (w tym miejscu opcja ta pozwala utworzyć nowy plik, zamiast dodawać kolejny interpreter). Wybierz zainstalowany interpreter, aby przejść do trybu edycji. Przykład dotyczy Pythona, dlatego wprowadź tradycyjny kod aplikacji „Witaj, świecie”: import android droid = android.Android() droid.makeToast("Witaj, Androidzie")
Kliknij przycisk Menu, a następnie wybierz opcję Save & Run (jeśli jest aktywna) lub Save & Exit (w przeciwnym razie). Pierwsza z tych opcji powoduje uruchomienie nowej aplikacji. Druga prowadzi do listy skryptów, z której należy wybrać nazwę nowego skryptu. W oknie, które się pojawi, dostępne są następujące opcje (od lewej do prawej):
20.4. Wprowadzen e do apl kacj Scr pt ng Layer for Andro d
|
557
• uruchom (ikona DOS-owego okna), • niedostępny, • edytuj (ikona ołówka), • zapisz (ikona dyskietki), • usuń (ikona kosza).
Po długim kliknięciu nazwy pliku w oknie pojawiają się opcje zmiany nazwy lub usunięcia skryptu. Po uruchomieniu tej prostej aplikacji w dolnej części ekranu zobaczysz komunikat toast.
Edycja kodu źródłowego Jeśli chcesz przechowywać skrypty w repozytorium kodu źródłowego i (lub) wolisz edytować je za pomocą tradycyjnej klawiatury w laptopie lub stacjonarnym komputerze, możesz swobodnie kopiować pliki (jeżeli telefon jest zrootowany, często można korzystać z repozytorium bezpośrednio w telefonie). Skrypty są przechowywane w katalogu sl4a/scripts na karcie SD. Jeśli telefon zamontowany jest w katalogu /mnt laptopa, można zobaczyć listę podobną do tej z listingu 20.5 (w systemie Windows zamiast nazwy /mnt używana jest litera E: lub F:). Listing 20.5. Lista plików ze skryptami laptop$ ls /mnt/sl4a/ Shell.log demo.sh.log dialer.py.log hello_world.py.log ifconfig.py.log notify_weather.py.log phonepicker.py.log say_chat.py.log say_time.py.log say_weather.py.log scripts/ sms.py.log speak.py.log take_picture.py.log test.py.log laptop$ ls /mnt/sl4a/scripts bluetooth_chat.py demo.sh dialer.py foo.sh hello_world.py ifconfig.py notify_weather.py phonepicker.py say_chat.py say_time.py say_weather.py sms.py speak.py take_picture.py test.py weather.py weather.pyc laptop$
Zobacz także Oficjalna witryna poświęcona bibliotece SL4A, http://code.google.com/p/android-scripting/. W witrynie tej znajduje się kod QR, który pozwala pobrać najnowszą wersję biblioteki. Ponadto istnieje kilka podręczników na temat tej biblioteki (ich listę znajdziesz we wspomnianej witrynie).
20.5. Tworzenie alertów za pomocą biblioteki SL4A Rachee Singh
Problem Programista chce tworzyć w Pythonie alerty lub okna dialogowe za pomocą biblioteki SL4A.
558 |
Rozdz ał 20. nne język programowan a framework
Rozwiązanie Za pomocą biblioteki SL4A można tworzyć w Pythonie różnego rodzaju okna ostrzegawcze. W oknach tych można umieszczać przyciski, listy i inne elementy.
Omówienie Najpierw uruchom aplikację do zarządzania skryptami SL4A w emulatorze lub na urządzeniu. Następnie dodaj nowy skrypt Pythona. W tym celu kliknij przycisk Menu i wybierz opcję Add (rysunek 20.2).
Rysunek 20.2. Początek dodawania nowego skryptu
Wybierz opcję Python 2.x, co pokazano na rysunku 20.3.
Rysunek 20.3. Wybieranie języka
20.5. Tworzen e alertów za pomocą b bl otek SL4A
|
559
To prowadzi do otwarcia edytora. Dwa pierwsze wiersze kodu są już gotowe (rysunek 20.4). Wprowadź nazwę skryptu (tu nazwano go alertdialog.py; rysunek 20.4).
Rysunek 20.4. Pisanie skryptu
Teraz można wprowadzić kod tworzący okno ostrzegawcze. Wpisz kod przedstawiony na listingu 20.6. Listing 20.6. Prosty skrypt Pythona oparty na bibliotece SL4A title = 'Przykladowe okno ostrzegawcze' text = 'Okno ostrzegawcze – typ 1.' droid.dialogCreateAlert(title, text) droid.dialogSetPositiveButtonText('Dalej') droid.dialogShow()
Kliknij przycisk Menu i wybierz opcję Save and Run. To spowoduje uruchomienie skryptu. Okno ostrzegawcze powinno wyglądać tak jak na rysunku 20.5.
Rysunek 20.5. Przykładowe okno ostrzegawcze
560
|
Rozdz ał 20. nne język programowan a framework
Teraz na podstawie kodu z listingu 20.7 utwórz okno ostrzegawcze obejmujące dwa przyciski. Listing 20.7. Kod okna z dwiema opcjami title = 'Przykladowe okno ostrzegawcze ' text = 'Okno ostrzegawcze – typ 2. (z przyciskami)' droid.dialogCreateAlert(title, text) droid.dialogSetPositiveButtonText('Tak') droid.dialogSetNeutralButtonText('Anuluj') droid.dialogShow()
Wygląd nowego okna przedstawiono na rysunku 20.6.
Rysunek 20.6. Okno ostrzegawcze obejmujące dwa przyciski
Teraz wypróbuj kod z listingu 20.8, aby utworzyć okno ostrzegawcze obejmujące listę. Listing 20.8. Tak można utworzyć okno ostrzegawcze z trzema opcjami title = 'Przykladowe okno ostrzegawcze' droid.dialogCreateAlert(title) droid.dialogSetItems(['mango', 'gruszka', 'truskawka']) droid.dialogShow()
Na rysunku 20.7 pokazano wygląd tego okna.
Rysunek 20.7. Okno dialogowe z trzema opcjami
20.5. Tworzen e alertów za pomocą b bl otek SL4A
|
561
20.6. Pobieranie dokumentów Google i wyświetlanie ich w kontrolce ListView za pomocą biblioteki SL4A Rachee Singh
Problem Programista chce pobierać dokumenty Google po zalogowaniu się za pomocą identyfikatora i hasła usługi.
Rozwiązanie Dokumenty Google to popularna usługa służąca do edycji i wymiany dokumentów. Za pomocą biblioteki gdata.docs.service można zalogować się do niej (po uzyskaniu identyfikatora i hasła od użytkownika), a następnie pobrać listę dokumentów.
Omówienie W urządzeniu lub emulatorze uruchom aplikację z biblioteką SL4A. Otwórz nowy skrypt Pythona, a następnie zapisz w nim kod z listingu 20.9. Jeśli wcześniej nie korzystałeś z języka Python, zapamiętaj, że do grupowania instrukcji służą tu wcięcia, a nie nawiasy klamrowe, dlatego trzeba bardzo konsekwentnie stosować początkowe odstępy. Listing 20.9. Skrypt do pobierania dokumentów Google import android import gdata.docs.service droid = android.Android() client = gdata.docs.service.DocsService() username = droid.dialogGetInput('Nazwa uzytkownika').result password = droid.dialogGetPassword('Haslo', 'Uzytkownik: ' + username).result def truncate(content, length=15, suffix='...'): if len(content) <=length: return content else: return content[:length] + suffix try: client.ClientLogin(username, password) except: droid.makeToast("Nieudana proba logowania") client.ssl = True docs_feed = client.GetDocumentListFeed() documentEntries = [] for entry in docs_feed.entry: documentEntries.append('%-18s %-12s %s' % (truncate(entry.title.text.
562
|
Rozdz ał 20. nne język programowan a framework
encode('UTF-8')), entry.GetDocumentType(), entry.resourceId.text)) droid.dialogCreateAlert('Dokumenty:') droid.dialogSetItems(documentEntries) droid.dialogShow()
Na rysunku 20.8 pokazano wygląd edytora po wprowadzeniu całego kodu.
Rysunek 20.8. Tworzenie skryptu do pobierania dokumentów Google
W kodzie w języku Python należy wywołać metodę gdata.docs.service.DocsService(), tak aby nawiązać połączenie z google’owym kontem użytkownika. Nazwę i hasło trzeba pobrać od użytkownika. Po udanym zalogowaniu użytkownika skrypt za pomocą metody GetDocument ListFeed() pobiera listę dokumentów Google. Skrypt formatuje każdą pozycję i dodaje ją do listy documentEntries. Lista ta jest przekazywana jako argument do okna ostrzegawczego, w którym wyświetlane są wszystkie wpisy. Na rysunku 20.9 pokazano listę z moimi dokumentami.
20.7. Używanie kodów QR do rozpowszechniania skryptów SL4A Rachee Singh
Problem Programista utworzył elegancki lub przydatny skrypt SL4A i chce rozpowszechniać go za pomocą kodu QR.
20.7. Używan e kodów QR do rozpowszechn an a skryptów SL4A
|
563
Rysunek 20.9. Lista dokumentów Google
Rozwiązanie Wykorzystaj program ze strony http://zxing.appspot.com/generator/ lub jeden z kilku innych generatorów kodów QR do utworzenia kodu QR, który w formie graficznej reprezentuje cały skrypt. Następnie udostępnij wygenerowaną grafikę.
Omówienie Większość osób uważa kody QR za wygodne narzędzie do udostępniania adresów URL. Rzeczywiście, w drukowanej wersji tej książki znajdują się kody QR pozwalające pobrać poszczególne przykładowe aplikacje. Format QR jest jednak dużo bardziej uniwersalny. Można go wykorzystać do zapisania dowolnych informacji, na przykład w formacie VCard (nazwy i adresu). Z tej receptury dowiesz się, jak w kodzie QR zapisać zwykły tekst skryptu SL4A. Inni użytkownicy Androida mogą następnie pobrać skrypt do urządzenia bez konieczności ponownego wprowadzania kodu. Kody QR to doskonałe narzędzia do udostępniania krótkich skryptów (w takim kodzie można zapisać tylko do 4296 znaków). Wykonaj podane poniżej operacje, aby wygenerować kod QR ze skryptem:
1. W przeglądarce telefonu otwórz stronę http://zxing.appspot.com/generator/. 2. Z listy rozwijanej wybierz opcję Text. 3. W pierwszym wierszu pola Text content wpisz nazwę skryptu. 4. W dalszych wierszach umieść kod skryptu. Możesz też skopiować skrypt z okna edytora aplikacji do zarządzania skryptami SL4A i wkleić go w przeglądarce w polu Text content.
5. Wybierz opcję Large dla wielkości kodu i kliknij przycisk Generate. Na rysunku 20.10 pokazano, jak wygląda to w praktyce.
564 |
Rozdz ał 20. nne język programowan a framework
Rysunek 20.10. Kod QR wygenerowany dla skryptu SL4A
Istnieje wiele czytników kodów QR przeznaczonych na Android. Każda taka aplikacja potrafi odczytać tekst zapisany w kodzie QR. ZXing, popularny skaner kodów kreskowych, zapisuje wczytany skrypt do schowka (można to zmienić za pomocą opcji When a Barcode is found w ustawieniach skanera). Następnie wystarczy uruchomić edytor skryptów SL4A, podać nazwę skryptu (najlepiej taką samą jak jego pierwotnej wersji, jeśli ją znasz; w zależności od tego, w jaki sposób wprowadzono kod w generatorze kodów QR, nazwa skryptu może znajdować się w jego pierwszym wierszu), a następnie wykonać długie kliknięcie w edytorze i wybrać opcję Paste. Teraz można zapisać skrypt i uruchomić go. Edytor powinien wyglądać tak jak na rysunku 20.11.
Rysunek 20.11. Pobrany skrypt
20.7. Używan e kodów QR do rozpowszechn an a skryptów SL4A
|
565
W ten sposób udało mi się uruchomić skrypt z kodu QR, przy czym musiałem tylko umieścić nazwę skryptu w komentarzu i wprowadzić ją w polu na nazwę pliku. Potem wystarczy wybrać opcję Save and Run (rysunek 20.12).
Rysunek 20.12. Uruchomiony skrypt wyświetlający powiadomienie
20.8. Używanie JavaScriptu do wykorzystania wbudowanych funkcji telefonu poprzez kontrolkę WebView Colin Wilcox
Problem Ponieważ wiele przeglądarek obsługuje język HTML5, programiści mogą korzystać z mechanizmów tego standardu, aby tworzyć aplikacje szybciej niż za pomocą tradycyjnej Javy. W wielu aplikacjach jest to doskonałe rozwiązanie, jednak w językach HTML5 i JavaScript nie wszystkie atrakcyjne funkcje urządzenia są dostępne. Częściowym rozwiązaniem problemu są frameworki do rozwijania aplikacji sieciowych, jednak także one nie zawsze udostępniają wszystkie potrzebne mechanizmy.
Rozwiązanie Można wykonywać kod Javy w reakcji na javascriptowe zdarzenia. Wymaga to zastosowania mostu między środowiskami JavaScriptu i Javy.
566
|
Rozdz ał 20. nne język programowan a framework
Omówienie Pomysł polega na dodaniu do strony HTML5 kodu w JavaScripcie, który zgłasza odpowiednie zdarzenia, i obsłudze tych zdarzeń w Javie przez wywoływanie kodu natywnego. Poniższy kod w języku HTML5 tworzy przycisk osadzony w kontrolce WebView. Kliknięcie tego przycisku powoduje otwarcie w urządzeniu aplikacji Contacts. Wykorzystywane są do tego intencje. import android.content.Context; import android.content.Intent; import android.util.Log;
Teraz należy napisać kod mostu. Przedstawiono go na listingu 20.10. Listing 20.10. Kod mostu public class JavaScriptInterface { private static final String TAG = "JavaScriptInterface"; Context iContext = null; /** Tworzenie obiektu z interfejsem i ustawianie kontekstu */ JavaScriptInterface(Context aContext) { // Zapisywanie kontekstu iContext = aContext; } public void launchContacts(); { iContext.startActivity(contactIntent); launchNativeContactsApp (); } }
Kod w Javie uruchamiający aplikację Contacts przedstawiono na listingu 20.11. Listing 20.11. Kod w Javie uruchamiający aplikację Contacts private void launchNativeContactsApp() { String packageName = "com.android.contacts"; String className = ".DialtactsContactsEntryActivity"; String action = "android.intent.action.MAIN"; String category1 = "android.intent.category.LAUNCHER"; String category2 = "android.intent.category.DEFAULT"; Intent intent = new Intent(); intent.setComponent(new ComponentName(packageName, packageName + className)); intent.setAction(action); intent.addCategory(category1); intent.addCategory(category2); startActivity(intent); }
W następnym fragmencie pokazano kod w JavaScripcie, który łączy pozostałe elementy. Kod ten jest wywoływany w reakcji na kliknięcie.
Jedyne warunki, które trzeba spełnić, aby kod zadziałał, to włączenie w przeglądarce obsługi JavaScriptu i określenie interfejsu. Za spełnienie tych warunków odpowiada poniższy kod: WebView iWebView = (WebView) findViewById(R.id.webview); iWebView.addJavascriptInterface(new JavaScriptInterface(this), "Android");
20.9. Tworzenie aplikacji niezależnych od platformy za pomocą frameworku PhoneGap Shraddha Shravagi
Problem Programista chce, aby aplikacja działała w różnych platformach, takich jak iOS, Android, Black Berry, Bada, Symbian i Windows Mobile.
Rozwiązanie Cordova (lepiej znana jako PhoneGap) to framework do rozwijania aplikacji mobilnych, dostępny jako oprogramowanie o otwartym dostępie do kodu źródłowego. Jeśli zamierzasz tworzyć aplikacje działające w wielu platformach, PhoneGap jest dobrym rozwiązaniem — tak dobrym, że między innymi Oracle i BlackBerry dostosowują swoje produkty do tego frameworku lub opierają je na nim. W PhoneGap programista nie używa tradycyjnych kontrolek interfejsu GUI. Zamiast tego należy utworzyć stronę internetową z przyciskami, która dzięki odpowiedniemu zastosowaniu stylów CSS wygląda podobnie jak aplikacje natywne. Phone Gap odpowiada następnie za uruchamianie tego rodzaju „aplikacji mobilnych”. Framework PhoneGap powstał w małej firmie Nitobi, przejętej jesienią 2011 roku przez Adobe Systems Inc. Firma Adobe udostępniła otwarty kod źródłowy frameworku organizacji Apache Software Foundation, gdzie prace nad PhoneGapem są kontynuowane. Framework przez krótki czas nosił nazwę Callback, a obecnie rozwijany jest pod nazwą Cordova.
Omówienie Najpierw opisano aplikację na Android. Nie zastosowano tu standardowych układów Androida ani techniki „jedna aktywność na ekran”. Zamiast tego aplikacja obejmuje pliki HTML i JavaScript, które można uruchamiać w różnych platformach. Program ten to w zasadzie mobilna aplikacja sieciowa, zapisana jako aplikacja na Android. Kod aktywności jest tak krótki, jak to tylko możliwe, ponieważ dla każdej platformy trzeba napisać jego odpowiednik.
568 |
Rozdz ał 20. nne język programowan a framework
Oto kroki potrzebne do zbudowania prostej aplikacji opartej na frameworku PhoneGap2:
1. Utwórz nową aplikację na Android. 2. Pobierz plik phonegap-wersja.zip (w trakcie powstawania tej książki aktualną wersją była
1.5.0) ze strony http://phonegap.com/ (w przyszłości adres URL zostanie zmieniony na witrynę organizacji Apache). Skopiuj plik cordova-wersja.jar z katalogu lib/android z pobranego archiwum ZIP i dodaj go do folderu lib oraz ścieżki budowania projektu.
3. W folderze assets utwórz nowy katalog (np. www). 4. Do katalogu assets/www skopiuj pliki phonegap-1.0.0.js i jquery.min.js. 5. W katalogu assets/www utwórz nowy plik, helloworld.html. 6. W ciele utworzonej strony HTML umieść następujący kod:
Witaj, świecie
W tym miejscu można umieścić kod napisany w języku HTML lub oparty na bibliotece jQuery mobile. Oto przykładowy kod dodający przycisk: Kliknij mnie!
7. W katalogu assets/www utwórz nowy plik, helloresponse.js. W pliku tym można umieścić cały kod oparty na bibliotece jQuery mobile i w JavaScripcie: function showAlert(){ alert('Javascriptowe powitania od PhoneGapa! '); }
8. W głównym pliku aktywności należy zaimportować pakiet com.phonegap.DroidGap, a następnie zmienić człon extends Activity na extends DroidGap.
9. W metodzie onCreate() aktywności należy do metody loadUrl klasy DroidGap przekazać identyfikator URI pliku HTML. Spowoduje to wyświetlenie tego pliku.
Kod w Javie powinien wyglądać tak jak na listingu 20.12. Listing 20.12. Aktywność PhoneGap import com.phonegap.DroidGap; public class HomeScreen extends DroidGap { /** Wywoływana, gdy aktywność tworzona jest po raz pierwszy */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Ustawianie adresu URL wczytywanego pliku super.loadUrl("file:///android_asset/www/helloworld.html"); } }
I to już wszystko. Można uruchomić aplikację. W pobranym archiwum znajdziesz doskonałe przykłady oparte na bibliotece jQuery mobile.
2
Warto zauważyć, że w wersji dostępnej w trakcie powstawania tej książki niektóre pliki obejmowały człon „phonegap”, a inne — „cordova”.
20.9. Tworzen e apl kacj n ezależnych od platformy za pomocą frameworku PhoneGap
| 569
Zobacz także Strona http://phonegap.com/. W książce Building Android Apps with HTML, CSS, and JavaScript (wydawnictwo O’Reilly) Jonathana Starka znajdziesz omówienie wymienionych „pomocniczych” technologii w kontekście PhoneGapa, a także dodatkowe informacje na temat rozwijania aplikacji za pomocą tego frameworku.
570
|
Rozdz ał 20. nne język programowan a framework
ROZDZIAŁ 21.
Łańcuchy znaków i internacjonalizacja
21.1. Wprowadzenie — internacjonalizacja Ian Darwin
Omówienie „Świat jest teatrem” — pisał William Szekspir. Jednak nie wszyscy aktorzy na tej wielkiej i niespokojnej scenie mówią językiem owego poety. Aby oprogramowanie było przydatne na skalę globalną, musi być dostępne w różnych językach. Użytkownik powinien mieć możliwość ustawienia opcji menu, etykiet przycisków, komunikatów w oknach dialogowych, nagłówków na pasku tytułu, a nawet komunikatów o błędach na wybrany język. Związane są z tym internacjonalizacja (inaczej umiędzynarodawianie; ang. internationalization) i lokalizacja (inaczej tworzenie wersji językowych; ang. localization). Ponieważ pojęcia te są długie, często zapisuje się je za pomocą pierwszej i ostatniej litery ich angielskich odpowiedników oraz liczby znaków w tych słowach (czyli I18N oraz L10N). Jeśli przechowujesz łańcuchy znaków w odrębnym pliku XML, jak zalecano w rozdziale 1., wykonałeś już część prac nad internacjonalizacją. Czy nie cieszysz się z tego, że zastosowałeś się do zaleceń? Android udostępnia klasę Locale, która pozwala wykrywać i ustawiać opcje internacjonalizacji. Domyślny obiekt Locale powstaje na podstawie ustawień językowych aktywnych w momencie uruchamiania aplikacji. Jeśli znasz techniki internacjonalizacji stosowane w tradycyjnej Javie, zauważysz, że w Androidzie wyglądają one podobnie. W tym rozdziale wyjaśniono je na podstawie przykładów.
Internacjonalizacja — podstawowe kroki proponowane przez Iana Internacjonalizacja i lokalizacja obejmują: Trening wrażliwości (internacjonalizacja, inaczej I18N) Uwzględnianie internacjonalizacji w aplikacji. Naukę języków (lokalizacja, inaczej L10N) Pisanie plików konfiguracyjnych dla każdego języka. 571
Lekcje kultury (opcjonalnie) Dostosowywanie sposobu wyświetlania liczb, ułamków, dat i komunikatów. Także grafiki mogą w poszczególnych kulturach oznaczać różne rzeczy. W recepturach z tego rozdziału pokazano, jak zająć się każdym z tych trzech obszarów.
Zobacz także W Wikipedii dostępny jest dobry artykuł na temat lokalizacji — http://en.wikipedia.org/wiki/ Internationalization_and_localization. Zapoznaj się też z książką Java Internationalization Andy’ego Deitscha i Davida Czarneckiego (wydawnictwo O’Reilly). Książka The GUI Guide: International Terminology for the Windows Interface Microsoftu dotyczyła (choć tytuł tego nie sugeruje) w mniejszym stopniu projektowania interfejsu użytkownika, a w większym — właśnie internacjonalizacji. Wraz z książką Microsoft rozpowszechniał 3,5-calową dyskietkę z sugerowanymi tłumaczeniami popularnych elementów interfejsu GUI systemu Microsoft Windows na kilkanaście języków. Obecnie książka ta jest nieco przestarzała, może jednak posłużyć za punkt wyjścia do tłumaczenia prostych tekstów na popularne języki. Często można ją znaleźć w witrynach z używanymi książkami.
21.2. Internacjonalizacja tekstu aplikacji Ian Darwin
Problem Programista chce, aby tekst przycisków, etykiet i innych elementów pojawiał się w języku wybranym przez użytkownika.
Rozwiązanie W podkatalogu res/values-XX/ aplikacji należy utworzyć plik strings.xml, a następnie umieścić w nim łańcuchy znaków przetłumaczone na dany język.
Omówienie Każdy projekt na Android utworzony za pomocą pakietu SDK obejmuje plik strings.xml w katalogu res/values. To w tym pliku należy umieszczać łańcuchy znaków wyświetlane w aplikacji — od nagłówka programu po etykiety przycisków, a nawet zawartość okien dialogowych. Łańcuch znaków można wskazać za pomocą nazwy na jeden z dwóch sposobów: • Za pomocą referencji w pliku układu, aby zastosować odpowiednią wersję łańcucha zna-
ków bezpośrednio do komponentu interfejsu GUI (np. android:text="@string/hello"). • Jeśli tekst jest potrzebny w kodzie Javy, można wyszukać łańcuch znaków w pliku, na
przykład za pomocą instrukcji getString(R.string.hello).
572
|
Rozdz ał 21. Łańcuchy znaków nternacjonal zacja
Aby udostępnić wszystkie łańcuchy znaków w innym języku, trzeba najpierw znaleźć kod języka w standardzie ISO-3166. Kilka popularnych kodów tego rodzaju przedstawiono w tabeli 21.1. Tabela 21.1. Popularne języki i ich kody Język
Kod
Chiński (t adycyjny)
ch tw
Chiński (up oszczony)
ch zh
Angielski
en
F ancuski
f
Niemiecki
de
Włoski iszpański Polski
it es pl
Na podstawie tych informacji można utworzyć nowy podkatalog, res/values-LL/ (człon LL należy zastąpić kodem języka ze standardu ISO). W katalogu tym należy umieścić kopię pliku strings.xml z przetłumaczonymi łańcuchami znaków (nazw nie trzeba tłumaczyć). Oto przykładowy plik strings.xml z prostej aplikacji:
Można utworzyć plik res/values-es/strings.xml z następującym tekstem hiszpańskim (zobacz rysunek 21.1):
Można też przygotować plik res/values-fr/strings.xml z tekstem francuskim:
Warto zauważyć, że kolejność łańcuchów znaków w pliku nie ma znaczenia, dlatego umieszczenie łańcucha app_name na ostatniej pozycji nie wpływa na działanie programu. Teraz gdy aplikacja będzie szukać łańcucha znaków hello za pomocą dowolnej z dwóch opisanych wcześniej metod, pobierze wersję dostosowaną do języka wybranego przez użytkownika. Jeśli użytkownik ustawi język, dla którego programista nie przygotował łańcuchów znaków, aplikacja będzie działać, ale wyświetli tekst z pliku domyślnego (z katalogu o nazwie values, bez członu z kodem języka). Zwykle taki plik obejmuje nazwy po angielsku, zależy to jednak od programisty.
21.2. nternacjonal zacja tekstu apl kacj
|
573
Wyszukiwanie dotyczy konkretnych łańcuchów znaków, dlatego jeśli w pliku dla danego języka odpowiedni łańcuch nie jest zdefiniowany, aplikacja pobierze wersję z domyślnego pliku strings.xml.
Czy to naprawdę jest aż tak proste? Tak. Teraz wystarczy utworzyć pakiet z aplikacją i zainstalować ją. Jeśli korzystasz ze środowiska Eclipse, wybierz opcję Run As/Android Application. Następnie przejdź do aplikacji Settings w emulatorze lub urządzeniu, kliknij Language, a później wybierz opcję French lub Spanish. Nagłówek aplikacji i zawartość okna powinny pojawić się w odpowiednim języku (rysunek 21.1).
Rysunek 21.1. Powitanie po hiszpańsku
Trzeba tylko pamiętać o tym, aby poszczególne wersje pliku strings.xml były zgodne z oryginałem.
Wersje regionalne Tu sytuacja jest nieco bardziej skomplikowana. W niektórych językach istnieją dialekty. W języku angielskim można wyróżnić angielski brytyjski (zdaniem niektórych jedyny prawdziwy), amerykański, kanadyjski, australijski itd. Na szczęście pojęcia techniczne są w tych odmianach języka zwykle takie same, dlatego stosowanie dialektów nie ma specjalnie dużego znaczenia. Natomiast we francuskim i hiszpańskim (wybrałem dwa znane mi języki) słownictwo stosowane w poszczególnych regionach czasem znacznie się różni. W języku francuskim używanym w Paryżu i tym w Kanadzie, dokąd ludzie zaczęli wyjeżdżać od XVI w., używa się wielu różnych słów na określenie tych samych pojęć. Jeszcze większe różnice niż we francuskim powstały między językiem hiszpańskim używanym w Hiszpanii i tym używanym w koloniach hiszpańskich, które aż do momentu spopularyzowania się radia były przez wiele lat odizolowane od siebie. Dlatego czasem warto utworzyć pliki dla konkretnych dialektów wymienionych języków, a także dla innych języków, w których występują istotne różnice między regionami.
574
|
Rozdz ał 21. Łańcuchy znaków nternacjonal zacja
W zakresie dialektów w Androidzie stosuje się nieco inne rozwiązania niż w Javie. W Androidzie dialekty oznacza się za pomocą litery r. Na przykład katalog values-fr-rCA jest przeznaczony na plik dla kanadyjskiej odmiany francuskiego. Warto zauważyć, że — podobnie jak w Javie — kody języków podaje się małymi literami, a dialekty (zwykle określane przy użyciu dwuliterowych kodów ISO państw) — wielkimi (wyjątkiem jest początkowa litera r). Może więc powstać zestaw plików przedstawiony w tabeli 21.2. Tabela 21.2. Przykładowe katalogi z wersjami językowymi Katalog
Znaczen e
values
Angielski język domyślny
values es
iszpański (odmiana kastylijska powszechnie stosowany)
values es rCU
Kubańska odmiana hiszpańskiego
values es rCL
Chilijska odmiana hiszpańskiego
Zobacz także Szczegółowe informacje znajdziesz w oficjalnej dokumentacji na temat lokalizacji w Androidzie (http://developer.android.com/guide/topics/resources/localization.html).
21.3. Wyszukiwanie i tłumaczenie łańcuchów znaków Ian Darwin
Problem Programista chce znaleźć wszystkie łańcuchy znaków w aplikacji, umożliwić ich internacjonalizację i rozpocząć tłumaczenie.
Rozwiązanie Istnieje kilka dobrych narzędzi do wyszukiwania łańcuchów znaków. Dostępne są też usługi tłumaczenia plików tekstowych (oparte na współpracy oraz komercyjne).
Omówienie Programista ma w aplikacji starszy i nowy kod w Javie. Nowy kod napisał specjalnie z myślą o Androidzie, natomiast starszy był używany w innym środowisku opartym na Javie. Trzeba znaleźć każdy literał typu String, umieścić go w pliku Strings.xml i przetłumaczyć na odpowiednie języki. Android Localizer firmy ArtfulBits Inc. (http://artfulbits.com/products/free/ailocalizer.aspx) to bezpłatne narzędzie o otwartym dostępie do kodu źródłowego, które można wykorzystać do wykonania wszystkich potrzebnych zadań.
21.3. Wyszuk wan e tłumaczen e łańcuchów znaków
|
575
MOTODEV Studio (http://developer.motorola.com/tools/motodevstudio/) to bezpłatne (po zarejestrowaniu się) narzędzie komercyjne, które udostępnia potrzebne tu funkcje, a także dodatkowe możliwości. Oba narzędzia tłumaczą łańcuchy znaków za pomocą usługi Google Translate, co pozwala uzyskać tekst stanowiący punkt wyjścia do dalszych prac. Teraz wyobraź sobie inną sytuację — aplikacja posiada natywną aplikację (w języku Objective-C) z systemu iOS, a programista rozwija natywną wersję w Javie, przeznaczoną na Android. Pliki właściwości z obu wersji mają zupełnie inny format. W systemie iOS plik przypomina pliki właściwości z Javy, jednak domyślne (zwykle angielskie) łańcuchy znaków znajdują się w nim po lewej stronie, a tłumaczenia — po prawej. Nie występują tu nazwy, a tylko same łańcuchy znaków. Możesz więc natrafić na następujący fragment: You-not us-are responsible=You-not us-are responsible
Nie można przekształcić takiego pliku bezpośrednio na format XML, ponieważ nazwa łańcucha jest używana jako identyfikator w wygenerowanej klasie R (to skrót od ang. resources, czyli zasoby), a łącznik (-) i cudzysłów (") są niedozwolone w identyfikatorach w Javie. Przy ręcznym przekształcaniu można uzyskać następujący kod:
Użytkownik johnthuss opracował aplikację w Javie, która wykonuje tego rodzaju przekształcenia z formatu z systemu iOS na format androidowy (http://stackoverflow.com/questions/3141118/ are-there-any-tools-to-convert-an-iphone-localized-string-file-to-a-string-resou/5838915#5838915). Aplikacja obsługuje znaki, które są niedozwolone w identyfikatorach. Teraz programista może przystąpić do tłumaczenia głównego pliku zasobów na inne języki. Choć może chciałbyś zaoszczędzić na tym aspekcie pracy, zwykle warto skorzystać z usług profesjonalnych tłumaczy, specjalizujących się w docelowym języku. Możesz też skorzystać z komercyjnych usług tłumaczenia grupowego, na przykład z serwisu Crowdin.net (http:// crowdin.net/page/android-localization). Tłumaczenia (zwłaszcza na języki, których Ty i współpracownicy nie znacie na bardzo wysokim poziomie) przygotowane przez niezależne jednostki przeważnie warto sprawdzić u innego tłumacza. Żenujące błędy w źle przetłumaczonym tekście mogą okazać się bardzo kosztowne. Jako ostrzeżenie często przytaczana jest historia Microsoftu, który nieświadomie zatrudnił sympatyzujących z Tajwanem tłumaczy do przetłumaczenia systemu Microsoft Windows na kontynentalną odmianę chińskiego. Oto jeden z opisów tej historii: „[…] Latem 1996 roku odkryto, że w niektórych aplikacjach Microsoftu przetłumaczonych na język chiński znalazły się ukryte hasła propagandowe, co doprowadziło do wzrostu napięcia w stosunkach między Microsoftem a Chinami” (tekst ten ukazał się w dzienniku „South China Morning Post” w październiku 1996 roku1). W internecie można szybko znaleźć wiele komercyjnych serwisów wykonujących tłumaczenia. Łatwo znajdziesz też narzędzia pomocne przy internacjonalizacji.
1
Historia ta opisana jest w tekście Software Localization: Notes on Technology and Culture, Kenneth Keniston, 17.01.1997, dokument nr 26, Program in Science, Technology, and Society, Massachusetts Institute of Technology, Cambridge, Massachusetts 02139. W internecie plik PDF z tym tekstem znajduje się na stronie http:// web.mit.edu/sts/pubs/pdfs/MIT_STS_WorkingPaper_26_Keniston_2.pdf (stan na 4.11.2011).
576
|
Rozdz ał 21. Łańcuchy znaków nternacjonal zacja
21.4. Niuanse związane z plikami strings.xml Daniel Fowler
Problem Wprowadzanie tekstu w plikach strings.xml jest zwykle proste, jednak czasem uzyskane efekty mogą okazać się zaskakujące.
Rozwiązanie Warto zrozumieć obsługę łańcuchów i znaków w plikach strings.xml, aby uniknąć dziwnych efektów.
Omówienie Jeśli na ekranie ma pojawiać się pewien tekst, można go podać w pliku układu. W poniższym kodzie tekst określono w atrybucie android:text:
Tekst można też ustawić w kodzie: TextView tview = (TextView) findViewById(R.id.textview1); tview.setText("To tekst");
Jednak zapisywanie łańcuchów znaków na stałe nie jest zalecane, ponieważ utrudnia to konserwację aplikacji. Późniejsza zmiana tekstu wymaga wyszukiwania deklaracji w różnych plikach napisanych kodem źródłowym w Javie oraz w plikach układu. Inne rozwiązanie to zapisanie wszystkich łańcuchów znaków z projektu w pliku strings.xml. Plik ten znajduje się w katalogu res/values. Umieszczenie tekstu w jednym pliku powoduje, że zmiany trzeba wprowadzić tylko w nim. Znacznie łatwiejszy jest też wtedy proces lokalizacji (zobacz recepturę 21.2). Oto przykładowy plik strings.xml:
Aby uzyskać dostęp do zadeklarowanego łańcucha znaków w innym pliku XML projektu, należy podać znak @, słowo string, ukośnik oraz nazwę danego łańcucha. Na podstawie wcześniejszego kodu można ustawić tekst dwóch kontrolek TextView w XML-owym pliku układu:
21.4. N uanse zw ązane z pl kam str ngs.xml
|
577
android:text="@string/text1" android:textSize="16dp"/>
W momencie zapisywania pliku strings.xml generowana jest klasa R.string (zobacz plik R.java w katalogu gen projektu). Obejmuje ona wartości typu static int, za pomocą których można wskazywać wybrane łańcuchy znaków w kodzie: tview = (TextView) findViewById(R.id.textview1); tview.setText(R.string.text1);
Klasy R nigdy nie należy edytować. Jest ona generowana przez pakiet SDK, a wszelkie wprowadzone zmiany zostaną usunięte. W pliku strings.xml w elemencie można wykorzystać inny łańcuch znaków, wskazując go w ten sam sposób jak w pliku układu:
Ponieważ znak @ służy do wskazywania innego łańcucha znaków, próba ustawienia tekstu na jeden taki symbol (
Przed początkowym symbolem @ trzeba dodać znak ucieczki, czyli lewy ukośnik (\), np. \@ i \@twittername. Jeśli symbol @ nie pojawia się na początku łańcucha znaków lub jest ustawiany w kodzie, nie trzeba poprzedzać go znakiem ucieczki, np. android:text=Twitter:@mytwittername lub tview.setText("@mytwittername");. Problem związany ze stosowaniem symbolu @ jako pierwszego lub jedynego znaku dotyczy także znaku zapytania (?). Jeśli pojawia się on na początku łańcucha znaków, także należy poprzedzić go znakiem ucieczki — android:text=\?. 578
|
Rozdz ał 21. Łańcuchy znaków nternacjonal zacja
Zamiast poprzedzać symbole @ i ? znakiem ucieczki, można też zastosować cudzysłów (przy czym nie jest konieczne jego zamykanie).
Cudzysłowy i odstępy przed tekstem oraz po nim są pomijane. Dwa poprzednie wiersze kodu działają w taki sam sposób jak dwa poniższe:
Istnieje znak, dla którego opisane podejście nie działa:
Pierwszy wiersz prowadzi do błędu z uwagi na znak &. Wynika to z formatu pliku XML. Format ten wymaga stosowania par znaczników, np.
Taki kod jest niedozwolony. Rozwiązaniem jest zastosowanie wewnętrznego elementu XML-a. Przypomina to używanie znaków ucieczki, ale dotyczy konkretnie XML-a. Technika polega na zastosowaniu symbolu ampersanda, &, po którym następuje nazwa elementu i średnik. Dla otwierającego nawiasu ostrego (czyli symbolu „mniejszy niż”) nazwą jest lt, a więc cała sekwencja to <, tak jak w poniższym kodzie:
W zależności od tego, jaki element powinien znajdować się w określonym miejscu pliku XML, należy stosować odpowiednie z pięciu sekwencji zdefiniowanych w XML-u. Wymieniono je w tabeli 21.3. Tabela 21.3. Wbudowane sekwencje XML-a Element
Nazwa
Stosowan e
Lewy nawias ost y (<)
lt
<
P awy nawias ost y (>)
gt
>
Ampe sand (&)
amp
&
Apost of (')
apos
'
Cudzysłów (")
quot
"
Teraz łatwo jest zrozumieć, dlaczego ampersand powoduje problemy. Służy do definiowania wewnętrznych elementów, jeśli więc ampersand ma pojawić się w tekście, należy zastosować element amp. Dlatego zamiast
21.4. N uanse zw ązane z pl kam str ngs.xml
|
579
Co ciekawe, XML-owy wewnętrzny element apos, choć poprawny w XML-u, jest traktowany jak błąd:
Następnym symbolem, dla którego trzeba zastosować znak ucieczki lub cudzysłów, jest apostrof:
Aby umieścić w tekście cudzysłów, nawet jako wewnętrzny element XML-a, należy zastosować znak ucieczki:
Aby utworzyć łańcuch znaków z odstępami, należy zastosować cudzysłów:
Poniższe łańcuchy znaków są dzielone na dwa wiersze. Umożliwia to litera n poprzedzona znakiem ucieczki.
Aby dodać tabulator, należy zastosować literę t poprzedzoną znakiem ucieczki:
W celu wyświetlenia znaku ucieczki (lewego ukośnika) należy poprzedzić go drugim takim symbolem:
580 |
Rozdz ał 21. Łańcuchy znaków nternacjonal zacja
Atrybut android:textstyle kontrolki TextView można wykorzystać w pliku układu do dodania pogrubienia lub kursywy: android:textStyle="bold" android:textStyle="italic" android:textStyle="bold|italic"
Ten sam efekt można uzyskać w pliku strings.xml za pomocą znacznika pogrubienia () lub kursywy (). Dodatkowo dostępny jest też znacznik podkreślenia (). Jednak zamiast stosować te znaczniki do całej kontrolki TextView, można użyć ich do wybranych fragmentów tekstu:
Zobacz także http://developer.android.com/guide/topics/resources/string-resource.html
21.4. N uanse zw ązane z pl kam str ngs.xml
|
581
582
|
Rozdz ał 21. Łańcuchy znaków nternacjonal zacja
ROZDZIAŁ 22.
Tworzenie pakietów, instalowanie, dystrybucja i sprzedaż aplikacji
22.1. Wprowadzenie — tworzenie pakietów, instalowanie i dystrybucja Ian Darwin
Omówienie Sukces Androida doprowadził do powstania wielu źródeł, w których można kupić aplikacje działające w tym systemie. Jednak nadal najważniejszym miejscem do rozpowszechniania aplikacji na Android jest sklep Google Play, dlatego w tym rozdziale opisano właśnie jego. Dowiesz się też, jak odpowiednio przygotować aplikację i jak utrudnić jej wsteczną inżynierię, a także poznasz inne informacje, które mogą okazać się potrzebne.
22.2. Tworzenie certyfikatu używanego przy podpisywaniu Zigurd Mednieks
Problem Programista chce opublikować aplikację. Aby wykonać to zadanie, potrzebuje klucza do podpisywania.
Rozwiązanie Aby wygenerować samodzielnie podpisany certyfikat, należy zastosować standardowe narzędzie keytool z pakietu JDK. 583
Omówienie Jednym z celów firmy Google w trakcie prac nad Androidem było uproszczenie procesu podpisywania aplikacji. Dlatego nie trzeba kontaktować się z centralną jednostką zarządzającą podpisami, aby uzyskać certyfikat potrzebny przy podpisywaniu — zamiast tego można samodzielnie utworzyć odpowiedni certyfikat. Po jego wygenerowaniu można podpisać aplikację za pomocą narzędzia jarsigner z pakietu JDK Javy. Także na tym etapie nie trzeba kontaktować się z żadnymi zewnętrznymi jednostkami. Jak więc widać, proces podpisywania jest niezwykle prosty. W tej recepturze zobaczysz, jak utworzyć zaszyfrowany certyfikat i jak wykorzystać go do podpisania aplikacji. Każdą rozwijaną aplikację na Android można podpisać za pomocą tego samego certyfikatu. Wprawdzie można wygenerować dowolną liczbę takich certyfikatów, ale wystarczy jeden. Podpisywanie wszystkich aplikacji za pomocą jednego certyfikatu pozwala uzyskać kilka wyjątkowych korzyści: Uproszczenie aktualizowania Certyfikaty powiązane są z nazwą pakietu aplikacji, dlatego po zmianie certyfikatu w nowej wersji aplikacji trzeba też zmodyfikować nazwę pakietu. Zmienianie certyfikatów jest możliwe, ale kłopotliwe. Uruchamianie kilku aplikacji z wykorzystaniem jednego identyfikatora użytkownika Jeśli wszystkie aplikacje podpisane są za pomocą tego samego certyfikatu, można je uruchomić w jednym procesie linuksowym. Pozwala to podzielić aplikację na mniejsze moduły (każdy z nich to odrębna aplikacja na Android), które wspólnie tworzą większy program. W tym podejściu można aktualizować moduły niezależnie od siebie, a nadal będą mogły się one swobodnie komunikować między sobą. Współużytkowanie kodu oraz danych Android umożliwia otwarcie lub zablokowanie dostępu do fragmentów aplikacji na podstawie certyfikatu kodu żądającego tego dostępu. Jeśli wszystkie programy mają ten sam certyfikat, programista może łatwo wykorzystać fragment jednej aplikacji w innych. W trakcie generowania pary kluczy i certyfikatu należy podać pożądany termin ważności certyfikatu. Choć przy rozwijaniu witryn ważność certyfikatów wynosi zwykle rok lub dwa lata, Google zaleca ustawienie tej wartości na przynajmniej 25 lat. Jeśli chcesz udostępniać aplikację w sklepie Google Play, certyfikat musi być ważny przynajmniej do 22.10.2033 (to 25 lat od momentu uruchomienia sklepu przez Google’a).
Generowanie pary kluczy (publicznego i prywatnego) oraz certyfikatu Aby wygenerować parę klucz publiczny – klucz prywatny, zastosuj narzędzie keytool, dostępne w pakiecie Sun JDK zainstalowanym w komputerze używanym do programowania. Narzędzie to pobiera od użytkownika kilka informacji i na ich podstawie generuje parę kluczy. • Klucz prywatny przechowywany jest w pliku keystore na komputerze i zabezpieczony jest
hasłem. Klucz prywatny służy do podpisywania aplikacji. Jeśli w aplikacji potrzebny jest klucz Google Maps API, należy zastosować skrót MD5 certyfikatu do wygenerowania takiego klucza.
584 |
Rozdz ał 22. Tworzen e pak etów, nstalowan e, dystrybucja sprzedaż apl kacj
• Klucz publiczny używany jest przez Android do odszyfrowywania certyfikatu. Klucz pu-
bliczny wysyłany jest razem z udostępnianą aplikacją, dzięki czemu można korzystać z niego w środowisku uruchomieniowym. Sprawdzanie certyfikatów odbywa się tylko na etapie instalowania aplikacji, dlatego po zakończeniu instalacji program będzie działał poprawnie, nawet jeśli ważność certyfikatu lub klucza wygaśnie. Narzędzie keytool jest stosunkowo proste. W wierszu poleceń systemu operacyjnego należy wpisać instrukcję podobną do poniższej: $ keytool -genkey -v -keystore myapp.keystore -alias myapp -keyalg RSA -validity 10000
Ta instrukcja prowadzi do wygenerowania pary kluczy i samodzielnie podpisanego certyfikatu (opcja -genkey) w trybie pełnym (opcja -v), co zapewnia programiście dostęp do wszystkich informacji. Instrukcja umieszcza wygenerowane elementy w pliku myapp.keystore (opcja -keystore). W przyszłości klucz można wskazać za pomocą nazwy myapp (opcja -alias). Do wygenerowania pary klucz publiczny – klucz prywatny narzędzie keytool ma użyć algorytmu RSA (opcja -keyalg), a klucz ma być ważny przez 10 000 dni (opcja -validity), czyli przez około 27 lat. Narzędzie keytool pobiera pewne informacje używane w trakcie generowania pary kluczy i certyfikatu. Oto te informacje: • hasło, które pozwala w przyszłości uzyskać dostęp do pliku keystore; • imię i nazwisko; • jednostka organizacji (nazwa działu firmy lub na przykład „prywatny”, jeśli programista
nie pracuje w żadnej organizacji); • nazwa organizacji (nazwa firmy lub dowolne inne określenie); • nazwa miasta; • nazwa stanu lub regionu; • dwuliterowy kod kraju.
Narzędzie keytool wyświetla programiście te informacje w celu potwierdzenia ich poprawności. Po zatwierdzeniu danych przez programistę narzędzie generuje parę kluczy i certyfikat. Następnie należy podać drugie hasło, używane dla samego klucza (można też zastosować to samo hasło co dla pliku keystore). Narzędzie keytool przy użyciu tego hasła zapisuje parę kluczy i certyfikat w pliku keystore.
Zobacz także Jeśli nie znasz używanych tu algorytmów, takich jak RSA i MD5, nie przejmuj się — nie musisz dokładnie rozumieć ich działania. Jeśli jednak masz w sobie ciekawość poznawczą, wszystkie potrzebne informacje na ich temat znajdziesz w internecie. Więcej informacji o bezpieczeństwie, parach kluczy i narzędziu keytool znajdziesz w witrynie firmy Sun (http://docs.oracle.com/javase/1.5.0/docs/tooldocs/#security).
22.2. Tworzen e certyf katu używanego przy podp sywan u
| 585
22.3. Podpisywanie aplikacji Zigurd Mednieks
Problem Programista chce podpisać aplikację przed udostępnieniem jej w sklepie Google Play.
Rozwiązanie Plik APK to standardowy plik w formacie JAR (ang. Java Archive), dlatego wystarczy zastosować zwykłe narzędzie jarsigner z pakietu JDK.
Omówienie Po wygenerowaniu klucza (i w razie potrzeby klucza Google Maps API) jesteś już prawie gotowy do podpisania aplikacji. Najpierw jednak trzeba przygotować wersję programu pozbawioną podpisu, którą można podpisać za pomocą certyfikatu cyfrowego. Aby uzyskać pożądany efekt, w oknie Package Explorer środowiska Eclipse kliknij prawym przyciskiem myszy nazwę projektu. Pojawi się długie menu kontekstowe. Wybierz opcję Android Tools, widoczną w dolnej części menu. Pojawi się nowe menu z potrzebną opcją Export Unsigned Application Package. Opcja ta powoduje otwarcie okna dialogowego Export Project, w którym można wybrać katalog na niepodpisaną wersję pliku APK. Nie ma znaczenia, gdzie umieścisz ten plik, natomiast należy zapamiętać tę lokalizację. Po utworzeniu niepodpisanej wersji pliku APK można podpisać go przy użyciu narzędzia jarsigner. W terminalu lub wierszu poleceń przejdź do katalogu z niepodpisanym plikiem APK. Aby podpisać aplikację MyApp za pomocą klucza wygenerowanego w recepturze 22.2, wpisz następującą instrukcję: $ jarsigner -verbose -keystore myapp.keystore MyApp.apk mykey
Powinieneś w ten sposób uzyskać podpisaną wersję aplikacji, którą można pobrać na dowolne urządzenie z Androidem i uruchomić. Zanim jednak udostępnisz program w sklepie Google Play, powinieneś wykonać jeszcze jeden krok. Ponieważ ponownie zbudowałeś aplikację, dlatego przetestuj ją jeszcze raz na urządzeniach (nie w emulatorze). Jeśli masz tylko jeden telefon z Androidem, kup dodatkowe lub poproś o pomoc właściciela urządzenia innego producenta. Warto zauważyć, że w najnowszej wersji wtyczki dla środowiska Eclipse dostępna jest też opcja Export Signed Application Package, która pozwala wykonać w jednym kreatorze operacje opisane w recepturach 22.2 i 22.3. Nowa opcja znajduje się w menu kontekstowym projektu (otwieranie tego menu opisano w pierwszym akapicie punktu „Omówienie” tej receptury), a także w menu File/Export, gdzie nazwa tej opcji to Export Android Project. Za pomocą tej opcji można też utworzyć plik keystore i wygenerować klucze. Jest to tak wygodne, że możesz zapomnieć, gdzie zapisałeś plik keystore, dlatego lepiej zrezygnować ze stosowania tej techniki.
586 |
Rozdz ał 22. Tworzen e pak etów, nstalowan e, dystrybucja sprzedaż apl kacj
22.4. Udostępnianie aplikacji w sklepie Google Play (dawny Android Market) Zigurd Mednieks
Problem Programista chce udostępniać lub sprzedawać aplikację w sklepie Google Play (to nowa nazwa sklepu Android Market). W trakcie powstawania tej książki aplikacje ze sklepu Android Market były udostępniane w kategorii Google Play razem z książkami i usługami.
Rozwiązanie Skorzystaj ze sklepu Google Play.
Omówienie Jeśli jesteś zadowolony z działania aplikacji w rzeczywistych urządzeniach z Androidem, możesz przesłać ją do sklepu Google Play (jest to usługa Google’a przeznaczona do udostępniania i pobierania aplikacji na Android). Procedura udostępniania aplikacji jest prosta:
1. Zarejestruj się jako programista aplikacji na Android (jeśli jeszcze tego nie zrobiłeś). 2. Prześlij podpisaną aplikację.
Rejestrowanie się jako programista aplikacji na Android Przejdź do witryny Google’a (https://play.google.com/apps/publish/signup) i wypełnij dostępny formularz. Oto kroki, które musisz wykonać: • Zaloguj się za pomocą konta Google (jeśli jeszcze go nie posiadasz, możesz założyć je bez-
płatnie za pomocą odnośnika Zarejestruj się na stronie logowania). • Zaakceptuj warunki korzystania z usługi. • Wnieś jednorazową opłatę w wysokości 25 dolarów (należy zrobić to za pomocą karty
kredytowej w systemie Google Checkout; jeśli nie posiadasz konta w tym systemie, możesz je szybko założyć).
• Jeżeli grę chcesz udostępniać odpłatnie, określ sposób obsługi płatności (możesz łatwo za-
łożyć konto w systemie Google Payments). W formularzach wystarczy podać podstawowe informacje (imię i nazwisko, numer telefonu itd.) i gotowe — jesteś zarejestrowany.
Przesyłanie aplikacji Teraz możesz przejść na stronę http://play.google.com/apps/publish/Home, aby przesłać aplikację. W celu określenia cech i kategorii programu należy podać następujące informacje:
22.4. Udostępn an e apl kacj w sklep e Google Play (dawny Andro d Market)
|
587
Nazwę i lokalizację pliku APK aplikacji Chodzi tu o plik APK aplikacji podpisany za pomocą prywatnego certyfikatu. Nazwę i opis aplikacji Są to bardzo ważne informacje, ponieważ stanowią istotę przekazu marketingowego kierowanego do potencjalnych użytkowników. Postaraj się zadbać o to, aby tytuł był zarazem opisowy i atrakcyjny. Opisz aplikację w taki sposób, żeby osoby z grupy docelowej chciały ją pobierać. Rodzaj aplikacji Obecnie wyróżnione są dwa rodzaje programów — aplikacje i gry. Kategoria Lista kategorii zależy od rodzaju programu. Dla aplikacji dostępne są opcje: Animowane tapety, Biblioteki i materiały demonstracyjne, Dla firm, Edukacja, Finanse, Fotografia, Komiksy, Komunikacja, Książki i materiały źródłowe, Medycyna, Multimedia i filmy, Muzyka i dźwięki, Narzędzia, Personalizacja, Podróże i informacje lokalne, Pogoda, Produktywność, Rozrywka, Sport, Społeczności, Styl życia, Transport, Wiadomości i czasopisma, Widżety, Zakupy, Zdrowie i fitness. Dla gier możesz wybrać następujące kategorie: Animowane tapety, Gry sportowe, Karty i kasyno, Rekreacyjne, Widżety, Wyścigi, Zręcznościowe, Łamigłówki i układanki. Cena Program można udostępniać bezpłatnie lub za określoną kwotę. W warunkach, które wcześniej zaakceptowałeś, określony jest procent kwoty, jaki otrzymuje autor aplikacji. Obszar geograficzny Możesz określić obszar, w jakim aplikacja ma być dostępna, lub pozwolić na pobieranie jej z dowolnego miejsca. Należy też potwierdzić, że aplikacja jest zgodna z google’ową polityką treści oraz że nie narusza przepisów eksportowych. Następnie możesz przesłać plik APK, a w ciągu kilku dni aplikacja pojawi się w internetowym katalogu w sklepie Google Play i będzie dostępna na wszystkich urządzeniach z Androidem podłączonych do internetu. Aby sprawdzić, czy aplikacja jest już dostępna, wpisz jej nazwę w polu Szukaj w witrynie sklepu. Możesz też podać w przeglądarce adres URL market://details?id=com.yourorg.yourprog (ostatni człon należy zastąpić nazwą pakietu aplikacji).
Co dalej? Teraz możesz wygodnie usiąść i patrzeć, jak rośnie Twoja sława lub stan konta (i lista e-maili do pomocy technicznej). Zachowaj cierpliwość względem użytkowników aplikacji — oni myślą inaczej niż programiści.
22.5. Integrowanie sieci AdMob z aplikacją Enrique Diaz
Problem Programista chce zarabiać na bezpłatnie udostępnianej aplikacji przez wyświetlanie w niej reklam. 588 |
Rozdz ał 22. Tworzen e pak etów, nstalowan e, dystrybucja sprzedaż apl kacj
Rozwiązanie Za pomocą bibliotek sieci AdMob można zacząć wyświetlać reklamy w bezpłatnej aplikacji i zarabiać na każdym kliknięciu reklamy przez użytkownika.
Omówienie AdMob to jedna z największych na świecie sieci reklamowych obsługujących urządzenia przenośne. Sieć oferuje rozwiązania z zakresu wykrywania aplikacji, budowania wizerunku marki i zarabiania z wykorzystaniem urządzeń przenośnych. Pakiet AdMob Android SDK obejmuje kod potrzebny do wyświetlania w aplikacji reklam z sieci AdMob.
Krok 1. W katalogu głównym projektu utwórz podkatalog libs. Jeśli korzystasz z androidowego narzędzia activitycreator, potrzebny podkatalog jest już gotowy. Skopiuj do niego plik JAR z biblioteką AdMob (admob-sdk-android.jar). Jeśli tworzysz projekty w środowisku Eclipse, wykonaj następujące operacje:
1. W oknie Package Explorer kliknij projekt prawym przyciskiem myszy i wybierz opcję Properties. 2. W lewym panelu wybierz opcję Java Build Path. 3. W oknie głównym wybierz zakładkę Libraries. 4. Wybierz opcję Add JARs. 5. Wybierz plik JAR skopiowany do katalogu libs. 6. Kliknij przycisk OK, aby do androidowego projektu dodać pobrany pakiet SDK.
Krok 2. Do pliku AndroidManifest.xml dodaj identyfikator wydawcy. Tuż przed zamykającym znacznikiem
Aby ustalić identyfikator wydawcy, zaloguj się do konta w sieci AdMob, otwórz zakładkę Sites and Apps i kliknij odnośnik Manage Settings. Na nowej stronie znajdziesz identyfikator wydawcy (pokazano go na rysunku 22.1).
Krok 3. Do pliku AndroidManifest.xml dodaj żądanie uprawnień INTERNET. Umieść je tuż przed końcowym znacznikiem :
Opcjonalnie możesz też zażądać uprawnień ACCESS_COARSE_LOCATION i (lub) ACCESS_FINE_LOCATION, aby umożliwić sieci AdMob wyświetlanie reklam z wykorzystaniem mechanizmu geotargetowania. 22.5. ntegrowan e s ec AdMob z apl kacją
| 589
Rysunek 22.1. Witryna sieci AdMob — tu znajdziesz identyfikator wydawcy
Gotowy plik AndroidManifest.xml powinien wyglądać podobnie jak ten z listingu 22.1. Listing 22.1. Plik AndroidManifest.xml po wklejeniu potrzebnego kodu
Krok 4. Wklej poniższy fragment do pliku attrs.xml:
590
|
Rozdz ał 22. Tworzen e pak etów, nstalowan e, dystrybucja sprzedaż apl kacj
Jeśli projekt nie obejmuje pliku attrs.xml, utwórz taki plik w katalogu /res/values. W pliku tym zapisz następujący kod:
Krok 5. Utwórz referencję do pliku attrs.xml w układzie. W tym celu dodaj wiersz rozpoczynający się od członu xmlns. W wierszu tym podaj nazwę pakietu określoną w pliku AndroidManifest.xml. Jeśli nazwa pakietu to com.example.sampleapp, wiersz ten powinien wyglądać tak: xmlns:myapp="http://schemas.android.com/apk/res/com.example.sampleapp"
Na listingu 22.2 pokazano kod układu prostego ekranu tylko z jedną reklamą. Listing 22.2. Układ z jedną reklamą
Krok 6. W czasie integrowania reklam z sieci AdMob z aplikacją należy stosować tryb testowy. W trybie testowym sieć zawsze udostępnia reklamy. Tryb ten można włączyć dla konkretnego urządzenia. Aby to zrobić, najpierw należy zażądać reklamy, a następnie poszukać w konsoli LogCat wiersza podobnego do poniższego: To get test ads on the emulator use AdManager.setTestDevices...
Po ustaleniu identyfikatora urządzenia można włączyć tryb testowy przez wywołanie w głównej aktywności metody AdManager.setTestDevices: AdManager.setTestDevices( new String[] { AdManager.TEST_EMULATOR, "E83D20734F72FB3108F104ABC0FFC738", // Identyfikator telefonu } ); }
22.5. ntegrowan e s ec AdMob z apl kacją
|
591
Po udanym zażądaniu reklam testowych spróbuj kliknąć reklamę każdego rodzaju, aby się upewnić, że poprawnie działa w aplikacji. Typ udostępnianych reklam można zmienić za pomocą metody AdManager.setTestAction. Wyświetloną reklamę można zobaczyć na rysunku 22.2.
Rysunek 22.2. Reklama w aplikacji
Zobacz także http://www.admob.com/; http://androidtitlan.org/2010/09/como-agregar-publicidad-con-admob-a-tu-android-app/; http://groups.google.com/group/admob-publisher-discuss.
22.6. Zaciemnianie i optymalizowanie kodu za pomocą ProGuarda Ian Darwin
Problem Programista chce zaciemnić kod, zoptymalizować go (ze względu na szybkość lub wielkość) lub osiągnąć oba te cele.
Rozwiązanie ProGuard, narzędzie do optymalizowania i zaciemniania kodu, jest obsługiwane przez skrypt Anta, dostępny w kreatorze nowych projektów na Android w środowisku Eclipse. Aby zastosować to narzędzie, wystarczy je włączyć.
592
|
Rozdz ał 22. Tworzen e pak etów, nstalowan e, dystrybucja sprzedaż apl kacj
Omówienie Zaciemnianie kodu polega na ukrywaniu informacji (na przykład dostępnych w plikach binarnych nazw używanych na etapie kompilacji), które mogłyby być przydatne przy wstecznej inżynierii kodu. Jeśli aplikacja obejmuje informacje, których firma nie chce udostępniać, prawdopodobnie warto je ukryć. Jeżeli program jest dostępny jako oprogramowanie o otwartym dostępie do kodu źródłowego, zaciemnianie kodu nie jest zazwyczaj konieczne. Decyzja należy do Ciebie. Optymalizowanie przypomina refaktoryzację (wykonywaną na poziomie kodu źródłowego), jednak zwykle ma ono prowadzić do przyspieszenia pracy kodu, zmniejszenia jego wielkości lub osiągnięcia obu tych celów. Standardowy proces rozwijania aplikacji na Android w środowisku Eclipse obejmuje kompilowanie projektu do kodu bajtowego Javy (odpowiada za to kompilator ze środowiska Eclipse) i późniejszą konwersję tego kodu na androidowy format DEX (ang. Dalvik Executable). ProGuard (http://proguard.sourceforge.net/) to opracowane przez Erica Lafortune’a bezpłatne narzędzie o otwartym dostępie do kodu źródłowego, przeznaczone do optymalizowania i zaciemniania kodu w Javie. ProGuard przeznaczony jest nie tylko dla Androida. Działa dla aplikacji konsolowych, apletów, aplikacji opartych na frameworku Swing, dla midletów Javy ME, aplikacji na Android i niemal dowolnych innych programów pisanych w Javie. ProGuard działa dla skompilowanego kodu Javy, dlatego narzędzie to trzeba uruchamiać przed przekształceniem kodu bajtowego na format DEX. Najłatwiej jest osiągnąć ten efekt za pomocą Anta (jest to standardowe narzędzie do budowania aplikacji w Javie). Kreator projektów na Android w środowisku Eclipse zapewnia obsługę ProGuarda w generowanym pliku build.xml (dotyczy to Androida 2.3, czyli wersji Gingerbread). Wystarczy zmodyfikować plik build.properties przez dodanie poniższego wiersza z nazwą pliku konfiguracyjnego: proguard.config=proguard.cfg
Informacje o korzystaniu z narzędzia dla starszych wersji Androida znajdziesz w podręczniku ProGuarda (http://proguard.sourceforge.net/index.html).
Plik konfiguracyjny Do sterowania działaniem ProGuarda służy plik konfiguracyjny (zwykle o nazwie proguard.cfg), w którym należy stosować specjalną składnię. Słowa kluczowe rozpoczynają się od znaku -, po którym następuje dane słowo kluczowe i parametry opcjonalne. Jeśli w parametrach trzeba podać klasy lub składowe Javy, należy zastosować składnię podobną do tej używanej w Javie, co ułatwia pracę. Oto bardzo uproszczony plik konfiguracyjny ProGuarda dla aplikacji na Android: -injars bin/classes -outjars bin/classes-processed.jar -libraryjars /usr/local/java/android-sdk/platforms/android-9/android.jar -dontpreverify -repackageclasses '' -allowaccessmodification -optimizations !code/simplification/arithmetic -keep public class com.example.MainActivity
22.6. Zac emn an e optymal zowan e kodu za pomocą ProGuarda
|
593
W pierwszych wierszach określono ścieżki używane w projekcie (w tym ścieżkę do tymczasowego katalogu na zoptymalizowane klasy). W następnych wierszach zastosowano różne opcje. Wstępne sprawdzanie poprawności dotyczy tylko projektów Javy, dlatego tu wyłączono tę opcję. Przedstawione tu optymalizacje są przeznaczone dla projektu na Android 1.5; dla nowszych wersji prawdopodobnie można z nich zrezygnować. Ostatni wiersz powoduje zachowanie klasy com.example.MainActivity w kodzie generowanym w procesie optymalizowania i zaciemniania. Klasę tę trzeba zachować, ponieważ obejmuje ona główną aktywność i jest wskazywana za pomocą nazwy w pliku AndroidManifest.xml. Kompletny, działający plik proguard.cfg generowany jest zwykle przez kreator nowych projektów na Android w środowisku Eclipse. Na listingu 22.3 przedstawiono plik z konfiguracją wygenerowany dla projektu na Android 2.3.3. Listing 22.3. Przykładowy plik proguard.cfg -optimizationpasses 5 -dontusemixedcaseclassnames -dontskipnonpubliclibraryclasses -dontpreverify -verbose -optimizations !code/simplification/arithmetic,!field/*,!class/merging/* -keep -keep -keep -keep -keep -keep -keep -keep
public public public public public public public public
class class class class class class class class
* extends android.app.Activity * extends android.app.Application * extends android.app.Service * extends android.content.BroadcastReceiver * extends android.content.ContentProvider * extends android.app.backup.BackupAgentHelper * extends android.preference.Preference com.android.vending.licensing.IlicensingService
-keepclasseswithmembernames class * { native
Początek przypomina kod z wcześniejszego przykładu. Dyrektywy keep, keepclasseswithmem bernames i keepclassmembers służą do określania klas, które trzeba zachować w aplikacji. Korzystanie z tych dyrektyw jest dość oczywiste — inaczej jest z opcją enum. Metody wyliczeniowe
594 |
Rozdz ał 22. Tworzen e pak etów, nstalowan e, dystrybucja sprzedaż apl kacj
z Javy 5 (values() i valueOf()) używane są czasem w interfejsie API Reflection, dlatego muszą pozostać widoczne. To samo dotyczy klas używanych przez ten interfejs. Pozycja dotycząca usługi ILicensingService potrzebna jest tylko wtedy, gdy korzystasz z androidowego narzędzia LVT (ang. License Validation Tool): -keep class com.android.vending.licensing.ILicensingService
Zobacz także Znacznie więcej informacji znajdziesz w podręczniku ProGuard Reference Manual (http://proguard. sourceforge.net/index.html) i w witrynie Google Developers (http://developer.android.com/tools/ help/proguard.html). Ponadto Matt Quigley na blogu Android Engineer zamieścił artykuł zatytułowany Optimizing, Obfuscating, and Shrinking your Android Applications with ProGuard (http://www.androidengineer.com/2010/07/optimizing-obfuscating-and-shrinking.html).
22.7. Odnośniki do aplikacji ze sklepu Google Play Daniel Fowler
Problem Programista chce umieścić w aplikacji odnośniki do innych programów ze sklepu Google Play, aby zachęcić użytkowników do wypróbowania oprogramowania.
Rozwiązanie Należy zastosować intencję oraz identyfikator URI z nazwą wydawcy lub nazwą pakietu.
Omówienie Androidowy system intencji doskonale nadaje się do wykorzystywania w aplikacji mechanizmów napisanych już przez innych programistów. Aplikację Google Play, przeznaczoną do przeglądania i instalowania oprogramowania, można za pomocą intencji uruchomić za pomocą dowolnego programu. Można więc w istniejącej aplikacji wyświetlać odnośniki do innych programów ze sklepu Google Play, co pozwala programistom i wydawcom zachęcić użytkowników do wypróbowania nowego oprogramowania. Do wyszukiwania aplikacji za pomocą programu Google Play służą standardowe intencje, opisane w recepturze 4.2. Potrzebny identyfikator URI to market://search?=szukany_tekst, gdzie człon szukany_tekst należy zastąpić odpowiednim tekstem, na przykład nazwą programu lub słowem kluczowym. Jako akcję intencji należy ustawić ACTION_VIEW. Identyfikator URI może też prowadzić bezpośrednio do strony sklepu Google Play, na której wyświetlone są szczegółowe informacje o danym pakiecie. W tym celu należy zastosować identyfikator market://details?id=nazwa_pakietu, gdzie człon nazwa_pakietu należy zastąpić niepowtarzalną nazwą pakietu aplikacji.
22.7. Odnośn k do apl kacj ze sklepu Google Play
|
595
Program opisany w tej recepturze (jego działanie pokazano na rysunku 22.3) pozwala za pomocą tekstu wyszukiwać aplikacje w sklepie Google Play i wyświetlać szczegółowe informacje na temat określonego programu. Na listingu 22.4 przedstawiono kod układu. Listing 22.4. Główny układ
Rysunek 22.3. Wyszukiwanie aplikacji
W kontrolce EditText można wprowadzić szukany tekst, a za pomocą kontrolki RadioButton można określić, czy program ma wyszukiwać aplikację, czy pokazywać szczegółowe informacje na jej temat (jeśli użytkownik zna pełną nazwę pakietu). Kontrolka Button służy do uruchamiania wyszukiwania.
596
|
Rozdz ał 22. Tworzen e pak etów, nstalowan e, dystrybucja sprzedaż apl kacj
Ważnym aspektem kodu z listingu 22.5 jest to, że aplikacja koduje szukany tekst. Listing 22.5. Główna aktywność public class Main extends Activity { // Opcja określająca, czy program ma szukać aplikacji, czy wyświetlać informacje RadioButton publisherOption; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); // Kliknięcie przycisku jest obsługiwane przez klasę wewnętrzną HandleClick findViewById(R.id.butSearch).setOnClickListener(new OnClickListener(){ public void onClick(View arg0) { String searchText; // Tworzenie referencji do pola z szukanym tekstem EditText searchFor=(EditText)findViewById(R.id.etSearch); try { // Kodowanie do adresu URL zapewnia obsługę odstępów i znaków przestankowych w szukanym tekście searchText = URLEncoder.encode(searchFor.getText().toString(),"UTF-8"); } catch (UnsupportedEncodingException e) { searchText = searchFor.getText().toString(); } Uri uri; // Zmienna na identyfikator URI dla intencji // Sprawdzanie opcji wyszukiwania RadioButton searchOption=(RadioButton)findViewById(R.id.rdSearch); if(searchOption.isChecked()) { uri=Uri.parse("market://search?q=" + searchText); } else { uri=Uri.parse("market://details?id=" + searchText); } Intent intent = new Intent(Intent.ACTION_VIEW, uri); try { main.this.startActivity(intent); } catch (ActivityNotFoundException anfe) { Toast.makeText(main.this, "Zainstaluj aplikację Google Play", Toast.LENGTH_SHORT).show(); } } }); } }
Przy zwykłym wyszukiwaniu tekst jest dołączany do identyfikatora URI market://search?q=. Aby znaleźć nazwę wydawcy, należy zastosować kwalifikator pub:. Wymaga to dołączenia nazwy wydawcy do członu market://search?q=pub:. W czasie powstawania tej książki w niektórych wersjach aplikacji Google Play występował błąd, który powodował, że jeśli nazwa wydawcy obejmowała więcej niż jedno słowo, system nie zwracał żadnych wyników. Dlatego identyfikator market://search?q=pub:IMDb działał, natomiast market://search?q=pub:O’Reilly+Media — już nie. Problem można rozwiązać przez stosowanie zwykłego wyszukiwania tekstowego (np. market://search?q=oreilly+media), jeśli nazwa wydawcy składa się z więcej niż jednego słowa. Przy wyszukiwaniu z kwalifikatorem pub: wielkość znaków ma znaczenie. Dlatego jeśli podasz identyfikator market://search?q=pub:IMDb, otrzymasz wyniki, ale nie uzyskasz ich dla identyfikatora market://search?q=pub:imdb. Można też znaleźć konkretną aplikację (za pomocą kwalifikatora id), jeśli znana jest nazwa pakietu. Jeśli nazwa pakietu aplikacji to com.example.myapp, należy podać identyfikator market://
22.7. Odnośn k do apl kacj ze sklepu Google Play
|
597
search?q=id:com.example.myapp. Jeszcze lepiej jest przejść bezpośrednio do strony ze szczegółowymi informacjami o aplikacji. W tym celu wprowadź identyfikator market://details?q=id:com. example.myapp. Wydawnictwo O’Reilly udostępnia bezpłatną aplikację, a szczegółowe informacje na jej temat można uzyskać przez podanie identyfikatora market://details?id=com.aldiko. android.oreilly.isbn9781449388294. Na rysunku 22.4 pokazano dane wyjściowe uzyskane dla wyszukiwania z rysunku 22.3.
Rysunek 22.4. Wyniki przeszukiwania sklepu
Opisane tu techniki pozwalają bardzo łatwo dodać przycisk lub opcję menu, które umożliwiają użytkownikom bezpośrednie przejście do innych aplikacji opublikowanych przez daną firmę.
Zobacz także http://developer.android.com/guide/publishing/publishing.html#marketintent
598 |
Rozdz ał 22. Tworzen e pak etów, nstalowan e, dystrybucja sprzedaż apl kacj
Skorowidz
A ADB, 37 AdMob, 589 akcelerometr, 521 czujniki, 521 dostępność czujnika, 522 executeShakeAction(), 525 getSensorList(), 522 isAccelerationChanged(), 525 onAccuracyChanged(), 527 onSensorChanged(), 523, 526, 527 orientacji, 527 SensorManager, 522 temperatury, 528 wykorzystywanie danych, 524 alerty, 315 komunikaty toast, 336 powiadomienia, 337 dźwięk, 339 reakcja na kliknięcie, 342 powiadomienie, 341 włączanie diody LED, 340 wyświetlanie, 338 przetwarzanie w tle, 330 Python, 558 Service, 338 wyskakujące okna dialogowe, 321 AlertDialog, 321, 532 AndEngine, 458 Android, 13 akcelerometr, 521 czujniki, 521 animacje, 455 aplikacje, 35 awarie, 125 Backup Manager, 102 cykl życia, 35 dostosowywanie do tabletów, 94
formatowanie czasu i daty, 97 Google Analytics, 90 kontrolowanie danych wejściowych, 99 latarka, 92 lokalizacja, 473 mapy, 473 menu kontekstowe, 75 menu opcji, 75 monitorowanie poziomu baterii, 83 możliwe stany, 35 obsługa połączeń telefonicznych, 417 obsługa wyjątków, 76 projektowanie, 73 sieciowe, 441 singleton, 79 stoper z odliczaniem wstecznym, 55 stosowanie wskazówek, 108 Tipster, 57 tworzenie ekranów powitalnych, 84 tworzenie kopii zapasowej, 102 współużytkowanie danych, 80 wymagania, 73 zmiana orientacji ekranu, 81 zrzutu ekranu, 54 biblioteki, 41 AndroidPlot, 41, 207 HttpClient, 442, 443 OpenStreetMap, 41 RGraph, 224 Bluetooth, 531 podłączanie innego urządzenia, 533 włączanie, 532 wykrywanie urządzeń, 537 żądania połączeń, 536 cykl życia, 35 diagram stanów, 36 ekran powitalny, 86 formatowanie liczb, 277 Double.toString(), 277 kody formatujące, 278 599
Android formatowanie liczb liczba mnoga, 281 metody formatujące, 278 fragment, 305 gęstość ekranu, 74 grafika, 185 animacja rastrowa, 228 animacje, 455 niestandardowa czcionka, 185, 188 OpenGL ES, 185, 188 paleta kolorów, 216 przybliżanie obrazu, 230 rysowanie płynnych linii, 194 skanowanie kodów, 204 tło domyślnego widoku, 221 trójwymiarowa, 188 tworzenie ikony, 208, 215 tworzenie wykresów, 224 wyświetlanie danych, 207 zdjęcia, 198, 200 gry, 455 frameworki, 455 GUI, 58, 235 alerty, 315 animacja, 269 czas, 322 ekran wczytywania, 291 komponenty, 58 kontrolka ListView, 343 lista rozwijana, 251 menu, 316 niestandardowe okno dialogowe, 328 obrotowy mechanizm wybierania, 325 obsługa długiego kliknięcia, 253 obsługa zmian konfiguracji, 238 odbiornik zdarzeń, 242 otwieranie nowego ekranu, 283 pole wyboru, 247 przekształcanie pól tekstowych, 260 przycisk wysyłania, 262 przyciski graficzne, 249 przyciski opcji, 247 tworzenie przycisku, 241 widżet, 236 widżet aplikacji, 236 wykrywanie gestów, 299 wyświetlanie pól tekstowych, 254 zakrywanie innych komponentów, 292 internacjonalizacja, 571 łańcuchy znaków, 575 tekst aplikacji, 572 Java, 19 pakiet JDK, 29
600 |
Skorow dz
pomijane interfejsy API, 20 RGraph, 224 środowisko IDE Eclipse, 29 kanały danych, 76 komunikacja, 145 AsyncTask, 145 dostawcy treści, 175, 178 e-mail, 147 intencje, 145 IPC, 179 komponenty obsługi, 145 komunikaty rozgłoszeniowe, 157 odbiorniki rozgłoszeniowe, 145 podtrzymywanie działania usługi, 155 przekazywanie łańcuchów znaków, 151 używanie wątków, 159 kontrolki, 59 rozmieszczanie, 59 mechanizmy wprowadzania danych, 75 multimedia, 367 Gallery, 368 mechanizm wykrywania twarzy, 373 odtwarzanie plików muzycznych, 377 rejestrowanie filmów, 371 YouTube, 367 NinePatch, 221 pakiet ADK, 21 pakiet SDK, 24 aktualizacja, 46 przykładowe programy, 44 wtyczka ADT, 29 sterowanie, 539 kopiowanie tekstu, 542 menedżer aktywności, 547 polecenia powłoki, 545 połączenie z siecią, 539 tryb dzwonka, 541 ustawienia projektu, 540 włączanie wibracji, 544 wyświetlanie powiadomień, 544 środowisko Eclipse, 24 parametry nowego projektu, 25 tworzenie nowego projektu, 45 uruchamianie projektu, 27 testy, 111 awarie, 125 BugSense, 129 chmura, 121 cykl życia, 134 debugowanie kodu, 128 konfigurowanie urządzeń AVD, 112 lokalny dziennik czasu wykonania, 131 projekt testowy, 122 sterowanie programowaniem, 111
urządzenia AVD, 23 konfigurowanie, 112 utrwalanie danych, 383 baza SQLite, 401, 402, 403 dodawanie danych kontaktowych, 412 pobieranie informacji o plikach, 383 ustawianie preferencji, 390 wczytywanie danych kontaktowych, 415 wczytywanie plików z aplikacji, 386 wyszukiwanie tekstu, 396 wyświetlanie zawartości katalogu, 387 wielkość ekranu, 74 Windows, 29 środowisko IDE Eclipse, 29 wtyczka ADT, 24 wyświetlanie czasu i daty, 97 DateFormat, 97 DateUtils, 99 Formatter, 99 java.util.Date, 98 TextView, 97 Time, 99 zachowywanie danych, 81 getLastNonConfigurationInstance(), 81 onCreate(), 82 onRetainNonConfigurationInstance(), 81 onSaveInstanceState(), 81 zdalna usługa, 179 bindService(), 181 invokeService(), 182 onBind(), 179 onCreate(), 180 onDestroy(), 180 releaseService(), 182 startService(), 181 stopService(), 182 zrzut ekranu, 52 Device Screen Capture, 53 Android Compatibility, 305 Android Localizer, 575 aplikacje, 35 akcelerometr czujnik orientacji, 527 dostępność, 522 pobieranie danych, 523 wykorzystywanie danych, 524 awarie, 125 adb logcat, 125 NPE, 126 Backup Manager, 102 implementacja, 102 polecenie bmgr, 107
Bluetooth akceptowanie połączeń, 536 pobieranie adresu MAC, 532 pobieranie nazwy urządzenia, 532 tworzenie serwera, 536 wymiana danych z urządzeniem, 534 certyfikat, 583 generowanie, 584 korzyści, 584 cykl życia rejestrowanie zdarzeń, 135 dostawca treści, 178 dostosowywanie do tabletów, 94 Google Analytics, 90 Java kod natywny, 552 język C kod natywny, 553 kontrolka ListView, 343 nagłówki sekcji, 352 obsługa zmian orientacji, 360 wyświetlanie listy wierszy, 344 zachowywanie pozycji, 356 kontrolowanie danych wejściowych, 99 KeyListener, 99 typy odbiorników, 102 konwersja tekstu na mowę, 382 kopia zapasowa, 102 Backup Manager, 102 latarka, 92 etapy rozwijania, 92 lokalizacja, 473 aktualizowanie lokalizacji, 474 fikcyjne współrzędne GPS, 477 geokodowanie, 479 geokodowanie odwrotne, 480 OpenStreetMap, 473 pobieranie danych o lokalizacji, 474 podawanie fikcyjnej lokalizacji, 478 urządzenia GPS, 473 mapy, 473 mapy Google’a, 480 AddressOverlay, 495 ItemizedOverlay, 491 MapTest, 481 MyOverlay, 488 obsługa długich kliknięć, 506 wyszukiwanie lokalizacji, 502 wyświetlanie ikony, 499 wyświetlanie warstwy, 495 menu kontekstowe, 75 menu opcji, 75
Skorow dz
|
601
aplikacje obsługa połączeń telefonicznych, 417 pobieranie danych, 430 połączenia przychodzące, 418, 420 połączenia wychodzące, 421 wiadomości SMS, 425 wybieranie numeru telefonu, 424 odbiornik dotyku, 231 odnośniki, 595 OpenStreetMap, 509 aktualizowanie lokalizacji, 517 obsługa dotknięć warstwy, 515 optymalizowanie kodu, 593 pobieranie dokumentów Google, 562 podpisywanie, 586 projektowanie, 73 ekrany powitalne, 84 formatowanie czasu i daty, 97 funkcje urządzenia, 75 kanały danych, 76 kontrolowanie danych wejściowych, 99 latarka, 92 mechanizmy wprowadzania danych, 75 obsługa wyjątków, 76 odbiornik rozgłoszeniowy, 83 wielkość i gęstość ekranu, 74 przekształcanie wyjątków, 78 rozpoznawanie mowy, 380 sieciowe, 441 parser danych, 446 przekształcanie tekstu, 450, 451 RESTful, 442 wyrażenia regularne, 444 wyświetlanie stron internetowych, 452 skanowanie kodów, 204 SCAN_FORMATS, 206 SCAN_MODE, 206 SL4A, 556 śledzenie korzystania, 90, 91 Google Analytics, 90 udostępnianie, 587 przesyłanie, 587 rejestrowanie, 587 współużytkowanie danych, 80 wykrywanie gestów, 299 wymagania, 73 wyświetlanie danych, 207 wyświetlanie reklam, 588 zaciemnianie kodu, 593 aplikacje sieciowe, 441 parser danych, 446 getRSS(), 447 przekształcanie tekstu, 450
602
|
Skorow dz
kontrolka TextView, 451 MD5, 450 na odnośniki, 451 na postać nieczytelną, 450 stosowanie RESTful, 442 converse(), 442 HTTP GET, 442 HTTP POST, 442 klient usługi, 443 URL, 442 URLConnection, 442 wyrażenia regularne, 444 BookRank, 445 wyświetlanie stron internetowych, 452 kontrolka WebView, 452
B Bluetooth, 531 podłączanie innego urządzenia, 533 wymiana danych z urządzeniem, 534 włączanie, 532 AlertDialog, 532 isEnabled(), 532 konfigurowanie, 533 onActivityResult(), 532 pobieranie adresu MAC, 532 pobieranie nazwy, 532 wykrywanie urządzeń, 537 tryb parowania, 537 żądania połączeń, 536 akceptowanie połączeń, 536 listenUsingRfcommWithServiceRecord(), 536 start(), 536 tworzenie serwera, 536 BugSense, 129
C cykl życia, 35 android.Activity, 35 diagram stanów, 36 rejestrowanie zdarzeń, 135
D dane, 383 baza SQLite, 401 data i czas, 403 insert(), 402 moveToFirst(), 403 moveToNext(), 403
dane baza SQLite onCreate(), 401 pobieranie danych, 402 query(), 403 SQLiteOpenHelper, 401 strftime(), 403 tworzenie, 401 zapisywanie danych, 402 informacje o plikach, 383 File, 384 metody zwracające informacje, 384 JSON, 406 generowanie danych, 406 przetwarzanie łańcucha znaków, 407 kontaktowe, 412 addContact(), 413 dodawanie danych, 412 pobieranie danych, 415 parser danych, 446 pobieranie, 430 akcelerometr, 523 baza SQLite, 402 dane kontaktowe, 415, dostawcy treści, 175 lokalizacja, 574 połączenia telefoniczne, 430 TelephonyManager, 430 pojemność karty SD, 390 ustawianie preferencji, 390 domyślne, 394 getBoolean(), 393 getDefaultSharedPreferences(), 393 getString(), 393 onCreate(), 392 onSharedPreferenceChanged(), 394 PreferenceActivity, 394 PreferenceCategory, 391 PreferenceScreen, 391, 392 wczytywanie plików z aplikacji, 386 openRawResource(), 387 wyszukiwanie tekstu, 396 DbAdapter, 396 wyświetlanie zawartości katalogu, 387 accept(), 389 FilenameFilter, 389 kontrolka ListView, 388 listFiles(), 388 XML, 407 interfejs DOM API, 407 interfejs XmlPullParser, 409 newInstance(), 410 newPullParser(), 410
przetwarzanie kodu, 408 require(), 411 statyczne zasoby, 411 dokumenty Google, 562 lista dokumentów, 564 pobieranie, 562 dostawcy treści, 175, 178 MyContantProvider, 177 pobieranie danych, 175 getContentResolver(), 177 onActivityResult(), 176 query(), 177 D-pad, 192 Droid, 185, 186
E EDGE, 441
F Facebook, 467 fragment, 305 frameworki, 455, 549 AndEngine, 458 AppCelerator Titanium, 549 Flixel, 456 PhoneGap, 549, 568
G Gallery, 368 geokodowanie, 479 gesty dotykowe, 230 odbiornik dotyku, 231 Google Analytics, 90 Google Play, 587 GPRS, 441 graficzny interfejs użytkownika, Patrz GUI grafika, 185 animacja rastrowa, 228 onWindowFocusChanged(), 229 start(), 229 niestandardowa czcionka, 185, 188 Iceberg, 186 OTF, 186 TTF, 186 Typeface.create(), 186 ustawianie, 187 OpenGL ES, 185 przybliżanie obrazu, 230 odbiornik dotyku, 231
Skorow dz
| 603
grafika rysowanie płynnych linii, 194 expandDirtyRect(), 195 getHistoricalX(int), 195 getHistoricalY(int), 195 getHistorySize(), 194 invalidate(), 195 TouchEvent, 194 skanowanie kodów, 204 SCAN_FORMATS, 206 SCAN_MODE, 206 tło domyślnego widoku, 221 kontrolka EditText, 222 trójwymiarowa, 188 buffer.position(0), 191 D-pad, 192 obracanie sześcianu, 192 onDrawFrame(), 190 onSurfaceChanged(), 190 requestFocus(), 194 setFocusableInTouchMode(), 194 wyświetlanie sześcianu, 190 tworzenie ikony, 208, 215 formaty ikon, 220 obramowanie, 217 paleta kolorów, 216 PNG, 209 SVG, 210 wielkość, 210 wymiary grafiki, 217 wymiary ikon, 217 tworzenie wykresów, 224 RGraph, 224 wyświetlanie danych, 207 AndroidPlot, 207 zdjęcia, 198, 200 android.media.Camera, 200 configure(), 202 onActivityResult(), 199 surfaceChanged(), 202 gry, 455 frameworki, 455 AndEngine, 458 Flixel, 456 GUI, 235, 238 alerty, 315 AlertDialog, 321 komunikaty toast, 336 powiadomienia, 337 przetwarzanie w tle, 330 wyskakujące okna dialogowe, 321 animacja, 269 getCurrentFocus(), 269 kod animacji, 269 onClick(), 269 604 |
Skorow dz
autouzupełnianie tekstu, 257, 258 AutoCompleteTextView, 257 onTextChanged(), 258 czas, 322 kontrolka Timepicker, 323 ustawianie czasu, 324 dołączanie odbiornika zdarzeń, 242 anonimowa klasa wewnętrzna, 244 implementacja aktywności, 244 interfejs jako typ, 243 klasa składowa, 243 klasa wewnętrzna, 242 new OnClickListener(){...}, 243 dotykowe informacje zwrotne, 270 oparte na wibracjach, 271 zdarzenia generujące, 272 działanie widoków, 249 ekran wczytywania, 291 LoadingScreen, 292 fragment, 305 IDE Eclipse, 235 SWT, 235 Java ME, 235 Java SE, 235 Swing, 235 kontrolka ListView, 343 brak danych, 347 nagłówki sekcji, 352 obsługa zmian orientacji, 360 pusta lista, 348 stronicowanie, 344 wyświetlanie listy wierszy, 344 zachowywanie pozycji, 356 lista rozwijana, 251 Adapter, 251 getSelectedItem(), 252 kontrolka Spinner, 251 toString(), 252 menu, 316 addSubMenu(), 319 definicja, 316 niestandardowe menu, 317 obsługa wyboru opcji, 317 onCreateOptionsMenu(), 316, 319 onOptionsItemSelected(), 317, 320 początkowe menu, 320 podmenu, 318, 321 tworzenie, 316 wyświetlanie, 316 niestandardowe okno dialogowe, 328 Dialog, 328 okno O programie, 333, 336 okno ProgressDialog, 330
run(), 330 z zakładkami, 331 niestandardowy pasek tytułu, 275, 277 obramowanie, 296 kolor, 297 kształt, 297 z zaokrąglonymi rogami, 298 obrotowy mechanizm wybierania, 325 działanie, 325 kontrolki Android-Wheel, 325 obsługa długiego kliknięcia, 253 setLongClickable(), 253 setOnLongClickListener(), 253 obsługa zmian konfiguracji, 238 refreshUI(), 240 zachowywanie stanu aplikacji, 240 otwieranie nowego ekranu, 283 kontrolka Button, 287 kontrolka TextView, 286 następne okno aplikacji, 290 pierwszy ekran, 290 warunki, 285 widżet aplikacji, 284 pole wyboru, 247 przechwytywanie klawiszy, 264 onKeyDown, 264 przekształcanie pól tekstowych, 260 przełączanie aktywności, 273 przyciski graficzne, 249 opcji, 247 tworzenie, 241 wysyłania, 262 stosowanie kontrolek, 246 RadioGroup, 247 Spinner, 246 ViewGroup, 247 TableLayout, 63 tworzenie przycisku, 241 onCreate(), 241 setOnClickListener(), 241 widżet, 236 widżet aplikacji, 236, 311 wykrywanie gestów, 299 GestureDetector, 299 onTouchEvent, 299 wyświetlanie pól tekstowych, 254 addTextChangedListener(), 256 afterTextChanged(), 256 beforeTextChanged(), 256 EditText, 254, 255 onCreate(), 256 onTextChanged(), 256 TextView, 254
zakrywanie innych komponentów, 292 DrawerButton, 294 SlidingDrawer, 293, 295 zaznaczanie grup, 265 kontrolka RatingBar, 265 onRatingChanged, 266 RatingBar, 265
I IDE Eclipse, 29, 235 SWT, 235 Inkscape, 209, 228 Document Properties, 212 Export Bitmap, 211, 213, 229 SVG, 210 instrukcje adb logcat, 125 create project, 21 generowane elementy, 22 lista argumentów, 22 Intent.createChooser, 151 Intent.putExtra(), 151 intencja intencja rozgłoszeniowa, 428 intencje, 146 e-mail z poziomu widoku, 147 z załącznikami, 150 Intent, 146 robienie zdjęć, 198 włączenie Bluetootha, 532 wybieranie numeru telefonu, 424 internacjonalizacja, 571 Locale, 571 łańcuchy znaków, 572 wyszukiwanie, 575 tekst aplikacji, 572 wersje regionalne, 574
J Java, 19 biblioteki RGraph, 224 definiowanie nowej klasy, 288 HTML5, 566 kod mostu, 567 interfejsy API, 20 kod natywny, 552 metody formatujące, 278 obsługa wyjątków, 76 Exception, 77 IOException, 77 Skorow dz
| 605
Java obsługa wyjątków kontrolowane, 76 niekontrolowane, 76 przekształcanie wyjątków, 77 RuntimeException, 77 Throwable, 77 VMError, 77 pakiet JDK, 29 parser ROME, 446 środowisko IDE Eclipse, 29 języki programowania, 549 C, 549 kod natywny, 551, 553 Clojure, 549 Erlang, 549 F#, 549 Groovy, 549 HTML5, 566 Scala, 549 Scheme, 549 skryptowe, 556 aplikacja SL4A, 556 BeanShell, 556 JavaScript, 557 JRuby, 557 Lua, 556 Perl, 556 Python, 556 Tcl, 557 JSON, 406
K keytool, 584 klasy AboutBox, 333, 334 ActivityInstrumentationTestCase2, 124 Adapter, 251, 350 AddressOverlay, 495 AlertDialog, 71, 321 android.Activity, 35 android.app.Activity, 64 android.app.Application, 79 android.media.Camera, 200 AndroidPlot, 207 ArrayAdapter, 350 AsyncTask, 145, 161, 164 BaseAdapter, 358 BookRank, 445 BugSenseHandler, 130 ChoiceFormat, 282 CountDownTimer, 55
606 |
Skorow dz
Cube, 190 CustomDialog, 331 DateFormat, 97 DateUtils, 99 DbAdapter, 396 DemoCharts, 363 Dialog, 328 EditText, 254 Exception, 77 FaceDetectionView, 374 File, 384 metody zwracające informacje, 384 FileSaver, 82 Formatter, 99 FragmentTestActivity, 306 Geocoder, 479 GestureDetector, 299 Handler, 167 IncomingCallInterceptor, 419 Intent, 146 IOException, 77 ItemizedOverlay, 490 java.util.Date, 98 KeyListener, 99 ListActivity, 349 LoadingScreen, 292 Locale, 571 MapTest, 481 MapView, 481 MD5, 450 MediaRecorder, 371 MetarItem, 498 MyContantProvider, 177 MyLocationListener, 475 MyLocationOverlay, 486 MyOverlay, 488 OutgoingCallInterceptor, 421 PackageInfo, 333 ProgressDialog, 164 RatingBar, 265 właściwości, 265 Runnable, 159 Runtime, 545 RuntimeException, 77 RuntimeLog, 131 kod, 132 ScaleBarOverlay, 513 SensorManager, 522 Service, 155 ServiceManager, 158 składowa, 243 SlidingDrawer, 292 SmsManager, 426
SQLiteOpenHelper, 401 StatFs, 390 TelephonyManager, 430 TestFragment, 307 TextView, 254 Thorwable, 77 hierarchia, 77 Thread, 159 Throwable, 77 TicTacToeActivity, 239 TicTacToeGame, 240 Time, 99 TouchEvent, 194 TrackService, 156 URL, 442 URLConnection, 442 View, 64, 194 WebSettings, 453 wewnętrzna, 242 anonimowa, 244 komponenty obsługi, 145 komunikacja, 145 dostawcy treści, 175, 178 pobieranie danych, 175 e-mail, 147 Intent.createChooser, 151 z poziomu widoku, 147 z załącznikami, 150 intencje, 146 e-mail, 147, 150 Intent, 146 IPC, 179 zdalne usługi, 179 komponenty obsługi, 145, 167 komunikaty rozgłoszeniowe, 157 kod klasy odbiornika, 158 publikowanie rozgłaszanych zdarzeń, 157 rejestrowanie odbiornika, 157 ServiceManager, 158 tworzenie odbiornika, 157 odbiorniki rozgłoszeniowe, 145 operacje w tle, 160 AsyncTask, 161, 164 doInBackground(), 162 execute(), 163 onClickListener(), 163 onCreate(), 162 onCreateDialog(), 164 onItemClick(), 162 pobieranie receptur, 166 ProgressDialog, 164 setCancelable(), 164 pobieranie danych, 152
finish(), 154 getIntent().getExtras().getString(), 151 setResult(), 154 z aktywności podrzędnej, 154 podtrzymywanie działania usługi, 155 onBind(), 156 onCreate, 155 onStartCommand(), 156 Service, 155 TrackService, 156 przesyłanie danych, 151 Intent.getExtras(), 153 Intent.putExtra(), 151 MySubActivity.finish(), 153 onActivityResult(), 153 z aktywności, 153 używanie wątków, 159 AsyncTask, 161 Handler, 167 kolejka wątków, 167 onCreate(), 159, 162 onItemClick(), 162 przesyłanie komunikatów, 166 run(), 159 Runnable, 159 start(), 159 Thread, 159 komunikat rozgłoszeniowy, 420 komunikaty toast, 315, 336 kontrolki, 59 Android-Wheel, 325 AutoCompleteTextView, 257 zapełnianie, 259 Button, 287 CheckBox, 246 EditText, 63, 64, 254, 255, 501 z hasłem, 261 GridView, 238 ImageSwitcher, 368 ListView, 293, 343, 388 brak danych, 347 DemoCharts, 363 dostosowanie zawartości, 357 getCount(), 347 getItem(), 347 getTag(), 345 getView(), 358 konfigurowanie, 345 ListActivity, 349 nagłówki sekcji, 352 notifyDataSetChanged(), 360 obsługa zmian orientacji, 360 onConfigure(), 363
Skorow dz
|
607
kontrolki ListView pusta lista, 348 setListAdapter(), 345 stronicowanie, 344 wyświetlanie listy wierszy, 344 z ikonami, 349 zachowywanie pozycji, 356 RadioButton, 246 RadioGroup, 247 RatingBar, 265 rozmieszczanie, 59 ScrollWheel, 326 Spinner, 246, 251 TableRaw, 60 TabView, 503 TabWidget, 503 TextView, 26, 64, 97, 238, 254, 286, 451 Timepicker, 322 ViewGroup, 247 WebView, 452 modyfikacja wyglądu, 453 WheelView, 325
L liczby, 277 formatowanie, 277 kody formatujące, 278 metody formatujące, 278 LinkedInem, 468 Linuks, 550 polecenia, 550 lokalizacja, 473, 571 geokodowanie, 479 Geocoder, 479 odwrotne, 480 określanie położenia użytkownika, 473 aktualizowanie lokalizacji, 474 fikcyjne współrzędne GPS, 477 informacje z GPS-a, 475 MyLocationListener, 475 onLocationChanged(), 475 pobieranie danych o lokalizacji, 474 podawanie fikcyjnej lokalizacji, 478 setMockLocation(), 477 tryb COARSE, 474 tryb FINE, 474
608 |
Skorow dz
M mapy, 473 Google’a, 480 AddressOverlay, 495 aktualna lokalizacja, 486 createItem(), 491 disableMyLocation(), 486 draw(), 490 drawCircle(), 500 getCenter(), 493 invalidate(), 489 isRouteDisplayed(), 481 ItemizedOverlay, 490 ItemizedOverlayXXXdraw(), 497 kilka znaczników, 493 kliknięcie znacznika, 494 konfigurowanie urządzenia AVD, 480 kontrolka TabView, 503 lista kontrolna, 484 MapTest, 481 MapView, 481 MapView.onTouchEvent(), 506 MetarItem, 498 MetarItem::draw(), 498 MyLocationOverlay, 486 MyOverlay, 488 obsługa długich kliknięć, 505 onCreate(), 492 populate(), 492 rejestrowanie klucza, 483 size(), 491 TabSpec.setContent(), 503 tworzenie nowego projektu, 481 tworzenie warstwy, 495 wyszukiwanie lokalizacji, 501, 502 wyświetlanie ikony, 499 wyświetlanie warstwy, 495 zmiany trybu mapy, 496 znacznik lokalizacji, 487 znacznik zastępczy, 493 OpenStreetMap, 509 aktualizowanie lokalizacji, 516 obsługa dotknięć warstwy, 514 przesuwanie mapy, 518 ScaleBarOverlay, 513 skala, 513 tworzenie warstw, 511 zmiana lokalizacji, 519 Monkey, 140 MOTODEV Studio, 576
multimedia, 367 konwersja mowy na tekst, 380 konwersja tekstu na mowę, 381 mechanizm wykrywania twarzy, 373 detectFaces(), 374 FaceDetectionView, 374 init(), 374 odtwarzanie filmów, 367 YouTube, 368 odtwarzanie plików muzycznych, 377 bez interakcji z użytkownikiem, 379 MediaControler, 377 MediaPlayer, 377 onCreate(), 377 onTouchEvent(), 378 porządkowanie zasobów odtwarzacza, 378 rejestrowanie filmów, 371 initRecorder(), 372 MediaRecorder, 371 onCreate(), 371 surfaceCreated(), 373
N nagłówki sekcji, 352 narzędzia ADB, 37 AndEngine, 458 android, 21 Android Localizer, 575 Google Analytics, 90 keytool, 584 Monkey, 140 MOTODEV Studio, 576 ProGuard, 592 StrictMode, 139 NinePatch, 221
O obsługa połączeń telefonicznych, 417 pobieranie danych, 430 określanie stanu telefonu, 431 TelephonyManager, 430 przechwytywanie połączeń przychodzących, 418, 420 IncomingCallInterceptor, 419 kod manifestu aplikacji, 419 kod przechwytujący, 418 odbiornik rozgłoszeniowy, 418 przechwytywanie połączeń wychodzących, 421 abortBroadcast(), 423 getResultData(), 424
klasa przechwytująca, 421 OutgoingCallInterceptor, 421 przechwycone połączenie, 423 setResultData(),421, 423 wiadomości SMS, 425 kod do wysyłania, 426 odbieranie, 428 odebrana wiadomość, 426 onReceive(), 428 sendMultipartTextMessage(), 426 sendTextMessage(), 426 SmsManager, 426 wysyłanie, 425, 429 wybieranie numeru, 424 odbiorniki rozgłoszeniowe, 83, 145, 418 odzyskiwanie, 345 OpenGL ES, 185, 187, 455 grafika trójwymiarowa, 188 buffer.position(0), 191 D-pad, 192 obracanie sześcianu, 192 onDrawFrame, 190 onSurfaceChanged, 190 requestFocus(), 194 setFocusableInTouchMode(true), 194 wyświetlanie sześcianu, 190 OpenMoko, 521 OpenStreetMap, 473, 509 optymalizowanie kodu, 593 OSM, Patrz OpenStreetMap OTF, 186
P Paint.NET, 218 Canvas Dialog, 218 Colors, 218 pakiet ADK, 21 android, 21 create project, 21 pakiet JDK, 29 pakiet SDK, 29 aktualizacja, 46 możliwe błędy, 49 instalacja, 31 przykładowe programy, 44 SDK Manager, 32, 47 okno komunikatów, 48 wtyczka ADT, 29 parser danych, 446 parser ROME, 446 PNG, 209 pojemność karty SD, 390
Skorow dz
| 609
polecenia powłoki, 545 powiadomienia, 338 ProGuard, 592 protokół HTTP, 441 protokół SPP, 531 przycisk graficzny, 249
R RESTful, 441 rozgłaszanie uporządkowane, 420
S sieci społecznościowe, 467 Facebook, 467 LinkedInem, 468 protokół HTTP, 467 obsługa kliknięcia, 468 pobieranie logo, 468 tworzenie przycisków graficznych, 468 Twitter, 467, 470 getTwitterTimeline(), 470 wczytywanie chronologicznej listy tweetów, 470 sieci, 441 3G, 441 EDGE, 441 GPRS, 441 protokół HTTP, 441 protokół HTTPS, 441 protokół SPP, 531 RESTful, 441, 442 społecznościowe, 467 Facebook, 467 LinkedInem, 468 protokół HTTP, 467 Twitter, 467, 470 standard HDP, 531 XML/SOAP, 441 singleton, 79, 240 standard HDP, 531 standardowe rozgłaszanie, 420 sterowanie, 539 kopiowanie tekstu, 543 getText(), 543 setText(), 543 menedżer aktywności, 547 polecenia powłoki, 545 exec(), 545 process.waitFor(), 545 Runtime, 545 uruchamianie, 546
610
|
Skorow dz
połączenie z siecią, 539 informacje o sieci, 540 tryb dzwonka, 541 ustawianie, 541, 542 ustawienia projektu, 540 włączanie wibracji, 544 wyświetlanie powiadomień, 544 ledARGB(), 544 notify(), 544 stoper z odliczaniem wstecznym, 55 CountDownTimer, 55 onFinish(), 55 onTick(), 55 StrictMode, 139 stronicowanie, 344 SVG, 210
Ś środowisko Eclipse, 24 parametry nowego projektu, 25 projekt testowy, 124 TextView, 26 tworzenie nowego projektu, 45 tworzenie projektu powiązanego, 39 środowisko IDE Eclipse, 29
T testy, 111 awarie, 125 adb logcat, 125 findViewById(), 127 NPE, 126 BugSense, 129 BugSenseHandler, 130 setContentView(), 130 chmura, 121 cykl życia, 134 onDestroy(), 138 onPause(), 138 onRestart(), 138 onStop(), 138 rejestrowanie zdarzeń, 135 scenariusze, 138 uruchamianie aktywności, 135, 136 wstrzymywanie aktywności, 135, 137 debugowanie kodu, 128 dane wyjściowe, 128 Log.d(), 128 lokalny dziennik czasu wykonania RuntimeLog, 131 lokalny dziennik czasu wykonania, 131
Monkey, 140 stosowanie, 141 projekt testowy, 122 ActivityInstrumentationTestCase2, 124 konfiguracja, 122 tworzenie, 122 w środowisku Eclipse, 124 w środowisku IntelliJ IDEA, 123 sterowanie programowaniem, 111 StrictMode, 139 urządzenia AVD, 113 definiowanie, 115 konfigurowanie, 114 tworzenie nowego urządzenia, 115 uruchamianie nowego urządzenia, 117 właściwości urządzeń, 118 Tipster, 57 AlertDialog, 71 android.app.Activity, 64 calculate(), 68 EditText, 63 findViewById(), 64 getCheckedRadioButtonId(), 71 OnCheckedChangeListener(), 66 OnKeyListener(), 67 requestFocus(), 65 reset(), 69 setEnabled(), 65 showErrorAlert(), 69, 71 TableLayout, 60, TableRaw, 60 View, 64 TTF, 186 Twittera, 467 tworzenie bazy SQLite, 401 ekranów powitalnych, 84 ikony, 208, 215 kopii zapasowej, 102 menu, 316 nowego urządzenia AVD, 115 odbiornika rozgłoszeniowego, 157 projektu testowego, 122 przycisku, 241, 468 serwera, 536 warstw mapy, 495, 511 wykresów, 224
U
opcje uruchomieniowe, 116, 119 testy, 113 tworzenie nowego urządzenia, 115 uruchamianie nowego urządzenia, 117 właściwości urządzeń, 118 urządzenia GPS, 473
W wiadomości SMS, 426 odbieranie, 428 get(), 428 intencja rozgłoszeniowa, 428 onReceive(), 428 SmsManager, 426 wysyłanie, 425, 429 kod, 426 okno Emulator Control, 430 sendMultipartTextMessage(), 426 sendTextMessage(), 426 widżet, 236 widżet aplikacji, 236, 284, 311 wtyczka ADT, 24, 29 instalacja, 33 wyrażenia regularne, 444 wyświetlanie czasu i daty, 97 danych, 207 ikony, 499 listy wierszy, 344 menu, 316 powiadomień, 338, 544 pól tekstowych, 254 reklam, 588 stron internetowych, 452 warstwy, 495 zawartości katalogu, 387
X XML/SOAP, 441
Y YouTube, 367
Z zaciemnianie kodu, 593
urządzenia AVD, 23, 113 definiowanie, 115 konfigurowanie, 112, 114, 480
Skorow dz
|
611
O autorze Ian F. Darwin pracuje w branży informatycznej od 30 lat. Napisał bezpłatne polecenie file(1) używane w systemach Linux i BSD, a także jest autorem książek Checking C Programs with Lint, Java Cookbook1 oraz ponad 100 artykułów i kursów dotyczących C, Uniksa, Javy i Androida. Ian jest nie tylko programistą i konsultantem, ale też szkoleniowcem z zakresu Uniksa, Javy i Androida w jednej z największych na świecie firm szkoleniowych w branży technicznej — Learning Tree International.
Kolofon Zwierzę na okładce książki Android. Receptury to legwan morski (Amblyrhynchus cristatus). Te jaszczurki żyją wyłącznie na wyspach Galapagos (każdą wyspę zamieszkuje inny podgatunek). Zdaniem badaczy legwany morskie są potomkami legwanów lądowych, które dostały się na wyspy z Ameryki Południowej na drewnianych kłodach. Legwan morski to jedyny gatunek legwanów, który żeruje w wodzie. Darwin uważał te gady za brzydkie i dziwaczne. Opisywał je jako „obrzydliwe, niezdarne jaszczurki” i „dzieci ciemności”. Jednak te duże, opływowe zwięrzęta (mierzą do 130 centymetrów długości) doskonale poruszają się w wodzie, a ich płaskie ogony są przystosowane do pływania. Opisywane jaszczurki żywią się wodorostami i algami. Potrafią głęboko nurkować (nawet do 15 metrów), choć zwykle pływają na dużo mniejszej głębokości. Potrafią pozostawać pod wodą do godziny, ale zazwyczaj jest to od 5 do 10 minut. Podobnie jak wszystkie gady, są one zmiennocieplne i muszą regulować temperaturę, wygrzewając się w słońcu. Ich czarna lub szara skóra maksymalizuje absorpcję ciepła po wyjściu z zimnego oceanu. Choć legwany morskie są niegroźnymi roślinożercami i często pozwalają ludziom zbliżać się do siebie, mogą być agresywne, gdy jest im zimno. Legwany morskie mają wyspecjalizowane gruczoły nosowe, które odfiltrowują sól morską z krwi. Zwierzęta te „wykichują” nadmiar soli, która w efekcie często zbiera się na ich łbach, tworząc charakterystyczną białą czapę lub perukę. Legwany morskie są narażone na ataki ze strony gatunków sprowadzonych na wyspy (w tym psów i kotów), a także na zanieczyszczenie oceanu i przerwy w dostępie do pożywienia, powodowane przez różne zjawiska pogodowe (między innymi El Niño). Rysunek na okładce pochodzi z książki Wood’s Animate Creation.
1
Wydanie polskie: Ian Darwin, Java. Receptury, Helion, Gliwice 2003. — przyp. tłum.