RecyclerView.Stateを使用してRecyclerViewのスクロール位置を保存する方法は?


107

AndroidのRecyclerView.Stateについて質問があります。

RecyclerViewを使用していますが、RecyclerView.Stateを使用してバインドするにはどうすればよいですか?

私の目的は、RecyclerViewのスクロール位置保存することです。

回答:


37

最後に保存した位置をどのように保存する予定RecyclerView.Stateですか?

あなたはいつでも古い保存状態に頼ることができます。拡張RecyclerViewやオーバーライドonSaveInstanceState()と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];
            }
        };
    }

1
今、私は別の質問があります、いいえ、いいえ、RecyclerView.Stateは何ができるのですか?
陈文涛

4
これは本当に機能しますか?私はこれを試しましたが、次のようなランタイム例外が発生し続けます:MyRecyclerView $ SavedStateをandroid.support.v7.widget.RecyclerView $ SavedStateにキャストできません
Daivid

2
インスタンスの復元状態では、次のようにsuperStateをスーパークラス(recytclerビュー)に渡す必要があります。
micnoy

5
RecyclerViewを拡張する場合、これは機能しません。BadParcelExceptionが発生します。
EpicPandaForce

1
これは必要ありません。LinearLayoutManager/ GridLayoutManagerなどを使用する場合、「スクロール位置」はすでに自動的に保存されています。(これは、LinearLayoutManager.SavedStateのmAnchorPositionです)。それが表示されない場合は、データを再適用する方法に問題があります。
ブライアンイエン

229

更新

recyclerview:1.2.0-alpha02リリースからスタートしStateRestorationPolicyました。それは与えられた問題へのより良いアプローチかもしれません。

このトピックは、Android開発者向けのメディア記事で取り上げられています

また、@rubén-vigueraは、以下の回答で詳細を共有しました。https://stackoverflow.com/a/61609823/892500

古い答え

LinearLayoutManagerを使用している場合、事前に作成された保存APIのlinearLayoutManagerInstance.onSaveInstanceState()と復元api linearLayoutManagerInstance.onRestoreInstanceState(...)が付属しています。

これで、返されたパーセルをoutStateに保存できます。例えば、

outState.putParcelable("KeyForLayoutManagerState", linearLayoutManagerInstance.onSaveInstanceState());

、保存した状態で復元位置を復元します。例えば、

Parcelable state = savedInstanceState.getParcelable("KeyForLayoutManagerState");
linearLayoutManagerInstance.onRestoreInstanceState(state);

まとめると、最終的なコードは次のようになります。

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());
}

編集:これは、LinearLayoutManagerのサブクラスであるため、GridLayoutManagerでも同じAPIを使用できます。提案をありがとう@wegsehen。

編集:バックグラウンドスレッドでもデータを読み込んでいる場合は、向きを変更すると位置が復元されるように、onPostExecute / onLoadFinishedメソッド内でonRestoreInstanceStateを呼び出す必要があります。

@Override
protected void onPostExecute(ArrayList<Movie> movies) {
    mLoadingIndicator.setVisibility(View.INVISIBLE);
    if (movies != null) {
        showMoviePosterDataView();
        mDataAdapter.setMovies(movies);
      mRecyclerView.getLayoutManager().onRestoreInstanceState(mSavedRecyclerLayoutState);
        } else {
            showErrorMessage();
        }
    }

21
これが明らかに最良の答えです。なぜこれが受け入れられないのですか?LayoutManagerには、LinearLayoutManagerだけでなく、GridLayoutManagerもあり、そうでない場合はfalse実装があることに注意してください。
Paul Woitaschek 2015

linearLayoutManager.onInstanceState()は常にnullを返します->ソースコードを確認してください。残念ながら動作しません。
ラッキーハンドラー2015

1
RecyclerViewは、独自のonSaveInstanceStateでLayoutManagerの状態を保存しませんか?サポートライブラリのv24を見る...
androidguy

3
これは単一で機能しRecyclerViewますが、各ページにViewPagera がある場合は機能しませんRecycerView
クリスB

1
@Ivan、私はそれがフラグメントで動作すると思います(チェックされたばかり)、onCreateViewではなく、データのロード後に動作します。ここで@Farmakerの回答を参照してください。
CoolMind 2018

81

お店

lastFirstVisiblePosition = ((LinearLayoutManager)rv.getLayoutManager()).findFirstCompletelyVisibleItemPosition();

戻す

((LinearLayoutManager) rv.getLayoutManager()).scrollToPosition(lastFirstVisiblePosition);

それでもうまくいかない場合は、

((LinearLayoutManager) rv.getLayoutManager()).scrollToPositionWithOffset(lastFirstVisiblePosition,0)

ストアを入れてonPause() 復元するonResume()


