RecyclerView scrolls to top on notifyDataSetChanged in chat screen

痞子三分冷 提交于 2019-12-23 06:58:30

问题


I am trying to create messaging kind of screen using recyclerView which will start from bottom and will loadMore data when user reached top end of chat. But I am facing this weird issue.

My recyclerView scrolls to top on calling notifyDataSetChanged. Due to this onLoadMore gets called multiple times.

Here is my code:

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

** In Adapter

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

** In Activity

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

It's just that recyclerView scrolls to top. If scrolling to top stops, this issue will be resolved. Please help me to figure out the cause of this issue. Thanks in advance.


回答1:


I think you shouldn't use onBindViewHolder that way, remove that code, the adapter should only bind model data, not listen scrolling.

I usually do the "onLoadMore" this way:

In the 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;
}

This works perfeclty for me, and the "totallyLoaded" is used if the server doesn't return more messages, to stop making server calls. Hope it helps you.




回答2:


You see, it's natural for List to scroll to the most top item when you insert new Items. Well you going in the right direction but I think you forgot adding setReverseLayout(true).

Here the setStackFromEnd(true) just tells List to stack items starting from bottom of the view but when used in combination with the setReverseLayout(true) it will reverse order of items and views so the newest item is always shown at the bottom of the view.

Your final layoutManager would seems something like this:

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



回答3:


This is my way to avoid scrollview move to top Instead of using notifyDataSetChanged(), I use notifyItemRangeChanged();

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

Update: For another reason, Your another view in the top is focusing so it will jump to top when you call any notifies, so remove all focuses by adding android:focusableInTouchMode="true" in the GroupView.




回答4:


I do not rely on onBindViewHolder for these kind of things. It can be called multiple times for a position. For the lists which has load more option maybe you should use something like this after your recyclerview inflated.

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

Hope it helps.




回答5:


I suggest you to use notifyItemRangeInserted method of RecyclerView.Adapter for LoadMore operations. You add a set of new items to your list so you do not need to notify whole dataset.

notifyItemRangeInserted(int positionStart, int itemCount)

Notify any registered observers that the currently reflected itemCount items starting at positionStart have been newly inserted.

For more information: https://developer.android.com/reference/android/support/v7/widget/RecyclerView.Adapter.html




回答6:


DON'T call notifyDataSetChanged() on the RecyclerView. Use the new methods like notifyItemChanged(), notifyItemRangeChanged(), notifyItemInserted(), etc... And if u use notifyItemRangeInserted()--

don't call setAdapter() method after that..!




回答7:


You need to nofity the item in specific range like below:

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

        }



回答8:


Checkout Firebase Friendlychat source-code on Github.

It behaves like you want, specially at:

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



回答9:


You have this issue because every time your condition be true you call loadMore method even loadMore was in running state, for solving this issue you must put one boolean value in your code and check that too.

check my following code to get more clear.

1- declare one boolean value in your adapter class

2- set it to true in your condition

3- set it to false after you've got data from database and notified your adapter.

so your code must be like as following code:

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

}

in Activity :

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

      chatMessagesAdapter. setLoadingDataInProgress(false);
}

This must fix your problem but I prefer to handle loadMore inside Activity or Presenter class with set addOnScrollListener on RecyclerView and check if findFirstVisibleItemPosition in LayoutManager is 0 then load data.

I've wrote one library for pagination, feel free to use or custom it.

PS: As other user mentioned don't use notifyDataSetChanged because this will refresh all view include visible views that you don't want to refresh those, instead use notifyItemRangeInsert, in your case you must notify from 0 to size of loaded data from database. In your case as you load from top, notifyDataSetChanged will change scroll position to top of new loaded data, so you MUST use notifyItemRangeInsert to get good feel in your app




回答10:


You need to nofity the item in specific range

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

    }


来源:https://stackoverflow.com/questions/43123400/recyclerview-scrolls-to-top-on-notifydatasetchanged-in-chat-screen

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!