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?
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
.
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);
}
}
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.
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);
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.
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);
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
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. =)
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.
Ne suffit-il pas simplement android:saveEnabled="true"
dans la déclaration XML ListView?
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!
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)
}
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());
}
}
}
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 pasandroid:saveEnabled="false"
attribut pour la liste dans le fichier de mise en page xml - pour
ExpandableListView
remplacer la méthodelong getCombinedChildId(long groupId, long childId)
afin qu'elle renvoie un nombre Long positif (l'implémentation par défaut dans la classeBaseExpandableListAdapter
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
ouExpandableListView
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éthodefindFragmentByTag(String tag)
. - assurez-vous que le
ListView
a unandroid: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
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;
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);
}
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