64 миллисекунды после нажатия

от автора

Если ваше приложение загружает данные из интернета, отображает в ListView и обрабатывает нажатия на ячейки, то можете продолжать читать. Это рассказ о том как можно закрашиться в течение 64 мс после клика на ячейку списка.

У нас был обычный список в котором было 2 типа ячеек: некликабельные категории и кликабельные ячейки
image
Random пикча с подкатегориями

Адаптер который мы использовали можно увидеть здесь:
github.com/siyusong/foodtruck-master-android/blob/master/src/com/foodtruckmaster/android/adapter/SeparatedListAdapter.java

Данные загружались с сервера, отображались в ListView, при нажатии на ячейку открывался отдельный экран с подробным описанием.
Для обработки нажатий использовали AdapterView.OnItemClickListener. Наши адаптеры в getItem возвращали объекты, которые передавались дальше на экраны детального описания.

Обработка нажатий делалась так:

public void onItemClick(AdapterView<?> parent, View view, int position, long id) {     Description desc = parent.getItemAtPosition(position);     DescriptionActivity.open(context, desc); } 

В crashlytics начали появляться крэши ClassCastException(String -> Description). Это означало что на некликабельные подзаголовки в списках все таки кликнули и вместо объекта Description мы получили String. На некликабельные ячейки можно кликнуть используя performItemClick, но такие методы мы не использовали и крэши были на всех экранах со списками и подзаголовками, хоть их было и немного.

Дальше мы будем копаться в исходниках 4.2.2
AbsListView, метод onTouchEvent

case MotionEvent.ACTION_UP: { switch (mTouchMode) {     case TOUCH_MODE_DOWN:     case TOUCH_MODE_TAP:     case TOUCH_MODE_DONE_WAITING:         ...         final AbsListView.PerformClick performClick = mPerformClick;         ...         if (mTouchMode == TOUCH_MODE_DOWN || mTouchMode == TOUCH_MODE_TAP) {             ...             if (mTouchModeReset != null) {                 removeCallbacks(mTouchModeReset);             }             mTouchModeReset = new Runnable() {                 @Override                 public void run() {                     mTouchMode = TOUCH_MODE_REST;                     child.setPressed(false);                     setPressed(false);                     if (!mDataChanged) {                         performClick.run();                     }                 }             };             if (!mDataChanged && mAdapter.isEnabled(motionPosition)) {                 ...                 postDelayed(mTouchModeReset,                         ViewConfiguration.getPressedStateDuration());             }              ...             return true;         }          ...     } 

В исходники android без пива лучше не лезть, видимо разработчики ос руководствовались тем же принципом.
Здесь видим что если мы кликнули на ячейку списка и она enabled, то вызываем PefrormClick через определенный интервал. В android 4.2.2 этот интервал 64 мс.

Так выглядит Runnable PerformClick

private class PerformClick extends WindowRunnnable implements Runnable {     int mClickMotionPosition;      public void run() {         // The data has changed since we posted this action in the event queue,         // bail out before bad things happen         if (mDataChanged) return;          final ListAdapter adapter = mAdapter;         final int motionPosition = mClickMotionPosition;         if (adapter != null && mItemCount > 0 &&                 motionPosition != INVALID_POSITION &&                 motionPosition < adapter.getCount() && sameWindow()) {             final View view = getChildAt(motionPosition - mFirstPosition);             // If there is no view, something bad happened (the view scrolled off the             // screen, etc.) and we should cancel the click             if (view != null) {                 performItemClick(view, motionPosition, adapter.getItemId(motionPosition));             }         }     } } 

Этот runnable вызывает performItemClick, где уже вызывается наш OnItemClickListener. Видим, что если данные в адаптере поменялись, то ливаем. Проверяем границы адаптера и прочее. Самое интересное что если установить новый адаптер, а не поменять данные в старом, то mDataChanged будет равным false, еще стоит заметить что нет проверки на isEnabled ячейки.

Т.е. мы кликаем на ячейку, в течение 64 мс меняем адаптер, выполняется этот runnable и в итоге клик происходит не по тем данным, которые мы видели на телефоне, а по новым. Причем если в новом адаптере у ячейки isEnabled = false, то она все равно кликнется, onItemClickListener вызовется.

Так в строке

Description desc = parent.getItemAtPosition(position); 

мы чудесным образом получали ClassCastException

Решение:
очевидно это баг ос, и самое простое решение было бы установка флага mDataChanged в true, либо очистка очереди сообщений при смене адаптера

Вывод:
Если вы кликнули на ячейку, а в этот момент загрузились новые данные с сервера и установились в список, значит вы кликнули по новым данным (если вы создавали адаптер заново)
Всегда проверяйте результат метода getItemAtPosition на null и на instanceof если у вас несколько типов ячеек и объектов item

ссылка на оригинал статьи http://habrahabr.ru/post/226897/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *