vertical studio recyclerview horizontal fillviewport android android-listview scroll scroll-position

android - studio - Mantenga/Guarde/Restaure la posición de desplazamiento cuando regrese a un ListView



scrollview android fillviewport (17)

Tengo un ListView largo que el usuario puede desplazarse antes de volver a la pantalla anterior. Cuando el usuario vuelva a abrir este ListView , quiero que la lista se desplace al mismo punto que estaba anteriormente. ¿Alguna idea sobre cómo lograr esto?


¡¡PRECAUCIÓN!! Hay un error en AbsListView que no permite que onSaveState () funcione correctamente si ListView.getFirstVisiblePosition () es 0.

Entonces, si tiene imágenes grandes que ocupan la mayor parte de la pantalla y se desplaza a la segunda imagen, pero se muestra un poco de la primera, la posición de desplazamiento no se guardará ...

de AbsListView.java:1650 (comentarios míos)

// this will be false when the firstPosition IS 0 if (haveChildren && mFirstPosition > 0) { ... } else { ss.viewTop = 0; ss.firstId = INVALID_POSITION; ss.position = 0; }

Pero en esta situación, la "parte superior" en el código a continuación será un número negativo que causa otros problemas que impiden que el estado se restaure correctamente. Así que cuando el ''top'' es negativo, consigue 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);


¿No es simplemente android:saveEnabled="true" en la declaración xml de ListView?


Adopté la solución sugerida por @ (Kirk Woll), y me funciona. También he visto en el código fuente de Android para la aplicación "Contactos", que utilizan una técnica similar. Me gustaría agregar algunos detalles más: En la parte superior de mi clase derivada de ListActivity:

private static final String LIST_STATE = "listState"; private Parcelable mListState = null;

Entonces, algún método anula:

@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 ponerlos en la lista.

En mi dispositivo Froyo, esto funciona tanto cuando cambia la orientación del teléfono como cuando edita un elemento y vuelve a la lista.


Encontré algo interesante sobre esto.

Probé setSelection y scrolltoXY pero no funcionó en absoluto, la lista permaneció en la misma posición, después de algunas pruebas y errores obtuve 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 Runnable intenta con runOnUiThread, tampoco funciona (al menos en algunos dispositivos)

Esta es una solución muy extraña para algo que debería ser sencillo.


Estoy publicando esto porque me sorprende que nadie haya mencionado esto.

Después de que el usuario haga clic en el botón Atrás, volverá a la vista de lista en el mismo estado en el que salió.

Este código anulará el botón "arriba" para que se comporte de la misma manera que el botón Atrás. En el caso de Vista de lista -> Detalles -> Volver a vista de lista (y no hay otras opciones) este es el código más simple para mantener la posición de desplazamiento 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 puede pasar a otra actividad desde la actividad de detalles, el botón arriba le devolverá a esa actividad, por lo que tendrá que manipular el historial del botón para que esto funcione.


Mi respuesta es para Firebase y la posición 0 es una solución alternativa.

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 convertir

posición 0 a agrega un elemento en blanco que no estás usando

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


Ninguna de las soluciones ofrecidas aquí pareció funcionar para mí. En mi caso, tengo un ListView en un Fragment que estoy reemplazando en un FragmentTransaction , por lo que se crea una nueva instancia de Fragment cada vez que se muestra el fragmento, lo que significa que el estado de ListView no se puede almacenar como miembro del Fragment .

En su lugar, terminé almacenando el estado en mi clase de Application personalizada. 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 almacene el estado fuera de la instancia del fragmento. Si no le gusta la idea de tener un campo estático en su clase de aplicación, supongo que podría hacerlo implementando una interfaz de fragmentos y almacenando el estado en su actividad.

Otra solución sería almacenarlo en SharedPreferences , pero se vuelve un poco más complicado, y debe asegurarse de que lo borra en el inicio de la aplicación a menos que desee que el estado se mantenga en todos los lanzamientos de la aplicación.

Además, para evitar la "posición de desplazamiento no guardada cuando el primer elemento está visible", puede mostrar un primer elemento 0px con 0px altura de 0px . Esto se puede lograr anulando getView() en su adaptador, de esta manera:

