如何使用RecyclerView.State保存RecyclerView的滚动位置?

How to save RecyclerView's scroll position using RecyclerView.State?

我有一个关于 Android RecyclerView.State 的问题。

我正在使用 RecyclerView,如何使用它并将其与 RecyclerView.State 绑定?

我的目的是保存 RecyclerView 的滚动位置

商店

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

恢复

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

如果不行,试试

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

存储在onPause() 并在 onResume()

中恢复

您打算如何使用 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 developers medium article 上讨论过。

另外,@rubén-viguera 在下面的回答中分享了更多细节。

旧答案

如果您使用的是 LinearLayoutManager,它会预建保存 api linearLayoutManagerInstance.onSaveInstanceState() and restore api linearLayoutManagerInstance.onRestoreInstanceState(...)

有了它,您可以将返回的 parcelable 保存到您的 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());
}

编辑: 您也可以将相同的 api 与 GridLayoutManager 一起使用,因为它是 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();
        }
    }

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

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

    }   

为什么我在 onPause() 中使用 OnSaveInstanceState() 意味着,当切换到另一个 activity onPause 时 called.It 将保存该滚动位置并在我们从另一个 activity.

回来

您不必再自己保存和恢复状态。如果您在 xml 和 recyclerView.setSaveEnabled(true)(默认情况下为 true)中设置唯一 ID,系统将自动执行此操作。 这里有更多关于这个的信息:http://trickyandroid.com/saving-android-view-state-correctly/

我想在离开我的列表时保存 Recycler View 的滚动位置 activity 然后单击后退按钮返回。为这个问题提供的许多解决方案要么比需要的复杂得多,要么不适合我的配置,所以我想我应该分享我的解决方案。

首先将您的实例状态保存在 onPause 中,正如许多人所展示的那样。我认为这里值得强调的是,这个版本的 onSaveInstanceState 是 RecyclerView.LayoutManager class.

中的一个方法
private LinearLayoutManager mLayoutManager;
Parcelable state;

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

使它正常工作的关键是确保在附加适配器后调用 onRestoreInstanceState,正如其他主题中的一些人所指出的那样。然而,实际的方法调用比许多人指出的要简单得多。

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

对我来说,问题是我每次更换适配器时都会设置一个新的布局管理器,从而失去 recyclerView 上的滚动位置。

当您需要使用 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。 干杯

这是我保存它们并在片段中维护 recyclerview 状态的方法。 首先,按照以下代码在您的父 activity 中添加配置更改。

android:configChanges="orientation|screenSize"

然后在你的 Fragment 中声明这些实例方法的

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

此方法将解决您的问题。 如果您有不同的布局管理器,那么只需更换上层布局管理器。

我想分享一下我最近遇到的RecyclerView的困境。我希望任何遇到同样问题的人都会受益。

我的项目要求: 所以我有一个 RecyclerView,它在我的 Main Activity (Activity-A) 中列出了一些可点击的项目。单击项目时,将显示新的 Activity 和项目详细信息 (Activity-B)。

我在 Manifest 文件中实现了 Activity-A 是 Activity-B 的父级,那样的话,我有一个后盾或 Activity-B

的 ActionBar 上的主页按钮

问题: 每次我按下 Activity-B ActionBar 中的返回或主页按钮时,Activity-A 都会进入从 onCreate()

开始的完整 Activity 生命周期

即使我实现了一个 onSaveInstanceState() 来保存 RecyclerView 的适配器列表,当 Activity-A 开始它的生命周期时,在 onCreate() 方法中 saveInstanceState 总是 null。

在互联网上进一步挖掘,我遇到了同样的问题,但那个人注意到 Anroid 设备下方的返回或主页按钮(默认 Back/Home 按钮),Activity-A 确实不进入 Activity 生命周期。

解决方案:

  1. 我在 Activity-B
  2. 的清单中删除了父 Activity 的标签
  3. 我在 Activity-B

    上启用了主页或后退按钮

    在 onCreate() 方法下添加此行 supportActionBar?.setDisplayHomeAsUpEnabled(true)

  4. 在 overide fun onOptionsItemSelected() 方法中,我根据 id 检查了 item.ItemId 单击了哪个项目。后退按钮的 ID 是

    android.R.id.home

    然后在 android.R.id.home

    中实现一个 finish() 函数调用

    这将结束 Activity-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 的数据来源,这里的解决方案并没有完全满足我的要求。

我在 onPause() 中提取 RecyclerViews 的 LinearLayoutManager 状态

private Parcelable state;

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

Parcelable state 保存在 onSaveInstanceState(Bundle outState) 中,并在 onCreate(Bundle savedInstanceState) 中再次提取,当 savedInstanceState != null.

但是,recyclerView 适配器由 DAO 调用 Room 数据库返回的 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 状态在数据库调用返回数据时被覆盖,或者恢复的状态对 'empty' LinearLayoutManager 毫无意义.

如果适配器数据直接可用(即不依赖于数据库调用),则可以在 RecyclerView 上设置适配器后恢复 LinearLayoutManager 上的实例状态。

这两种情况之间的区别让我犹豫了很久。

在 Android API 级别 28,我只是确保在我的 Fragment#onCreateView 方法中设置了我的 LinearLayoutManagerRecyclerView.Adapter,一切都只是工作™️。我不需要做任何 onSaveInstanceStateonRestoreInstanceState 的工作。

解释了为什么这样做。

