Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to save and restore RecyclerView 's scroll exact position? [duplicate]

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 .

like image 309
DawnYu Avatar asked Sep 12 '15 09:09

DawnYu


People also ask

How do I save and restore the scroll position and state of a RecyclerView Android?

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.

How do you retain the scroll position of a RecyclerView on back press from another fragment?

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.

How do you refresh a recycler view?

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.


1 Answers

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!

like image 97
David Medenjak Avatar answered Sep 22 '22 02:09

David Medenjak