Mantener / Guardar / Restaurar desplazamiento de posición al volver a un ListView
-
26-09-2019 - |
Pregunta
I tienen una larga ListView
que el usuario puede desplazarse alrededor antes de regresar a la pantalla anterior. Cuando el usuario abre esta ListView
nuevo, quiero que la lista se desplaza al mismo punto que era anteriormente. Cualquier ideas sobre cómo lograr esto?
Solución
Prueba esto:
// save index and top position
int index = mList.getFirstVisiblePosition();
View v = mList.getChildAt(0);
int top = (v == null) ? 0 : (v.getTop() - mList.getPaddingTop());
// ...
// restore index and position
mList.setSelectionFromTop(index, top);
Explicación:
ListView.getFirstVisiblePosition()
vuelve el elemento de lista superior visible. Sin embargo, este artículo puede ser desplazado parcialmente fuera de la vista, y si desea restaurar la posición de desplazamiento exacto de la lista que necesita para obtener este desplazamiento. Así ListView.getChildAt(0)
devuelve el View
para el elemento de la lista superior, y vuelve luego View.getTop() - mList.getPaddingTop()
su desplazamiento relativo de la parte superior de la ListView
. Entonces, para restaurar la posición de desplazamiento del ListView
, que llamamos ListView.setSelectionFromTop()
con el índice del elemento que queremos y un desplazamiento a la posición de su borde superior de la parte superior de la ListView
.
Otros consejos
Parcelable state;
@Override
public void onPause() {
// Save ListView state @ onPause
Log.d(TAG, "saving listview state @ onPause");
state = listView.onSaveInstanceState();
super.onPause();
}
...
@Override
public void onViewCreated(final View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
// Set new items
listView.setAdapter(adapter);
...
// Restore previous state (including selected item index and scroll position)
if(state != null) {
Log.d(TAG, "trying to restore listview state..");
listView.onRestoreInstanceState(state);
}
}
he adoptado la solución sugerida por @ (Kirk Woll), y funciona para mí. También he visto en el código fuente de Android de la aplicación "Contactos", que utilizan una técnica similar. Me gustaría añadir algunos detalles más: En la parte superior de mi clase ListActivity derivados:
private static final String LIST_STATE = "listState";
private Parcelable mListState = null;
A continuación, algunas anulaciones método:
@Override
protected void onRestoreInstanceState(Bundle state) {
super.onRestoreInstanceState(state);
mListState = state.getParcelable(LIST_STATE);
}
@Override
protected void onResume() {
super.onResume();
loadData();
if (mListState != null)
getListView().onRestoreInstanceState(mListState);
mListState = null;
}
@Override
protected void onSaveInstanceState(Bundle state) {
super.onSaveInstanceState(state);
mListState = getListView().onSaveInstanceState();
state.putParcelable(LIST_STATE, mListState);
}
Por supuesto "loadData" es mi función para recuperar datos de la base de datos y ponerla en la lista.
En mi dispositivo Froyo, esto funciona en ambos cuando se cambia la orientación del teléfono, y cuando se edita un elemento y volver a la lista.
Una forma muy sencilla:
/** Save the position **/
int currentPosition = listView.getFirstVisiblePosition();
//Here u should save the currentPosition anywhere
/** Restore the previus saved position **/
listView.setSelection(savedPosition);
El método setSelection se restablecerá la lista hasta el elemento suministrado. Si no está en modo de contacto del elemento de hecho se seleccionará si está en modo táctil del elemento sólo se colocará en la pantalla.
Un enfoque más complicado:
listView.setOnScrollListener(this);
//Implements the interface:
@Override
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
mCurrentX = view.getScrollX();
mCurrentY = view.getScrollY();
}
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
}
//Save anywere the x and the y
/** Restore: **/
listView.scrollTo(savedX, savedY);
he encontrado algo interesante acerca de esto.
He intentado setSelection y scrolltoXY pero no funcionaba en absoluto, la lista se mantuvo en la misma posición, después de algún ensayo y error Tengo el siguiente código que funciona
final ListView list = (ListView) findViewById(R.id.list);
list.post(new Runnable() {
@Override
public void run() {
list.setSelection(0);
}
});
Si en lugar de publicar el Ejecutable intenta runOnUiThread no funciona bien (al menos en algunos dispositivos)
Este es un muy extraño solución para algo que debería ser sencillo.
ATENCIÓN !! Hay un error en AbsListView que no permite la onSaveState () para trabajar correctamente si la ListView.getFirstVisiblePosition () es 0.
Así que si usted tiene imágenes grandes que ocupan la mayor parte de la pantalla y se desplaza a la segunda imagen, pero un poco de la primera está mostrando, la posición de desplazamiento no se guardará ...
desde AbsListView.java:1650 (comentarios mío)
// this will be false when the firstPosition IS 0
if (haveChildren && mFirstPosition > 0) {
...
} else {
ss.viewTop = 0;
ss.firstId = INVALID_POSITION;
ss.position = 0;
}
Sin embargo, en esta situación, el 'top' en el código siguiente será un número negativo que causa otros problemas que impiden que el estado de ser restaurado correctamente. Así que cuando el 'top' es negativo, obtener el siguiente hijo
// save index and top position
int index = getFirstVisiblePosition();
View v = getChildAt(0);
int top = (v == null) ? 0 : v.getTop();
if (top < 0 && getChildAt(1) != null) {
index++;
v = getChildAt(1);
top = v.getTop();
}
// parcel the index and top
// when restoring, unparcel index and top
listView.setSelectionFromTop(index, top);
private Parcelable state;
@Override
public void onPause() {
state = mAlbumListView.onSaveInstanceState();
super.onPause();
}
@Override
public void onResume() {
super.onResume();
if (getAdapter() != null) {
mAlbumListView.setAdapter(getAdapter());
if (state != null){
mAlbumListView.requestFocus();
mAlbumListView.onRestoreInstanceState(state);
}
}
}
Eso es suficiente
Para algunos en busca de una solución a este problema, la raíz del problema puede estar donde está configurando el adaptador de vistas de lista. Después de configurar el adaptador en la vista de lista, se restablece la posición de desplazamiento. Pero es algo a tener en cuenta. Moví la configuración del adaptador en mi onCreateView después nos agarra la referencia a la vista de lista, y resuelto el problema para mí. =)
Am publicar esto porque estoy había mencionado a nadie sorprende esto.
Después usuario hace clic en el botón de retroceso que volverá a la vista de lista en el mismo estado en que salió de ella.
Este código reemplazará el botón "arriba" a comportarse de la misma forma que el botón de retroceso por lo que en el caso de Listview -> Detalles -> Volver a Listview (y no hay otras opciones) este es el código más simple para mantener la scrollPosition y el contenido en la vista de lista.
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
onBackPressed();
return(true);
}
return(super.onOptionsItemSelected(item)); }
Precaución: Si se puede ir a otra actividad de la actividad de detalles el botón hasta que volverá de nuevo a esa actividad por lo que tendrá para manipular la historia BackButton para que esto funcione.
No es simplemente android:saveEnabled="true"
en el ListView lo suficientemente declaración XML?
mejor solución es:
// save index and top position
int index = mList.getFirstVisiblePosition();
View v = mList.getChildAt(0);
int top = (v == null) ? 0 : (v.getTop() - mList.getPaddingTop());
// ...
// restore index and position
mList.post(new Runnable() {
@Override
public void run() {
mList.setSelectionFromTop(index, top);
}
});
DEBE LLAMAR EN CORREOS Y EN HILO!
Se puede mantener el estado de desplazamiento después de una recarga si se guarda el estado antes de que vuelva a cargar y restaurarlos después. En mi caso hice una solicitud de red asíncrona y vuelve a cargar la lista en una devolución de llamada después de que se ha completado. Aquí es donde yo restaure el estado. Ejemplo de código es Kotlin.
val state = myList.layoutManager.onSaveInstanceState()
getNewThings() { newThings: List<Thing> ->
myList.adapter.things = newThings
myList.layoutManager.onRestoreInstanceState(state)
}
Si está utilizando fragmentos alojados en una actividad que puede hacer algo como esto:
public abstract class BaseFragment extends Fragment {
private boolean mSaveView = false;
private SoftReference<View> mViewReference;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
if (mSaveView) {
if (mViewReference != null) {
final View savedView = mViewReference.get();
if (savedView != null) {
if (savedView.getParent() != null) {
((ViewGroup) savedView.getParent()).removeView(savedView);
return savedView;
}
}
}
}
final View view = inflater.inflate(getFragmentResource(), container, false);
mViewReference = new SoftReference<View>(view);
return view;
}
protected void setSaveView(boolean value) {
mSaveView = value;
}
}
public class MyFragment extends BaseFragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
setSaveView(true);
final View view = super.onCreateView(inflater, container, savedInstanceState);
ListView placesList = (ListView) view.findViewById(R.id.places_list);
if (placesList.getAdapter() == null) {
placesList.setAdapter(createAdapter());
}
}
}
Si va a guardar / restaurar la posición de desplazamiento del mismo ListView
que son esencialmente de la duplicación de la funcionalidad ya implementada en el marco androide. La posición de desplazamiento fina restauraciones ListView
simplemente bien por sí sola excepción de una advertencia: como @aaronvargas mencionan que hay un error en AbsListView
que no le dejará para restablecer la posición de desplazamiento bien para el primer elemento de la lista. Sin embargo, la mejor manera de restaurar la posición de desplazamiento no es para restaurarlo. marco Android hará que sea mejor para usted. Sólo asegúrese de que haya cumplido con las siguientes condiciones:
- asegurarse de que no haya llamado método
setSaveEnabled(false)
y no establecer el atributo deandroid:saveEnabled="false"
de la lista en el archivo de diseño xml - para el método de anulación
ExpandableListView
long getCombinedChildId(long groupId, long childId)
para que vuelva número de larga positivo (aplicación por defecto enBaseExpandableListAdapter
clase rendimientos número negativo). Estos son algunos ejemplos:
.
@Override
public long getChildId(int groupPosition, int childPosition) {
return 0L | groupPosition << 12 | childPosition;
}
@Override
public long getCombinedChildId(long groupId, long childId) {
return groupId << 32 | childId << 1 | 1;
}
@Override
public long getGroupId(int groupPosition) {
return groupPosition;
}
@Override
public long getCombinedGroupId(long groupId) {
return (groupId & 0x7FFFFFFF) << 32;
}
- si
ListView
oExpandableListView
se utiliza en un fragmento no recrear el fragmento sobre la actividad de recreo (después de la rotación de la pantalla por ejemplo). Obtener el fragmento con el métodofindFragmentByTag(String tag)
. - asegurarse de que el
ListView
tiene unaandroid:id
y es único.
Para evitar la salvedad antes mencionada con el primer elemento de la lista se puede crear su adaptador de la forma en que regresa maniquí especial de cero píxeles de altura para la vista ListView
en la posición 0.
Aquí está el ejemplo simple muestra de proyectos ListView
y ExpandableListView
restaurar su posición de desplazamiento finas mientras que sus posiciones de desplazamiento no se guardan de forma explícita / restaurado. preciso de la posición de desplazamiento se restaura perfectamente incluso para los complejos escenarios con conmutación temporal para alguna otra aplicación, rotación de pantalla doble y cambiar de nuevo a la aplicación de prueba. Tenga en cuenta, si usted está saliendo explícitamente la aplicación (pulsando el botón Atrás) no se guardará la posición de desplazamiento (así como todas las demás vistas no guardará su estado).
https://github.com/voromto/RestoreScrollPosition/releases
Para una actividad derivan de ListActivity que implementa LoaderManager.LoaderCallbacks utilizando un SimpleCursorAdapter no funcionó para restaurar la posición en onreset (), porque la actividad fue casi siempre reinicia y el adaptador se vuelve a cargar cuando la vista detalles se cerró. El truco consistía en restaurar la posición en onLoadFinished ():
en onListItemClick ():
// save the selected item position when an item was clicked
// to open the details
index = getListView().getFirstVisiblePosition();
View v = getListView().getChildAt(0);
top = (v == null) ? 0 : (v.getTop() - getListView().getPaddingTop());
en onLoadFinished ():
// restore the selected item which was saved on item click
// when details are closed and list is shown again
getListView().setSelectionFromTop(index, top);
en onBackPressed ():
// Show the top item at next start of the app
index = 0;
top = 0;
Ninguna de las soluciones que se ofrecen aquí parecía funcionar para mí. En mi caso, tengo una ListView
en un Fragment
que estoy reemplazando en un FragmentTransaction
, por lo que se crea una nueva instancia Fragment
cada vez que se muestra el fragmento, lo que significa que el estado ListView
no se puede almacenar como un miembro de la Fragment
.
En su lugar, terminé almacenar el estado de mi clase Application
personalizado. El siguiente código debe darle una idea de cómo funciona esto:
public class MyApplication extends Application {
public static HashMap<String, Parcelable> parcelableCache = new HashMap<>();
/* ... code omitted for brevity ... */
}
public class MyFragment extends Fragment{
private ListView mListView = null;
private MyAdapter mAdapter = null;
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mAdapter = new MyAdapter(getActivity(), null, 0);
mListView = ((ListView) view.findViewById(R.id.myListView));
Parcelable listViewState = MyApplication.parcelableCache.get("my_listview_state");
if( listViewState != null )
mListView.onRestoreInstanceState(listViewState);
}
@Override
public void onPause() {
MyApplication.parcelableCache.put("my_listview_state", mListView.onSaveInstanceState());
super.onPause();
}
/* ... code omitted for brevity ... */
}
La idea básica es que permite almacenar el estado fuera de la instancia fragmento. Si no te gusta la idea de tener un campo estático en su clase de aplicación, supongo que se podría hacer mediante la implementación de una interfaz de fragmentos y almacenar el estado en su actividad.
Otra solución sería la de almacenar en SharedPreferences
, pero se vuelve un poco más complicado, y usted tendría que asegurarse de que se borre en el inicio de aplicaciones a menos que desee el estado que se conserva después de los lanzamientos de aplicaciones.
Además, para evitar la "posición de desplazamiento no se guarda al primer elemento es visible", se puede visualizar un primer elemento ficticio con la altura 0px
. Esto puede lograrse reemplazando getView()
en su adaptador, como esto:
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if( position == 0 ) {
View zeroHeightView = new View(parent.getContext());
zeroHeightView.setLayoutParams(new ViewGroup.LayoutParams(0, 0));
return zeroHeightView;
}
else
return super.getView(position, convertView, parent);
}
Mi respuesta es para Firebase y la posición 0 es una solución
Parcelable state;
DatabaseReference everybody = db.getReference("Everybody Room List");
everybody.addValueEventListener(new ValueEventListener() {
@Override
public void onDataChange(@NonNull DataSnapshot dataSnapshot) {
state = listView.onSaveInstanceState(); // Save
progressBar.setVisibility(View.GONE);
arrayList.clear();
for (DataSnapshot messageSnapshot : dataSnapshot.getChildren()) {
Messages messagesSpacecraft = messageSnapshot.getValue(Messages.class);
arrayList.add(messagesSpacecraft);
}
listView.setAdapter(convertView);
listView.onRestoreInstanceState(state); // Restore
}
@Override
public void onCancelled(@NonNull DatabaseError databaseError) {
}
});
y convertView
Posición 0 a añadir un elemento en blanco que no se está utilizando
public class Chat_ConvertView_List_Room extends BaseAdapter {
private ArrayList<Messages> spacecrafts;
private Context context;
@SuppressLint("CommitPrefEdits")
Chat_ConvertView_List_Room(Context context, ArrayList<Messages> spacecrafts) {
this.context = context;
this.spacecrafts = spacecrafts;
}
@Override
public int getCount() {
return spacecrafts.size();
}
@Override
public Object getItem(int position) {
return spacecrafts.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@SuppressLint({"SetTextI18n", "SimpleDateFormat"})
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = LayoutInflater.from(context).inflate(R.layout.message_model_list_room, parent, false);
}
final Messages s = (Messages) this.getItem(position);
if (position == 0) {
convertView.getLayoutParams().height = 1; // 0 does not work
} else {
convertView.getLayoutParams().height = RelativeLayout.LayoutParams.WRAP_CONTENT;
}
return convertView;
}
}
he visto este trabajo temporalmente sin molestar al usuario, espero que funcione para usted
utilizar este código a continuación:
int index,top;
@Override
protected void onPause() {
super.onPause();
index = mList.getFirstVisiblePosition();
View v = challengeList.getChildAt(0);
top = (v == null) ? 0 : (v.getTop() - mList.getPaddingTop());
}
y siempre que su actualización de sus datos utilizan este código a continuación:
adapter.notifyDataSetChanged();
mList.setSelectionFromTop(index, top);
Estoy usando FirebaseListAdapter y no pude conseguir ninguna de las soluciones para el trabajo. Terminé haciendo esto. Supongo que hay formas más elegantes, pero esto es una solución completa y de trabajo.
Antes onCreate:
private int reset;
private int top;
private int index;
Dentro de la FirebaseListAdapter:
@Override
public void onDataChanged() {
super.onDataChanged();
// Only do this on first change, when starting
// activity or coming back to it.
if(reset == 0) {
mListView.setSelectionFromTop(index, top);
reset++;
}
}
onStart:
@Override
protected void onStart() {
super.onStart();
if(adapter != null) {
adapter.startListening();
index = 0;
top = 0;
// Get position from SharedPrefs
SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(this);
top = sharedPref.getInt("TOP_POSITION", 0);
index = sharedPref.getInt("INDEX_POSITION", 0);
// Set reset to 0 to allow change to last position
reset = 0;
}
}
onStop:
@Override
protected void onStop() {
super.onStop();
if(adapter != null) {
adapter.stopListening();
// Set position
index = mListView.getFirstVisiblePosition();
View v = mListView.getChildAt(0);
top = (v == null) ? 0 : (v.getTop() - mListView.getPaddingTop());
// Save position to SharedPrefs
SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(this);
sharedPref.edit().putInt("TOP_POSITION" + "", top).apply();
sharedPref.edit().putInt("INDEX_POSITION" + "", index).apply();
}
}
Debido a que también tenía que resolver esto por FirebaseRecyclerAdapter que lo pongo aquí la solución para eso también:
Antes onCreate:
private int reset;
private int top;
private int index;
Dentro de la FirebaseRecyclerAdapter:
@Override
public void onDataChanged() {
// Only do this on first change, when starting
// activity or coming back to it.
if(reset == 0) {
linearLayoutManager.scrollToPositionWithOffset(index, top);
reset++;
}
}
onStart:
@Override
protected void onStart() {
super.onStart();
if(adapter != null) {
adapter.startListening();
index = 0;
top = 0;
// Get position from SharedPrefs
SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(this);
top = sharedPref.getInt("TOP_POSITION", 0);
index = sharedPref.getInt("INDEX_POSITION", 0);
// Set reset to 0 to allow change to last position
reset = 0;
}
}
onStop:
@Override
protected void onStop() {
super.onStop();
if(adapter != null) {
adapter.stopListening();
// Set position
index = linearLayoutManager.findFirstVisibleItemPosition();
View v = linearLayoutManager.getChildAt(0);
top = (v == null) ? 0 : (v.getTop() - linearLayoutManager.getPaddingTop());
// Save position to SharedPrefs
SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(this);
sharedPref.edit().putInt("TOP_POSITION" + "", top).apply();
sharedPref.edit().putInt("INDEX_POSITION" + "", index).apply();
}
}