I have Recycler view which lays inside of SwipeRefreshLayout. Also, have ability to open each item in another activity.
After returning back to Recycler I need scroll to chosen item, or to previous Y.
How to do that?
Yes, I googled, found articles in StackOverFlow about saving instance of layout manager, like this one: RecyclerView store / restore state between activities.
But, it doesn't help me.
UPDATE
Right now I have this kind of resolving problem, but, of course, it also doesn't work.
private int scrollPosition;
...//onViewCreated - it is fragment
recyclerView.setHasFixedSize(true);
LinearLayoutManager llm = new LinearLayoutManager(getActivity());
recyclerView.setLayoutManager(llm);
data = new ArrayList<>();
adapter.setData(getActivity(), data);
recyclerView.setAdapter(adapter);
...
#Override
public void onResume() {
super.onResume();
recyclerView.setScrollY(scrollPosition);
}
#Override
public void onPause() {
super.onPause();
scrollPosition = recyclerView.getScrollY();
}
Yes, I have tried scrollTo(int, int) - doen't work.
Now I tried just scroll, for example, to Y = 100, but it doesn't scrolling at all.
Save the current state of recycle view position #onPause:
positionIndex= llManager.findFirstVisibleItemPosition();
View startView = rv.getChildAt(0);
topView = (startView == null) ? 0 : (startView.getTop() - rv.getPaddingTop());
Restore the scroll position #onResume:
if (positionIndex!= -1) {
llManager.scrollToPositionWithOffset(positionIndex, topView);
}
or another way can be #onPause:
long currentVisiblePosition = 0;
currentVisiblePosition = ((LinearLayoutManager)rv.getLayoutManager()).findFirstCompletelyVisibleItemPosition();
restore #onResume:
((LinearLayoutManager) rv.getLayoutManager()).scrollToPosition(currentVisiblePosition);
currentVisiblePosition = 0;
A lot of these answers seem to be over complicating it.
The LayoutManager supports onRestoreInstanceState out of the box so there is no need to save scroll positions etc. The built in method already saves pixel perfect positions.
example fragment code (null checking etc removed for clarity):
private Parcelable listState;
private RecyclerView list;
#Override
public void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
listState=savedInstanceState.getParcelable("ListState");
}
#Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putParcelable("ListState", list.getLayoutManager().onSaveInstanceState());
}
then just call
list.getLayoutManager().onRestoreInstanceState(listState);
once your data has been reattached to your RecyclerView
Beginning from version 1.2.0-alpha02 of androidx recyclerView library, it is now automatically managed. Just add it with:
implementation "androidx.recyclerview:recyclerview:1.2.0-alpha02"
And use:
adapter.stateRestorationPolicy = StateRestorationPolicy.PREVENT_WHEN_EMPTY
The StateRestorationPolicy enum has 3 options:
ALLOW — the default state, that restores the RecyclerView state immediately, in the next layout pass
PREVENT_WHEN_EMPTY — restores the RecyclerView state only when the adapter is not empty (adapter.getItemCount() > 0). If your data is loaded async, the RecyclerView waits until data is loaded and only then the state is restored. If you have default items, like headers or load progress indicators as part of your Adapter, then you should use the PREVENT option, unless the default items are added using MergeAdapter. MergeAdapter waits for all of its adapters to be ready and only then it restores the state.
PREVENT — all state restoration is deferred until you set ALLOW or PREVENT_WHEN_EMPTY.
Note that at the time of this answer, recyclerView library is still in alpha03, but alpha phase is not suitable for production purposes.
User your recycler view linearlayoutmanager for getting scroll position
int position = 0;
if (linearLayoutManager != null) {
scrollPosition = inearLayoutManager.findFirstVisibleItemPosition();
}
and when restoring use following code
if (linearLayoutManager != null) {
cardRecyclerView.scrollToPosition(mScrollPosition);
}
Hope this helps you
to save position to Preferences, add this to your onStop()
int currentVisiblePosition = ((LinearLayoutManager) recyclerView.getLayoutManager()).findFirstCompletelyVisibleItemPosition();
getPreferences(MODE_PRIVATE).edit().putInt("listPosition", currentVisiblePosition).apply();
then restore position like this
if (getItemCount() == 0) {
int savedListPosition = getPreferences(MODE_PRIVATE).getInt("listPosition", 0);
recyclerView.getLayoutManager().scrollToPosition(savedListPosition); }
this last code should be added inside an event of the Adapter (not sure witch event but in my case was onEvent() - com.google.firebase.firestore.EventListener)
For some reason there are a lot of quite misleading tips/suggestions on how to save and restore scroll position in your_scrolling_container upon orientation changes.
Taking current scroll position and saving it in Activity’s onSaveInstanceState
Extending a certain scrollable View to do same there
Preventing Activity from being destroyed on rotation
And yeah, they are working fine, but…
But in fact, everything is much simpler, because Android is already doing it for you!
If you take a closer look at
RecyclerView/ListView/ScrollView/NestedScrollView sources, you’ll see that each of them is saving its scroll position in onSaveInstanceState. And during the first layout pass they are trying to scroll to this position in onLayout method.
There are only 2 things you need to do, to make sure it’s gonna work fine:
Set an id for your scrollable view, which is probably already done. Otherwise Android won’t be able to save View state automatically.
Provide a data before the first layout pass, to have the same scroll boundaries you had before rotation. That’s the step where developers usually have some issues.
The easiest and transition compatible way I found is:
#Override
public void onPause() {
super.onPause();
recyclerView.setLayoutFrozen(true);
}
#Override
public void onResume() {
super.onResume();
recyclerView.setLayoutFrozen(false);
}
in onSaveInstanceState() method of fragment you can save the scroll position of RecycleView
#Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
LinearLayoutManager layoutManager = (LinearLayoutManager)
recyclerView.getLayoutManager();
outState.putInt("scrolled_position",
layoutManager.findFirstCompletelyVisibleItemPosition());
}
then you can retrieve saved scroll position in onViewStateRestored() method
#Override
public void onViewStateRestored(#Nullable Bundle savedInstanceState) {
super.onViewStateRestored(savedInstanceState);
if (savedInstanceState != null) {
int scrollPosition = savedInstanceState.getInt("scrolled_position");
recyclerView.scrollToPosition(scrollPosition);
}
}
You can use scrollToPosition or smoothScrollToPosition to scroll to any item position in RecyclerView.
If you want to scroll to item position in adapter, then you would have to use adapter's scrollToPosition or smoothScrollToPosition.
Related
I am using firebase database with firebaseui's FirebaseRecyclerAdapter. I have followed the example here:
https://github.com/firebase/FirebaseUI-Android/tree/master/database
-except I'm using a fragment.
The app works fine and I am having no issues except that when the device is rotated the recycler view does not restore its scroll position and instead resets to the top item.
- for example user scrolls 8 items down and then rotates device. user should be in the same scroll position, but instead position has returned to top.
I guess I could just put in additional code to handle this and scroll to the correct position, but I know from experience that the RecyclerView should handle this on its own. does the the FirebaseRecyclerAdapter require me to handle this?
Answering my own question-
The only way I've been able to get this to work is to save state like this:
#Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
mRvPositionIndex = mLayoutManager.findFirstVisibleItemPosition();
View startView = mRecyclerView.getChildAt(0);
mRvTopView = (startView == null) ? 0 : (startView.getTop() - mRecyclerView.getPaddingTop());
outState.putInt(RV_POS_INDEX, mRvPositionIndex);
outState.putInt(RV_TOP_VIEW, mRvTopView);
}
and then in onCreateView(), right after I set the adapter I do this
if (savedInstanceState != null) {
mRvPositionIndex = savedInstanceState.getInt(RV_POS_INDEX);
mRvTopView = savedInstanceState.getInt(RV_TOP_VIEW);
mAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
public void onItemRangeInserted(int positionStart, int itemCount) {
mLayoutManager.scrollToPositionWithOffset(mRvPositionIndex, mRvTopView);
}
});
}
I've done a little testing so far but seems to work fine. If someone has a better solution please let me know.
I found also that solution that works fine even in a normal activity
in onCreate:
mDataObserver = new RecyclerView.AdapterDataObserver() {
#Override
public void onItemRangeInserted(int positionStart, int itemCount) {
mLayoutManager.scrollToPositionWithOffset(mRvPositionIndex, 0);
}
};
myFirebaseRecyclerAdapter.registerAdapterDataObserver(mDataObserver);
don't forget to unregister the AdapterDataObserver.
I'm new to Android and I'm trying to do the following task on my school project:
I have a grid view of movies which the user can scroll endlessly.
When the app starts I fetch the first 20 movies and each time the user scrolls to the bottom of the grid I execute an AsyncTask to fetch 20 more movies and add them to the Adapter.
When the user clicks on a movie he goes to a new child activity to see the movie details.
I'm having troubles maintaining the GridView's scroll position in the following cases:
When the user goes to the details activity and returns to the main activity of the movies.
When the user changes the device orientation.
And when dealing with theses 2 cases I also need to take in consideration that maybe the user scrolled a lot and had 100 movies in the adapter and when he goes back the activity start from the start with only the first 20 movies, so I would be able to scroll to his last position.
Can someone please tell me how can I give the best user experience in my project by not losing the user's scroll position at any case?
I don't know if this is the best practice, but in my case it is.
I decided to set my adapter as a global static variable, in this way I maintain the amount of data loaded via the API, and I don't need to perform a request for every time the user moves between activities.
For maintaining the scroll position I used the onItemClickListener when moving to the details activity and the savedInstanceState when changing orientation.
Here is my code for that:
//Static variables
private static MoviesAdapter mMoviesAdapter;
private static int mGridViewPosition = 0;
//Call this method when user clicks the back button
public static void ClearStaticData(){
mMoviesAdapter.clear();
mMoviesAdapter = null;
}
#Override
public void onSaveInstanceState(Bundle outState) {
int index = mGridView.getFirstVisiblePosition();
outState.putInt(GRID_VIEW_POSITION, index);
super.onSaveInstanceState(outState);
}
#Override
public View onCreateView(...) {
if (mMoviesAdapter == null) {
mMoviesAdapter = new MoviesAdapter(...);
} else {
RestoreGridPosition();
}
}
private void RestoreGridPosition(){
if(mGridViewPosition > 0 && mMoviesAdapter.getCount() >= mGridViewPosition)
mGridView.setSelection(mGridViewPosition);
}
Since I fill my adapter via API call, I think this is probably the best solution to save the data and not to perform requests every time.
Try not finishing mainActivity once a gridItem is clicked so when user navigates back to mainActivity (from detailsActivity) he will have all the data that was there before.
You can handle this situation with activity's lifecycle callbacks:
You can get currently visible GridView item's position like this:
int mCurrentPosition = gridview.getFirstVisiblePosition();
When an orientation change is occurring the activity is recreated and going through the following stages:
onSaveInstanceState
onRestoreInstanceState
You can then save the position before orientation change is happening and get it back when its being restored.
Save Your Activity State
#Override
public void onSaveInstanceState(Bundle savedInstanceState) {
// Save the user's current scroll state
savedInstanceState.putInt(STATE_POSITION, mCurrentPosition);
// Always call the superclass so it can save the view hierarchy state
super.onSaveInstanceState(savedInstanceState);
}
Restore Your Activity State
public void onRestoreInstanceState(Bundle savedInstanceState) {
// Always call the superclass so it can restore the view hierarchy
super.onRestoreInstanceState(savedInstanceState);
// Restore state members from saved instance
mCurrentPosition = savedInstanceState.getInt(STATE_POSITION);
}
Here once you have the previous position you can move to the desired position in the gridView:
gridview.smoothScrollToPosition(int mCurrentPosition)
This is taken from android docs: Recreating an Activity
Scrolling gridView to position GridView scrolling stackoverflow
I have a fragment that contains a RecyclerView. When I return to the fragment from somewhere else, I do
if(null!=savedInstanceState) {
mScrollPosition = savedInstanceState.getInt(ITEM_POSITION);
mRecyclerView.scrollToPosition(mScrollPosition);
}
but now I don't know what to put inside onSaveInstanceState:
#Override
public void onSaveInstanceState(Bundle bundle){
super.onSaveInstanceState(bundle);
Log.d(TAG, "onSaveInstanceState");
mScrollPosition=mRecyclerView.getScrollPosition();//???
bundle.putInt(NITEM_POSITION,mScrollPosition);
}
There is no mRecyclerView.getScrollPosition().
update
after testing it turns out that using mRecyclerView.scrollToPosition is not fine enough to ensure a smooth user experience. In cases where the item views are tall enough, scrollToPosition(ofFirstVisibleItem) can be way off. So is there a finer solution to this?
You can get the scroll from the LayoutManager. For example:
mScrollPosition = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition();
also check this answer How to save RecyclerView's scroll position using RecyclerView.State?
How does one refresh the data displayed in RecyclerView (calling notifyDataSetChanged on its adapter) and make sure that the scroll position is reset to exactly where it was?
In case of good ol' ListView all it takes is retrieving getChildAt(0), checking its getTop() and calling setSelectionFromTop with the same exact data afterwards.
It doesn't seem to be possible in case of RecyclerView.
I guess I'm supposed to use its LayoutManager which indeed provides scrollToPositionWithOffset(int position, int offset), but what's the proper way to retrieve the position and the offset?
layoutManager.findFirstVisibleItemPosition() and layoutManager.getChildAt(0).getTop()?
Or is there a more elegant way to get the job done?
I use this one.^_^
// Save state
private Parcelable recyclerViewState;
recyclerViewState = recyclerView.getLayoutManager().onSaveInstanceState();
// Restore state
recyclerView.getLayoutManager().onRestoreInstanceState(recyclerViewState);
It is simpler, hope it will help you!
I have quite similar problem. And I came up with following solution.
Using notifyDataSetChanged is a bad idea. You should be more specific, then RecyclerView will save scroll state for you.
For example, if you only need to refresh, or in other words, you want each view to be rebinded, just do this:
adapter.notifyItemRangeChanged(0, adapter.getItemCount());
EDIT: To restore the exact same apparent position, as in, make it look exactly like it did, we need to do something a bit different (See below how to restore the exact scrollY value):
Save the position and offset like this:
LinearLayoutManager manager = (LinearLayoutManager) mRecycler.getLayoutManager();
int firstItem = manager.findFirstVisibleItemPosition();
View firstItemView = manager.findViewByPosition(firstItem);
float topOffset = firstItemView.getTop();
outState.putInt(ARGS_SCROLL_POS, firstItem);
outState.putFloat(ARGS_SCROLL_OFFSET, topOffset);
And then restore the scroll like this:
LinearLayoutManager manager = (LinearLayoutManager) mRecycler.getLayoutManager();
manager.scrollToPositionWithOffset(mStatePos, (int) mStateOffset);
This restores the list to its exact apparent position. Apparent because it will look the same to the user, but it will not have the same scrollY value (because of possible differences in landscape/portrait layout dimensions).
Note that this only works with LinearLayoutManager.
--- Below how to restore the exact scrollY, which will likely make the list look different ---
Apply an OnScrollListener like so:
private int mScrollY;
private RecyclerView.OnScrollListener mTotalScrollListener = new RecyclerView.OnScrollListener() {
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
mScrollY += dy;
}
};
This will store the exact scroll position at all times in mScrollY.
Store this variable in your Bundle, and restore it in state restoration to a different variable, we'll call it mStateScrollY.
After state restoration and after your RecyclerView has reset all its data reset the scroll with this:
mRecyclerView.scrollBy(0, mStateScrollY);
That's it.
Beware, that you restore the scroll to a different variable, this is important, because the OnScrollListener will be called with .scrollBy() and subsequently will set mScrollY to the value stored in mStateScrollY. If you do not do this mScrollY will have double the scroll value (because the OnScrollListener works with deltas, not absolute scrolls).
State saving in activities can be achieved like this:
#Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt(ARGS_SCROLL_Y, mScrollY);
}
And to restore call this in your onCreate():
if(savedState != null){
mStateScrollY = savedState.getInt(ARGS_SCROLL_Y, 0);
}
State saving in fragments works in a similar way, but the actual state saving needs a bit of extra work, but there are plenty of articles dealing with that, so you shouldn't have a problem finding out how, the principles of saving the scrollY and restoring it remain the same.
Keep scroll position by using #DawnYu answer to wrap notifyDataSetChanged() like this:
val recyclerViewState = recyclerView.layoutManager?.onSaveInstanceState()
adapter.notifyDataSetChanged()
recyclerView.layoutManager?.onRestoreInstanceState(recyclerViewState)
Yes you can resolve this issue by making the adapter constructor only one time, I am explaining the coding part here :
if (appointmentListAdapter == null) {
appointmentListAdapter = new AppointmentListAdapter(AppointmentsActivity.this);
appointmentListAdapter.addAppointmentListData(appointmentList);
appointmentListAdapter.setOnStatusChangeListener(onStatusChangeListener);
appointmentRecyclerView.setAdapter(appointmentListAdapter);
} else {
appointmentListAdapter.addAppointmentListData(appointmentList);
appointmentListAdapter.notifyDataSetChanged();
}
Now you can see I have checked the adapter is null or not and only initialize when it is null.
If adapter is not null then I am assured that I have initialized my adapter at least one time.
So I will just add list to adapter and call notifydatasetchanged.
RecyclerView always holds the last position scrolled, therefore you don't have to store last position, just call notifydatasetchanged, recycler view always refresh data without going to top.
Thanks
Happy Coding
The top answer by #DawnYu works, but the recyclerview will first scroll to the top, then go back to the intended scroll position causing a "flicker like" reaction which isn't pleasant.
To refresh the recyclerView, especially after coming from another activity, without flickering, and maintaining the scroll position, you need to do the following.
Ensure you are updating you recycler view using DiffUtil. Read more about that here: https://www.journaldev.com/20873/android-recyclerview-diffutil
Onresume of your activity, or at the point you want to update your activity, load data to your recyclerview. Using the diffUtil, only the updates will be made on the recyclerview while maintaining it position.
Hope this helps.
Here is an option for people who use DataBinding for RecyclerView.
I have var recyclerViewState: Parcelable? in my adapter. And I use a BindingAdapter with a variation of #DawnYu's answer to set and update data in the RecyclerView:
#BindingAdapter("items")
fun setRecyclerViewItems(
recyclerView: RecyclerView,
items: List<RecyclerViewItem>?
) {
var adapter = (recyclerView.adapter as? RecyclerViewAdapter)
if (adapter == null) {
adapter = RecyclerViewAdapter()
recyclerView.adapter = adapter
}
adapter.recyclerViewState = recyclerView.layoutManager?.onSaveInstanceState()
// the main idea is in this call with a lambda. It allows to avoid blinking on data update
adapter.submitList(items.orEmpty()) {
adapter.recyclerViewState?.let {
recyclerView.layoutManager?.onRestoreInstanceState(it)
}
}
}
Finally, the XML part looks like:
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/possible_trips_rv"
android:layout_width="match_parent"
android:layout_height="0dp"
app:items="#{viewState.yourItems}"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
I was making a mistake like this, maybe it will help someone :)
If you use recyclerView.setAdapter every time new data come, it calls the adapter clear() method every time you use it, which causes the recyclerview to refresh and start over. To get rid of this, you need to use adapter.notiftyDatasetChanced().
1- You need to save scroll position like this
rvProduct.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
recyclerViewState = rvProduct.getLayoutManager().onSaveInstanceState(); // save recycleView state
}
});
2- And after you call notifyDataSetChanged then onRestoreInstanceState like this example
productsByBrandAdapter.addData(productCompareList);
productsByBrandAdapter.notifyDataSetChanged();
rvProduct.getLayoutManager().onRestoreInstanceState(recyclerViewState); // restore recycleView state
I have not used Recyclerview but I did it on ListView. Sample code in Recyclerview:
setOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
rowPos = mLayoutManager.findFirstVisibleItemPosition();
It is the listener when user is scrolling. The performance overhead is not significant. And the first visible position is accurate this way.
Create Extention and use it entirely your App, if you are using DiffUtil you don't need to add adapter.notifyDataSetChanged()
fun RecyclerView.reStoreState(){
val recyclerViewState = this.layoutManager?.onSaveInstanceState()
this.layoutManager?.onRestoreInstanceState(recyclerViewState)
}
Then use it like this below
yourRecyclerView.reStoreState()
adapter.submitList(yourData)
yourRecyclerView.adapter = adapter
#BindingAdapter("items")
fun <T> RecyclerView.setItems(items: List<T>?) {
(adapter as? ListAdapter<T, *>)?.submitList(items) {
layoutManager?.onSaveInstanceState().let {
layoutManager?.onRestoreInstanceState(it)
}
}
}
mMessageAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
#Override
public void onChanged() {
mLayoutManager.smoothScrollToPosition(mMessageRecycler, null, mMessageAdapter.getItemCount());
}
});
The solution here is to keep on scrolling recyclerview when new message comes.
The onChanged() method detects the action performed on recyclerview.
That's working for me in Kotlin.
Create the Adapter and hand over your data in the constructor
class LEDRecyclerAdapter (var currentPole: Pole): RecyclerView.Adapter<RecyclerView.ViewHolder>() { ... }
change this property and call notifyDataSetChanged()
adapter.currentPole = pole
adapter.notifyDataSetChanged()
The scroll offset doesn't change.
If you have one or more EditTexts inside of a recyclerview items, disable the autofocus of these, putting this configuration in the parent view of recyclerview:
android:focusable="true"
android:focusableInTouchMode="true"
I had this issue when I started another activity launched from a recyclerview item, when I came back and set an update of one field in one item with notifyItemChanged(position) the scroll of RV moves, and my conclusion was that, the autofocus of EditText Items, the code above solved my issue.
best.
Just return if the oldPosition and position is same;
private int oldPosition = -1;
public void notifyItemSetChanged(int position, boolean hasDownloaded) {
if (oldPosition == position) {
return;
}
oldPosition = position;
RLog.d(TAG, " notifyItemSetChanged :: " + position);
DBMessageModel m = mMessages.get(position);
m.setVideoHasDownloaded(hasDownloaded);
notifyItemChanged(position, m);
}
I had this problem with a list of items which each had a time in minutes until they were 'due' and needed updating. I'd update the data and then after, call
orderAdapter.notifyDataSetChanged();
and it'd scroll to the top every time. I replaced that with
for(int i = 0; i < orderArrayList.size(); i++){
orderAdapter.notifyItemChanged(i);
}
and it was fine. None of the other methods in this thread worked for me. In using this method though, it made each individual item flash when it was updated so I also had to put this in the parent fragment's onCreateView
RecyclerView.ItemAnimator animator = orderRecycler.getItemAnimator();
if (animator instanceof SimpleItemAnimator) {
((SimpleItemAnimator) animator).setSupportsChangeAnimations(false);
}
I have a question about Android's RecyclerView.State.
I am using a RecyclerView, how could I use and bind it with RecyclerView.State?
My purpose is to save the RecyclerView's scroll position.
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);
}
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
Load the adapter with data
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.
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);
}
}
You don't have to save and restore the state by yourself anymore. If you set unique ID in xml and recyclerView.setSaveEnabled(true) (true by default) system will automatically do it.
Here is more about this: http://trickyandroid.com/saving-android-view-state-correctly/
Beginning from version 1.2.0-alpha02 of androidx recyclerView library, it is now automatically managed. Just add it with:
implementation "androidx.recyclerview:recyclerview:1.2.0-alpha02"
And use:
adapter.stateRestorationPolicy = StateRestorationPolicy.PREVENT_WHEN_EMPTY
The StateRestorationPolicy enum has 3 options:
ALLOW — the default state, that restores the RecyclerView state immediately, in the next layout pass
PREVENT_WHEN_EMPTY — restores the RecyclerView state only when the adapter is not empty (adapter.getItemCount() > 0). If your data is loaded async, the RecyclerView waits until data is loaded and only then the state is restored. If you have default items, like headers or load progress indicators as part of your Adapter, then you should use the PREVENT option, unless the default items are added using MergeAdapter. MergeAdapter waits for all of its adapters to be ready and only then it restores the state.
PREVENT — all state restoration is deferred until you set ALLOW or PREVENT_WHEN_EMPTY.
Note that at the time of this answer, recyclerView library is still in alpha03, but alpha phase is not suitable for production purposes.
Since I find that most options are too long or complicated, here is my short Kotlin option with viewBinding and viewModel for achieving this:
If you want to have your RecyclerView state lifecycle aware, put this code in your ViewModel, otherwise you can use a basic Repository and work from there:
private lateinit var state: Parcelable
fun saveRecyclerViewState(parcelable: Parcelable) { state = parcelable }
fun restoreRecyclerViewState() : Parcelable = state
fun stateInitialized() : Boolean = ::state.isInitialized
and this inside your onPause()
binding.recyclerView.layoutManager?.onSaveInstanceState()?.let { viewModel.saveRecyclerViewState(it) }
and finally this in your onResume()
if (viewModel.stateInitialized()) {
binding.recyclerView.layoutManager?.onRestoreInstanceState(
viewModel.restoreRecyclerViewState()
)
}
Enjoy :)
I've had the same requirement, but the solutions here didn't quite get me across the line, due to the source of data for the recyclerView.
I was extracting the RecyclerViews' LinearLayoutManager state in onPause()
private Parcelable state;
Public void onPause() {
state = mLinearLayoutManager.onSaveInstanceState();
}
Parcelable state is saved in onSaveInstanceState(Bundle outState), and extracted again in onCreate(Bundle savedInstanceState), when savedInstanceState != null.
However, the recyclerView adapter is populated and updated by a ViewModel LiveData object returned by a DAO call to a Room database.
mViewModel.getAllDataToDisplay(mSelectedData).observe(this, new Observer<List<String>>() {
#Override
public void onChanged(#Nullable final List<String> displayStrings) {
mAdapter.swapData(displayStrings);
mLinearLayoutManager.onRestoreInstanceState(state);
}
});
I eventually found that restoring the instance state directly after the data is set in the adapter would keep the scroll-state across rotations.
I suspect this either is because the LinearLayoutManager state that I'd restored was being overwritten when the data was returned by the database call, or the restored state was meaningless against an 'empty' LinearLayoutManager.
If the adapter data is available directly (ie not contigent on a database call), then restoring the instance state on the LinearLayoutManager can be done after the adapter is set on the recyclerView.
The distinction between the two scenarios held me up for ages.
Here is my Approach to save them and maintain the state of recyclerview in a fragment.
First, add config changes in your parent activity as below code.
android:configChanges="orientation|screenSize"
Then in your Fragment declare these instance method's
private Bundle mBundleRecyclerViewState;
private Parcelable mListState = null;
private LinearLayoutManager mLinearLayoutManager;
Add below code in your #onPause method
#Override
public void onPause() {
super.onPause();
//This used to store the state of recycler view
mBundleRecyclerViewState = new Bundle();
mListState =mTrailersRecyclerView.getLayoutManager().onSaveInstanceState();
mBundleRecyclerViewState.putParcelable(getResources().getString(R.string.recycler_scroll_position_key), mListState);
}
Add onConfigartionChanged Method like below.
#Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
//When orientation is changed then grid column count is also changed so get every time
Log.e(TAG, "onConfigurationChanged: " );
if (mBundleRecyclerViewState != null) {
new Handler().postDelayed(new Runnable() {
#Override
public void run() {
mListState = mBundleRecyclerViewState.getParcelable(getResources().getString(R.string.recycler_scroll_position_key));
mTrailersRecyclerView.getLayoutManager().onRestoreInstanceState(mListState);
}
}, 50);
}
mTrailersRecyclerView.setLayoutManager(mLinearLayoutManager);
}
This approch will reslove your problem.
if You have diffrent layout manager then just replace upper layout manager.
On Android API Level 28, I simply ensure that I set up my LinearLayoutManager and RecyclerView.Adapter in my Fragment#onCreateView method, and everything Just Worked™️. I didn't need to do any onSaveInstanceState or onRestoreInstanceState work.
Eugen Pechanec's answer explains why this works.
This is how I restore RecyclerView position with GridLayoutManager after rotation when you need to reload data from internet with AsyncTaskLoader.
Make a global variable of Parcelable and GridLayoutManager and a static final string:
private Parcelable savedRecyclerLayoutState;
private GridLayoutManager mGridLayoutManager;
private static final String BUNDLE_RECYCLER_LAYOUT = "recycler_layout";
Save state of gridLayoutManager in onSaveInstance()
#Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putParcelable(BUNDLE_RECYCLER_LAYOUT,
mGridLayoutManager.onSaveInstanceState());
}
Restore in onRestoreInstanceState
#Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
//restore recycler view at same position
if (savedInstanceState != null) {
savedRecyclerLayoutState = savedInstanceState.getParcelable(BUNDLE_RECYCLER_LAYOUT);
}
}
Then when loader fetches data from internet you restore recyclerview position in onLoadFinished()
if(savedRecyclerLayoutState!=null){
mGridLayoutManager.onRestoreInstanceState(savedRecyclerLayoutState);
}
..of course you have to instantiate gridLayoutManager inside onCreate.
Cheers
For me, the problem was that I set up a new layoutmanager every time I changed my adapter, loosing que scroll position on recyclerView.
You can either use adapter.stateRestorationPolicy = StateRestorationPolicy.PREVENT_WHEN_EMPTY which is introduced in recyclerview:1.2.0-alpha02
https://medium.com/androiddevelopers/restore-recyclerview-scroll-position-a8fbdc9a9334
but it has some issues such as not working with inner RecyclerView, and some other issues you can check out in medium post's comment section.
Or you can use ViewModel with SavedStateHandle which works for inner RecyclerViews, screen rotation and process death.
Create a ViewModel with saveStateHandle
val scrollState=
savedStateHandle.getLiveData<Parcelable?>(KEY_LAYOUT_MANAGER_STATE)
use Parcelable scrollState to save and restore state as answered in other posts or by adding a scroll listener to RecyclerView and
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
scrollState.value = mLayoutManager.onSaveInstanceState()
}
}
I would just like to share the recent predicament I encounter with the RecyclerView. I hope that anyone experiencing the same problem will benefit.
My Project Requirement:
So I have a RecyclerView that list some clickable items in my Main Activity (Activity-A). When the Item is clicked a new Activity is shown with the Item Details (Activity-B).
I implemented in the Manifest file the that the Activity-A is the parent of Activity-B, that way, I have a back or home button on the ActionBar of the Activity-B
Problem:
Every time I pressed the Back or Home button in the Activity-B ActionBar, the Activity-A goes into the full Activity Life Cycle starting from onCreate()
Even though I implemented an onSaveInstanceState() saving the List of the RecyclerView's Adapter, when Activity-A starts it's lifecycle, the saveInstanceState is always null in the onCreate() method.
Further digging in the internet, I came across the same problem but the person noticed that the Back or Home button below the Anroid device (Default Back/Home button), the Activity-A does not goes into the Activity Life-Cycle.
Solution:
I removed the Tag for Parent Activity in the manifest for Activity-B
I enabled the home or back button on Activity-B
Under onCreate() method add this line supportActionBar?.setDisplayHomeAsUpEnabled(true)
In the overide fun onOptionsItemSelected() method, I checked for the item.ItemId on which item is clicked based on the id. The Id for the back button is
android.R.id.home
Then implement a finish() function call inside the android.R.id.home
This will end the Activity-B and bring Acitivy-A without going through the entire life-cycle.
For my requirement this is the best solution so far.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_show_project)
supportActionBar?.title = projectName
supportActionBar?.setDisplayHomeAsUpEnabled(true)
}
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
when(item?.itemId){
android.R.id.home -> {
finish()
}
}
return super.onOptionsItemSelected(item)
}
I had the same problem with a minor difference, I was using Databinding, and I was setting recyclerView adapter and the layoutManager in the binding methods.
The problem was that data-binding was happening after onResume so it was resetting my layoutManager after onResume and that means it was scrolling back to original place every time.
So when I stopped setting layoutManager in Databinding adaptor methods, it works fine again.
Alternative way watch this,
First of all, define variables,
private LinearLayoutManager mLayoutManager;
int lastPosition;
public static final String MyPREFERENCES__DISPLAY = "MyPrefs_display" ;
SharedPreferences sharedpreferences_display;
SharedPreferences.Editor editor_display;
then,
mLayoutManager = new LinearLayoutManager(this);
recyclerView.setLayoutManager(mLayoutManager);
recyclerView.scrollToPosition(sharedpreferences_display.getInt("last_saved_position", 0));
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrollStateChanged(#NonNull RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
lastPosition = mLayoutManager.findFirstVisibleItemPosition();
}
});
if you want to save position by button,
btn.setOnClickListener(v -> {
editor_display.putInt("last_saved_position", lastPosition).apply();
Toast.makeText(Arama_Fav_Activity.this, "Saved", Toast.LENGTH_SHORT).show();
});
In my case I was setting the RecyclerView's layoutManager both in XML and in onViewCreated. Removing the assignment in onViewCreated fixed it.
with(_binding.list) {
// layoutManager = LinearLayoutManager(context)
adapter = MyAdapter().apply {
listViewModel.data.observe(viewLifecycleOwner,
Observer {
it?.let { setItems(it) }
})
}
}
Activity.java:
public RecyclerView.LayoutManager mLayoutManager;
Parcelable state;
mLayoutManager = new LinearLayoutManager(this);
// Inside `onCreate()` lifecycle method, put the below code :
if(state != null) {
mLayoutManager.onRestoreInstanceState(state);
}
#Override
protected void onResume() {
super.onResume();
if (state != null) {
mLayoutManager.onRestoreInstanceState(state);
}
}
#Override
protected void onPause() {
super.onPause();
state = mLayoutManager.onSaveInstanceState();
}
Why I'm using OnSaveInstanceState() in onPause() means, While switch to another activity onPause would be called.It will save that scroll position and restore the position when we coming back from another activity.