これは動作しますが、あなたはリセットする必要がありますlastFirstVisiblePosition0、それを復元した後。このケースはRecyclerView、ファイルエクスプローラアプリなど、1つのに異なるデータをロードするフラグメント/アクティビティがある場合に役立ちます。
ブルーウェア2015

優れた!詳細なアクティビティからリストに戻ることと、画面回転時の状態を保存することの両方を満たします
Javi

recyclerViewが入っている場合はどうすればよいSwipeRefreshLayoutですか?
モロゾフ

2
最初の復元オプションが機能しないのはなぜですか?
lucidbrot

1
@MehranJaliliのようですRecyclerView
ヴラド

22

リストアクティビティから移動して[戻る]ボタンをクリックして戻るときに、Recycler Viewのスクロール位置を保存したいと思いました。この問題に提供されたソリューションの多くは、必要以上に複雑であるか、私の構成では機能しなかったため、私のソリューションを共有したいと思いました。

まず、インスタンスの状態をonPauseに保存します。このバージョンのonSaveInstanceStateがRecyclerView.LayoutManagerクラスのメソッドであることをここで強調する価値があると思います。

private LinearLayoutManager mLayoutManager;
Parcelable state;

        @Override
        public void onPause() {
            super.onPause();
            state = mLayoutManager.onSaveInstanceState();
        }

これを適切に機能させるための鍵は、アダプターを接続した後に必ずonRestoreInstanceStateを呼び出すことです。ただし、実際のメソッド呼び出しは、多くの人が示しているよりもはるかに単純です。

private void someMethod() {
    mVenueRecyclerView.setAdapter(mVenueAdapter);
    mLayoutManager.onRestoreInstanceState(state);
}

1
それが正確な解決策です。:D
Afjalur Ra​​hman Rana

1
私にとってはonSaveInstanceStateは呼び出されないので、onPauseで状態を保存する方が良いオプションのようです。また、フラグメントに戻るためにバックスタックをポップします。
filthy_wizard

状態のロード用。これはonViewCreatedで行います。デバッグモードでブレークポイントを使用している場合にのみ機能するようです。または、遅延を追加して実行をコルーチンに入れる場合。
filthy_wizard

21

onCreate()で変数を設定し、onPause()でスクロール位置を保存し、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);
    }
}

アイテムのオフセットも保持します。いいね。ありがとう!
t0m


14

サポートライブラリにバンドルされているすべてのレイアウトマネージャーは、スクロール位置を保存および復元する方法をすでに知っています。

スクロール位置は、次の条件が満たされたときに発生する最初のレイアウトパスで復元されます。

  • レイアウトマネージャーが RecyclerView
  • アダプターが RecyclerView

通常、レイアウトマネージャーはXMLで設定するため、ここで行う必要があるのは

  1. アダプターにデータをロードする
  2. 次に、アダプターをRecyclerView

これはいつでも実行できます。Fragment.onCreateViewまたはに制限されていませんActivity.onCreate

例:データが更新されるたびにアダプターが接続されるようにします。

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
    }
}

保存されたスクロール位置は、次のデータから計算されます。

  • 画面上の最初のアイテムのアダプター位置
  • リストの上部からのアイテムの上部のピクセルオフセット(垂直レイアウトの場合)

したがって、たとえば縦向きと横向きでアイテムの寸法が異なる場合など、わずかな不一致が予想されます。


RecyclerView/ ScrollView/持っている必要が何であれandroid:id、その状態のために保存されます。


これで終わりです。アダプターを最初に設定し、使用可能な場合はデータを更新していました。上記で説明したように、データが利用可能になるまでそのままにしておくと、自動的に元の場所に戻ります。
NeilS、

@Eugenに感謝します。LayoutManagerのスクロール位置の保存と復元に関するドキュメントはありますか?
アダムHurwitz

@AdamHurwitzどんなドキュメントを期待しますか?私が説明したのは、LinearLayoutManagerの実装の詳細です。そのソースコードを読んでください。
Eugen Pechanec

1
ここにリンクがありLinearLayoutManager.SavedStateます:cs.android.com/androidx/platform/frameworks/support
Eugen Pechanec

ありがとう、@ EugenPechanec。これは、デバイスのローテーションに役立ちました。また、下部のナビゲーションビューを使用しています。別の下部のタブに切り替えて戻ったとき、リストは最初から再開します。これがなぜかに関するアイデアはありますか?
schv09

4

それらを保存し、フラグメントのrecyclerviewの状態を維持するための私のアプローチを次に示します。まず、以下のコードのように、親アクティビティに構成変更を追加します。

android:configChanges="orientation|screenSize"

次に、フラグメントでこれらのインスタンスメソッドを宣言します

