web-dev-qa-db-fra.com

Conserver/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 revenir à l'écran précédent. Lorsque l'utilisateur ouvre à nouveau cette ListView, je souhaite que la liste défile jusqu'au même point que précédemment. Des idées sur la façon de réaliser ceci?

376
rantravee

Essaye ça:

// 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é et si vous souhaitez restaurer la position exacte du défilement de la liste, vous devez obtenir ce décalage. Donc ListView.getChildAt(0) renvoie View pour le premier élément de la liste, puis View.getTop() - mList.getPaddingTop() renvoie son décalage relatif à partir du haut de ListView. Ensuite, pour restaurer la position de défilement de la ListView, appelez ListView.setSelectionFromTop() avec l'index de l'élément souhaité et un décalage pour positionner son bord supérieur à partir du haut de la ListView.

621
ian
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);
    }
}
524
Eugene Mymrin

J'ai adopté la solution proposée par @ (Kirk Woll) et cela fonctionne pour moi. J'ai également vu dans le code source Android de l'application "Contacts" qu'ils utilisaient une technique similaire. J'aimerais ajouter quelques détails supplémentaires: En haut de ma classe dérivée de 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 dans la liste.

Sur mon appareil Froyo, cela fonctionne à la fois lorsque vous modifiez l'orientation du téléphone et lorsque vous modifiez un élément et revenez à la liste.

51
Giorgio Barchiesi

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éinitialisera la liste à l’élément fourni. S'il n'est pas en mode tactile, l'élément sera réellement sélectionné si en mode tactile, l'élément ne sera positionné qu'à 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
Francesco Laurita

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

J'ai essayé setSelection et scrolltoXY mais cela ne fonctionnait pas du tout, la liste restait dans la même position, après quelques essais et erreurs, j'ai obtenu 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 fichier Runnable, vous essayez runOnUiThread, cela ne fonctionne pas non plus (du moins sur certains périphériques).

Il s'agit d'une solution de contournement très étrange pour quelque chose qui devrait être simple.

17
shalafi

MISE EN GARDE!! AbsListView présente un bogue empêchant onSaveState () de fonctionner correctement si ListView.getFirstVisiblePosition () a pour valeur 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 que la première s'affiche un peu, la position de défilement ne sera pas enregistrée ... 

de AbsListView.Java:1650 (commentaires du mien)

// 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 «haut» dans le code ci-dessous sera un nombre négatif, ce qui entraînera d'autres problèmes empêchant la restauration correcte de l'état. Alors, quand le «haut» 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
aaronvargas

Pour certains à la recherche d'une solution à ce problème, la racine du problème peut être l'emplacement où vous configurez votre adaptateur de vues de liste. Une fois que vous avez défini l'adaptateur dans la liste, il réinitialise la position de défilement. Juste quelque chose à considérer. J'ai déplacé la définition de l'adaptateur dans mon onCreateView après avoir saisi la référence à la liste, ce qui a résolu le problème pour moi. =)

4
Ryan Newsom
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);
        }
    }
}

C'est assez

4
Leo Phan

Je publie ceci parce que je suis surpris que personne ne l’ait mentionné.

Une fois que l'utilisateur a cliqué sur le bouton Précédent, il reviendra à la liste dans le même état qu'il en était sorti. 

Ce code remplacera le bouton "haut" afin qu'il se comporte de la même manière que le bouton précédent. Ainsi, dans le cas de Listview -> Détails -> Retour à la liste (et aucune autre option), il s'agit du code le plus simple pour conserver la position de défilement et le contenu. dans la liste.

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

Attention: Si vous pouvez passer à une autre activité à partir de l'activité Détails, le bouton haut vous y ramènera. Vous devrez donc manipuler l'historique du bouton arrière pour que cela fonctionne. 

1
Mazvél

Vous pouvez conserver l'état de défilement après un rechargement si vous enregistrez l'état avant de le recharger et le restaurer après. Dans mon cas, j'ai fait une demande réseau asynchrone et rechargé la liste dans un rappel une fois celle-ci terminée. C'est là que je restaure l'état. L'échantillon de code est Kotlin.