更新: 自版本 1.2.0-alpha02 以来,有一个新的 API 来控制何时发生状态恢复(包括滚动位置)。

RecyclerView.Adapter lazy state restoration:

Added a new API to the RecyclerView.Adapter class which allows Adapter to control when the layout state should be restored.

For example, you can call:

myAdapter.setStateRestorationStrategy(StateRestorationStrategy.WHEN_NOT_EMPTY);

to make RecyclerView wait until Adapter is not empty before restoring the scroll position.

另请参阅:


支持库中捆绑的所有布局管理器都知道如何保存和恢复滚动位置。

RecyclerView/ScrollView/whatever needs to have a android:id for its state to be saved.

默认情况下,滚动位置会在满足以下条件时发生的第一次布局传递中恢复:

  • 布局管理器附加到 RecyclerView
  • 适配器已连接到 RecyclerView

通常您在 XML 中设置布局管理器,所以您现在要做的就是

  1. 用数据加载适配器
  2. 然后 将适配器连接到 RecyclerView

您可以随时执行此操作,不限于 Fragment.onCreateViewActivity.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
    }
}

保存的滚动位置根据以下数据计算得出:

  • 屏幕上第一项的适配器位置
  • 项目顶部距列表顶部的像素偏移(对于垂直布局)

因此您可能会出现轻微的不匹配,例如如果您的商品在纵向和横向方向上的尺寸不同。

我遇到了同样的问题,但有细微差别,我使用的是 Databinding,我在绑定方法中设置了 recyclerView 适配器和 layoutManager。

问题是数据绑定发生在 onResume 之后,所以它在 onResume 之后重置了我的布局管理器,这意味着它每次都滚动回原来的位置。 所以当我停止在数据绑定适配器方法中设置 layoutManager 时,它再次正常工作。

在我的例子中,我在 XML 和 onViewCreated 中设置了 RecyclerView 的 layoutManager。删除 onViewCreated 中的分配修复了它。

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

从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 会等待数据加载完毕,然后才恢复状态。如果您有默认项目,例如 headers 或加载进度指示器作为适配器的一部分,那么您应该使用 PREVENT 选项,除非默认项目是使用 MergeAdapter 添加的。 MergeAdapter 等待其所有适配器准备就绪,然后才恢复状态。
  • PREVENT — 所有状态恢复都会延迟到您设置 ALLOW 或 PREVENT_WHEN_EMPTY.

请注意,在回答此问题时,recyclerView 库仍处于 alpha03,但alpha 阶段不适合生产目的

您可以使用 adapter.stateRestorationPolicy = StateRestorationPolicy.PREVENT_WHEN_EMPTY recyclerview:1.2.0-alpha02

中介绍的

https://medium.com/androiddevelopers/restore-recyclerview-scroll-position-a8fbdc9a9334

但它有一些问题,例如无法使用内部 RecyclerView,还有一些其他问题,您可以在 medium post 的评论部分查看。

或者您可以将 ViewModelSavedStateHandle 一起使用,这适用于内部 RecyclerView、屏幕旋转和 进程死亡

saveStateHandle

创建一个 ViewModel
val scrollState=
    savedStateHandle.getLiveData<Parcelable?>(KEY_LAYOUT_MANAGER_STATE)

使用 Parcelable scrollState 来保存和恢复状态,如其他 post 中的回答或通过向 RecyclerView 添加滚动监听器和

recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {

        override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
            super.onScrollStateChanged(recyclerView, newState)
            if (newState == RecyclerView.SCROLL_STATE_IDLE) {
               scrollState.value = mLayoutManager.onSaveInstanceState()
            }
        }

因为我发现大多数选项太长或太复杂,这里是我的简短 Kotlin 选项,其中包含 viewBindingviewModel 实现这个:

如果您想了解 RecyclerView 状态生命周期,请将此代码放入您的 ViewModel,否则您可以使用基本存储库并从那里开始工作:

private lateinit var state: Parcelable
fun saveRecyclerViewState(parcelable: Parcelable) { state = parcelable }
fun restoreRecyclerViewState() : Parcelable = state
fun stateInitialized() : Boolean = ::state.isInitialized

这在你的 onPause()

binding.recyclerView.layoutManager?.onSaveInstanceState()?.let { viewModel.saveRecyclerViewState(it) }

最后这个在你的 onResume()

if (viewModel.stateInitialized()) {
        binding.recyclerView.layoutManager?.onRestoreInstanceState(
            viewModel.restoreRecyclerViewState()
        )
    }

尽情享受吧:)

另一种方式watch this,

首先,定义变量,

private LinearLayoutManager mLayoutManager;
int lastPosition;

public static final String MyPREFERENCES__DISPLAY = "MyPrefs_display" ;
SharedPreferences sharedpreferences_display;
SharedPreferences.Editor editor_display;

那么,

mLayoutManager = new LinearLayoutManager(this);
recyclerView.setLayoutManager(mLayoutManager);

recyclerView.scrollToPosition(sharedpreferences_display.getInt("last_saved_position", 0));

recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
    @Override
    public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
        super.onScrollStateChanged(recyclerView, newState);
        lastPosition = mLayoutManager.findFirstVisibleItemPosition();
    }
});

如果您想通过按钮保存位置,

btn.setOnClickListener(v -> {
    editor_display.putInt("last_saved_position", lastPosition).apply();
    Toast.makeText(Arama_Fav_Activity.this, "Saved", Toast.LENGTH_SHORT).show();
});