Maintenir / enregistrer / restaurer la position de défilement lors du retour à un ListView

, j'ai une longue ListView, que l'utilisateur peut faire défiler avant de retourner à l'écran précédent. Lorsque l'utilisateur ouvre à nouveau ce ListView, Je veux que la liste défile au même point qu'auparavant. Toutes les idées sur la façon de réaliser cet objectif?

370
demandé sur Jonik 2010-06-10 15:56:28

17 réponses

, Essayez ceci:

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

Explication:

ListView.getFirstVisiblePosition() renvoie l'élément de liste visible en haut. Mais cet élément peut être partiellement défilé hors de la vue, et si vous voulez restaurer la position de défilement exacte de la liste, vous devez obtenir ce décalage. Donc ListView.getChildAt(0) renvoie le View pour l'élément de liste supérieur, puis View.getTop() - mList.getPaddingTop() renvoie son décalage relatif par rapport au haut du ListView. Ensuite, pour restaurer la position de défilement de ListView, nous appelons ListView.setSelectionFromTop() avec l'index de l'élément que nous voulons et un décalage pour positionner son sommet bord du haut de la ListView.

618
répondu ian 2015-01-05 09:23:12
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);
    }
}
517
répondu Eugene Mymrin 2015-03-19 10:37:21

J'ai adopté la solution suggérée par @(Kirk Woll), et cela fonctionne pour moi. J'ai également vu dans le code source Android pour L'application" Contacts", qu'ils utilisent une technique similaire. Je voudrais ajouter quelques détails supplémentaires: En haut de ma classe dérivée ListActivity:

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

Ensuite, certaines méthodes remplacent:

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

Bien sûr, "loadData" est ma fonction pour récupérer des données de la base de données et les mettre sur la liste.

Sur mon appareil Froyo, cela fonctionne à la fois lorsque vous changez de téléphone orientation, et lorsque vous modifiez un élément, et revenir à la liste.

51
répondu Giorgio Barchiesi 2011-04-17 18:44:09

Un moyen très simple:

/** Save the position **/
int currentPosition = listView.getFirstVisiblePosition();

//Here u should save the currentPosition anywhere

/** Restore the previus saved position **/
listView.setSelection(savedPosition);

La méthode setSelection réinitialiser la liste de l'élément fourni. Si pas en mode tactile l'élément sera effectivement sélectionné si en mode tactile l'élément ne sera positionné sur l'écran.

Une approche plus compliquée:

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);
27
répondu Francesco Laurita 2015-04-25 20:03:55

J'ai trouvé quelque chose d'intéressant à ce sujet.

J'ai essayé setSelection et scrolltoXY mais cela n'a pas fonctionné du tout, la liste est restée dans la même position, après quelques essais et erreurs, j'ai eu le code suivant qui fonctionne

final ListView list = (ListView) findViewById(R.id.list);
list.post(new Runnable() {            
    @Override
    public void run() {
        list.setSelection(0);
    }
});

Si au lieu de poster le Runnable vous essayez runOnUiThread cela ne fonctionne pas non plus (au moins sur certains appareils)

C'est une solution de contournement très étrange pour quelque chose qui devrait être simple.

17
répondu shalafi 2015-04-25 19:49:08

Attention!! Il y a un bug dans AbsListView qui ne permet pas à onSaveState() de fonctionner correctement si ListView.getFirstVisiblePosition () vaut 0.

Donc, si vous avez de grandes images qui occupent la majeure partie de l'écran, et que vous faites défiler jusqu'à la deuxième image, mais qu'un peu de la première s'affiche, la position de défilement ne sera pas enregistrée...

Depuis AbsListView.java: 1650 (commentaires mine)

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

Mais dans cette situation, le ' top ' dans le code ci-dessous sera un nombre négatif ce qui provoque d'autres problèmes qui empêchent l'état pour être correctement restaurée. Donc, lorsque le 'top' est négatif, obtenez le prochain enfant

// 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);
10
répondu aaronvargas 2015-04-25 20:03:15
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);
        }
    }
}

Ça suffit

4
répondu Leo Phan 2016-06-24 02:15:00

Pour certains à la recherche d'une solution à ce problème, la racine du problème peut être l'endroit où vous définissez votre adaptateur de vues de liste. Après avoir défini l'adaptateur sur listview, il réinitialise la position de défilement. Juste quelque chose à considérer. J'ai déplacé le réglage de l'adaptateur dans mon onCreateView après avoir saisi la référence à listview, et cela a résolu le problème pour moi. =)

3
répondu Ryan Newsom 2016-08-25 20:25:18

Je poste ceci parce que je suis surpris que personne ne l'ait mentionné.

Après que l'utilisateur clique sur le bouton Retour, il reviendra à listview dans le même état qu'il en est sorti.

