刷新回收视图中的数据并保持其滚动位置

如何刷新在 RecyclerView中显示的数据(在其适配器上调用 notifyDataSetChanged)并确保滚动位置被重置到确切的位置?

在良好的 ol’ListView的情况下,它只需要检索 getChildAt(0),检查它的 getTop(),然后用相同的确切数据调用 setSelectionFromTop

It doesn't seem to be possible in case of RecyclerView.

我想我应该使用它的 LayoutManager的确提供了 scrollToPositionWithOffset(int position, int offset),但是什么是正确的方法来检索位置和偏移?

layoutManager.findFirstVisibleItemPosition()layoutManager.getChildAt(0).getTop()

还是有更优雅的方式来完成工作?

153937 次浏览

我没有使用过回收视图,但是我在 ListView 上使用过。 Recyclerview 的代码示例:

setOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
rowPos = mLayoutManager.findFirstVisibleItemPosition();

It is the listener when user is scrolling. The performance overhead is not significant. And the first visible position is accurate this way.

我也有类似的问题,我想出了如下的解决方案。

使用 notifyDataSetChanged是一个坏主意。您应该更具体,然后 RecyclerView将为您保存滚动状态。

例如,如果您只需要刷新,或者换句话说,您希望每个视图都重新绑定,只需执行以下操作:

adapter.notifyItemRangeChanged(0, adapter.getItemCount());

EDIT: To restore the exact same 很明显 position, as in, make it look exactly like it did, we need to do something a bit different (See below how to restore the exact scrollY value):

保存位置并像下面这样偏移:

LinearLayoutManager manager = (LinearLayoutManager) mRecycler.getLayoutManager();
int firstItem = manager.findFirstVisibleItemPosition();
View firstItemView = manager.findViewByPosition(firstItem);
float topOffset = firstItemView.getTop();


outState.putInt(ARGS_SCROLL_POS, firstItem);
outState.putFloat(ARGS_SCROLL_OFFSET, topOffset);

然后像这样恢复卷轴:

LinearLayoutManager manager = (LinearLayoutManager) mRecycler.getLayoutManager();
manager.scrollToPositionWithOffset(mStatePos, (int) mStateOffset);

这将使列表恢复到其确切的 很明显位置。显而易见,因为对于用户来说它看起来是一样的,但是它没有相同的滚动条 Y 值(因为横向/纵向布局尺寸可能不同)。

Note that this only works with LinearLayoutManager.

——-下面是如何恢复确切的滚动条 Y,这可能会使列表看起来不同——-

  1. 这样应用 OnScrollListener:

    private int mScrollY;
    private RecyclerView.OnScrollListener mTotalScrollListener = new RecyclerView.OnScrollListener() {
    @Override
    public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
    super.onScrolled(recyclerView, dx, dy);
    mScrollY += dy;
    }
    };
    

This will store the exact scroll position at all times in mScrollY.

  1. Store this variable in your Bundle, and restore it in state restoration to a different variable, we'll call it mStateScrollY.

  2. After state restoration and after your RecyclerView has reset all its data reset the scroll with this:

    mRecyclerView.scrollBy(0, mStateScrollY);
    

That's it.

Beware, that you restore the scroll to a different variable, this is important, because the OnScrollListener will be called with .scrollBy() and subsequently will set mScrollY to the value stored in mStateScrollY. If you do not do this mScrollY will have double the scroll value (because the OnScrollListener works with deltas, not absolute scrolls).

State saving in activities can be achieved like this:

@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt(ARGS_SCROLL_Y, mScrollY);
}

要还原,请在 onCreate ()中调用:

if(savedState != null){
mStateScrollY = savedState.getInt(ARGS_SCROLL_Y, 0);
}

碎片状态保存的工作方式类似,但实际的状态保存需要一些额外的工作,但有很多文章处理这个问题,所以你应该不难找到如何保存和恢复滚动条的原则保持不变。

我用这个。 ^ _ ^

// Save state
private Parcelable recyclerViewState;
recyclerViewState = recyclerView.getLayoutManager().onSaveInstanceState();


// Restore state
recyclerView.getLayoutManager().onRestoreInstanceState(recyclerViewState);

这样更简单,希望对你有所帮助!

只要返回,如果旧的位置和位置是相同的;

private int oldPosition = -1;


public void notifyItemSetChanged(int position, boolean hasDownloaded) {
if (oldPosition == position) {
return;
}
oldPosition = position;
RLog.d(TAG, " notifyItemSetChanged :: " + position);
DBMessageModel m = mMessages.get(position);
m.setVideoHasDownloaded(hasDownloaded);
notifyItemChanged(position, m);
}

I had this problem with a list of items which each had a time in minutes until they were 'due' and needed updating. I'd update the data and then after, call

orderAdapter.notifyDataSetChanged();

每次都会滚动到顶部,我把它换成了

 for(int i = 0; i < orderArrayList.size(); i++){
orderAdapter.notifyItemChanged(i);
}

还不错。这个线程中的其他方法对我都不管用。但是在使用这个方法时,它在更新每个单独的条目时使其闪烁,因此我还必须将其放在父片段的 onCreateView 中

RecyclerView.ItemAnimator animator = orderRecycler.getItemAnimator();
if (animator instanceof SimpleItemAnimator) {
((SimpleItemAnimator) animator).setSupportsChangeAnimations(false);
}

是的,您可以通过只使用一次适配器构造函数来解决这个问题,我在这里解释编码部分:

