32

If we use DiffUtil.Callback, and do

adapter.setItems(itemList);
diff.dispatchUpdatesTo(adapter);

how can we make sure that adding of new elements will scroll to that new position.

I have a case where I see item disappear, and a new one is created as a first element at the top, but not visible. It is hidden on top until you scroll down to make it visible. Before using DiffUtil, I was implementing this manually, and after I knew I was inserting at some position (on top) I could scroll to.

tynn
  • 38,113
  • 8
  • 108
  • 143
miroslavign
  • 2,033
  • 2
  • 24
  • 26
  • 1
    Just add the item and use `RecyclerView.scrollToPosition(int position)`. Worked when I had to do it, even with DiffUtil. – Nicolas Apr 17 '17 at 23:29

4 Answers4

78

There's an easy way to do this that also preserves the user's scroll position if items are inserted outside the viewable area:

import android.os.Parcelable;

Parcelable recyclerViewState = recyclerView.getLayoutManager().onSaveInstanceState();
// apply diff result here (dispatch updates to the adapter)
recyclerView.getLayoutManager().onRestoreInstanceState(recyclerViewState);              

Using this approach, the new items are made visible if they are inserted where the user can see them -- but the user's viewpoint is preserved if the items are inserted outside of view.

Lorne Laliberte
  • 6,261
  • 2
  • 35
  • 36
  • 2
    Thanks! it works, just save instance state before update items and restore it right after update – Deni Erdyneev Aug 02 '18 at 09:40
  • 2
    Thanks. I use this as workaround for certain edge cases. A tracker is opened regarding unwanted auto scroll - https://issuetracker.google.com/issues/70149059 – Cheok Yan Cheng Oct 03 '18 at 18:29
  • 1
    This should be the accepted answer. Works really well! Thanks a lot! – dknchris Apr 19 '19 at 18:42
  • It seems that this is the only solution that worked out. But why? I like new ListAdapter, but sometimes it is pain in the a... Is it becasuse DiffUtil content compare is wrong? – Antonis Radz Oct 29 '19 at 14:27
23

You have a dispatchUpdatesTo(ListUpdateCallback) method to use as well.

So you could just implement a ListUpdateCallback which gives you the first element inserted

class MyCallback implements ListUpdateCallback {
    int firstInsert = -1;
    Adapter adapter = null;
    void bind(Adapter adapter) {
        this.adapter = adapter;
    }
    public void onChanged(int position, int count, Object payload) {
        adapter.notifyItemRangeChanged(position, count, payload);
    }
    public void onInserted(int position, int count) {
        if (firstInsert == -1 || firstInsert > position) {
            firstInsert = position;
        }
        adapter.notifyItemRangeInserted(position, count);
    }
    public void onMoved(int fromPosition, int toPosition) {
        adapter.notifyItemMoved(fromPosition, toPosition);
    }
    public void onRemoved(int position, int count) {
        adapter.notifyItemRangeRemoved(position, count);
    }
}

and then just scroll the RecyclerView manually

myCallback.bind(adapter)
adapter.setItems(itemList);
diff.dispatchUpdatesTo(myCallback);
recycler.smoothScrollToPosition(myCallback.firstInsert);
tynn
  • 38,113
  • 8
  • 108
  • 143
19

I used RecyclerView.AdapterDataObserver to detect when new items were added, and then scroll to the top of my list;

adapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
    @Override
    public void onItemRangeInserted(int positionStart, int itemCount) {
        super.onItemRangeInserted(positionStart, itemCount);
        recycleView.smoothScrollToPosition(0);
    }
});
gregdev
  • 1,863
  • 3
  • 23
  • 28
  • 4
    This works well if you want to _always_ scroll to top when new items added. Thanks @gregdev – scottyab May 21 '19 at 11:59
  • I ended up with using this one and `onItemRangeRemoved` since I'm ok with always scrolling to top when the range is changed. `onItemRangeChanged` wasn't working correctly for me. Thanks! – Micer Sep 30 '19 at 14:25
0

It worked for me

        recyclerview.setItemAnimator(new DefaultItemAnimator() {
            @Override
            public boolean canReuseUpdatedViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, @NonNull List<Object> payloads) {
                return true;
            }
        });
        adapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
            @Override
            public void onItemRangeInserted(int positionStart, int itemCount) {
                //when a new item is inserted
                if (positionStart == 0 && itemCount == 1/*number of item inserted*/) {
                    scrollToFirst();
                    mRecordListAdapter.unregisterAdapterDataObserver(this);
                }
            }

        });



    private void scrollToFirst() {
        //when an item is added to the first position
        //however as per the recyclerview the view is added out of the visible area
        //(as the 2nd item after update was the first item before list update)
        //thus the newly item won't be visible
        //Therefore if the 1st item is visible then scroll to the first pos
        //so that the newly item can be visible when inserted

        var position = ((LinearLayoutManager) recyclerview.getLayoutManager()).findFirstCompletelyVisibleItemPosition();
        if (position < 1 && position != RecyclerView.NO_POSITION) {
            recyclerview.scrollToPosition(0);
        }
    }

Prem Thakur
  • 72
  • 1
  • 8