val state = myList.layoutManager.onSaveInstanceState()

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

    myList.adapter.things = newThings
    myList.layoutManager.onRestoreInstanceState(state)
}
1
Michael Peterson

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 AU POST ET AU FIL

1
Andrew Sneck

Android:saveEnabled="true" dans la déclaration ListView xml ne suffit-il pas?

1
Andrea Richiardi

utilisez ce code ci-dessous:

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

et chaque fois que vous actualisez vos données, utilisez le code ci-dessous:

adapter.notifyDataSetChanged();
mList.setSelectionFromTop(index, top);
0
Rohit Lalwani

Pour une activité dérivée de ListActivity qui implémente LoaderManager.LoaderCallbacks à l'aide d'un SimpleCursorAdapter, la restauration de la position dans onReset () n'a pas fonctionné, car l'activité était presque toujours redémarrée et l'adaptateur était rechargé lorsque la vue de détail était fermée. L'astuce consistait à rétablir 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());

in 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
Perotin

J'utilise FirebaseListAdapter et je n'ai pu faire fonctionner aucune des solutions. J'ai fini par faire ça. Je suppose qu'il existe des moyens plus élégants, mais il s'agit d'une solution complète et efficace.

Avant onCreate:

private int reset;
private int top;
private int index;

À l'intérieur de 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();
    }
}

Comme je devais également résoudre ce problème pour FirebaseRecyclerAdapter je publie la solution ici pour cela aussi:

Avant onCreate:

private int reset;
private int top;
private int index;

À l'intérieur de 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();
    }
}
0
John T

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 vierge 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 fonctionnera pour vous

0
Trk

Si vous enregistrez/restaurez la position de défilement de ListView vous dupliquez essentiellement les fonctionnalités déjà implémentées dans la structure Android. La variable ListView restaure la position de défilement précise, sauf une mise en garde: comme l'a mentionné @aaronvargas, il existe un bogue dans AbsListView qui ne permet pas de rétablir la position de défilement précis du premier élément de la liste. Néanmoins, le meilleur moyen de restaurer la position du défilement est de ne pas le restaurer. Le framework Android le fera mieux pour vous. Assurez-vous simplement que vous avez rempli les conditions suivantes:

  • assurez-vous que vous n'avez pas appelé la méthode setSaveEnabled(false) et que vous n'avez pas défini l'attribut Android:saveEnabled="false" pour la liste dans le fichier de présentation XML
  • pour la méthode ExpandableListView override long getCombinedChildId(long groupId, long childId) afin qu'elle renvoie un nombre long positif (l'implémentation par défaut de 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 reconstitution d'activité (après la rotation de l'écran, par exemple). Obtenez le fragment avec la méthode findFragmentByTag(String tag).
  • assurez-vous que ListView a un Android:id et qu'il est unique.

Pour éviter les mises en garde susmentionnées avec le premier élément de liste, vous pouvez créer votre adaptateur de la manière dont il renvoie une vue spéciale de hauteur zéro pixel pour la ListView à la position 0 . Voici un exemple de projet simple ListView et ExpandableListView restaure leurs positions de défilement alors que leurs positions de défilement ne sont pas explicitement enregistrées/restaurées. La position de défilement est parfaitement restaurée, même pour les scénarios complexes, avec basculement temporaire vers une autre application, rotation double écran et 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
Sergey

Aucune des solutions proposées ici ne semblait fonctionner pour moi. Dans mon cas, j'ai une ListView dans une Fragment que je remplace dans une FragmentTransaction, de sorte qu'une nouvelle instance Fragment est créée chaque fois que le fragment est affiché, ce qui signifie que l'état ListView ne peut pas être stocké en tant que membre de la 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 la façon dont 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 l'é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, vous pouvez le faire en implémentant une interface de fragment et en stockant l'état dans votre activité.

Une autre solution consisterait à le stocker dans SharedPreferences, mais cela devient un peu plus compliqué et vous devrez vous assurer de l'effacer lors du lancement de l'application, à moins que vous ne souhaitiez que l'état persiste pendant le lancement des applications.

En outre, pour éviter que la "position de défilement ne soit pas enregistrée lorsque le premier élément est visible", vous pouvez afficher un premier élément factice avec 0px height. Ceci 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
Magnus W

Si vous utilisez des fragments hébergés dans 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
saulobrito