Maintain/Save/Restore scroll position when returning to a ListView

214,400

Solution 1

Try this:

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

Explanation:

ListView.getFirstVisiblePosition() returns the top visible list item. But this item may be partially scrolled out of view, and if you want to restore the exact scroll position of the list you need to get this offset. So ListView.getChildAt(0) returns the View for the top list item, and then View.getTop() - mList.getPaddingTop() returns its relative offset from the top of the ListView. Then, to restore the ListView's scroll position, we call ListView.setSelectionFromTop() with the index of the item we want and an offset to position its top edge from the top of the ListView.

Solution 2

Parcelable state;

@Override
public void onPause() {    
    // Save ListView state @ onPause
    Log.d(TAG, "saving listview state");
    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);
    }
}

Solution 3

I adopted the solution suggested by @(Kirk Woll), and it works for me. I have also seen in the Android source code for the "Contacts" app, that they use a similar technique. I would like to add some more details: On top on my ListActivity-derived class:

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

Then, some method overrides:

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

Of course "loadData" is my function to retrieve data from the DB and put it onto the list.

On my Froyo device, this works both when you change the phone orientation, and when you edit an item and go back to the list.

Solution 4

A very simple way:

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

//Here u should save the currentPosition anywhere

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

The method setSelection will reset the list to the supplied item. If not in touch mode the item will actually be selected if in touch mode the item will only be positioned on screen.

A more complicated approach:

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

Solution 5

I found something interesting about this.

I tried setSelection and scrolltoXY but it did not work at all, the list remained in the same position, after some trial and error I got the following code that does work

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

If instead of posting the Runnable you try runOnUiThread it does not work either (at least on some devices)

This is a very strange workaround for something that should be straight forward.

Share:
214,400
rantravee
Author by

rantravee

Updated on April 16, 2021

