środa, 23 marca 2016

[DSP2016] Android prosto z poligonu odc.3 (search, odtwarzanie stanu)

Pozdrawiam wszystkich serdecznie, Daj się Poznać 2016 challange. Dziś złożę raport z kolejnego developerskiego rejonu, a konkretnie z search i odtwarzania stanu aktywności w Android.

Na dzień dobry może to, co aktualnie w tym temacie na githubie mamy.

device-2016-03-23-205436  device-2016-03-23-205536 

device-2016-03-23-205652

Wizualnie może nie jest to dużo, ale kryje się pod tym wiele spraw, o których zaraz tutaj opowiem. Domyślnie nie działa też domyślnie tak wszystko, jak typowy użytkownik mógłby się spodziewać.

Po pierwsze jak siadamy do pisania searcha, to dokumentacja Android wita nas opisem złożonych funkcjonalności realizowanych typowo przed dodatkową aktywność otwieraną przez system w wyniku akcji użytkownika na jednym z dwóch komponentów UI, z których możemy skorzystać. Trzeba jeszcze konfigurować ileś rzeczy po manifeście, eh. Jak chcemy być bardziej zaawansowani to możemy obsłużyć sobie podpowiedzi (albo na podstawie ostatnio wpisywanych fraz albo w oparciu o własny provider), głos czy mocniej ingerować w konfigurację. Powinniśmy też orientować się w wytycznych Material Design. Kilka oficjalnych lików, z którymi możemy się zapoznać, by nabrać jakiejś orientacji:

Powstaje jednak pytanie czy chcemy sobie sprawić całkiem sporo zachodu do całkiem prostej funkcjonalności, aczkolwiek rozumiem filozofię pełnofunkcjonalnego search w Android, jest całkiem podobna do filozofii search w Windows 8 (może to było wtedy jakieś wzorowanie się na wyszukiwaniu Android). Od Windows 8.1 zaczęto odchodzić od cięższego myślenia systemowego z wykorzystaniem kontraktów na rzecz kontrolki, którą może sobie programista wykorzystać. Teraz właśnie poszukiwałem w Androidzie podobnej lżejszej nieco alternatywy do tej promowanej na “pierwszych stronach”. Otóż SearchView nie musi działać z tą całą systemową maszynerią, dev może przejąć nad nim w całości kontrolę i wykorzystać do typowych scenariuszy bez szeregu definicji w manifeście aplikacji. Dodatkowo chciałem to pożenić z appbarem oraz RecyclerView, których używam w aktywności prezentującej pliki muzyczne. Znalazłem kilka mniej oficjalnych całkiem niezłych artykułów:

Prościej? Prościej, dodatkowo RecyclerView pozwala na animacje różnego rodzaju. Na razie się nimi nie bawiłem, ale może za jakiś czas do nich wrócę.

Wracając już do kodu generalnie w adapterze do RecyclerView dodałem metodę:

public void setFilter(List<MediaFileItem> items){
mValues = items;
notifyDataSetChanged();
}

Ta metoda jest wywoływana wtedy, ktoś otworzy search i coś do niego zastanie wpisane. Zanim jednak wskaże odpowiednie miejsce w kodzie, może kilka słów jak udało mi się osadzić SearchView w appbarze. Dla całej aktywności z listą (activity_file_list.xml) zdefiniowałem appbar i layout podobnie jak na głównej aktywności (activity_main.xml). Wiązało się to też pewnymi zmianami w manifeście aplikacji. Jak to jest z tym appbarem można poczytać sobie w dokumentacji na stronie http://developer.android.com/training/appbar/index.html. Plik menu na aktywności z plikami wygląda u mnie następująco:

<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="com.apps.kruszyn.lightorganapp.FileListActivity">

<item android:id="@+id/action_search"
android:title="Search"
android:icon="@drawable/ic_search_white_48dp"
app:actionViewClass="android.support.v7.widget.SearchView"
app:showAsAction="always|collapseActionView" />

</menu>

