RecyclerView 在聊天界面调用 notifyDataSetChanged 后自动滚动到顶部

15

我正在尝试使用RecyclerView创建一种类似于消息的屏幕,该屏幕从底部开始,并在用户到达聊天的顶部端时加载更多数据。但是我遇到了这个奇怪的问题。

在调用notifyDataSetChanged时,我的RecyclerView会滚动到顶部。由于这个原因,onLoadMore会被多次调用。

以下是我的代码:

LinearLayoutManager llm = new LinearLayoutManager(context);
llm.setOrientation(LinearLayoutManager.VERTICAL);
llm.setStackFromEnd(true);
recyclerView.setLayoutManager(llm);

** 在适配器中

 @Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
    if (messages.size() > 8 && position == 0 && null != mLoadMoreCallbacks) {
        mLoadMoreCallbacks.onLoadMore();
    }

** 在活动中

@Override
public void onLoadMore() {
    // Get data from database and add into arrayList
      chatMessagesAdapter.notifyDataSetChanged();
}

只是recyclerView滚动到了顶部。如果滚动到顶部停止,这个问题就会解决。请帮我找出问题的原因。提前致谢。


你可以尝试在LayoutManager中添加mLayoutManager.setReverseLayout(true);。 - Muhib Pirani
重新设置您的适配器并进行通知,也许会有所帮助。 - parik dhakan
将addOnScrollListener添加到您的RecyclerView中,并找到当前可见项的正确位置,然后调用onLoadMore()方法,并尝试从您的活动而不是适配器调用此方法。 - Bhavnik
10个回答

5

我认为你不应该那样使用onBindViewHolder,删除那段代码,适配器只应该绑定模型数据,而不是监听滚动。

通常我会这样实现"onLoadMore":

在Activity中:

private boolean isLoading, totallyLoaded; //
RecyclerView mMessages;
LinearLayoutManager manager;
ArrayList<Message> messagesArray;
MessagesAdapter adapter;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    //...
    mMessages.setHasFixedSize(true);
    manager = new LinearLayoutManager(this);
    manager.setStackFromEnd(true);
    mMessages.setLayoutManager(manager);
    mMessages.addOnScrollListener(new RecyclerView.OnScrollListener() {
        @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
            super.onScrolled(recyclerView, dx, dy);

            if (manager.findFirstVisibleItemPosition() == 0 && !isLoading && !totallyLoaded) {
                onLoadMore();
                isLoading = true;
            }
        }
    });
    messagesArray = new ArrayList<>();
    adapter = new MessagesAdapter(messagesArray, this);
    mMessages.setAdapter(adapter);
}


@Override
public void onLoadMore() {
    //get more messages...
    messagesArray.addAll(0, moreMessagesArray);
    adapter.notifyItemRangeInserted(0, (int) moreMessagesArray.size();
    isLoading = false;
}

这对我来说非常有效,如果服务器不返回更多消息,则使用“totallyLoaded”来停止服务器调用。希望它能帮到你。

2

你看,当你插入新项时,List自然会滚动到最顶部的项。虽然你已经朝着正确的方向前进了,但我认为你忘记添加setReverseLayout(true)了。

这里的setStackFromEnd(true)仅告诉List从视图底部开始堆叠项,但是当与setReverseLayout(true)结合使用时,它将反转项目和视图的顺序,因此最新的项目始终显示在视图底部。

你最终的LayoutManager应该像这样:

        mLayoutManager = new LinearLayoutManager(getActivity());
        mLayoutManager.setReverseLayout(true);
        mLayoutManager.setStackFromEnd(true);
        mRecyclerView.setLayoutManager(mLayoutManager);

1

这是我避免滚动视图移动到顶部的方法 我使用notifyItemRangeChanged();而不是notifyDataSetChanged()

List<Object> tempList = new ArrayList<>();
tempList.addAll(mList);
mList.clear();
mList.addAll(tempList);
notifyItemRangeChanged(0, mList.size());

更新: 出于另一个原因,您顶部的另一个视图正在聚焦,因此当您调用任何通知时,它会跳转到顶部,因此请通过在GroupView中添加android:focusableInTouchMode="true"来删除所有聚焦。

1
不要在RecyclerView上调用notifyDataSetChanged()。使用新的方法,如notifyItemChanged()notifyItemRangeChanged()notifyItemInserted()等等... 如果使用notifyItemRangeInserted(),则不要在之后调用setAdapter()方法..!

0

我不依赖于onBindViewHolder处理这些事情。它可能会对一个位置调用多次。对于具有加载更多选项的列表,您可以在recyclerview膨胀后使用类似于这样的东西。

    recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {

        @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
            super.onScrolled(recyclerView, dx, dy);
            if ((((LinearLayoutManager) recyclerView.getLayoutManager()).findFirstCompletelyVisibleItemPosition() == 0)) {
                if (args.listModel.hasMore && null != mLoadMoreCallback && !loadMoreStarted) {
                    mLoadMoreCallbacks.onLoadMore();
                }
            }
        }
    });

希望能有所帮助。


