How to save and restore RecyclerView scroll accurate position?

Sorry for the bad english.

I want an exact position. And after the activity / fragment has been destroyed (regardless of whether I destroy it or the system destroys it), I open the activity / fragment again, RecyclerView can also the same position, since it was destroyed.

“same” does not mean “position position”, because perhaps the item is only displayed for the last time.

I want to restore exactly the same position.

I tried several ways below, but no one is perfect.

Can anybody help?

1. The first way.

I use onScrolled to calculate the exact scroll position when stopping the scroll, save the position. When the spinner changes the dataset or onCreat fragment, restore the position of the selected dataset. Some datasets can have many rows.

It can save and restore after the application is destroyed, but can there be too many calculations?

It will cause

Skipped 60 frames! The application may be doing too much work on its main thread. attempt to finish an input event but the input event receiver has already been disposed. android total arena pages for jit.

And if scrollY is huge, for example 111111 , when you open the RecyclerView fragment, it will first display the list starting from the first element, and after some delay it will scroll to scrollY position.

How to do this without delay?

  recyclerView.addOnScrollListener(new OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, final int dy) { super.onScrolled(recyclerView, dx, dy); handler.post(new Runnable() { @Override public void run() { if (isTableSwitched) { scrollY = 0; isTableSwitched = false; } scrollY = scrollY + dy; Log.i("dy——scrollY——table", String.valueOf(dy) + "——" + String.valueOf(scrollY) + tableName); } }); } @Override public void onScrollStateChanged(RecyclerView recyclerView, int newState) { super.onScrollStateChanged(recyclerView, newState); switch (newState) { case RecyclerView.SCROLL_STATE_IDLE: saveRecyclerPosition(tableName, scrollY); break; case RecyclerView.SCROLL_STATE_DRAGGING: break; case RecyclerView.SCROLL_STATE_SETTLING: break; } } }); public void saveRecyclerPosition(String tableName, int y) { prefEditorSettings.putInt(KEY_SCROLL_Y + tableName, y); prefEditorSettings.commit(); } public void restoreRecyclerViewState(final String tableName) { handler.post(new Runnable() { @Override public void run() { recyclerView.scrollBy(0, prefSettings.getInt(KEY_SCROLL_Y + tableName, 0)); } }); } //use Spinner to choose dataset spnWordbook.setOnItemSelectedListener(new OnItemSelectedListener() { @Override public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { isTableSwitched = true; tableName= parent.getItemAtPosition(position).toString(); prefEditorSettings.putString(KEY_SPINNER_SELECTED_TABLENAME, tableName); prefEditorSettings.commit(); wordsList = WordsManager.getWordsList(tableName); wordCardAdapter.updateList(wordsList); restoreRecyclerViewState(tableName); } @Override public void onNothingSelected(AdapterView<?> parent) { } }); 

2. The second way.

This works fine, but it can only work in the fragment life cycle.

I am trying to use ObjectOutputStream and ObjectInputStream to save and restore a HashMap , but it does not work.

How to keep it in memory?

 private HashMap <String, Parcelable> hmRecyclerViewState = new HashMap<String, Parcelable>(); public void saveRecyclerPosition(String tableName) { recyclerViewState = recyclerView.getLayoutManager().onSaveInstanceState(); hmRecyclerViewState.put(tableName, recyclerViewState); } public void restoreRecyclerViewState(final String tableName) { if ( hmRecyclerViewState.get(tableName) != null) { recyclerViewState = hmRecyclerViewState.get(tableName); recyclerView.getLayoutManager().onRestoreInstanceState(recyclerViewState); recyclerViewState = null; } 

3. The third way.

I am changing the second method, use the Bundle instead of the HashMap , but it has the same issue, does not work when from the fragment life cycle.

I use to manually serialize / deserialize the android package to save the Bundle in memory, but when it is restored, it does not receive a valid Bundle .

==================================================== ======================= Solution

Thanks everyone!

Finally, I will solve this problem, you can see it in my code , just see these 2 methods: saveRecyclerViewPosition and restoreWordRecyclerViewPosition .

+5
source share
1 answer

So now I had time to spend;) Here is the complete answer:

The main layout. Yes, it can be optimized. This is not the point;)

 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" tools:context=".MainActivity"> <android.support.v7.widget.RecyclerView android:id="@+id/recycler" android:layout_width="match_parent" android:layout_height="match_parent"/> </LinearLayout> 

What you want to do is get the first visible element and its position. If you have fixed identifiers, you can add logic to search for id / position in the adapter for id.

Some basic adapters:

 public class TestAdapter extends RecyclerView.Adapter { @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { return new RecyclerView.ViewHolder(LayoutInflater.from(parent.getContext()).inflate(android.R.layout.simple_list_item_1, parent, false)) { }; } @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { ((TextView) holder.itemView).setText("Position " + position); } @Override public int getItemCount() { return 30; } } 

You save it however you want, I used SharedPreferences and read it again at startup. I postDelayed the scroll, because it does not work, if it scrolls the position, it immediately follows it. This is just a basic example, and probably the best way if you play a little with LayoutManager.

 import android.content.SharedPreferences; import android.os.Bundle; import android.os.Handler; import android.preference.PreferenceManager; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.util.Log; import android.view.View; public class MainActivity extends AppCompatActivity { private static final String TAG = "MainActivity"; private RecyclerView recyclerView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); recyclerView = (RecyclerView) findViewById(R.id.recycler); recyclerView.setLayoutManager(new LinearLayoutManager(this)); recyclerView.setAdapter(new TestAdapter()); } @Override protected void onResume() { super.onResume(); final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); recyclerView.scrollToPosition(preferences.getInt("position", 0)); new Handler().postDelayed(new Runnable() { @Override public void run() { recyclerView.scrollBy(0, - preferences.getInt("offset", 0)); } }, 500); } @Override protected void onPause() { super.onPause(); SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); View firstChild = recyclerView.getChildAt(0); int firstVisiblePosition = recyclerView.getChildAdapterPosition(firstChild); int offset = firstChild.getTop(); Log.d(TAG, "Postition: " + firstVisiblePosition); Log.d(TAG, "Offset: " + offset); preferences.edit() .putInt("position", firstVisiblePosition) .putInt("offset", offset) .apply(); } } 

What this will do, it will display the correct element again, and after these 500 ms have returned to the correct position, it has been saved.

Be sure to add zero checks and sorts!

Hope this helps!

+6
source

All Articles