Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save nesquena/d09dc68ff07e845cc622 to your computer and use it in GitHub Desktop.
Save nesquena/d09dc68ff07e845cc622 to your computer and use it in GitHub Desktop.
Endless RecyclerView scrolling for different layout managers
public abstract class EndlessRecyclerViewScrollListener extends RecyclerView.OnScrollListener {
// The minimum amount of items to have below your current scroll position
// before loading more.
private int visibleThreshold = 5;
// The current offset index of data you have loaded
private int currentPage = 0;
// The total number of items in the dataset after the last load
private int previousTotalItemCount = 0;
// True if we are still waiting for the last set of data to load.
private boolean loading = true;
// Sets the starting page index
private int startingPageIndex = 0;
RecyclerView.LayoutManager mLayoutManager;
public EndlessRecyclerViewScrollListener(LinearLayoutManager layoutManager) {
this.mLayoutManager = layoutManager;
}
public EndlessRecyclerViewScrollListener(GridLayoutManager layoutManager) {
this.mLayoutManager = layoutManager;
visibleThreshold = visibleThreshold * layoutManager.getSpanCount();
}
public EndlessRecyclerViewScrollListener(StaggeredGridLayoutManager layoutManager) {
this.mLayoutManager = layoutManager;
visibleThreshold = visibleThreshold * layoutManager.getSpanCount();
}
public int getLastVisibleItem(int[] lastVisibleItemPositions) {
int maxSize = 0;
for (int i = 0; i < lastVisibleItemPositions.length; i++) {
if (i == 0) {
maxSize = lastVisibleItemPositions[i];
}
else if (lastVisibleItemPositions[i] > maxSize) {
maxSize = lastVisibleItemPositions[i];
}
}
return maxSize;
}
// This happens many times a second during a scroll, so be wary of the code you place here.
// We are given a few useful parameters to help us work out if we need to load some more data,
// but first we check if we are waiting for the previous load to finish.
@Override
public void onScrolled(RecyclerView view, int dx, int dy) {
int lastVisibleItemPosition = 0;
int totalItemCount = mLayoutManager.getItemCount();
if (mLayoutManager instanceof StaggeredGridLayoutManager) {
int[] lastVisibleItemPositions = ((StaggeredGridLayoutManager) mLayoutManager).findLastVisibleItemPositions(null);
// get maximum element within the list
lastVisibleItemPosition = getLastVisibleItem(lastVisibleItemPositions);
} else if (mLayoutManager instanceof GridLayoutManager) {
lastVisibleItemPosition = ((GridLayoutManager) mLayoutManager).findLastVisibleItemPosition();
} else if (mLayoutManager instanceof LinearLayoutManager) {
lastVisibleItemPosition = ((LinearLayoutManager) mLayoutManager).findLastVisibleItemPosition();
}
// If the total item count is zero and the previous isn't, assume the
// list is invalidated and should be reset back to initial state
if (totalItemCount < previousTotalItemCount) {
this.currentPage = this.startingPageIndex;
this.previousTotalItemCount = totalItemCount;
if (totalItemCount == 0) {
this.loading = true;
}
}
// If it’s still loading, we check to see if the dataset count has
// changed, if so we conclude it has finished loading and update the current page
// number and total item count.
if (loading && (totalItemCount > previousTotalItemCount)) {
loading = false;
previousTotalItemCount = totalItemCount;
}
// If it isn’t currently loading, we check to see if we have breached
// the visibleThreshold and need to reload more data.
// If we do need to reload some more data, we execute onLoadMore to fetch the data.
// threshold should reflect how many total columns there are too
if (!loading && (lastVisibleItemPosition + visibleThreshold) > totalItemCount) {
currentPage++;
onLoadMore(currentPage, totalItemCount, view);
loading = true;
}
}
// Call this method whenever performing new searches
public void resetState() {
this.currentPage = this.startingPageIndex;
this.previousTotalItemCount = 0;
this.loading = true;
}
// Defines the process for actually loading more data based on page
public abstract void onLoadMore(int page, int totalItemsCount, RecyclerView view);
}
@Nishanthbabu
Copy link