Comments

  • rantravee
    rantravee about 3 years

    I have a long ListView that the user can scroll around before returning to the previous screen. When the user opens this ListView again, I want the list to be scrolled to the same point that it was previously. Any ideas on how to achieve this?

  • SHRISH M
    SHRISH M almost 14 years
    there was an error in your answer. The method is called setSelection
  • rantravee
    rantravee almost 14 years
    The idea works well, but there is a pseudo problem.The first visible position might be shown only a little bit, (maybe just a small part of the row) , and setSelection() set this row to be completely visible when the restore is made.
  • Francesco Laurita
    Francesco Laurita almost 14 years
    Please take a look at my second option. I have just added it to my first answer
  • rantravee
    rantravee almost 14 years
    view.getScrollX() and view.getScrollY() always return 0 !
  • HXCaine
    HXCaine over 13 years
    Returns 0 for me too, I've been trying to do the second method for ages but always get 0. The first method is insufficient for large list items. Anybody?
  • HXCaine
    HXCaine over 13 years
    Not really understanding how this works. I'm using listView.getFirstVisiblePosition() only and understand that, but not sure what's going on here. Any chance of a brief explanation? :)
  • ian
    ian over 13 years
    So ListView.getFirstVisiblePosition() returns the top visible list item. But this item may be partially scrolled out of view, and if you want to restore the exact scroll position of the list you need to get this offset. So ListView.getChildAt(0) returns the View for the top list item, and then View.getTop() returns its relative offset from the top of the ListView. Then, to restore the ListView's scroll position, we call ListView.setSelectionFromTop() with the index of the item we want and an offset to position its top edge from the top of the ListView. All clear?
  • ian
    ian over 13 years
    Take a look at my (accepted) solution above using View.getChildAt() and View.getTop(). You can't use View.getScrollY() on a ListView because a ListView maintains its scrolling internally.
  • Phil
    Phil over 12 years
    This can be made even simpler be using one line to save: int index = mList.getFirstVisiblePosition(); and only one line to restore: mList.setSelectionFromTop(index, 0);. Great answer though (+1)! I have been looking for an elegant solution to this problem.
  • ofavre
    ofavre about 12 years
    I experienced the same issue! And setSelectionFromTop() doesn't work.
  • Mira Weller
    Mira Weller almost 12 years
    I think this is the best answer as this method restores the exact scroll position and not only the first visible item.
  • Gio
    Gio over 11 years
    Great answer but won't work when dynamically loading content and you want to save state in the onPause() method.
  • Cal
    Cal over 11 years
    Is this supposed to work for ExpandableListViews as well? I'm calling it but it doesn't scroll.
  • Omar Rehman
    Omar Rehman over 11 years
    +1. listnew.post(), doesn't work on some devices, and on some other devices it does not work in certain cases, but runOnUIThread works fine.
  • Dan J
    Dan J over 11 years
    @Omar, can you give some examples of devices and OSes that this doesn't work on? I tried an HTC G2 (2.3.4) and a Galaxy Nexus (4.1.2) and it worked on both. None of the other answers in this thread worked for any of my devices.
  • Omar Rehman
    Omar Rehman over 11 years
    listview.post works fine on my Galaxy Note with 4.0.4, but doesn't work on my Nexus One with 2.3.6. However, onOnUIThread(action) works fine on both devices.
  • user1106888
    user1106888 about 11 years
    Great Code worked very well with my list view thank you so much..really appreciate it
  • Bryan
    Bryan about 11 years
    This isn't working for me. I'm using a listview with data from an SQLite database. When I first load the listview activity, and scroll down 10-20 items, then I change to another application on my device. When I open my listview application again, the listview is at the very top of the list. The 'index' and 'top' variables are both 0 in the onRestore method.
  • Bryan
    Bryan about 11 years
    This solution worked for me. The others did not. How does saving/restoring the ListView instance state behave with regards to deleting and inserting items in the ListView?
  • Lars Blumberg
    Lars Blumberg about 11 years
    Although this answer works, I find the answer stackoverflow.com/questions/3014089/… from Eugene more elegant as it REALLY saves and restores the complete list view state.
  • JesseBuesking
    JesseBuesking almost 11 years
    If the technique in the link that @LarsBlumberg added doesn't work for you, I've created a gist that contains ian's technique, but supports lists that dynamically grow (reaching bottom gets more items).
  • passsy
    passsy almost 11 years
    great solution. Just one Problem. If the items are big and you scroll, but the first item is still visible the list jumps back to top. if you scroll to the second item or anywhere else everything works
  • Scott Kennedy
    Scott Kennedy over 10 years
    This doesn't work properly if your list items don't all have unique ids. ian's solution does.
  • User
    User over 10 years
    @Phil your simplistic solution doesn't work to restore the exact scrolling position.
  • User
    User over 10 years
    Didn't work in my case, I save in onPause and restore in onCreateView() (using the list in a fragment). When I navigate back to the fragment this state is restored and then the list is not visible.
  • RightHandedMonkey
    RightHandedMonkey about 10 years
    I was using a loader on a ListFragment and I added this code to the ListFragment.onLoadFinished(...) method. Worked like a champ. Still feels like a hack though. It seems that the ArrayAdapter setData(..) is calling clear() which appears the culprit for me. It may be possible to correct it by not calling clear, but that makes the code more complicated. It also may be possible to correct this using a CursorLoader instead of a Custom Loader.
  • l0gg3r
    l0gg3r about 10 years
    nice, don't forget to call listView.requestFocus(); before calling onRestoreInstanceState
  • Papajohn000
    Papajohn000 about 10 years
    @passsy Did you ever find a solution the list jumping back to the top?
  • Shakti Malik
    Shakti Malik about 10 years
    What changes would you make if listview is inside a fragment ?
  • Mgamerz
    Mgamerz about 10 years
    FYI if you use this in a fragment (I'm using a list fragment) and navigate to other fragments, this will crash because the content view is not created when calling mListState = getListView().onSaveInstanceState(). Mainly on device rotation.
  • Piotr
    Piotr almost 10 years
    does it supposed to work when listview has a headerview?
  • Brad
    Brad almost 10 years
    android:saveEnabled is set to true by default. This doesn't do anything.
  • VinceStyling
    VinceStyling almost 10 years
    As aaronvargas said, this couldn't work when ListView.getFirstVisiblePosition() is 0.
  • jdowdell
    jdowdell almost 10 years
    as @nbarraille said, the code should read more like "v.getTop() - mList.getPaddingTop()." Otherwise you'll spend an hour like I did trying to figure out why it always restores just a hair off...
  • Rahul Mandaliya
    Rahul Mandaliya over 9 years
    thanks its good example and you used originally android method of code
  • Rahul Mandaliya
    Rahul Mandaliya over 9 years
  • virsir
    virsir over 9 years
    Does not work if the listview has header and footer.
  • Dmitry
    Dmitry over 9 years
    onRestoreInstanceState is never called :(
  • Piovezan
    Piovezan about 9 years
    @GiorgioBarchiesi Who is @(Kirk Wool) whose solution works for you? The user apparently changed his/her name.
  • speedynomads
    speedynomads about 9 years
    This works perfectly and is much cleaner and more reliable than checking first visible position.
  • Aritra Roy
    Aritra Roy about 9 years
    This is the best solution to this problem. Less lines of code and works flawlessly. I didn't have any problem with this. Thanks a lot.
  • Giorgio Barchiesi
    Giorgio Barchiesi about 9 years
    Yes, that's the official religion. But in my case the data is loaded locally, has very limited length, and loads in a split second, so I went erethic :-)
  • eselk
    eselk almost 9 years
    If the view is destroyed, like when rotating, the state variable will be null when recreated. If you experience this issue, like I did, see Giorgio's answer which saves/restores the variable.
  • st_bk
    st_bk over 8 years
    Sweet. For those using it.sephiroth.android.library.widget.HListView (github.com/sephiroth74/HorizontalVariableListView) it is (v.getLeft() - mListView.getPaddingLeft()); to get the padding value.
  • hidro
    hidro over 8 years
    If your list view depends on CursorLoader then wait until you call ListAdapter.swapCursor(Cursor) before calling ListView.onRestoreInstanceState(Parcelable)
  • Someone Somewhere
    Someone Somewhere about 8 years
    what if onRestoreInstanceState() is never called !!??
  • Thupten
    Thupten about 8 years
    If you press back from activity B and your fragment was in activity A. Then activity A might have already been destroyed and so your fragment and the above listview's state is gone too. So the fragment's state need to be stored/restored on activity A. check this link. stackoverflow.com/questions/15313598/…
  • AJW
    AJW about 8 years
    Would your code above help me with a RecyclerView list where instancestate is not being saved between activities? I posted the following question here: stackoverflow.com/questions/35413495/…?
  • AJW
    AJW about 8 years
    Would your code above help me with a RecyclerView list where instancestate is not being saved between activities? I posted the following question here: stackoverflow.com/questions/35413495/…?
  • AJW
    AJW about 8 years
    Hi, would your code above help me with a RecyclerView list where instancestate is not being saved between activities? I posted the following question here: stackoverflow.com/questions/35413495/…?
  • BMB
    BMB about 8 years
    Code worked perfectly for my needs, doesn't crash on orientation change either (at least on my devices, two different phones and a tablet), as some have suggested.
  • GPack
    GPack almost 8 years
    I dont understand why it needs programmatically calling the ListView's methods onSaveInstanceState() and onRestoreInstanceState(), are not them automatically called by the framework?
  • Giorgio Barchiesi
    Giorgio Barchiesi almost 8 years
    @GPack: my solution was tested on Froyo, and needed those calls, I have kept them as-is in more recent Android versions, and never tested if you can avoid the mentioned calls. But I don't think the O.S. does it automatically as you suggest.
  • GPack
    GPack almost 8 years
    @GiorgioBarchiesi I tried it on Android 4/5/6, I can confirm that the ListView's callbacks are always automatically called by the framework. Not tried on Android 2.2., but I think that are called on it too. onRestoreInstanceState() is called before the onResume() callback and restores the selected item and scroll position.
  • tafi
    tafi almost 8 years
    It works like a charm but I don't fully understand it. If the first item is still visible, how does it make sense to get the next child? Shouldn't setSelectionFromTop with the new index cause the list to be shown starting from the second child? How am I still seeing the first one?
  • tmele54
    tmele54 almost 8 years
    @tafi, The first item could be 'partially' visible. So the top of that item would be above the screen, and thus would be a negative number. (This causes other issues that I don't recall...) So we use the 'top' of the second Item for placement, which should always(?) be available, or there wouldn't be any scrolling in the first place!
  • Ajay Sivan
    Ajay Sivan over 7 years
    It work's only when I set the Parcelable state as static
  • xxtesaxx
    xxtesaxx over 7 years
    While this might work and give you the behaviour you want, you should consider @Eugene Mymrins answer and use onSaveInstanceState() and onRestoreInstanceState() on the listview
  • Roel
    Roel about 7 years
    state is allways null on onViewCreated because it is not restored
  • Loyea
    Loyea over 4 years
    top no need to minus mList.getPaddingTop(). your idea is right, but top is a mistake. If you check the method setSelectionFromTop(int position, int y), the introduce of y:The distance from the top edge of the ListView (plus padding) that the item will be positioned. I tried myself, your top is wrong. Your solution only work properly when ListView not set paddingTop, if your listview have paddingTop, you can not restore listview position exactly.
  • NizarETH
    NizarETH over 3 years
    not working if you load data from a server