6

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 .

Kurdev
  • 49
  • 8
DawnYu
  • 2,278
  • 1
  • 12
  • 14
  • probably just get the first displayed item id and its offset from the layoutmanager, then you can move to the position on recreate and adjust the scroll by the offset. this will probably also work if your dataset changed in the meantime. – David Medenjak Sep 12 '15 at 09:57
  • @bleeding182 thanks.I also have thought this way, but I don't know how to get the offset of an item ? every item has different height – DawnYu Sep 12 '15 at 10:07
  • you can just get the view from the layoutmanager, then read getTop() or getDecoratedTop(), the difference between that and the recyclerview.top will be your offset ;) And even if you dont get the offset right, you''ll be at least on the right position without skipping frames – David Medenjak Sep 12 '15 at 11:02
  • I can not get the item view when I use `lastCompletelyPosition = ((LinearLayoutManager)recyclerView.getLayoutManager()).findLastCompletelyVisibleItemPosition();lastCompleteChild = recyclerView.getChildAt(lastCompletelyPosition);` in `onScrollStateChanged` , `lastCompleteChild.getTop()` will throw `InvocationTargetException` – DawnYu Sep 12 '15 at 14:51
  • look at here : http://stackoverflow.com/questions/35111902/recyclerview-adapter-change-selected-position-in-list-scrolling – Saeid Jan 03 '17 at 19:00

1 Answers1

9

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!

David Medenjak
  • 33,993
  • 14
  • 106
  • 134