can u give me the source code

@fengjunren
Copy link

@PLNech
Copy link

PLNech commented May 23, 2016

Thank you for this useful gist!

One small thing: the condition line 57 (mLayoutManager instanceof GridLayoutManager) will always be false, as GridLayoutManager is a subclass of LinearLayoutManager a layout cannot pass this condition without already passing the previous instanceof LinearLayoutManager test.
This being said, findLastVisibleItemPosition is the same for GridLayoutManager and LinearLayoutManager as it is only implemented in the latter, so there is no point in having a special case for GridLayoutManager.

TL; DR: You can remove lines 57 and 58 ;)

@a3adel
Copy link

a3adel commented Aug 2, 2016

You should check if(dy>0) to make sure the user is scrolling down so that the loading is not called each time the scrolls

@rahulshimpi015
Copy link

There is one problem, it does not work with SwipeRefreshLayout. It's working perfect without SwipeRefreshLayout. And I noticed that for first call previousTotalItemCount should be zero. But when I refresh it takes last value. For example, if four items are visible and I refresh then previousTotalItemCount would be four but it should be zero.

@geolyth
Copy link

geolyth commented Sep 22, 2016

how can i obtain same effect on Scroll To Top?

@hahn
Copy link

hahn commented Nov 24, 2016

it not work when recyclerview inside viewpager. how to fix it?
thanks

@ananth10
Copy link

I have to implement Endless scroll for recyclerview but i am showing some content above recyclerview , so i have used recyclerview inside nestedscrollview , if i use like this , recyclerview draws all views at first time. so
endless scroll for calling web service keep on calling. could you suggest me any solution for this?

@CapnSpellcheck
Copy link

The check (lastVisibleItemPosition + visibleThreshold) > totalItemCount has an off-by-one error. I found it by using visibleThreshold = 1 since I don't want to load too early. If totalItemCount == 10 then lastVisibleItemPosition will be 9 at the very bottom, so 9 + 1 = 10 which cannot be greater. You can fix this just by using >=.

@nvcruzhe
Copy link

nvcruzhe commented Apr 4, 2017

@rahulshimpi015

For ones that have used this code plus a SwipeRefreshLayout, notice that once the list has got all the elements on the server sido or data provider, and then you pull to refresh the paginator stops working.

Just check that the variable loading, is true, avoiding to get into the condition set on line 82, also avoiding the method onLoadMore(...) to be executed.

SOLUTION:
Once you do pull to refresh reset all variables to the initial state.
this.currentPage = 0;
this.previousTotalItemCount = 0;
this.loading = false;

once this is done, the code will work like charm!

@dhiyaulhaqZA
Copy link

Thanks alot 👍

@adam-hurwitz
Copy link

What is the recommended page size to use?

@BenjaminRichardson
Copy link

BenjaminRichardson commented Sep 22, 2017

Thank you so much for this snippet at the rest of your guide! I'm finding it hugely useful.

A couple quick questions:
Why do we need the concept of a page? It seems like you could pass along the current total item count or something similar and let the loadNextDataFromApi function determine what "page" needs to be retrieved.

Related to that, I'm curious why you chose to have the scroll listener dictate what is to be retrieved instead of loadNextDataFromApi tracking where it left off and returning the next set of data. I guess they could both work, I was just curious if I missed something.

Thank you again for the great guide!

@Coccoonx
Copy link

Please, any solution of endless scrolling usage with RecyclerView inside a NestedScrollView ?

@AucT
Copy link

AucT commented Nov 12, 2017

don't work after pause or after turn off and on internet

@javieritis
Copy link

the same problem @AucT ..... any solution?

@howkymike
Copy link

It's awesome, but does anyone know how we can handle orientation change using this class?
I tried to use onSaveInstanceState() to save page number but then I'm unable to scroll up - only down :/
( I populate views from the Internet and I use page parameter to access the proper page on a website from which I download content)

@quevon24
Copy link

There is a way to load more when scroll up? Like Facebook mesenger app which appends the messages at top when scroll up

