web-dev-qa-db-ja.com

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

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

私はRecyclerViewを使用していますが、RecyclerView.Stateとどのように使用およびバインドできますか?

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

90
陈文涛

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];
            }
        };
    }
32

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

編集:GridLayoutManagerで同じAPIを使用することもできます。これは、LinearLayoutManagerのサブクラスであるためです。 @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();
        }
    }
190

格納

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

リストア

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

それがうまくいかない場合は、試してください

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

ストアをonPause()に配置し、onResume()に復元します

73
Gillis Haasnoot

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);
    }
}
18
farhad.kargaran

リストアクティビティから離れてナビゲートし、戻るボタンをクリックして戻るときに、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);
}
17
Braden Holt

もう自分で状態を保存および復元する必要はありません。 xmlで一意のIDを設定し、recyclerView.setSaveEnabled(true)(デフォルトではtrue)を設定すると、システムが自動的に設定します。詳細は次のとおりです。 http://trickyandroid.com/saving-Android-view-state-correctly/

11
AppiDevo

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

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

  • レイアウトマネージャーが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
    }
}

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

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

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

3
Eugen Pechanec

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

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

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

OnSaveInstance()でgridLayoutManagerの状態を保存します

 @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
Farmaker

それらを保存し、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);
}

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

2

Android AP​​Iレベル28では、RecyclerView.AdapterメソッドでLinearLayoutManagerFragment#onCreateViewをセットアップし、Just Worked™️をすべて設定するだけです。 onSaveInstanceStateonRestoreInstanceStateの作業をする必要はありませんでした。

Eugen Pechanecの答え これがなぜ機能するかを説明しています。

1
Heath Borders

私にとっての問題は、アダプターを変更するたびに新しいレイアウトマネージャーを設定し、recyclerViewのキュースクロール位置を失うことでした。

0

私も同じ要件を抱えていましたが、recyclerViewのデータソースが原因で、ここでの解決策ではまったく理解できませんでした。

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

private Parcelable state;

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

パーセル可能なstateonSaveInstanceState(Bundle outState)に保存され、savedInstanceState != nullのときにonCreate(Bundle savedInstanceState)で再度抽出されます。

ただし、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つのシナリオの違いは、長い間私を支えてきました。

0
MikeP

私が遭遇した最近の苦境をRecyclerViewと共有したいと思います。同じ問題を経験している人が利益になることを期待しています。

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

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

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

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

インターネットでさらに掘り下げて、同じ問題に遭遇しましたが、Anroidデバイスの下の[戻る]または[ホーム]ボタン(デフォルトの[戻る]/[ホーム]ボタン)、アクティビティAがアクティビティライフサイクルに入りません。

溶液:

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

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

  3. オーバーライドの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)
}
0
Andy Parinas

わずかな違いで同じ問題が発生しました。Databindingを使用し、バインディングメソッドでrecyclerViewアダプターとlayoutManagerを設定していました。

問題は、データバインディングがonResumeの後に発生したため、onResumeの後にlayoutManagerをリセットしていたため、毎回元の場所にスクロールしていたことです。したがって、DatabindingアダプターメソッドでlayoutManagerの設定を停止すると、再び正常に動作します。

0