@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); }


Para algunos que buscan una solución a este problema, la raíz del problema puede estar donde está configurando su adaptador de vistas de lista. Después de configurar el adaptador en la vista de lista, se restablece la posición de desplazamiento. Solo algo a tener en cuenta. Cambié la configuración del adaptador a mi onCreateView después de que agarremos la referencia a la vista de lista, y resolví el problema por mí. =)


Para una actividad derivada de ListActivity que implementa LoaderManager.LoaderCallbacks usando un SimpleCursorAdapter, no funcionó para restaurar la posición en onReset (), porque la actividad casi siempre se reinicia y el adaptador se vuelve a cargar cuando se cierra la vista de detalles. El truco fue 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;


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() devuelve el elemento de lista visible superior. Pero este elemento puede desplazarse parcialmente fuera de la vista, y si desea restaurar la posición exacta de desplazamiento de la lista, necesita obtener este desplazamiento. Por lo tanto, ListView.getChildAt(0) devuelve la View para el elemento de la lista superior, y luego View.getTop() - mList.getPaddingTop() devuelve su desplazamiento relativo desde la parte superior de ListView . Luego, para restaurar la posición de desplazamiento de ListView , llamamos a ListView.setSelectionFromTop() con el índice del elemento que queremos y un desplazamiento para colocar su borde superior desde la parte superior de ListView .


Puede mantener el estado de desplazamiento después de una recarga si guarda el estado antes de recargarlo y restaurarlo después. En mi caso, hice una solicitud de red asíncrona y recargué la lista en una devolución de llamada después de que se completara. Aquí es donde restauro 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á guardando / restaurando la posición de desplazamiento de ListView usted mismo está esencialmente duplicando la funcionalidad ya implementada en el marco de Android. ListView restaura la posición de desplazamiento fino solo por su cuenta excepto una advertencia: como mencionó @aaronvargas, hay un error en AbsListView que no permite restaurar la posición de desplazamiento fino para el primer elemento de la lista. Sin embargo, la mejor manera de restaurar la posición de desplazamiento no es restaurarla. Android framework lo hará mejor para ti. Solo asegúrate de cumplir con las siguientes condiciones:

  • asegúrese de no haber llamado al setSaveEnabled(false) y no de configurar el android:saveEnabled="false" para la lista en el archivo de diseño xml
  • para ExpandableListView reemplaza el long getCombinedChildId(long groupId, long childId) para que devuelva un número largo positivo (la implementación predeterminada en la clase BaseExpandableListAdapter devuelve un número negativo). Aquí hay 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 se usa ListView o ExpandableListView en un fragmento, no vuelva a crear el fragmento en la recreación de la actividad (después de la rotación de la pantalla, por ejemplo). Obtenga el fragmento con el findFragmentByTag(String tag) .
  • asegúrese de que el ListView tenga un android:id y que sea único.

Para evitar la advertencia mencionada con el primer elemento de la lista, puede diseñar su adaptador de la manera en que devuelve la vista de altura de píxeles ficticios especiales para el ListView en la posición 0. Aquí se muestra el ejemplo simple que muestra ListView y ExpandableListView restauran sus posiciones de desplazamiento fino mientras que sus posiciones de desplazamiento son no explícitamente guardado / restaurado. La posición de desplazamiento fino se restaura perfectamente incluso en los escenarios complejos con el cambio temporal a alguna otra aplicación, la rotación de doble pantalla y el cambio a la aplicación de prueba. Tenga en cuenta que si está saliendo de la aplicación explícitamente (presionando el botón Atrás), la posición de desplazamiento no se guardará (y todas las demás Vistas no guardarán su estado). https://github.com/voromto/RestoreScrollPosition/releases


Si está utilizando fragmentos alojados en una actividad, 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()); } } }


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 restablecerá la lista al elemento suministrado. Si no está en el modo táctil, el elemento se seleccionará realmente si en el modo táctil el elemento solo 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);


LA 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); } });

¡USTED DEBE LLAMAR EN POST Y EN HILO!


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); } }


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