Подтвердить что ты не робот

Добавление элементов в ListView, сохранение положения прокрутки и НЕ просмотр прокрутки

Я создаю интерфейс, похожий на интерфейс чата Google Hangouts. Новые сообщения добавляются в нижней части списка. Прокрутка вверх до списка приведет к загрузке предыдущей истории сообщений. Когда история поступает из сети, эти сообщения добавляются в верхнюю часть списка и не должны вызывать какой-либо прокрутки из положения, которое пользователь остановил при загрузке нагрузки. Другими словами, в верхней части списка отображается "индикатор загрузки":

enter image description here

который затем заменяется in-situ любой загруженной историей.

enter image description here

У меня все это работает... за исключением того, что мне приходилось прибегать к размышлениям. Существует множество вопросов и ответов, связанных с сохранением и восстановлением позиции прокрутки при добавлении элементов в адаптер, подключенный к ListView. Моя проблема в том, что когда я делаю что-то вроде следующего (упрощенный, но должен быть понятным):

public void addNewItems(List<Item> items) {
    final int positionToSave = listView.getFirstVisiblePosition();
    adapter.addAll(items);
    listView.post(new Runnable() {

        @Override
        public void run() {
            listView.setSelection(positionToSave);
        }
    });
}

Тогда то, что пользователь увидит, - это быстрая вспышка в верхней части ListView, а затем быстрый флэш обратно в нужное место. Проблема довольно очевидна и обнаруживается для многих людей: setSelection() несчастлив только после notifyDataSetChanged() и перерисовки ListView. Поэтому нам нужно post() видеть, чтобы дать ему шанс нарисовать. Но это выглядит ужасно.

Я "исправил" его, используя отражение. Я ненавижу это. По своей сути я хочу выполнить reset первую позицию ListView, не пройдя через римарол цикла вытягивания, пока я не установил положение. Для этого есть полезное поле ListView: mFirstPosition. По gawd, это именно то, что мне нужно настроить! К сожалению, это пакет-частный. К сожалению, похоже, что нет никакого способа установить его программным путем или повлиять на него каким-либо образом, который не включает цикл invalidate... уступает уродливое поведение.

Итак, отражение с отказом при отказе:

try {
    Field field = AdapterView.class.getDeclaredField("mFirstPosition");
    field.setAccessible(true);
    field.setInt(listView, positionToSave);
}
catch (Exception e) { // CATCH ALL THE EXCEPTIONS </meme>
    e.printStackTrace();
    listView.post(new Runnable() {

        @Override
            public void run() {
                listView.setSelection(positionToSave);
            }
        });
    }
}

Это работает? Да. Это отвратительно? Да. Будет ли это работать в будущем? Кто знает? Есть ли способ лучше? Что мой вопрос.

Как это сделать без отражения?

Ответ может быть "написать свой собственный ListView, который может справиться с этим". Я просто спрошу, есть ли у вас код для ListView.

EDIT: рабочее решение без отражения на основе комментария/ответа Luksprog.

Luksprog рекомендовал OnPreDrawListener(). Захватывающий! Раньше я сталкивался с ViewTreeObservers, но никогда не был одним из них. После некоторых беспорядков, следующий тип вещи, кажется, работает совершенно отлично.

public void addNewItems(List<Item> items) {
    final int positionToSave = listView.getFirstVisiblePosition();
    adapter.addAll(items);
    listView.post(new Runnable() {

        @Override
        public void run() {
            listView.setSelection(positionToSave);
        }
    });

    listView.getViewTreeObserver().addOnPreDrawListener(new OnPreDrawListener() {

        @Override
        public boolean onPreDraw() {
            if(listView.getFirstVisiblePosition() == positionToSave) {
                listView.getViewTreeObserver().removeOnPreDrawListener(this);
                return true;
            }
            else {
                return false;
            }
        }
    });
}

Очень круто.

4b9b3361

Ответ 1

Как я уже сказал в своем комментарии, OnPreDrawlistener может быть другим вариантом для решения проблемы. Идея использования слушателя состоит в том, чтобы пропустить отображение ListView между двумя состояниями (после добавления данных и после установки выбора в правильное положение). В OnPreDrawlistener (установите с помощью listViewReference.getViewTreeObserver().addOnPreDrawListener(listener);) вы проверите текущую видимую позицию ListView и протестируйте ее против позиции, которую должен показать ListView. Если они не совпадают, сделайте метод слушателя false, чтобы пропустить рамку и установить выделение на ListView в нужное положение. Установка правильного выбора снова вызовет прослушиватель рисования, на этот раз позиции будут соответствовать, и в этом случае вы отменили регистрацию OnPreDrawlistener и вернули true.

Ответ 2

Я разбивал себе голову, пока не нашел решение, подобное этому. Перед добавлением набора элементов вам нужно сохранить верхнее расстояние элемента firstVisible, и после добавления элементов выполните setSelectionFromTop().

Вот код:

// save index and top position
int index = mList.getFirstVisiblePosition();
View v = mList.getChildAt(0);
int top = (v == null) ? 0 : v.getTop();

// for (Item item : items){
    mListAdapter.add(item);
}

// restore index and top position
mList.setSelectionFromTop(index, top);

Он работает без какого-либо перехода для меня со списком около 500 предметов:)

Я взял этот код из этого сообщения SO: Сохраняя позицию в ListView после вызова notifyDataSetChanged

Ответ 3

Код, предложенный автором вопроса, работает, но это опасно.
Например, это условие:

listView.getFirstVisiblePosition() == positionToSave

всегда может быть правдой, если никакие элементы не были изменены.

У меня были некоторые проблемы с этим aproach в ситуации, когда любое количество элементов было добавлено как выше, так и ниже текущего элемента. Поэтому я придумал улучшенную версию:

/* This listener will block any listView redraws utils unlock() is called */
private class ListViewPredrawListener implements OnPreDrawListener {

    private View view;
    private boolean locked;

    private ListViewPredrawListener(View view) {
        this.view = view;
    }

    public void lock() {
        if (!locked) {
            locked = true;
            view.getViewTreeObserver().addOnPreDrawListener(this);
        }
    }

    public void unlock() {
        if (locked) {
            locked = false;
            view.getViewTreeObserver().removeOnPreDrawListener(this);
        }
    }

    @Override
    public boolean onPreDraw() {
        return false;
    }
}

/* Method inside our BaseAdapter */
private updateList(List<Item> newItems) {
    int pos = listView.getFirstVisiblePosition();
    View cell = listView.getChildAt(pos);
    String savedId = adapter.getItemId(pos); // item the user is currently looking at
    savedPositionOffset = cell == null ? 0 : cell.getTop(); // current item top offset

    // Now we block listView drawing until after setSelectionFromTop() is called
    final ListViewPredrawListener predrawListener = new ListViewPredrawListener(listView);
    predrawListener.lock();

    // We have no idea what changed between items and newItems, the only assumption
    // that we make is that item with savedId is still in the newItems list
    items = newItems; 
    notifyDataSetChanged();
    // or for ArrayAdapter:
    //clear(); 
    //addAll(newItems);

    listView.post(new Runnable() {
        @Override
        public void run() {
            // Now we can finally unlock listView drawing
            // Note that this code will always be executed
            predrawListener.unlock();

            int newPosition = ...; // Calculate new position based on the savedId
            listView.setSelectionFromTop(newPosition, savedPositionOffset);
        }
    });
}