Если ваше приложение загружает данные из интернета, отображает в ListView и обрабатывает нажатия на ячейки, то можете продолжать читать. Это рассказ о том как можно закрашиться в течение 64 мс после клика на ячейку списка.
У нас был обычный список в котором было 2 типа ячеек: некликабельные категории и кликабельные ячейки
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
Автор: anton9088