Sorry for my poor English.
I want the exact position. And after the activity/fragment has been destroyed (no matter I destroy it or the System destroys it), I reopen the activity/fragment, the RecyclerView
can also as the same position as it is been destroyed.
The "same" not means "the item position", because maybe the item just display partly last time.
I want to restore the exact same position.
I have tried some ways below, but no one is perfect.
Someone can help?
1.First way.
I use onScrolled
to calculate the scroll exact position, when the scroll stop, save the position. When spinner changes the dataset or fragment onCreat, restore the position of the chosen dataset. Some datasets may have many many rows.
It can save and restore after the app is destroyed, but there may be too much calculate ?
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, like 111111
, when open the fragment, the RecyclerView will first show the list begin from the first item, and after some delay, it scrolls to the scrollY
position.
How to make it no 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.Second way.
This runs perfectly, but it can only work in the fragment lifecycle.
I try to use ObjectOutputStream
and ObjectInputStream
to save and restore the HashMap
, but it doesn't work.
How to save 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.Third way.
I change the second way, use Bundle
instead of HashMap
, but it has the same question, doesn't work when out of the fragment lifecycle.
I use manually serialize/deserialize android bundle to save bundle
in memory, but when restoring it, it doesn't get the valid bundle
.
======================================================================= The solution
Thanks, everyone!
Finally I solve this problem, you can see it in my code, just see these 2 methods: saveRecyclerViewPosition and restoreWordRecyclerViewPosition .
Restoring scroll position the official way RecyclerView offers a new API to let the Adapter block layout restoration until it is ready. The recyclerview:1.2. 0-alpha02 solution allows you to set a state restoration policy via the StateRestorationPolicy enum. This has 3 options.
You can save a recycler view's state in a Bundle and then when you come back, the state will be there for you to scroll back. If you're using view models, you can save the recycler view state there, and it will be there even if you rotate the screen.
The SwipeRefreshLayout widget is used for implementing a swipe-to-refresh user interface design pattern. Where the user uses the vertical swipe gesture to refresh the content of the views.
So now I had time to waste ;) Here is a full answer:
Basic layout. Yes it could be optimized. That's 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 item and its position. If you have fixed ids you can add logic to look up id / position in adapter for id.
Some Basic Adapter:
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's state however you like, I used SharedPreferences, and read it again on start. I postDelayed the scroll, because it does not work if it scroll to position is immediately followed by it. It's just a basic example, and there is probably a better way if you play around with LayoutManager a bit more.
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 again the correct item, and after those 500 ms jump back to the correct position it was saved.
Be sure to add null checks and the sorts!
Hope this helps!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With