CE code remplacera le bouton " up " pour se comporter de la même manière que le bouton back, donc dans le cas de Listview -> Details -> Back To Listview (et pas d'autres options), c'est le code le plus simple pour maintenir la position de défilement et le contenu dans listview.

 public boolean onOptionsItemSelected(MenuItem item) {
     switch (item.getItemId()) {
         case android.R.id.home:
             onBackPressed();
             return(true);
     }
     return(super.onOptionsItemSelected(item)); }

Attention: Si vous pouvez aller à une autre activité de l'activité détails le bouton haut vous ramènera à cette activité, vous devrez donc manipuler l'historique des boutons arrière pour que cela fonctionne.

1
répondu Mazvél 2013-07-05 11:18:21

Ne suffit-il pas simplement android:saveEnabled="true" dans la déclaration XML ListView?

1
répondu Andrea Richiardi 2014-02-07 23:48:07

LA MEILLEURE SOLUTION EST:

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

VOUS DEVEZ APPELER EN POST ET EN FIL!

1
répondu Andrew Sneck 2016-08-13 17:55:30

Vous pouvez conserver l'état de défilement après un rechargement si vous enregistrez l'état avant de le recharger et le restaurez après. Dans mon cas, j'ai fait une demande de réseau asynchrone et rechargé la liste dans un rappel après l'avoir terminée. C'est là que j'ai restauration de l'état. L'exemple de Code est Kotlin.

val state = myList.layoutManager.onSaveInstanceState()

getNewThings() { newThings: List<Thing> ->

    myList.adapter.things = newThings
    myList.layoutManager.onRestoreInstanceState(state)
}
1
répondu Michael Peterson 2017-07-17 19:44:14

Si vous utilisez des fragments hébergés sur une activité, vous pouvez faire quelque chose comme ceci:

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());
          }
     }
}
0
répondu saulobrito 2014-09-24 16:21:10

Si vous enregistrez/restaurez la position de défilement de ListView vous-même, vous dupliquez essentiellement la fonctionnalité déjà implémentée dans Android framework. Le ListView restaure la position de défilement fine tout seul sauf une mise en garde: comme @ aaronvargas l'a mentionné, il y a un bug dans AbsListView qui ne permettra pas de restaurer la position de défilement fine pour le premier élément de la liste. Néanmoins, la meilleure façon de rétablir la position de défilement n'est pas à la restaurer. Android framework le fera mieux pour vous. Assurez-vous simplement vous avez rempli les conditions suivantes:

  • assurez-vous que vous n'avez pas appelé setSaveEnabled(false) méthode et ne définissez pas android:saveEnabled="false" attribut pour la liste dans le fichier de mise en page xml
  • pour ExpandableListView remplacer la méthode long getCombinedChildId(long groupId, long childId) afin qu'elle renvoie un nombre Long positif (l'implémentation par défaut dans la classe BaseExpandableListAdapter renvoie un nombre négatif). Voici des exemples:

.

@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 ou ExpandableListView est utilisé dans un fragment, ne recréez pas le fragment lors de la récréation de l'activité (après la rotation de l'écran exemple). Obtenez le fragment avec la méthode findFragmentByTag(String tag).
  • assurez-vous que le ListView a un android:id et qu'il est unique.

Pour éviter la mise en garde susmentionnée avec le premier élément de liste, vous pouvez créer votre adaptateur de la façon dont il renvoie une vue spéciale de hauteur de pixels zéro pour le ListView à la position 0. Voici l'exemple simple du projet montre ListView et ExpandableListView restaurer leurs positions de défilement fines alors que leurs positions de défilement ne sont pas explicitement enregistrées / restaurées. La position de défilement Fine est restaurée parfaitement même pour les scénarios complexes avec le passage temporaire à une autre application, la rotation à double écran et le retour à l'application de test. Veuillez noter que si vous quittez explicitement l'application (en appuyant sur le bouton Précédent), la position de défilement ne sera pas enregistrée (de même que toutes les autres vues ne sauvegarderont pas leur état). https://github.com/voromto/RestoreScrollPosition/releases

0
répondu Sergey 2015-09-07 13:19:24

Pour une activité dérivée de ListActivity qui implémente LoaderManager.LoaderCallbacks en utilisant un SimpleCursorAdapter il n'a pas fonctionné pour restaurer la position dans onReset (), car l'activité a été presque toujours redémarrée et l'adaptateur a été rechargé lorsque la vue de détails a été fermée. L'astuce était de restaurer la position dans onLoadFinished ():

Dans 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());

Dans onLoadFinished():

// restore the selected item which was saved on item click
// when details are closed and list is shown again
getListView().setSelectionFromTop(index, top);

Dans onBackPressed():

// Show the top item at next start of the app
index = 0;
top = 0;
0
répondu Perotin 2016-08-17 12:53:57

Aucune des solutions proposées ici semblait fonctionner pour moi. Dans mon cas, j'ai un ListView dans un Fragment que je remplace dans un FragmentTransaction, donc une nouvelle instance Fragment est créée chaque fois que le fragment est montré, ce qui signifie que l'état ListView ne peut pas être stocké en tant que membre du Fragment.

Au lieu de cela, j'ai fini par stocker l'état dans ma classe Application personnalisée. Le code ci-dessous devrait vous donner une idée de comment cela fonctionne:

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 ... */

}

L'idée de base est que vous stockez le état en dehors de l'instance de fragment. Si vous n'aimez pas l'idée d'avoir un champ statique dans votre classe d'application, je suppose que vous pourriez le faire en implémentant une interface de fragment et en stockant l'état dans votre activité.

Une autre solution serait de le stocker dans SharedPreferences, mais cela devient un peu plus compliqué, et vous devez vous assurer de l'effacer au lancement de l'application, sauf si vous voulez que l'État soit conservé lors des lancements d'applications.

 

Aussi, pour éviter le "position de défilement non enregistrée lorsque le premier élément est visible" , vous pouvez afficher un premier élément factice avec 0px Hauteur. Cela peut être réalisé en remplaçant getView() dans votre adaptateur, comme ceci:

@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);
}
0
répondu BadCash 2018-04-05 13:19:12

Ma réponse est pour Firebase et la position 0 est une solution de contournement

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

Et convertView

Position 0 a Ajouter un élément vide que vous n'utilisez pas

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

J'ai vu ce travail temporairement sans déranger l'utilisateur, j'espère que cela fonctionne pour vous

0
répondu Trk 2018-09-21 13:59:21