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 elandroid:saveEnabled="false"
para la lista en el archivo de diseño xml - para
ExpandableListView
reemplaza ellong getCombinedChildId(long groupId, long childId)
para que devuelva un número largo positivo (la implementación predeterminada en la claseBaseExpandableListAdapter
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
oExpandableListView
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 elfindFragmentByTag(String tag)
. - asegúrese de que el
ListView
tenga unandroid: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