private  Bundle mBundleRecyclerViewState;
private Parcelable mListState = null;
private LinearLayoutManager mLinearLayoutManager;

@onPauseメソッドに以下のコードを追加します

@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);
}

以下のようにonConfigartionChangedメソッドを追加します。

@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);
}

このアプローチはあなたの問題を愛します。異なるレイアウトマネージャーがある場合は、上部のレイアウトマネージャーを置き換えるだけです。


データを保持するためのハンドラーは必要ない場合があります。ライフサイクルメソッドは、保存/読み込みには十分です。
マヒ

@sayaMahiちゃんと説明できますか?また、onConfigurationChangedがdestroyメソッドを呼び出せないことも確認しました。したがって、これは適切な解決策ではありません。
パワンクマールシャルマ

3

これは、AsyncTaskLoaderを使用してインターネットからデータをリロードする必要がある場合に、回転後にGridLayoutManagerを使用してRecyclerViewの位置を復元する方法です。

ParcelableとGridLayoutManagerのグローバル変数と静的な最終文字列を作成します。

private Parcelable savedRecyclerLayoutState;
private GridLayoutManager mGridLayoutManager;
private static final String BUNDLE_RECYCLER_LAYOUT = "recycler_layout";

gridLayoutManagerの状態をonSaveInstance()に保存します。

 @Override
        protected void onSaveInstanceState(Bundle outState) {
            super.onSaveInstanceState(outState);
            outState.putParcelable(BUNDLE_RECYCLER_LAYOUT, 
            mGridLayoutManager.onSaveInstanceState());
        }

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);
        }
    }

次に、ローダーがインターネットからデータをフェッチすると、onLoadFinished()でrecyclerviewの位置を復元します

if(savedRecyclerLayoutState!=null){
                mGridLayoutManager.onRestoreInstanceState(savedRecyclerLayoutState);
            }

..もちろん、onCreate内でgridLayoutManagerをインスタンス化する必要があります。乾杯


3

私は同じ要件を抱えていましたが、recyclerViewのデータソースのため、ここでのソリューションは私を一線に導いてくれませんでした。

onPause()でRecyclerViewsのLinearLayoutManager状態を抽出していました

private Parcelable state;

Public void onPause() {
    state = mLinearLayoutManager.onSaveInstanceState();
}

Parcelable stateはに保存されonSaveInstanceState(Bundle outState)、に再度抽出されますonCreate(Bundle savedInstanceState)savedInstanceState != null

ただし、recyclerViewアダプターは、R​​oomデータベースへのDAO呼び出しによって返されるViewModel LiveDataオブジェクトによって入力および更新されます。

mViewModel.getAllDataToDisplay(mSelectedData).observe(this, new Observer<List<String>>() {
    @Override
    public void onChanged(@Nullable final List<String> displayStrings) {
        mAdapter.swapData(displayStrings);
        mLinearLayoutManager.onRestoreInstanceState(state);
    }
});

最終的に、アダプターにデータが設定された直後にインスタンスの状態を復元すると、ローテーション全体でスクロール状態が維持されることがわかりました。

これはLinearLayoutManager、データベース呼び出しによってデータが返されたときに復元した状態が上書きされていたか、復元された状態が「空の」LinearLayoutManagerに対して無意味だったためと考えられます。

アダプターデータが直接利用できる場合(つまり、データベース呼び出しに隣接していない場合)、アダプターがrecyclerViewに設定された後で、LinearLayoutManagerのインスタンス状態を復元できます。

2つのシナリオの違いは、私を長い間抱き締めてきました。


3

AndroidのAPIレベル28で、私は単に私が私を設定していることを確認LinearLayoutManagerし、RecyclerView.Adapter私の中でFragment#onCreateViewの方法、およびすべてのものだけでは™️働きました。私は何もする必要もonSaveInstanceStateonRestoreInstanceState働く必要もありませんでした。

Eugen Pechanecの回答は、これが機能する理由を説明しています。


2

androidx recyclerViewライブラリのバージョン1.2.0-alpha02から、自動的に管理されるようになりました。それを追加するだけです:

implementation "androidx.recyclerview:recyclerview:1.2.0-alpha02"

そして使用:

adapter.stateRestorationPolicy = StateRestorationPolicy.PREVENT_WHEN_EMPTY

StateRestorationPolicy列挙には3つのオプションがあります。

  • ALLOW —次のレイアウトパスでRecyclerView状態をすぐに復元するデフォルトの状態
  • PREVENT_WHEN_EMPTY —アダプタが空でない場合(adapter.getItemCount()> 0)にのみ、RecyclerView状態を復元します。データが非同期で読み込まれる場合、RecyclerViewはデータが読み込まれるまで待機し、その後、状態が復元されます。あなたのアダプタの一部として、ヘッダーまたはロードの進行状況インジケーターのようなデフォルトのアイテムを持っている場合は、デフォルトの項目が追加されていない限り、あなたは使用して、PREVENTオプションを使用する必要がありますMergeAdapterを。MergeAdapterは、すべてのアダプターの準備が整うのを待ってから、状態を復元します。
  • PREVENT — ALLOWまたはPREVENT_WHEN_EMPTYを設定するまで、すべての状態の復元は延期されます。

