Добавление элементов в 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;
}
}
});
}
Очень круто.
Ответы
Ответ 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);
}
});
}