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:

Introduzca aquí la descripción de la imagen

La cual se reemplaza in situ con cualquier historial cargado.

Introduzca aquí la descripción de la imagen

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.

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); } }); } 
  • ¿Cómo puedo añadir una línea 1dp a la parte inferior de mi célula textview (no un diseño de tabla)
  • Crear ListView personalizado con sombra
  • Los elementos de la lista de reproducción de Android cambian de color de fondo al desplazarse
  • El elemento se vuelve más grande al deslizar ListView
  • Pinch Android y Zoom de la imagen en la actividad
  • Android Studio Project: parte inferior de la interfaz de usuario está cortada
  • Eliminar elemento haciendo clic en cualquier elemento de listview
  • El método getView () de ArrayAdapter repite dos o tres elementos en la lista
  • Configuración de más de un adaptador para una vista de lista única
  • ListFragment falta el divisor
  • Android cómo modificar EditText editable dentro de un ListView
  • FlipAndroid es un fan de Google para Android, Todo sobre Android Phones, Android Wear, Android Dev y Aplicaciones para Android Aplicaciones.