Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to save RecyclerView's scroll position using RecyclerView.State?

People also ask

Can you display a scrolling list of items in a RecyclerView?

To be able to scroll through a vertical list of items that is longer than the screen, you need to add a vertical scrollbar. Inside RecyclerView , add an android:scrollbars attribute set to vertical .

How do I scroll to the top of RecyclerView?

In the above code we have added recycler view to window manger as relative parent layout and add FloatingActionButton. FloatingActionButton supports CoordinatorLayout. So we have used parent layout is CoordinatorLayout. When you click on FloatingActionButton, it will scroll to top position.

How do I stop refreshing RecyclerView data scroll to top position android?

Just set your LayoutManager and adapter for the first time. Make a setDataList method in your adapter class. And set your updated list to adapter list. And then every time of calling API set that list to setDataList and call adapter.


Update

Starting from recyclerview:1.2.0-alpha02 release StateRestorationPolicy has been introduced. It could be a better approach to the given problem.

This topic has been covered on android developers medium article.

Also, @rubén-viguera shared more details in the answer below. https://stackoverflow.com/a/61609823/892500

Old answer

If you are using LinearLayoutManager, it comes with pre-built save api linearLayoutManagerInstance.onSaveInstanceState() and restore api linearLayoutManagerInstance.onRestoreInstanceState(...)

With that, you can save the returned parcelable to your outState. e.g.,

outState.putParcelable("KeyForLayoutManagerState", linearLayoutManagerInstance.onSaveInstanceState());

, and restore restore position with the state you saved. e.g,

Parcelable state = savedInstanceState.getParcelable("KeyForLayoutManagerState");
linearLayoutManagerInstance.onRestoreInstanceState(state);

To wrap all up, your final code will look something like

private static final String BUNDLE_RECYCLER_LAYOUT = "classname.recycler.layout";

/**
 * This is a method for Fragment. 
 * You can do the same in onCreate or onRestoreInstanceState
 */
@Override
public void onViewStateRestored(@Nullable Bundle savedInstanceState) {
    super.onViewStateRestored(savedInstanceState);

    if(savedInstanceState != null)
    {
        Parcelable savedRecyclerLayoutState = savedInstanceState.getParcelable(BUNDLE_RECYCLER_LAYOUT);
        recyclerView.getLayoutManager().onRestoreInstanceState(savedRecyclerLayoutState);
    }
}

@Override
public void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    outState.putParcelable(BUNDLE_RECYCLER_LAYOUT, recyclerView.getLayoutManager().onSaveInstanceState());
}

Edit: You can also use the same apis with the GridLayoutManager, as it is a subclass of LinearLayoutManager. Thanks @wegsehen for the suggestion.

Edit: Remember, if you are also loading data in a background thread, you will need to a call to onRestoreInstanceState within your onPostExecute/onLoadFinished method for the position to be restored upon orientation change, e.g.

@Override
protected void onPostExecute(ArrayList<Movie> movies) {
    mLoadingIndicator.setVisibility(View.INVISIBLE);
    if (movies != null) {
        showMoviePosterDataView();
        mDataAdapter.setMovies(movies);
      mRecyclerView.getLayoutManager().onRestoreInstanceState(mSavedRecyclerLayoutState);
        } else {
            showErrorMessage();
        }
    }

Store

lastFirstVisiblePosition = ((LinearLayoutManager)rv.getLayoutManager()).findFirstCompletelyVisibleItemPosition();

Restore

((LinearLayoutManager) rv.getLayoutManager()).scrollToPosition(lastFirstVisiblePosition);

and if that doesn't work, try

((LinearLayoutManager) rv.getLayoutManager()).scrollToPositionWithOffset(lastFirstVisiblePosition,0)

Put store in onPause() and restore in onResume()


How do you plan to save last saved position with RecyclerView.State?