Aby przechwycić wpisywanie liter w SearchView wystarczy zaimplementować odpowiedni listener (tak, wiem, w C# w Xamarinie miałbym do tego event, ale idę tutaj na całość w Javie). Listenerem tym u mnie jest cała aktywność po zaimplementowaniu interfejsu SearchView.OnQueryTextListener.

@Override
public boolean onQueryTextSubmit(String query) {
return false;
}

@Override
public boolean onQueryTextChange(String newText) {

searchText = newText;

final List<MediaFileItem> filteredList = filter(mModel, newText);
mAdapter.setFilter(filteredList);

return true;
}

Listenery przypinamy do SearchView w metodzie onCreateOptionsMenu:

final MenuItem item = menu.findItem(R.id.action_search);
mSearchView = (SearchView) MenuItemCompat.getActionView(item);
mSearchView.setOnQueryTextListener(this);

MenuItemCompat.setOnActionExpandListener(item,
new MenuItemCompat.OnActionExpandListener() {
@Override
public boolean onMenuItemActionCollapse(MenuItem item) {
mAdapter.setFilter(mModel);
return true;
}

@Override
public boolean onMenuItemActionExpand(MenuItem item) {
return true;
}
});

Jednak nie wszystko tak gładko idzie jak by się mogło wydawać. SearchView po otworzeniu nie rozciąga się nam pełną szerokość appbara, wygląda to trochę kiepsko porównując chociażby do app-ek Muzyka Play czy Prezentacje.  Skorzystałem z końcowej rady na http://stackoverflow.com/questions/18063103/searchview-in-optionsmenu-not-full-width. Jedna linijka:

mSearchView.setMaxWidth(Integer.MAX_VALUE);

poprawia efekt. Od siebie dodałem pod nią linjkę, która ustawia mój tekst w placeholderze SearchView:

mSearchView.setQueryHint("Search songs");

Wszystko już super działa? O nie, co się dzieje jak obrócę ekran telefonu (np. z orientacji portret na landscape), to search mi się zamyka i czyści się fraza. Nie będę udawał, że tego się nie spodziewałem, ale chciałem nadać trochę ekspresji przy wprowadzeniu do tematu stanu aktywności. Na oficjalnej stronie https://developer.android.com/training/basics/activity-lifecycle/recreating.html znajdziemy całkiem zgrabne zebranie tego tematu. Zapamiętuje u siebie czy SearchView był otwarty oraz czy wpisano do niego jakąś frazę:

static final String QUERY_STRING = "queryString";
static final String SEARCH_OPEN = "searchOpen";
private String searchText = null;
private boolean searchOpen = false;
@Override
public void onSaveInstanceState(Bundle savedInstanceState) {

savedInstanceState.putBoolean(SEARCH_OPEN, searchOpen);
savedInstanceState.putString(QUERY_STRING, searchText);

super.onSaveInstanceState(savedInstanceState);
}

public void onRestoreInstanceState(Bundle savedInstanceState) {

super.onRestoreInstanceState(savedInstanceState);

searchOpen = savedInstanceState.getBoolean(SEARCH_OPEN);
searchText = savedInstanceState.getString(QUERY_STRING);
}

Oczywiście gdzieś w aktywności ustawiam zmienne searchText i searchOpen. Generalnie robię to w listenerach dla SearchView (szczegóły w FileListActivity.java). W onCreateOptionsMenu w przypadku gdy SearchView było otwarte i wpisane, otwieram i ustawiam query:

if (searchText) {
MenuItemCompat.expandActionView(item);

if (!TextUtils.isEmpty(searchText))
mSearchView.setQuery(searchText, false);

mSearchView.clearFocus();
}

Wszystko jest super, czy o to mi chodzi? Odpalam app-kę i …  fraza pozostaje skasowana po obróceniu ekranu. WTF??? Okazuje się, że na developera Android zastawił kolejną pułapkę. Dobre wyjaśnienie sprawy znajduje się na końcu wątku http://stackoverflow.com/questions/22582201/restore-state-of-androids-search-view-widget. Otóż zdarzenie zmiany frazy jest wywoływane przy otwieraniu SearchView, a tym samym nasz searchText z “restore” był zastępowany pustym stringiem. Trzeba zapamiętać searchText w innej dodatkowej zmiennej przed otwarciem SearchView. Po zrobieniu tego w taki sposób (FileListActivity.java) mogę ogłosić – działa odzyskiwanie stanu wyszukiwarki przy obracaniu ekranem i nie tylko!  Jako user nie jestem wkurzony, że muszę wpisywać frazę do wyszukiwarki od nowa.

To tyle na dziś. Trzeba dalej iść –Winking smile

Brak komentarzy: