AndroidのRecyclerView.Stateについて質問があります。
RecyclerViewを使用していますが、RecyclerView.Stateを使用してバインドするにはどうすればよいですか?
私の目的は、RecyclerViewのスクロール位置を保存することです。
AndroidのRecyclerView.Stateについて質問があります。
RecyclerViewを使用していますが、RecyclerView.Stateを使用してバインドするにはどうすればよいですか?
私の目的は、RecyclerViewのスクロール位置を保存することです。
回答:
最後に保存した位置をどのように保存する予定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];
}
};
}
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();
}
}
RecyclerView
ますが、各ページにViewPager
a がある場合は機能しませんRecycerView
。
onCreateView
ではなく、データのロード後に動作します。ここで@Farmakerの回答を参照してください。
お店
lastFirstVisiblePosition = ((LinearLayoutManager)rv.getLayoutManager()).findFirstCompletelyVisibleItemPosition();
戻す
((LinearLayoutManager) rv.getLayoutManager()).scrollToPosition(lastFirstVisiblePosition);
それでもうまくいかない場合は、
((LinearLayoutManager) rv.getLayoutManager()).scrollToPositionWithOffset(lastFirstVisiblePosition,0)
ストアを入れてonPause()
復元するonResume()
lastFirstVisiblePosition
に0
、それを復元した後。このケースはRecyclerView
、ファイルエクスプローラアプリなど、1つのに異なるデータをロードするフラグメント/アクティビティがある場合に役立ちます。
SwipeRefreshLayout
ですか?
RecyclerView
リストアクティビティから移動して[戻る]ボタンをクリックして戻るときに、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);
}
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);
}
}
自分で状態を保存して復元する必要はもうありません。xmlに一意のIDを設定し、recyclerView.setSaveEnabled(true)(デフォルトではtrue)を設定すると、システムによって自動的に行われます。これについての詳細は次のとおりです。http://trickyandroid.com/saving-android-view-state-correctly/
サポートライブラリにバンドルされているすべてのレイアウトマネージャーは、スクロール位置を保存および復元する方法をすでに知っています。
スクロール位置は、次の条件が満たされたときに発生する最初のレイアウトパスで復元されます。
RecyclerView
RecyclerView
通常、レイアウトマネージャーはXMLで設定するため、ここで行う必要があるのは
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
、その状態のために保存されます。
LinearLayoutManager.SavedState
ます:cs.android.com/androidx/platform/frameworks/support
それらを保存し、フラグメントの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);
}
このアプローチはあなたの問題を愛します。異なるレイアウトマネージャーがある場合は、上部のレイアウトマネージャーを置き換えるだけです。
これは、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をインスタンス化する必要があります。乾杯
私は同じ要件を抱えていましたが、recyclerViewのデータソースのため、ここでのソリューションは私を一線に導いてくれませんでした。
onPause()でRecyclerViewsのLinearLayoutManager状態を抽出していました
private Parcelable state;
Public void onPause() {
state = mLinearLayoutManager.onSaveInstanceState();
}
Parcelable state
はに保存されonSaveInstanceState(Bundle outState)
、に再度抽出されますonCreate(Bundle savedInstanceState)
。savedInstanceState != null
。
ただし、recyclerViewアダプターは、Roomデータベースへの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つのシナリオの違いは、私を長い間抱き締めてきました。
AndroidのAPIレベル28で、私は単に私が私を設定していることを確認LinearLayoutManager
し、RecyclerView.Adapter
私の中でFragment#onCreateView
の方法、およびすべてのものだけでは™️働きました。私は何もする必要もonSaveInstanceState
、onRestoreInstanceState
働く必要もありませんでした。
Eugen Pechanecの回答は、これが機能する理由を説明しています。
androidx recyclerViewライブラリのバージョン1.2.0-alpha02から、自動的に管理されるようになりました。それを追加するだけです:
implementation "androidx.recyclerview:recyclerview:1.2.0-alpha02"
そして使用:
adapter.stateRestorationPolicy = StateRestorationPolicy.PREVENT_WHEN_EMPTY
StateRestorationPolicy列挙には3つのオプションがあります。
この回答の時点では、recyclerViewライブラリはまだalpha03にありますが、アルファ段階は運用目的には適していません。
私にとっての問題は、アダプターを変更するたびに新しいlayoutmanagerをセットアップし、recyclerViewのqueスクロール位置を失うことでした。
私が遭遇した最近の苦境を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がアクティビティライフサイクルに入らないことに気付きました。
解決:
アクティビティBでホームボタンまたは戻るボタンを有効にしました
onCreate()メソッドの下に、次の行を追加しますsupportActionBar?.setDisplayHomeAsUpEnabled(true)
オーバーライドする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)
}
私は小さな違いで同じ問題を抱えていました、私はデータバインディングを使用していました、と私はrecyclerViewアダプタと結合方法でのlayoutManagerを設定しました。
問題は、onResumeの後にデータバインディングが発生していたため、onResumeの後でレイアウトマネージャーがリセットされ、毎回元の場所にスクロールして戻ってしまうことでした。したがって、DataBindingアダプターメソッドでのlayoutManagerの設定を停止すると、再び正常に機能します。
私の場合layoutManager
、XMLとの両方でRecyclerViewを設定していましたonViewCreated
。割り当てを削除してonViewCreated
修正しました。
with(_binding.list) {
// layoutManager = LinearLayoutManager(context)
adapter = MyAdapter().apply {
listViewModel.data.observe(viewLifecycleOwner,
Observer {
it?.let { setItems(it) }
})
}
}
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が呼び出されます。これにより、そのスクロール位置が保存され、別のアクティビティから戻ったときに位置が復元されます。