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

时间:2015-01-07 09:30:57

标签: android state android-recyclerview

我对Android' RecyclerView.State提出了一个问题。

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

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

18 个答案:

答案 0 :(得分:186)

如果您使用的是LinearLayoutManager,则会附带预先构建的save api linearLayoutManagerInstance.onSaveInstanceState()并恢复api linearLayoutManagerInstance.onRestoreInstanceState(...)

这样,您可以将返回的parcelable保存到outState。如,

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

,并使用您保存的状态恢复恢复位置。 e.g,

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

答案 1 :(得分:72)

存储

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

恢复

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

如果这不起作用,请尝试

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

将商店放入onPause() 并在onResume()

中恢复

答案 2 :(得分:32)

您打算如何使用RecyclerView.State保存上次保存的位置?

你总是可以依靠ol&#39;好的保存状态。扩展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];
            }
        };
    }

答案 3 :(得分:18)

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

答案 4 :(得分:17)

我希望在导航离开列表活动后再保存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);
}

答案 5 :(得分:11)

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

答案 6 :(得分:8)

从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中,但是 alpha阶段不适合用于生产目的

答案 7 :(得分:3)

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

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

此方法将解决您的问题。 如果您有不同的布局管理器,则只需更换上部布局管理器即可。

答案 8 :(得分:3)

当您需要使用AsyncTaskLoader从Internet重新加载数据时,这是我在轮换后使用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);
        }
    }

然后,当加载程序从Internet获取数据时,您将恢复onLoadFinished()中的recyclelerview位置

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

..当然你必须在onCreate中实例化gridLayoutManager。 干杯

答案 9 :(得分:2)

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

在满足以下条件时会在第一次布局遍历中恢复滚动位置:

  • 布局管理器已附加到RecyclerView
  • 将adpater附加到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
    }
}

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

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

因此,您可能会遇到轻微的不匹配,例如如果您的商品的纵向和横向尺寸不同。

答案 10 :(得分:1)

在Android API级别28上,我只需要确保通过LinearLayoutManager方法设置RecyclerView.AdapterFragment#onCreateView以及所有Just Worked™️设置。我不需要做任何onSaveInstanceStateonRestoreInstanceState的工作。

Eugen Pechanec's answer解释了它为什么起作用。

答案 11 :(得分:1)

我有一个相同的问题,只是稍有不同,我正在使用数据绑定,并且我在绑定方法中设置了recyclerView适配器和layoutManager。

问题在于 onResume 之后发生了数据绑定,因此它在onResume之后重置了我的layoutManager,这意味着它每次都滚动回到原始位置。 因此,当我停止在数据绑定适配器方法中设置layoutManager时,它又能正常工作。

答案 12 :(得分:0)

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

答案 13 :(得分:0)

我只想分享我在RecyclerView中遇到的困境。我希望遇到同样问题的任何人都会受益。

我的项目要求: 因此,我有一个RecyclerView,该列表列出了我的主活动(Activity-A)中的一些可单击项。单击“项目”后,将显示一个新的“活动”以及“项目详细信息”(活动B)。

我在清单文件中实现了Activity-A是Activity-B的父级,这样,我在Activity-B的ActionBar上具有后退或主页按钮

问题: 每当我按下Activity-B ActionBar中的“后退”或“主页”按钮时,Activity-A就会进入完整的Activity生命周期,从onCreate()开始

即使我实现了保存RecyclerView适配器列表的onSaveInstanceState(),当Activity-A启动生命周期时,onCreate()方法中的saveInstanceState始终为null。

在Internet上进行进一步的挖掘时,我遇到了相同的问题,但该人注意到Anroid设备下方的“后退”或“主页”按钮(默认的“后退/主页”按钮),Activity-A并未进入“活动生命周期”

解决方案:

  1. 我在活动B的清单中删除了父活动的标签
  2. 我在Activity-B上启用了主页或后退按钮

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

  3. 在上面的有趣onOptionsItemSelected()方法中,我根据id检查了单击了哪个item的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)
}

答案 14 :(得分:0)

我有相同的要求,但是由于recyclerView的数据源,这里的解决方案并不能使我完全理解。

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

private Parcelable state;

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

可拆分的state保存在onSaveInstanceState(Bundle outState)中,并在onCreate(Bundle savedInstanceState)时再次在savedInstanceState != null中提取。

但是,recyclerView适配器是由ViewModel LiveData对象填充和更新的,该对象是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状态已被覆盖,或者对于“空” LinearLayoutManager而言,还原的状态无意义。

如果适配器数据直接可用(即,在数据库调用中不连续),则可以在recyclerView上设置适配器后,在LinearLayoutManager上恢复实例状态。

两种情况之间的区别使我久违了。

答案 15 :(得分:0)

就我而言,我同时在XML和layoutManager中设置了RecyclerView的onViewCreated。删除onViewCreated中的分配即可解决问题。

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

答案 16 :(得分:0)

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

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

但是它有一些问题,例如不能与内部RecyclerView一起使用,还有一些其他问题,您可以在中篇文章的评论部分中查看。

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

使用ViewModel创建一个saveStateHandle

val scrollState=
    savedStateHandle.getLiveData<Parcelable?>(KEY_LAYOUT_MANAGER_STATE)

使用Parcelable scrollState来保存和恢复状态,如在其他帖子中所回答的那样,或者通过向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()
            }
        }

答案 17 :(得分:-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()意味着,在切换到另一个活动onPause时会调用它。当我们从另一个活动回来时,它将保存该滚动位置并恢复位置。 / p>