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
    
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.