Agregar elementos a ListView, mantener la posición de desplazamiento y NO ver un salto de desplazamiento
Estoy creando una interfaz similar a la de chat de Google Hangouts. Los nuevos mensajes se agregan al final de la lista. Desplazarse hasta la parte superior de la lista activará una carga del historial de mensajes anterior. Cuando el historial viene de la red, esos mensajes se añaden a la parte superior de la lista y no deben activar ningún tipo de desplazamiento desde la posición que el usuario había detenido cuando se disparó la carga. En otras palabras, un "indicador de carga" se muestra en la parte superior de la lista:
- Cómo cambiar el enfoque del siguiente elemento de la lista automáticamente
- Mostrar alertDialog con los botones de radio cuando haga clic en un listViewItem
- Cambiar el ancho de AutoCompleteTextView en Android
- Separador (divisor) después del último elemento de ListView
- Cómo administrar divisores en un PreferenceFragment?
La cual se reemplaza in situ con cualquier historial cargado.
Tengo todo esto trabajando … excepto una cosa que he tenido que recurrir a la reflexión para lograr. Hay un montón de preguntas y respuestas que implican simplemente guardar y restaurar una posición de desplazamiento al agregar elementos al adaptador conectado a un ListView. Mi problema es que cuando hago algo como lo siguiente (simplificado pero debería ser auto-explicativo):
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); } }); }
Entonces lo que el usuario verá es un flash rápido a la parte superior de la ListView, a continuación, un rápido flash de nuevo a la ubicación correcta. El problema es bastante obvio y descubierto por muchas personas : setSelection()
es infeliz hasta después de notifyDataSetChanged()
y un redibujo de ListView
. Así que tenemos que post()
a la vista para darle la oportunidad de dibujar. Pero eso parece terrible.
Lo he "arreglado" usando la reflexión. Lo odio. En su núcleo, lo que quiero lograr es restablecer la primera posición de la ListView
sin pasar por la rigamarole del ciclo de dibujo hasta después de haber establecido la posición. Para ello, hay un campo útil de ListView: mFirstPosition
. ¡Por gawd, eso es exactamente lo que necesito ajustar! Por desgracia, es paquete privado. También desafortunadamente, no parece haber ninguna forma de establecerlo de forma programática o influenciarlo de ninguna manera que no implique un ciclo de invalidación … produciendo el comportamiento feo.
Por lo tanto, la reflexión con una fallback en el fracaso:
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); } }); } }
¿Funciona? Sí. ¿Es horrible? Sí. ¿Funcionará en el futuro? ¿Quién sabe? ¿Hay una mejor manera? Esa es mi pregunta.
¿Cómo puedo lograr esto sin reflexión?
Una respuesta podría ser "escribir su propio ListView
que puede manejar esto." Simplemente preguntaré si has visto el código de ListView
.
EDIT: Solución de trabajo sin reflexión basada en el comentario / respuesta de Luksprog.
Luksprog recomendó un OnPreDrawListener()
. ¡Fascinante! He metido con ViewTreeObservers antes, pero nunca uno de estos. Después de algunos desorden, el siguiente tipo de cosas parece funcionar perfectamente.
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; } } }); }
Muy genial.
- ListView.addFooterView () no funciona si no hay elementos en listview
- ¿Dónde debo colocar onClickListener en un ListView personalizado?
- Haciendo clic en el fragmento invoca actividad detrás de él
- Cómo traducir el lienzo y seguir recibiendo los eventos táctiles en los lugares correctos
- No se puede convertir de android.app.FragmentManager a android.support.v4.app.FragmentManager
- ¿Cómo evitar un doble toque en un ListView?
- ListView con añadir y eliminar botones en cada fila en android
- Arrastrar y soltar, ListView y las vistas de elementos que pierden el evento ACTION_DRAG_STARTED
Como dije en mi comentario, un OnPreDrawlistener
podría ser otra opción para resolver el problema. La idea de usar el listener es saltar mostrando el ListView
entre los dos estados (después de agregar los datos y después de establecer la selección a la posición correcta). En el OnPreDrawListener
(establecer con listViewReference.getViewTreeObserver().addOnPreDrawListener(listener);
) comprobará la posición visible actual de ListView
y la probará contra la posición que debe mostrar el ListView
. Si no coinciden, haga que el método del oyente devuelva false
para omitir el marco y establecer la selección en el ListView
en la posición correcta. Establecer la selección correcta activará de nuevo el oyente de selección, esta vez las posiciones coincidirán, en cuyo caso OnPreDrawlistener
el registro de OnPreDrawlistener
y devolvería true
.
Estaba rompiendo mi cabeza hasta que encontré una solución similar a esto. Antes de agregar un conjunto de elementos, tiene que guardar la distancia superior del elemento firstVisible y después de agregar los elementos setSelectionFromTop ().
Aquí está el código:
// 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);
Funciona sin ningún salto para mí con una lista de alrededor de 500 artículos 🙂
Tomé este código de esta publicación de SO: Manteniendo la posición en ListView después de llamar a notifyDataSetChanged
El código sugerido por el autor de la pregunta funciona, pero es peligroso.
Por ejemplo, esta condición:
listView.getFirstVisiblePosition() == positionToSave
Siempre puede ser cierto si no se cambian elementos.
Tuve algunos problemas con esta aproximación en una situación donde cualquier número de elementos se agregaron tanto por encima como por debajo del elemento actual. Así que me vino con una versión sligtly mejorado:
/* 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); } }); }