if (appointmentListAdapter == null) {
appointmentListAdapter = new AppointmentListAdapter(AppointmentsActivity.this);
appointmentListAdapter.addAppointmentListData(appointmentList);
appointmentListAdapter.setOnStatusChangeListener(onStatusChangeListener);
appointmentRecyclerView.setAdapter(appointmentListAdapter);


} else {
appointmentListAdapter.addAppointmentListData(appointmentList);
appointmentListAdapter.notifyDataSetChanged();
}

现在您可以看到我已经检查了适配器是否为 null,并且只在它为 null 时进行初始化。

If adapter is not null then I am assured that I have initialized my adapter at least one time.

因此,我将只添加列表适配器和调用通知数据集更改。

回收视图总是保持滚动的最后一个位置,因此您不必存储最后一个位置,只需要调用 notifydatasetchange,回收视图总是刷新数据而不需要到顶部。

谢谢 编码愉快

 mMessageAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
@Override
public void onChanged() {
mLayoutManager.smoothScrollToPosition(mMessageRecycler, null, mMessageAdapter.getItemCount());
}
});

这里的解决方案是在收到新消息时继续滚动回收视图。

OnChanged ()方法检测在回收视图上执行的操作。

如果在一个回收视图项目中有一个或多个 EditText,禁用这些项目的自动对焦,将这个配置放在回收视图的 家长视图中:

android:focusable="true"
android:focusableInTouchMode="true"

当我从一个回收视图项目启动另一个活动时,我遇到了这个问题,当我回来并用 notifyItemChanged (position)设置一个项目中一个字段的更新时,RV 移动的滚动,我的结论是,EditText 项目的自动对焦,上面的代码解决了我的问题。

最好的。

保持滚动位置使用@DawnYu 答案来包装 notifyDataSetChanged(),如下所示:

val recyclerViewState = recyclerView.layoutManager?.onSaveInstanceState()
adapter.notifyDataSetChanged()
recyclerView.layoutManager?.onRestoreInstanceState(recyclerViewState)

我在 Kotlin 就是这样。

  1. 创建适配器并在构造函数中移交数据
class LEDRecyclerAdapter (var currentPole: Pole): RecyclerView.Adapter<RecyclerView.ViewHolder>()  { ... }
  1. 更改此属性并调用 notifyDataSetChanged ()
adapter.currentPole = pole
adapter.notifyDataSetChanged()

The scroll offset doesn't change.

@ Dawn Yu 给出的最佳答案是可行的,但是回收视图会首先滚动到顶部,然后回到预定的滚动位置,引起“闪烁”反应,这不是一个令人愉快的反应。

要刷新回收视图,特别是在从另一个活动发出之后,不要闪烁,并保持滚动位置,需要执行以下操作。

  1. Ensure you are updating you recycler view using DiffUtil. Read more about that here: https://www.journaldev.com/20873/android-recyclerview-diffutil
  2. Onresume of your activity, or at the point you want to update your activity, load data to your recyclerview. Using the diffUtil, only the updates will be made on the recyclerview while maintaining it position.

希望这个能帮上忙。

Here is an option for people who use DataBinding for RecyclerView. 我的适配器中有 var recyclerViewState: Parcelable?。我使用 BindingAdapter和@Dawn Yu 答案的变体来设置和更新 RecyclerView中的数据:

@BindingAdapter("items")
fun setRecyclerViewItems(
recyclerView: RecyclerView,
items: List<RecyclerViewItem>?
) {
var adapter = (recyclerView.adapter as? RecyclerViewAdapter)
if (adapter == null) {
adapter = RecyclerViewAdapter()
recyclerView.adapter = adapter
}


adapter.recyclerViewState = recyclerView.layoutManager?.onSaveInstanceState()
// the main idea is in this call with a lambda. It allows to avoid blinking on data update
adapter.submitList(items.orEmpty()) {
adapter.recyclerViewState?.let {
recyclerView.layoutManager?.onRestoreInstanceState(it)
}
}
}

Finally, the XML part looks like:

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/possible_trips_rv"
android:layout_width="match_parent"
android:layout_height="0dp"
app:items="@{viewState.yourItems}"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>

我犯了一个这样的错误,也许它会帮助别人:)

如果每次新数据到来时都使用 recyclerView.setAdapter,那么每次使用时它都会调用适配器 clear()方法,这会导致 recyclerview刷新并重新启动。为了消除这个问题,您需要使用 adapter.notiftyDatasetChanced()

你需要像这样保存滚动位置

rvProduct.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
recyclerViewState = rvProduct.getLayoutManager().onSaveInstanceState(); // save recycleView state
}
});

2-在调用 notifyDataSetChanged 之后,像这个例子一样调用 onRestoreInstanceState

productsByBrandAdapter.addData(productCompareList);
productsByBrandAdapter.notifyDataSetChanged();
rvProduct.getLayoutManager().onRestoreInstanceState(recyclerViewState); // restore recycleView state

创建扩展,并使用它完全您的应用程序,如果您使用的区分工具,您不需要添加 adapter.notifyDataSetChanged()

fun RecyclerView.reStoreState(){
val recyclerViewState = this.layoutManager?.onSaveInstanceState()
this.layoutManager?.onRestoreInstanceState(recyclerViewState)
}

然后像下面这样使用它

yourRecyclerView.reStoreState()
adapter.submitList(yourData)
yourRecyclerView.adapter = adapter
@BindingAdapter("items")
fun <T> RecyclerView.setItems(items: List<T>?) {
(adapter as? ListAdapter<T, *>)?.submitList(items) {
layoutManager?.onSaveInstanceState().let {
layoutManager?.onRestoreInstanceState(it)
}
}
}