@alphater
Copy link

Hey, great code But I encountered a problem during use: when loading more, it will be repeated many times, I do not know if it is a case, so I modified the code, added an identifier to determine whether Loaded then reset this identifier in resetState.

@sharukhmohammed
Copy link

This doesn't work with NestedScrollView, all the pagination is happening at once, without user intervention.
Waiting for a solution for this great snippet.

@mapo-lp
Copy link

mapo-lp commented Oct 4, 2018

This doesn't work with NestedScrollView, all the pagination is happening at once ...

@wangzhiyuan
Copy link

This doesn't work with NestedScrollView, all the pagination is happening at once ...

I also met

@mobimation
Copy link

mobimation commented Oct 29, 2019

Replying to 2017 post :-) but useful to mention:
BenjaminRichardson asked on 22 Sep 2017:

Why do we need the concept of a page? It seems like you could pass along the current total item count or something similar and let the loadNextDataFromApi function determine what "page" needs to be retrieved.

Related to that, I'm curious why you chose to have the scroll listener dictate what is to be retrieved instead of loadNextDataFromApi tracking where it left off and returning the next set of data. I guess they could both work, I was just curious if I missed something."

This is a "convenience pattern" to play part of implementing apps with a seamless scrolling mechanism.
Usually a provider of JSON data have data organized as a huge array of entries.
A server side API pattern for retrieval is to specify a "size" parameter to specify how many entries you want.
The API interprets the size as the desired size of a "page".
Typical example request URL could be http://nicecompany.com/v6/entities?format=json&size=40
This means you are using a "version 6" of the API as given by the v6 path..
You are here requesting 40 entries of data for some list.
In the returned data you will typically find an array of the 40 entries you requested,
and a pagination record such as "pagination".
It will hold some values intended to help out with the view recycling:
page (current page is "1" since you did not specify one in the URL).
size (the amount if array entries returned. If the amount available is less than 40 then you see this here).
totalhits (the total amount of available entries)
totalpages (the total amount of pages available based on your given amount of 40)
nextpage: An URL that will return the next 40 entries. Notice how your specified page amount is being used
by the API to divide the total data set into pages. The last page may have a lower amount of entries than 40 but > 0.
in case you have reached the last page there will not be any "nextpage" element shown.
Example: nextpage : http://nicecompany.com/v6/entities?format=json&size=40&page=3
This simply returns the amount of entries you need, computed for you based on the page size you specified.
previouspage: In case you have specified a "&page=2" or higher page number part of the URL you will then find a
previouspage element that contains an URL that will give you the previous dataset. This arrangement supports
the view recycling scheme in modern user interfaces where as you scroll your list reach a position
where it is time to fetch data for the next or earlier set of list entries to show. When you give this URL to your
asynchronous background loading service it will begin fetching the array of entries so they are already
available ahead of the point in time when the list eventually reaches the position when the data is needed.
Your recycling handler code then replaces some older piece of cached data with the new data and your list can display the
list as if there is an endless list of entries loaded.

So this server side scheme is playing along with the list implementation pattern to accomplish a seamless user experience.

The basic reason for the paging solution is that the device displaying the list typically does not have enough RAM to hold lots
of data and only the currently visible portion and some amount around that is needed at every moment to satisfy the list component data needs. So they must be fetched in portions that represents a window into the total set of data.

@xxjonesriderxx
Copy link

xxjonesriderxx commented Dec 15, 2019

This does not work with .setStackFromEnd(true)
if you want to add new items to the top wenn swipping down (scrolling up) this do not work propperly

@CoffeeWarrior
Copy link

Thank you for this bit of code & especially the comments in it. I was trying to get a deeper understanding of how this works & the comments really help a great deal :) 👍

@ShareemGelitoTeofilo
Copy link

Awesome code! Thank you for this. Keep up the good work.

@TaslimOseni
Copy link

support add footerview(progressbar) https://gist.github.com/junfengren/f06c6f19e5832c62fe93b61ac8f72ff9

This URL is broken.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment