Przyszła pora na kolejny odcinek w stylu chłodnym okiem. Dziś na warsztat wziąłem część funkcjononalności wprowadzonych w Android 4. Co prawda u bram już puka wersja piąta, ale od czegoś trzeba zacząć. Co dziś mam do skomentowania?
Fragmenty - moim zdaniem przerost infrastrukturalny nad treścią w stosunku do kontrolek XAML czy JavaScript. Nie będę już wspominać o idei komunikowania się z aktywnością za pomocą interfejsu, aby się nie powtarzać. Twórcy przewidzieli, że fragmenty mogą się odtwarzać i że powinniśmy zapewnić odtwarzanie ich stanu. Trudno zrozumieć czemu obrócenie ekranu wszystko zabija i martw tu się developerze, by odtworzyć stan wszystkiego. Każdy ekran to musi być osobna aktywność, do tego stopnia że na wąskich ekranach otwiera się dodatkową aktywność, a na szerokich kontrolki z dwóch aktywności umieszcza się w jednej. W XAML/HTML obrócenie ekranu nie wpływa na żywotność strony, ot wszystko się dynamicznie przemieszcza i tyle, pomysł z otwieraniem nowej strony na wąskich ekranach smartfonów też czasami się wykorzystuje, ale można też zrobić jedną kontrolkę przełączającą dwa widoki.
ActionBar - dobrze, że powstało coś bardziej funkcjonalnego niż podstawowe menu z dawniejszych czasów. Tak porównując to Windows Phone jako powstały znacznie później od razu wyszedł od idei paska aplikacyjnego i minimum przycisków sprzętowych. Rodzina Windows/Windows Phone oferuje różne paski w zależności od typu urządzenia. Pomiędzy Windows 8.1 a WP 8.1 kontrolki paska XAML/WinJS z tym samym kodem mogą być już używane. W Androidzie po prostu jest jeden system na smartfonach i tabletach. Zakładki możemy najbliżej porównać do kontrolek Hub i Pivot (tylko na WP), które wydają mi się bardziej nowoczesne, aczkolwiek nie są częścią paska aplikacyjnego czy nawigacyjnego. Sama obsługa zakładek w Android wydaje mi się niepotrzebnie skomplikowana, od razu listenery, jakieś ręczne sterowanie dodawaniem itd.
Komunikacja sieciowa - obsługa statusu wydaje się dość typowa, obsługa Wi-Fi Direct może nie jest najgorsza, ale przez brak zwykłych eventów nieco skomplikowana choćby przez broadcast receiver i implementacje listenerów. Z kolei obsługa czytania tagów NFC wydaje bardzo prosta, prostsza niż w WP, przy czym powstaje pytanie, jakie jeszcze inne scenariusze z NFC są realizowane przez Androida (w WP mamy także komunikację z innymi urządzeniami oraz płatności). Jak natknę się jeszcze na NFC, to być może zyskam szersze spojrzenie.
Android 4.0
- więcej niż prosta ewolucja
- platforma obsługująca różne czynniki
- fragmenty
- ActionBar
- właściwości sieciowe zgodne z nowoczesnymi sieciami mobilnymi
- limity na ilość danych, kontrola użytkownika
- komunikacja peer-to-peer pomiędzy urządzeniami
- Wi-Fi Direct
- NFC (odczyt tagów)
- lepszy dostęp do funkcjonalności platformy
- integracja interfejsu kalendarza z aplikacją
- uproszczony dostęp do danych kalendarza
- kalendarze –> zdarzenia –> instancje, uczestnicy, przypomnienia
Jeszcze o fragmentach
Fragmenty mogą być używane do
- tworzenia adaptywnego UI
- zakładek
- menu kontekstowego
- modyfikacji action bar
Klasy bazujące na fragmentach
- Fragment
- ListFragment
- DialogFragment
- PreferenceFragment
Dostawanie aktywności z poziomu fragmentu
- metoda onAttach wywoływana w momencie podpinania fragmentu do aktywności
- metoda getActivity
FragmentManager
- każda aktywność ma własny, getFragmentManager
- przechwytuje szczegóły tworzenia, pokazywania, ukrywania fragmentów
- przechowuje referencje do wszystkich fragmentów w aktywności, findFragmentById
Utrzymywanie stanu fragmentu
- zmiany w layout często powodują tworzenie nowych instancji fragmentów
- bieżący stan fragmentu zapisujemy za pomocą onSaveInstanceState
- zapisany stan jest przekazywany do nowej instancji w metodach onCreate i onCreateView
public class CourseDescriptionFragment extends Fragment {
…
int _courseIndex;
…
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
…
int initialIndex = savedInstanceState == null ? 0 : savedInstanceState.getInt(“courseIndex”, 0);
setDisplayedDescription(initialIndex);
…
}
@Override
public void onSaveInstanceState(Bundle outState) {
outState.putInt(“courseIndex”, _courseIndex);
}
…
}
Dynamiczne zarządzanie wyświetlaniem fragmentu
- często zachodzi potrzeba do przełączania między widokami jednej i wielu aktywności
- FragmentManager pozwala ustalić, które fragmenty są aktualnie wyświetlane, metoda fragmentu isVisible
W zależności od orientacji ekranu możemy wyświetlać jedną aktywność z dwoma fragmentami lub dwie aktywności z jednym fragmentem
public class DynamicActivity extends Activity {
…
public void onSelectedCourseChanged (int courseIndex) {
if (descriptionFragment != null && descriptionFragment.isVisible())
//orientacja pozioma - master-details
descriptionFragment.setDisplayedDescription(courseIndex);
else {
//orientacja pionowa - nowy ekran
Intent intent = new Intent(this, DynamicDetailsActivity.class);
intent.putExtra(“courseIndex”, courseIndex);
startActivity(intent);
}
}
}
ActionBar
- lepiej dostosowany do różnych urządzeń i ekranów
- także tradycyjne zachowanie menu i obsługa wychodzącego z użycia przycisku sprzętowego ‘menu’
- różne warianty nawigacji
- domyślnie aplikacje używają starego zachowania zgodnego z menu
- dostępny tylko na aktywnościach używających skórek wywodzących się z Theme.Halo
- automatycznie przy min sdk / target sdk 11 lub nowszym w manifeście
- ikona na pasku może być klikalna
- powrót do ekranu home aplikacji
- przeniesienie użytkownika o jeden poziom wyżej w hierarchii aplikacji
- opcja podziału na dwa paski (górny i dolny) na wąskim ekranie - w manifeście (w aktywności lub całej aplikacji)
- zastępuje TabActivity w nawigacji zakładkowej
Potrzeba zarządzania aktywnościami taska przy implementacji home
- użytkownik powinien mieć wrażenie powrotu
- nowa aktywność jest domyślnie dodawana na końcu bieżącego stosu
- przy powrocie “home” wszystkie inne aktywności w stosie powinny zostać wyczyszczone
- osiągamy za pomocą FLAG_ACTIVITY_CLEAR_TOP
private void onHome() {
Intent intent = new Intent(this, MyActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
}
Nawigacja zakładkowa
- zakładki są umieszczane bezpośrednio na ActionBar
- adaptacja do dostępnej przestrzeni na ekranie
- na wąskich ekranach - pasek z zakładkami pod ikoną aplikacji
- na szerokich ekranach - zakładki są umieszczone między ikoną aplikacji a opcjami
- tworzenie
- wywołujemy setNavigationMode na ActionBar z wartością NAVIGATION_MODE_TABS
- dla każdej zakładki tworzymy instancję ActionBar.Tab
- tworzymy Fragment dla zawartości
- dostarczamy implementację interfejsu ActionBar.TabListener do zarządzania zakładką
- onTabSelected - przy pierwszym wywołaniu musi dodać fragment do aktywności, przy sukcesie musi uczynić fragment widocznym
- onTabUnselected - musi ukryć fragment
- onTabReselected - naciśnięcie wybranej już zakładki, zwykle nic nie robimy
- każdą ActionBar.Tab dodajemy do ActionBar
- poprzez metodę ActionBar.addTab
public class MyTabListener implements ActionBar.TabListener {
Fragment _tabFragment;
boolean _isFirstSelect = true;
public MyTabListener(Fragment fragment) {
_tabFragment = fragment;
}
public void onTabSelected(ActionBar.Tab tab, FragmentTransaction fragmentTransaction) {
if (_isFirstSelect) {
fragmentTransaction.add(R.id.content, _tabFragment, null);
_isFirstSelect = false;
}
else {
fragmentTransaction.attach(_tabFragment);
}
}
public void onTabUnselected(ActionBar.Tab tab, FragmentTransaction fragmentTransaction) {
fragmentTransaction.detach(_tabFragment);
}
public void onTabReselected(ActionBar.Tab tab, FragmentTransaction fragmentTransaction) {
}
}
public class TabNavigatedActivity extends Activity {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActionBar actionBar = getActionBar();
actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
Fragment tab1Fragment = Fragment.instantiate(this, Tab1Fragment.class.getName());
MyTabListener tab1Listener = new MyTabListener(tab1Fragment);
ActionBar.Tab tab1 = actionBar.newTab().setText(“Tab 1”).setTabListener(tab1Listener);
…
actionBar.addTab(tab1);
…
}
}
Sieć
Konfigurowalność sieci mobilnej
- całkowity limit
- limit dla konkretnej aplikacji
- blokowanie korzystania z sieci w tle dla danej aplikacji
Ustalanie stanu sieci za pomocą ConnectivityManager
- Context.getSystemService(CONNECTIVITY_SERVICE);
- ConnectivityManager.getActiveNetworkInfo
- zwraca instancję NetworkInfo dla bieżącego połączenia lub null jeśli nie ma w ogóle łączności
- NetworkInfo.isConnected - czy można skorzystać z połączenia
- Nie używać ConnectivityManager.getBackgroundDataSetting, deprecated, zwraca zawsze true
Udostępnienie użytkownikom kontroli nad korzystaniem z sieci przez aplikację
- udostępnienie preferencji do ograniczeń z korzystania z sieci
- udostępnienie PreferenceActivity dla strony Android “Data Usage”
- zdefiniowanie Intent Filter dla akcji MANAGE_NETWORK_USAGE
- potrzebna kategoria DEFAULT
<activity …>
<intent-filter>
<action android:name=”android.intent.action.MANAGE_NETWORK_USAGE” />
<category android:name=”android.intent.category.DEFAULT” />
</intent-filter>
</activity>
w aktywności lub serwisie
ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
…
HttpGet httpGet = new HttpGet(_url);
HttpResponse httpResponse;
HttpClient httpClient = newHttpClientWithThreadSafeClientConnManager();
InputStreamReader streamReader;
….
httpResponse = httpClient.execute(httpGet);
HttpEntity httpEntity = httpResponse.getEntity();
if (httpEntity != null && httpEntity.isStreaming()) {
InputStream inStream = httpEntity.getContent();
if (inStream != null) {
streamReader = new InputStreamReader(inStream);
while ((bytesRead = streamReader.read(buffer, 0, size)) > 0) { … }
inStream.close();
}
}
Blokada na łączność w tle nie musi zadziałać na serwis, wykonywanie w tle zależy od tego, czy aktywność jest w tle
Wi-Fi Direct
Wymagane deklaracje w manifeście
<uses-sdk android:minSdkVersion=”14” /> (Android 4.0 lub nowszy)
<uses-feature android:name=”android.hardware.wifi.direct” android:required=”true” />
<uses-permission android:name=”android.permission.ACCESS_WIFI_STATE” />
<uses-permission android:name=”android.permission.CHANGE_WIFI_STATE”/>
<uses-permission android:name=”android.permission.INTERNET” />
Implementacja
Główny przebieg
- inicjalizacja systemu
- inicjalizacja żądań by wykonać specyficzne akcje
Utworzony w runtime broadcast receiver
- nasłuchiwanie notyfikacji o zakończeniu wykonywanych akcji
- w niektórych przypadkach wykrywanie sukcesu lub błędu
- w niektórych przypadkach inicjalizacja następnego kroku wieloetapowej akcji
WiFiP2pManager
- initialize - przygotowanie API, zwraca WiFiP2pManager.Channel
- discoverPeers - wyszukanie dostępnych peer-ów
- requestPeers - pobranie bieżącej listy dostępnych peer-ów
- connect - ustanowienie połączenia z peer-ami
Wi-Fi Direct informuje aplikacje o istotnych zdarzeniach za pomocą broadcast intents
- WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION (przy włączaniu/wyłączaniu Wi-Fi Direct, WiFiP2pManager.EXTRA_WIFI_STATE - bieżący stan)
- WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION (zmiana informacji o Wi-Fi Direct tego urządzenia)
- WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION (informuje, że wyniki zapytania o listę dostępnych peer-ów są już dostępne)
- WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION (informuje, że połączenie zostało ustanowione lub zerwane)
public class MainActivity extends Activity implements WifiP2pManager.ChannelListener {
private WifiP2pManager _wfdManager;
private WifiP2pManager.Channel _wfdChannel;
private WifiDirectReceiver _wfdReceiver;
@Override
public void onCreate(Bundle savedInstanceState) {
…
_wfdManager = (WifiP2pManager) getSystemService(WIFI_P2P_SERVICE);
_wfdChannel = _wfdManager.initialize(this, getMainLooper(), this);
}
private void registerWfdReceiver() {
_wfdReceiver = new WiFiDirectReceiver(_wfdManager, _wfdChannel, this);
_wfdReceiver.registerReceiver();
}
private void unregisterWfdReceiver() {
if (_wfdReceiver != null)
_wfdReceiver.unregisterReceiver();
_wfdReceiver = null;
}
public void onChannelDisconnected() {
_wfdChannel = _wfdManager.initialize(this, getMainLooper(), this);
…
}
public void onClickMenuDiscover(MenuItem item) {
if (isWfdReceiverRegisteredAndFeatureEnabled()) {
_wfdManager.discoverPeers(_wfdChannel, new ActionListenerHandler(this));
}
}
public void onClickMenuConnect(MenuItem item) {
if (isWfdReceiverRegisteredAndFeatureEnabled()) {
WifiP2pDevice theDevice = _wfdReceiver.getFirstAvailableDevice();
if (theDevice != null) {
WifiP2pConfig config = new WifiP2pConfig();
config.deviceAddress = theDevice.deviceAddress;
config.wps.setup = WpsInfo.PBC;
_wfdManager.connect(_wfdChannel, config, new ActionListenerHandler(this));
}
}
}
}
public class ActionListenerHandler implements WifiP2pManager.ActionListener {
MainActivity _activity;
public ActionListenerHandler(MainActivity activity) {
_activity = activity;
}
public void onSuccess() {
}
public void onFailure(int i) {
}
}
public class WiFiDirectReceiver extends BroadcastReceiver implements
WifiP2pManager.PeerListListener, WifiP2pManager.ConnectionInfoListener
{
…
public void registerReceiver() {
_appMainActivity.registerReceiver(this, getIntentFilter());
}
public void unregisterReceiver() {
_appMainActivity.unregisterReceiver(this);
}
private IntentFilter getIntentFilter() {
if (_intentFilter == null) {
_intentFilter = new IntentFilter();
_intentFilter.addAction(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION);
_intentFilter.addAction(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION);
_intentFilter.addAction(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION);
_intentFilter.addAction(WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION);
}
return _intentFilter;
}
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
…
if (WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION.equals(action)) {
int wfdState = intent.getIntExtra(WifiP2pManager.EXTRA_WIFI_STATE, -1);
/* WifiP2pManager.WIFI_P2P_STATE_ENABLED ? */
}
else if (WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION.equals(action)) {
WifiP2pDevice thisDevice = intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_DEVICE);
…
}
else if (WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION.equals(action)) {
_wfdManager.requestPeers(_wfdChannel, this);
}
else if (WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION.equals(action)) {
NetworkInfo info = intent.getParcelableExtra(WifiP2pManager.EXTRA_NETWORK_INFO);
if (info != null && info.isConnected()) {
_wfdManager.requestConnectionInfo(_wfdChannel);
}
}
}
public void onPeersAvailable(WifiP2pDeviceList wifiP2pDeviceList) {
//warunki
_wfdDevices = wifiP2pDeviceList.getDeviceList().toArray(new WifiP2pDevice[0]);
}
public void onConnectionInfoAvailable(WifiP2pInfo wifiP2pInfo) {
if (wifiP2pInfo.groupFormed) {
if (wifiP2pInfo.isGroupOwner) {
//otwieramy ServerSocket
}
else {
//otwieramy socket do wifiP2pInfo.groupOwnerAddress
}
}
}
}
NFC
Android mapuje czytanie tagów NFC na standardowe intencje
- Kiedy tag zostanie odczytany, system wysyła intent
- akcja “android.nfc.action.NDEF_DISCOVERED”
- URI / MIME z taga staje się danymi w intencji
- Aktywności przechwytują tagi NFC przez utworzenie odpowiedniego filtra intencji
- powinien zawierać kategorię “DEFAULT”
<activity …>
<intent-filter>
<action android:name=”android.nfc.action.NDEF_DISCOVERED” />
<category android:name=”android.intent.category.DEFAULT” />
<data android:mimeType=”text/plain” />
</intent-filter>
</activity>
Brak komentarzy:
Prześlij komentarz