Skip to content

Instantly share code, notes, and snippets.

@antonshkurenko
Last active March 27, 2018 11:48
Show Gist options
  • Save antonshkurenko/d2db07820e9ef00ab722327d2fb25f7a to your computer and use it in GitHub Desktop.
Save antonshkurenko/d2db07820e9ef00ab722327d2fb25f7a to your computer and use it in GitHub Desktop.
EndlessScrollListenerBothSides
package io.github.tonyshkurenko.endlessscrollingbothsides;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
* Project: EndlessScrollingBothSides
* Follow me: @tonyshkurenko
*
* @author Anton Shkurenko
* @since 3/27/18
*/
public final class PageProvider {
private static final long SLEEP = TimeUnit.SECONDS.toMillis(1);
private static final int PAGE_SIZE = 100;
final Executor executor = Executors.newSingleThreadExecutor();
public void request(final int page, final PageCallback callback) {
executor.execute(new Runnable() {
@Override public void run() {
try {
Thread.sleep(SLEEP);
} catch (InterruptedException ignored) {
}
final List<Integer> ints = new ArrayList<>(PAGE_SIZE);
for (int i = 0; i < PAGE_SIZE; i++) {
ints.add(page * PAGE_SIZE + i);
}
callback.onSuccess(ints);
}
});
}
public interface PageCallback {
void onSuccess(List<Integer> ints);
}
}
package io.github.tonyshkurenko.endlessscrollingbothsides.pagesprovider;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import io.github.tonyshkurenko.endlessscrollingbothsides.PageProvider;
import io.github.tonyshkurenko.endlessscrollingbothsides.R;
import java.util.List;
public class PagesProviderActivity extends AppCompatActivity {
PagesProviderRecyclerViewAdapter adapter;
@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_pages);
final RecyclerView list = findViewById(R.id.recycler_view);
final LinearLayoutManager layout = new LinearLayoutManager(this);
list.setLayoutManager(layout);
list.setAdapter(adapter = new PagesProviderRecyclerViewAdapter());
final PageProvider provider = new PageProvider();
list.addOnScrollListener(
new TwoWayEndlessPagesProviderScrollListener(adapter, layout, provider));
provider.request(0, new PageProvider.PageCallback() {
@Override public void onSuccess(final List<Integer> ints) {
runOnUiThread(new Runnable() {
@Override public void run() {
adapter.initValues(ints);
}
});
}
});
}
}
package io.github.tonyshkurenko.endlessscrollingbothsides.pagesprovider;
import android.support.annotation.NonNull;
import android.support.v4.util.SparseArrayCompat;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import io.github.tonyshkurenko.endlessscrollingbothsides.BuildConfig;
import io.github.tonyshkurenko.endlessscrollingbothsides.ViewHolder;
import java.util.List;
/**
* Project: EndlessScrollingBothSides
* Follow me: @tonyshkurenko
*
* Create pages after and before
*
* @author Anton Shkurenko
* @since 3/23/18
*/
public class PagesProviderRecyclerViewAdapter extends RecyclerView.Adapter<ViewHolder> {
private static final String TAG = PagesProviderRecyclerViewAdapter.class.getSimpleName();
static final int PAGE_LIMIT = 5;
private final SparseArrayCompat<List<Integer>> pages = new SparseArrayCompat<>();
private int firstPage = 0;
private int currentCount = 0;
private boolean loading = true;
@NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new ViewHolder(LayoutInflater.from(parent.getContext())
.inflate(android.R.layout.simple_list_item_1, parent, false));
}
@Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
holder.bind(getItem(position));
}
@Override public int getItemCount() {
return currentCount;
}
int getItem(int position) {
int count = currentCount;
for (int i = firstPage + pages.size() - 1; i >= firstPage; i--) {
final List<Integer> page = pages.get(i);
final int pageSize = page.size();
if (position >= count - pageSize) {
return page.get(pageSize - (count - position));
} else {
count -= pageSize;
}
}
throw new IllegalStateException("Wrong position");
}
void initValues(List<Integer> vals) {
pages.append(this.firstPage, vals);
currentCount += vals.size();
loading = false;
notifyItemRangeInserted(0, currentCount);
printPages();
}
void appendValues(List<Integer> ints) {
if (BuildConfig.DEBUG) {
Log.d(TAG,
"appendValues() called (before), count: " + currentCount + ", bottom(): " + bottom());
printPages();
}
if (pages.size() >= PAGE_LIMIT) {
pages.remove(firstPage++);
}
pages.append(firstPage + pages.size(), ints);
currentCount += ints.size();
if (BuildConfig.DEBUG) {
Log.d(TAG,
"Notify inserted: idx: " + (currentCount - ints.size()) + ", count: " + currentCount);
}
notifyItemRangeInserted(currentCount - ints.size(), ints.size());
if (BuildConfig.DEBUG) {
Log.d(TAG,
"appendValues() called (after), count: " + currentCount + ", bottom(): " + bottom());
printPages();
}
}
void removeValues(List<Integer> ints) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "removeValues() called (before), count: " + currentCount + ", top(): " + top());
printPages();
}
if (pages.size() >= PAGE_LIMIT) {
final int removedSize = pages.get(firstPage + pages.size() - 1).size();
pages.remove(firstPage + pages.size() - 1);
currentCount -= removedSize;
notifyItemRangeRemoved(currentCount, removedSize);
}
firstPage--;
pages.append(firstPage, ints);
if (BuildConfig.DEBUG) {
Log.d(TAG, "Notify removed: idx: " + (currentCount) + ", count: " + currentCount);
}
if (BuildConfig.DEBUG) {
Log.d(TAG, "removeValues() called (after), count: " + currentCount + ", top(): " + top());
printPages();
}
}
int bottom() {
final int size = pages.size();
if (size > 2) {
return currentCount - pages.get(firstPage + size - 1).size();
} else if (size > 0) {
return currentCount - pages.get(firstPage).size() / 3;
} else {
return 0;
}
}
int top() {
final int size = pages.size();
if (size > 2) {
int count = currentCount;
for (int i = firstPage + pages.size() - 1; i > firstPage; i--) {
final List<Integer> page = pages.get(i);
final int pageSize = page.size();
count -= pageSize;
}
return count;
} else if (size > 0) {
int count = currentCount;
if (size > 1) {
count -= pages.get(firstPage + 1).size();
}
return count - (pages.get(firstPage).size() / 3 * 2);
} else {
return 0;
}
}
void setLoading(boolean loading) {
this.loading = loading;
}
boolean isLoading() {
return loading;
}
int nextPage() {
return firstPage + pages.size();
}
int prevPage() {
return firstPage - 1;
}
private void printPages() {
for (int i = 0; i < pages.size(); i++) {
final int pageNum = pages.keyAt(i);
if (BuildConfig.DEBUG) {
Log.v(TAG, "Page #" + pageNum + ": " + pages.get(pageNum).toString());
}
}
}
}
package io.github.tonyshkurenko.endlessscrollingbothsides.pagesprovider;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import io.github.tonyshkurenko.endlessscrollingbothsides.PageProvider;
import java.util.List;
/**
* Project: EndlessScrollingBothSides
* Follow me: @tonyshkurenko
*
* @author Anton Shkurenko
* @since 3/26/18
*/
public class TwoWayEndlessPagesProviderScrollListener extends RecyclerView.OnScrollListener {
PagesProviderRecyclerViewAdapter mAdapter;
LinearLayoutManager mLayoutManager;
PageProvider mProvider;
public TwoWayEndlessPagesProviderScrollListener(PagesProviderRecyclerViewAdapter adapter,
LinearLayoutManager layoutManager, PageProvider provider) {
mAdapter = adapter;
mLayoutManager = layoutManager;
mProvider = provider;
}
@Override public void onScrolled(final RecyclerView view, int dx, int dy) {
final int lastVisibleItemPosition = mLayoutManager.findLastVisibleItemPosition();
final int firstVisibleItemPosition = mLayoutManager.findFirstVisibleItemPosition();
if (!mAdapter.isLoading()) {
if (dy > 0) { // scroll down
if (lastVisibleItemPosition > mAdapter.bottom()) {
mAdapter.setLoading(true);
mProvider.request(mAdapter.nextPage(), new PageProvider.PageCallback() {
@Override public void onSuccess(final List<Integer> ints) {
view.post(new Runnable() {
@Override public void run() {
mAdapter.setLoading(false);
mAdapter.appendValues(ints);
}
});
}
});
}
} else if (dy < 0) { // scroll top
if (firstVisibleItemPosition < mAdapter.top()) {
mAdapter.setLoading(true);
mProvider.request(mAdapter.prevPage(), new PageProvider.PageCallback() {
@Override public void onSuccess(final List<Integer> ints) {
view.post(new Runnable() {
@Override public void run() {
mAdapter.setLoading(false);
mAdapter.removeValues(ints);
}
});
}
});
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment