Last active
March 27, 2018 11:48
-
-
Save antonshkurenko/d2db07820e9ef00ab722327d2fb25f7a to your computer and use it in GitHub Desktop.
EndlessScrollListenerBothSides
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
}); | |
} | |
}); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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