User Experience list iPhona w Androidzie

Produkty Apple mają najlepszy user experience (UX) – wiadomo. Do korzystania z nich nie są potrzebne żadne instrukcje, produkty są intuicyjne, a ponadto mają świetny look & feel. Jedną z innowacji wprowadzonych przez Apple w pierwszych generacjach iPhona – „wodotryskowe” usuwanie elementów z listy – zaadaptowałem na platformie Android.

Pierwsze implementacje mechanizmu usuwania elementów z listy polegały na pojawieniu się w przy każdym elemencie okrągłego przycisku w kolorze czerwonym, po naciśnięciu którego wyświetlał się prostokątny przycisk z napisem „Delete”. W miarę upływu czasu okrągły przycisk został zastąpiony przez poziomy gest „wysunięcia elementu listy”. Po wykonaniu takiego gestu pojawiał się znany już przycisk „Delete”. Po naciśnięciu przycisku element z listy wysuwał się w lewo, a pozostałe elementy z listy przesuwały się do góry, zakrywając miejsce po usuniętym elemencie.

iPhone

Podobną wersję tego mechanizmu zaimplementowałem w przykładowej aplikacji zarządzającej listą użytkowników. Na początku od razu zaznaczę, że moje rozwiązanie nie jest jeszcze idealne. Niedociągnięcia opisałem pod koniec tego postu.

Kod aplikacji znajduje się w tym repozytorium. Opis mechanizmów związanych ze sposobem dostępu do bazy danych tj. content providery, data base helpery itp. zostanie pominięty. Skupię się jedynie na liście  elementów (ListView) pokazywanej w głównej aktywności (ListActivity).

Główna aktywność aplikacji HomeActivity dziedziczy po ListActivity i przy pomocy własnego adaptera ListCursorAdapter (dziedziczącego po CursorAdapter) wyświetla dane w kontrolce ListView (wbudowanej w ListActivity). Widok każdego elementu listy zdefiniowany jest w res/layout/listitem.xml

Każdy element listy posiada ukryty przycisk „Delete” (android:visibility=”gone”). Obsługa przycisków zdefiniowana została na stałe i kierowana jest do funkcji onDeleteButtonClick(View view) umieszczonej w głównej aktywności.

Należy pamiętać, że Android tworzy tylko tyle widoków elementów listy (funkcja newView w adapterze), ile jednocześnie można ich wyświetlić w danej aktywności. Podmienianie w widokach danych (funkcja bindView w adapterze) powoduje wrażenie, że na liście jest tyle widoków, ile elementów listy do wyświetlenia (sprytne i bardzo wydajne).

Podczas bindowania widoku elementu listy ustawiany jest callback onTouchListener, dzięki któremu będziemy mieli informację o dotyku na danym elemencie listy. Dodatkowo ustawiana jest widoczność przycisku „Delete”. W adapterze stworzona została kolekcja TreeSet pamiętająca widoczność tego przycisku na każdym elemencie listy – jest to konsekwencja mechanizmu opisanego w poprzednim akapicie. Do tego wszystkiego zastosowałem jeszcze jeden  trick – w tag’u przycisku „Delete” umieściłem odnośnik do widoku, w którym ten przycisk się znajduje. Było mi to potrzebne podczas obsługi kliknięcia przycisku – musiałem wiedzieć, który widok jest aktualnie obsługiwany (aby wykonać na nim animację).

Callback onTouchListener sprawdza, czy gest wykonany na elemencie list jest fling’iem. Jeśli tak, widoczność przycisku „Delete” jest przełączana. W ten sposób można wyłączać i włączać widoczność tego przycisku.

  1. // OnTouchListener methods
  2. public boolean onTouch(View itemView, MotionEvent event) {
  3.    actualProcessedItemView = itemView;
  4.    return mGestureDetector.onTouchEvent(event);
  5. }
  6. // GestureDetector.OnGestureListener methods
  7. public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
  8.    if (Math.abs(velocityY / velocityX) < 0.5f) {
  9.       changeDeleteButtonVisibility(actualProcessedItemView);
  10.    }
  11.    return true;
  12. }

Podczas kliknięcia przycisku „Delete” na elemencie listy wykonywana jest animacja, na końcu której element listy zostaje usunięty. Widać tu także użycie tag’u przycisku, który posłużył do wydobycia referencji widoku elementu listy.

  1. public void onDeleteButtonClick(View button) {
  2.    deleteItem(button);
  3. }
  4.  
  5. private void deleteItem(View btnDelete) {
  6.    View itemView = (View) btnDelete.getTag();
  7.    int itemPosition = getListView().getPositionForView(btnDelete);
  8.    final long id = adapter.getItemId(itemPosition);
  9.  
  10.    deleteItemAnimation.setAnimationListener(new Animation.AnimationListener() {
  11.       public void onAnimationEnd(Animation animation) {
  12.          delUser(id);
  13.          animation.setAnimationListener(null);
  14.       }
  15.       public void onAnimationRepeat(Animation animation) { }
  16.       public void onAnimationStart(Animation animation) { }
  17.    });
  18.  
  19.    adapter.clearDeleteButtonStatusFor(itemPosition);
  20.  
  21.    itemView.startAnimation(deleteItemAnimation);
  22. }

Rozwiązanie prezentuje się następująco:

Nie jest to podejście idealne. Problem jest następujący: po wyświetleniu kilku przycisków „Delete” i naciśnięciu ich jeden po drugim w bardzo małym odstępie czasu animacje pojedynczych elementów są przerywane i tylko pierwszy element z takiej sekwencji zostanie faktyczne usunięty.

Nadal pracuję nad udoskonaleniem tego rozwiązania. Być może będę musiał rozszerzyć samą klasę ListView – zobaczymy. Jeśli macie jakieś pomysły, czekam na komentarze.

  • Zanim doczytam do końca, to pozwolę sobie zostawić komentarz, że jeżeli opowiadasz i opisujesz jak działa jakiś mechanism… to mógłbyś się postarać i dostarczyć jakikolwiek krótki filmik… choćby pokazujący pierwowzór.

    czemu? Bo będąc w 1/3 posta… dalej nie wiem, czy interesuje mnie cały post, gdyż nie widziałem jeszcze efektu. Nie marnuj czasu czytelników :P

  • Jeżeli chodzi o film z iPhona, to pierwotnie taki filmik miał się pojawić, ale nic nie udało mi się znaleźć – niestety nie posiadam applowskiego telefonu. Jeżeli chodzi o film z androida, to nie maiłem możliwości żeby go dobrze nagrać.