この回答の時点では、recyclerViewライブラリはまだalpha03にありますが、アルファ段階は運用目的には適していません


非常に多くの労力が節約されました。そうでなければ、私たちは、リストのクリック率を保存する要素を復元し、onCreateViewに再描画を防止、scrollToPositionを適用する(または他の回答のようにしてください)しなければならなかったし、まだユーザーが戻ってきた後の違いに気づくことができ
ラーフルKahale

また、アプリをナビゲートした後にRecyclerViewの位置を保存したい場合、そのためのツールはありますか?
StayCool

@RubenVigueraなぜアルファはプロダクション用ではないのですか?それは本当に悪いことですか?
StayCool

1

私にとっての問題は、アダプターを変更するたびに新しいlayoutmanagerをセットアップし、recyclerViewのqueスクロール位置を失うことでした。


1

私が遭遇した最近の苦境をRecyclerViewと共有したいと思います。同じ問題を経験している誰もが利益を得ることを願っています。

私のプロジェクト要件:メインアクティビティ(Activity-A)にいくつかのクリック可能なアイテムを一覧表示するRecyclerViewがあります。アイテムをクリックすると、新しいアクティビティがアイテムの詳細とともに表示されます(アクティビティB)。

マニフェストファイルに、Activity-AがActivity-Bの親であることを実装しました。そのようにして、Activity-BのActionBarに戻るボタンまたはホームボタンがあります。

問題:Activity-Bアクションバーの[戻る]または[ホーム]ボタンを押すたびに、Activity-AがonCreate()から始まる完全なアクティビティライフサイクルに入ります。

RecyclerViewのアダプターのリストを保存するonSaveInstanceState()を実装しましたが、アクティビティAがそのライフサイクルを開始すると、onCreate()メソッドでsaveInstanceStateが常にnullになります。

さらにインターネットを調べたところ、同じ問題に遭遇しましたが、Anroidデバイスの下にある[戻る]または[ホーム]ボタン(デフォルトの[戻る/ホーム]ボタン)、アクティビティ-Aがアクティビティライフサイクルに入らないことに気付きました。

解決:

  1. アクティビティBのマニフェストで親アクティビティのタグを削除しました
  2. アクティビティBでホームボタンまたは戻るボタンを有効にしました

    onCreate()メソッドの下に、次の行を追加しますsupportActionBar?.setDisplayHomeAsUpEnabled(true)

  3. オーバーライドするfun onOptionsItemSelected()メソッドで、IDに基づいてアイテムがクリックされたitem.ItemIdを確認しました。戻るボタンのIDは

    android.R.id.home

    次に、android.R.id.home内にfinish()関数呼び出しを実装します

    これにより、アクティビティBが終了し、ライフサイクル全体を通さずにAcitivy-Aが提供されます。

私の要件では、これはこれまでのところ最高のソリューションです。

     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)
}

1

私は小さな違いで同じ問題を抱えていました、私はデータバインディングを使用していました、と私はrecyclerViewアダプタと結合方法でのlayoutManagerを設定しました。

問題は、onResumeの後にデータバインディングが発生していたため、onResumeの後でレイアウトマネージャーがリセットされ、毎回元の場所にスクロールして戻ってしまうことでした。したがって、DataBindingアダプターメソッドでのlayoutManagerの設定を停止すると、再び正常に機能します。


どのようにしてそれを達成しましたか?
Kedar Sukerkar

0

私の場合layoutManager、XMLとの両方でRecyclerViewを設定していましたonViewCreated。割り当てを削除してonViewCreated修正しました。

with(_binding.list) {
//  layoutManager =  LinearLayoutManager(context)
    adapter = MyAdapter().apply {
        listViewModel.data.observe(viewLifecycleOwner,
            Observer {
                it?.let { setItems(it) }
            })
    }
}

-1

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();

    }   

なぜ私が手段を使用OnSaveInstanceState()しているのかonPause()、別のアクティビティへの切り替え中に、Pauseが呼び出されます。これにより、そのスクロール位置が保存され、別のアクティビティから戻ったときに位置が復元されます。


2
Public staticフィールドは適切ではありません。グローバルデータとシングルトンは適切ではありません。
weston

@weston理解しました。savingstateをonpauseに変更して、グローバルを削除しました。
Steve
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.