【发布时间】:2015-03-05 03:45:15
【问题描述】:
我有一个关于 Android 的 RecyclerView.State 的问题。
我正在使用 RecyclerView,如何使用 RecyclerView.State 并将其与 RecyclerView.State 绑定?
我的目的是保存 RecyclerView 的滚动位置。
【问题讨论】:
标签: android state android-recyclerview
我有一个关于 Android 的 RecyclerView.State 的问题。
我正在使用 RecyclerView,如何使用 RecyclerView.State 并将其与 RecyclerView.State 绑定?
我的目的是保存 RecyclerView 的滚动位置。
【问题讨论】:
标签: android state android-recyclerview
从recyclerview:1.2.0-alpha02 开始发布StateRestorationPolicy 已被引入。这可能是解决给定问题的更好方法。
android developers medium article 已涵盖此主题。
此外,@rubén-viguera 在下面的答案中分享了更多详细信息。 https://stackoverflow.com/a/61609823/892500
如果你使用的是LinearLayoutManager,它自带了预建的save apilinearLayoutManagerInstance.onSaveInstanceState()和restore apilinearLayoutManagerInstance.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();
}
}
【讨论】:
RecyclerView,但如果您在每个页面上都有ViewPager 和RecycerView,则不能。
onCreateView,但在数据加载后。在此处查看@Farmaker 的答案。
商店
lastFirstVisiblePosition = ((LinearLayoutManager)rv.getLayoutManager()).findFirstCompletelyVisibleItemPosition();
恢复
((LinearLayoutManager) rv.getLayoutManager()).scrollToPosition(lastFirstVisiblePosition);
如果这不起作用,请尝试
((LinearLayoutManager) rv.getLayoutManager()).scrollToPositionWithOffset(lastFirstVisiblePosition,0)
把商店放在onPause()
并在onResume()中恢复
【讨论】:
lastFirstVisiblePosition 重置为0。当您有一个片段/活动在一个 RecyclerView 中加载不同的数据时,这种情况很有用,例如,文件资源管理器应用程序。
SwipeRefreshLayout中怎么办?
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];
}
};
}
【讨论】:
我想在离开我的列表活动时保存 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);
}
}
【讨论】:
更新:从1.2.0-alpha02 版本开始,有一个新的 API 可以控制何时发生状态恢复(包括滚动位置)。
RecyclerView.Adapter惰性状态恢复:向 RecyclerView.Adapter 类添加了一个新 API,允许 Adapter 控制何时恢复布局状态。
例如,您可以调用:
myAdapter.setStateRestorationStrategy(StateRestorationStrategy.WHEN_NOT_EMPTY);让 RecyclerView 等到 Adapter 不为空后再恢复滚动位置。
另见:
支持库中捆绑的所有布局管理器都已经知道如何保存和恢复滚动位置。
RecyclerView/ScrollView/whatever 需要有一个android:id 才能保存其状态。
默认情况下,当满足以下条件时,会在第一次布局时恢复滚动位置:
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
}
}
保存的滚动位置由以下数据计算得出:
因此,您可能会遇到轻微的不匹配,例如如果您的商品在纵向和横向上具有不同的尺寸。
【讨论】:
LinearLayoutManager.SavedState的链接:cs.android.com/androidx/platform/frameworks/support/+/…
您不必再自己保存和恢复状态。如果您在 xml 中设置唯一 ID,并且 recyclerView.setSaveEnabled(true)(默认为 true),系统将自动执行此操作。 以下是有关此的更多信息:http://trickyandroid.com/saving-android-view-state-correctly/
【讨论】:
从 androidx recyclerView 库的 1.2.0-alpha02 版本开始,它现在是自动管理的。只需添加:
implementation "androidx.recyclerview:recyclerview:1.2.0-alpha02"
并使用:
adapter.stateRestorationPolicy = StateRestorationPolicy.PREVENT_WHEN_EMPTY
StateRestorationPolicy 枚举有 3 个选项:
请注意,在此回答时,recyclerView 库仍处于 alpha03,但 alpha 阶段不适合生产目的。
【讨论】:
我也有同样的要求,但由于 recyclerView 的数据源,这里的解决方案并没有让我完全理解。
我在 onPause() 中提取 RecyclerViews 的 LinearLayoutManager 状态
private Parcelable state;
Public void onPause() {
state = mLinearLayoutManager.onSaveInstanceState();
}
可打包的state 保存在onSaveInstanceState(Bundle outState) 中,并在savedInstanceState != null 时再次提取到onCreate(Bundle savedInstanceState)。
但是,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 上恢复实例状态。
这两种场景之间的区别让我久久不能释怀。
【讨论】:
这是我保存它们并在片段中保持 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);
}
此方法将解决您的问题。 如果您有不同的布局管理器,则只需替换上层布局管理器。
【讨论】:
在 Android API 级别 28 上,我只需确保在我的 Fragment#onCreateView 方法中设置了我的 LinearLayoutManager 和 RecyclerView.Adapter,以及所有 Just Worked™️。我不需要做任何onSaveInstanceState 或onRestoreInstanceState 的工作。
Eugen Pechanec's answer 解释了为什么会这样。
【讨论】:
由于我发现大多数选项都太长或太复杂,这是我的简短 Kotlin 选项,其中包含 viewBinding 和 viewModel 用于实现此目的:
如果你想让你的 RecyclerView 状态生命周期感知,把这段代码放在你的 ViewModel 中,否则你可以使用一个基本的 Repository 并从那里开始工作:
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()
)
}
享受:)
【讨论】:
当您需要使用 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 上的滚动位置。
【讨论】:
您可以使用recyclerview:1.2.0-alpha02 中介绍的adapter.stateRestorationPolicy = StateRestorationPolicy.PREVENT_WHEN_EMPTY
https://medium.com/androiddevelopers/restore-recyclerview-scroll-position-a8fbdc9a9334
但它存在一些问题,例如无法使用内部 RecyclerView,以及您可以在中型帖子的评论部分查看的其他一些问题。
或者您可以将ViewModel 与SavedStateHandle 一起使用,这适用于内部RecyclerView、屏幕旋转和进程死亡。
使用saveStateHandle 创建一个ViewModel
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()
}
}
【讨论】:
我想分享一下我最近遇到的 RecyclerView 困境。我希望任何遇到同样问题的人都能从中受益。
我的项目要求: 所以我有一个 RecyclerView 列出了我的主要活动(活动-A)中的一些可点击项目。单击该项目时,将显示一个带有项目详细信息 (Activity-B) 的新 Activity。
我在 Manifest 文件中实现了 Activity-A 是 Activity-B 的父级,这样我在 Activity-B 的 ActionBar 上有一个返回或主页按钮 p>
问题: 每次我按下 Activity-B ActionBar 中的 Back 或 Home 按钮时,Activity-A 都会从 onCreate() 开始进入完整的 Activity 生命周期
虽然我实现了一个 onSaveInstanceState() 来保存 RecyclerView 的 Adapter 列表,但当 Activity-A 启动它的生命周期时,在 onCreate() 方法中 saveInstanceState 始终为 null。
在互联网上进一步挖掘,我遇到了同样的问题,但该人注意到 Anroid 设备下方的返回或主页按钮(默认返回/主页按钮),Activity-A 没有进入活动生命周期.
解决方案:
我在 Activity-B 上启用了主页或返回按钮
在 onCreate() 方法下添加这一行 supportActionBar?.setDisplayHomeAsUpEnabled(true)
在overide fun onOptionsItemSelected() 方法中,我根据id 检查了item.ItemId 是哪个项目被点击的。后退按钮的 ID 是
android.R.id.home
然后在android.R.id.home里面实现一个finish()函数调用
这将结束 Activity-B 并带来 Activity-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)
}
【讨论】:
我遇到了同样的问题,但有一点点不同,我使用的是 Databinding,我在绑定方法中设置了 recyclerView 适配器和 layoutManager。
问题是数据绑定是在 onResume 之后发生的,所以它在 onResume 之后重置了我的 layoutManager,这意味着它每次都在滚动回原来的位置。 因此,当我停止在数据绑定适配器方法中设置 layoutManager 时,它又可以正常工作了。
【讨论】:
在我的例子中,我在 XML 和 onViewCreated 中都设置了 RecyclerView 的 layoutManager。删除onViewCreated 中的分配修复了它。
with(_binding.list) {
// layoutManager = LinearLayoutManager(context)
adapter = MyAdapter().apply {
listViewModel.data.observe(viewLifecycleOwner,
Observer {
it?.let { setItems(it) }
})
}
}
【讨论】:
另一种方式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();
});
【讨论】:
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() 的意思是,当切换到另一个活动时会调用onPause。它将保存滚动位置并在我们从另一个活动返回时恢复该位置。
【讨论】: