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


Answers:


37

您打算如何保存RecyclerView.State

您始终可以依靠OL的良好保存状态。扩展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可以做什么?
陈文涛2015年

4
这真的有效吗?我已经尝试过了,但是却不断遇到这样的运行时异常:MyRecyclerView $ SavedState无法转换为android.support.v7.widget.RecyclerView $ SavedState
Daivid 2015年

2
在还原实例状态上,我们需要像这样将superState传递到其超类(回收视图):super.onRestoreInstanceState((((ScrollPositionSavingRecyclerView.SavedState)state).getSuperState());
micnoy

5
如果扩展RecyclerView,这将不起作用,您将得到BadParcelException。
EpicPandaForce 2015年

1
不需要这样做:如果使用LinearLineManager / GridLayoutManager之类的东西,则“滚动位置”已自动为您保存。(这是LinearLayoutManager.SavedState中的mAnchorPosition)。如果您没有看到该信息,则说明重新应用数据的方式出了点问题。
Brian Yencho '19

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

编辑:您还可以将相同的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();
        }
    }

21
这显然是最好的答案,为什么不接受呢?请注意,任何LayoutManager不仅具有LinearLayoutManager,还具有GridLayoutManager,如果没有,则为假实现。
Paul Woitaschek 2015年

linearLayoutManager.onInstanceState()始终返回null->检查源代码。不幸的是不起作用。
luckyhandler

1
RecyclerView不会在自己的onSaveInstanceState中保存其LayoutManager的状态吗?看着v24的支持库...
androidguy

3
这适用于单个页面,RecyclerView但如果每个页面上都有一个ViewPager,则不会RecycerView
Kris B

1
@Ivan,我认为它可以工作在一个片段中(只是经过检查),但不能工作在片段中onCreateView,但是要在数据加载之后进行。参见@Farmaker答案。
CoolMind

81

商店

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

恢复

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

如果这不起作用,请尝试

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

放入商店onPause() 并还原onResume()


这工作,但你可能需要重新设置lastFirstVisiblePosition,以0恢复它之后。当您有一个片段/活动将一个不同的数据加载到一个RecyclerView文件(例如文件浏览器应用程序)中时,这种情况很有用。
blueware

优秀的!它既满足了从详细信息活动返回到列表的要求,又满足了屏幕旋转时的保存状态
Javi

如果我的recyclerView在里面SwipeRefreshLayout怎么办?
莫罗佐夫

2
为什么第一个还原选项不起作用,而第二个还原选项会起作用?
lucidbrot

1
@MehranJalili似乎是RecyclerView
Vlad

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

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

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

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


RecyclerView/ ScrollView/任何需要有一个android:id对自己的状态进行保存。


这是给我的。我最初设置适配器,然后在可用时更新其数据。如上所述,将其保留直到有可用数据时,它会自动回滚到原来的位置。
NeilS

谢谢@Eugen。是否有关于LayoutManager的保存和还原滚动位置的任何文档?
亚当·赫维兹

@AdamHurwitz您需要什么样的文档?我描述的是LinearLayoutManager的实现细节,因此请阅读其源代码。
Eugen Pechanec

1
这里是一个链接LinearLayoutManager.SavedStatecs.android.com/androidx/platform/frameworks/support/+/…
Eugen Pechanec

谢谢@EugenPechanec。这对我来说可以旋转设备。我还使用了底部导航视图,当我切换到另一个底部选项卡并返回时,该列表再次从头开始。关于这是为什么的任何想法?
schv09

4

这是我保存它们并维护片段中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);
}

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


您可能不需要处理程序来保留数据。生命周期方法足以保存/加载。
Mahi'8

@sayaMahi您能用适当的方式描述吗?我还看到onConfigurationChanged阻止调用destroy方法。所以这不是一个合适的解决方案。
Pawan kumar sharma

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()中恢复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适配器由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上还原实例状态。

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


3

在Android API级别28上,我只需要确保按照自己的方法设置了LinearLayoutManager和方法,并设置了Just Justed™️的所有内容。我不需要做任何事情或工作。RecyclerView.AdapterFragment#onCreateViewonSaveInstanceStateonRestoreInstanceState

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中,但alpha相不适用于生产目的


节省了太多的劳累。另外,我们不得不保存列表单击索引,还原元素,防止在onCreateView中重绘,应用scrollToPosition(或执行其他答案),但是用户回来后
仍会

另外,如果我想在通过应用导航后保存RecyclerView位置,是否有一些工具?
StayCool

@RubenViguera为什么alpha不用于生产目的?我的意思是真的很糟糕吗?
StayCool

1

对我来说,问题在于每次更改适配器时,我都会设置一个新的layoutmanager,从而失去了recyclerView上的滚动位置。


1

我只想分享我在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. 我在活动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)
}

1

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

问题在于onResume之后发生了数据绑定,因此它在onResume之后重置了我的layoutManager,这意味着它每次都滚动回到原始位置。因此,当我停止在数据绑定适配器方法中设置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()in onPause()方式,会在切换到另一个活动时调用onPause。它将保存该滚动位置并在我们从另一个活动返回时恢复该位置。


2
公共静态字段不好。全局数据和单例数据不好。
weston

@weston我现在明白了。我将savestate更改为onpause以删除全局变量。
史蒂夫
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.