我的loadMore功能运行良好。由于notifyDatasetChanged之后,我的recyclerView会滚动到顶部,因此loadmore函数会被多次调用。如果它不滚动到顶部,它应该可以正常工作。 - user1288005
你的RecyclerView有wrap_content属性吗?如果有,这可能是为了测量目的而发生的。如果不是这个问题,也许使用notifyItemChanged或notifyItemInserted会是解决方案。顺便说一下,对于普通(从顶部开始)列表,我也调用了notifyDatasetChanged来实现我的解决方案,但在调用后它并不会滚动到顶部。它会在最后一个可见项下面添加更多项。 - letitthebee

0
我建议您在编写“LoadMore”操作时使用RecyclerView.AdapternotifyItemRangeInserted方法。这样,您只需通知新添加的一组项目,而不需要通知整个数据集。
notifyItemRangeInserted(int positionStart, int itemCount)

通知所有已注册的观察者,从positionStart开始反映itemCount个项目已经被新插入。
更多信息: https://developer.android.com/reference/android/support/v7/widget/RecyclerView.Adapter.html

1
我已经尝试过这个方法,它可以按照我的要求工作,但有时会出现以下错误:非致命异常:java.lang.IndexOutOfBoundsException: 检测到不一致。无效的项目位置13(偏移量:13)。状态:21 在android.support.v7.widget.RecyclerView $ Recycler.getViewForPosition(RecyclerView.java:4405) 在android.support.v7.widget.RecyclerView $ Recycler.getViewForPosition(RecyclerView.java:4363) - user1288005
我认为你发送的第一个参数(positionStart)有误,因此出现了这个错误。如果在加载更多之前你的列表中有10个项目,并且你加载了另外10个项目,那么你的positionStart应该是10,itemCount是10。 - savepopulation
1
如果这对您有用,如果您接受我的答案并点赞,我会很高兴。谢谢。 - savepopulation

0

你遇到了这个问题,因为每次你的条件成立时,你都会调用loadMore方法,即使loadMore正在运行中,为了解决这个问题,你必须在你的代码中放置一个布尔值并进行检查。

请查看以下代码以获得更清晰的理解。

1- 在你的适配器类中声明一个布尔值

2- 在你的条件中将其设置为true

3- 在从数据库获取数据并通知适配器后将其设置为false。

所以你的代码应该像下面的代码一样:

public class YourAdapter extend RecylerView.Adapter<.....> {

   private boolean loadingDataInProgress = false;


   public void setLoadingDataInProgress(boolean loadingDataInProgress) {
       this.loadingDataInProgress = loadingDataInProgress
   }


   .... 
   // other code


   @Override
   public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
      if (messages.size() > 8 && position == 0 && null != mLoadMoreCallbacks && !loadingDataInProgress){
         loadingDataInProgress = true;
         mLoadMoreCallbacks.onLoadMore();
    }


   ......
   //// other adapter code

}

在Activity中:

@Override
public void onLoadMore() {
      // Get data from database and add into arrayList
      chatMessagesAdapter.notifyDataSetChanged();

      chatMessagesAdapter. setLoadingDataInProgress(false);
}

这个方法可以解决你的问题,但我更喜欢在 Activity 或 Presenter 类中处理 loadMore,并在 RecyclerView 上设置 addOnScrollListener,检查 LayoutManager 中的 findFirstVisibleItemPosition 是否为 0,然后加载数据。

我写了一个 分页库,可以随意使用或自定义。

PS:正如其他用户提到的,不要使用 notifyDataSetChanged,因为这会刷新所有视图,包括您不想刷新的可见视图,而是使用 notifyItemRangeInsert。在您的情况下,您必须从 0 到从数据库加载的数据大小进行通知。 在您的情况下,由于您从顶部加载,notifyDataSetChanged 将更改滚动位置以便显示新加载的数据的顶部,因此您必须使用 notifyItemRangeInsert 来获得良好的应用程序体验。


0
你需要像下面这样通知特定范围内的项目:
       @Override
        public void onLoadMore() {
            // Get data from database and add into arrayList
            List<Messages> messegaes=getFromDB();
            chatMessagesAdapter.setMessageItemList(messages);
            // Notify adapter with appropriate notify methods
            int curSize = chatMessagesAdapter.getItemCount();
            chatMessagesAdapter.notifyItemRangeInserted(curSize,messages.size());

        }

0

查看 Firebase Friendlychat 的源代码请前往 Github

它的表现非常好,尤其是在以下方面:

    mFirebaseAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
        @Override
        public void onItemRangeInserted(int positionStart, int itemCount) {
            super.onItemRangeInserted(positionStart, itemCount);
            int friendlyMessageCount = mFirebaseAdapter.getItemCount();
            int lastVisiblePosition = mLinearLayoutManager.findLastCompletelyVisibleItemPosition();
            // If the recycler view is initially being loaded or the user is at the bottom of the list, scroll
            // to the bottom of the list to show the newly added message.
            if (lastVisiblePosition == -1 ||
                    (positionStart >= (friendlyMessageCount - 1) && lastVisiblePosition == (positionStart - 1))) {
                mMessageRecyclerView.scrollToPosition(positionStart);
            }
        }
    });

0

你需要通知特定范围内的项目

   @Override
    public void onLoadMore() {
        // Get data from database and add into arrayList
        List<Messages> messegaes=getFromDB();
        chatMessagesAdapter.setMessageItemList(messages);
        // Notify adapter with appropriate notify methods
        int curSize = chatMessagesAdapter.getItemCount();
        chatMessagesAdapter.notifyItemRangeInserted(curSize,messages.size());

    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接