You can always rely on ol' good save state. Extend RecyclerView and override onSaveInstanceState() and onRestoreInstanceState():

    @Override
    protected Parcelable onSaveInstanceState() {
        Parcelable superState = super.onSaveInstanceState();
        LayoutManager layoutManager = getLayoutManager();
        if(layoutManager != null && layoutManager instanceof LinearLayoutManager){
            mScrollPosition = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition();
        }
        SavedState newState = new SavedState(superState);
        newState.mScrollPosition = mScrollPosition;
        return newState;
    }

    @Override
    protected void onRestoreInstanceState(Parcelable state) {
        super.onRestoreInstanceState(state);
        if(state != null && state instanceof SavedState){
            mScrollPosition = ((SavedState) state).mScrollPosition;
            LayoutManager layoutManager = getLayoutManager();
            if(layoutManager != null){
              int count = layoutManager.getItemCount();
              if(mScrollPosition != RecyclerView.NO_POSITION && mScrollPosition < count){
                  layoutManager.scrollToPosition(mScrollPosition);
              }
            }
        }
    }

    static class SavedState extends android.view.View.BaseSavedState {
        public int mScrollPosition;
        SavedState(Parcel in) {
            super(in);
            mScrollPosition = in.readInt();
        }
        SavedState(Parcelable superState) {
            super(superState);
        }

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            super.writeToParcel(dest, flags);
            dest.writeInt(mScrollPosition);
        }
        public static final Parcelable.Creator<SavedState> CREATOR
                = new Parcelable.Creator<SavedState>() {
            @Override
            public SavedState createFromParcel(Parcel in) {
                return new SavedState(in);
            }

            @Override
            public SavedState[] newArray(int size) {
                return new SavedState[size];
            }
        };
    }

I wanted to save Recycler View's scroll position when navigating away from my list activity and then clicking the back button to navigate back. Many of the solutions provided for this problem were either much more complicated than needed or didn't work for my configuration, so I thought I'd share my solution.

First save your instance state in onPause as many have shown. I think it's worth emphasizing here that this version of onSaveInstanceState is a method from the RecyclerView.LayoutManager class.

private LinearLayoutManager mLayoutManager;
Parcelable state;

        @Override
        public void onPause() {
            super.onPause();
            state = mLayoutManager.onSaveInstanceState();
        }

The key to getting this to work properly is to make sure you call onRestoreInstanceState after you attach your adapter, as some have indicated in other threads. However the actual method call is much simpler than many have indicated.

private void someMethod() {
    mVenueRecyclerView.setAdapter(mVenueAdapter);
    mLayoutManager.onRestoreInstanceState(state);
}

I Set variables in onCreate(), save scroll position in onPause() and set scroll position in onResume()

public static int index = -1;
public static int top = -1;
LinearLayoutManager mLayoutManager;

@Override
public void onCreate(Bundle savedInstanceState)
{
    //Set Variables
    super.onCreate(savedInstanceState);
    cRecyclerView = ( RecyclerView )findViewById(R.id.conv_recycler);
    mLayoutManager = new LinearLayoutManager(this);
    cRecyclerView.setHasFixedSize(true);
    cRecyclerView.setLayoutManager(mLayoutManager);

}

@Override
public void onPause()
{
    super.onPause();
    //read current recyclerview position
    index = mLayoutManager.findFirstVisibleItemPosition();
    View v = cRecyclerView.getChildAt(0);
    top = (v == null) ? 0 : (v.getTop() - cRecyclerView.getPaddingTop());
}

@Override
public void onResume()
{
    super.onResume();
    //set recyclerview position
    if(index != -1)
    {
        mLayoutManager.scrollToPositionWithOffset( index, top);
    }
}

Update: Since version 1.2.0-alpha02 there's a new API to control when state restoration (including scroll position) happens.

RecyclerView.Adapter lazy state restoration:

Added a new API to the RecyclerView.Adapter class which allows Adapter to control when the layout state should be restored.

For example, you can call:

myAdapter.setStateRestorationStrategy(StateRestorationStrategy.WHEN_NOT_EMPTY);

to make RecyclerView wait until Adapter is not empty before restoring the scroll position.

See also:

  • RecyclerView.Adapter#setStateRestorationPolicy
  • RecyclerView.Adapter.StateRestorationPolicy

All layout managers bundled in the support library already know how to save and restore scroll position.

The RecyclerView/ScrollView/whatever needs to have an android:id for its state to be saved.

By default, scroll position is restored on the first layout pass which happens when the following conditions are met:

  • a layout manager is attached to the RecyclerView
  • an adapter is attached to the RecyclerView

Typically you set the layout manager in XML so all you have to do now is

  1. Load the adapter with data
  2. Then attach the adapter to the RecyclerView

You can do this at any time, it's not constrained to Fragment.onCreateView or Activity.onCreate.

Example: Ensure the adapter is attached every time the data is updated.

viewModel.liveData.observe(this) {
    // Load adapter.
    adapter.data = it

    if (list.adapter != adapter) {
        // Only set the adapter if we didn't do it already.
        list.adapter = adapter
    }
}

The saved scroll position is calculated from the following data:

  • Adapter position of the first item on the screen
  • Pixel offset of the top of the item from the top of the list (for vertical layout)

Therefore you can expect a slight mismatch e.g. if your items have different dimensions in portrait and landscape orientation.