package org.edx.mobile.view.adapters;
import android.support.annotation.NonNull;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.AbsListView;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import org.edx.mobile.R;
import org.edx.mobile.model.Page;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
public class InfiniteScrollUtils {
private static final int VISIBILITY_THRESHOLD = 2; // Load more when only this number of items exists off-screen
public static <T> InfiniteListController configureListViewWithInfiniteList(@NonNull final ListView list, @NonNull final ArrayAdapter<T> adapter, @NonNull final PageLoader<T> pageLoader) {
final PageLoadController controller = new PageLoadController<>(
configureListContentController(list, adapter),
pageLoader);
controller.loadMore();
list.setOnScrollListener(new ListViewOnScrollListener(new Runnable() {
@Override
public void run() {
controller.loadMore();
}
}));
return controller;
}
@NonNull
public static <T> ListContentController<T> configureListContentController(@NonNull ListView list, @NonNull final ArrayAdapter<T> adapter) {
final View footerView = LayoutInflater.from(list.getContext()).inflate(R.layout.list_view_footer_progress, list, false);
list.addFooterView(footerView, null, false);
final View loadingIndicator = footerView.findViewById(R.id.loading_indicator);
list.setAdapter(adapter);
return new ListContentController<T>() {
@Override
public void clear() {
adapter.clear();
}
@Override
public void addAll(List<T> items) {
adapter.addAll(items);
}
@Override
public void setProgressVisible(boolean visible) {
loadingIndicator.setVisibility(visible ? View.VISIBLE : View.GONE);
}
};
}
public static <T> InfiniteListController configureRecyclerViewWithInfiniteList(@NonNull final RecyclerView recyclerView, @NonNull final ListContentController<T> adapter, @NonNull final PageLoader<T> pageLoader) {
// Don't allow the progress spinner item positioning changes to be animated
recyclerView.setItemAnimator(null);
final LinearLayoutManager linearLayoutManager = new LinearLayoutManager(recyclerView.getContext());
recyclerView.setLayoutManager(linearLayoutManager);
final PageLoadController controller = new PageLoadController<>(adapter, pageLoader);
controller.loadMore();
recyclerView.addOnScrollListener(new RecyclerViewOnScrollListener(linearLayoutManager, new Runnable() {
@Override
public void run() {
controller.loadMore();
}
}));
return controller;
}
public interface ListContentController<T> {
void clear();
void addAll(List<T> items);
void setProgressVisible(boolean visible);
}
public interface PageLoader<T> {
void loadNextPage(@NonNull PageLoadCallback<T> callback);
}
public static abstract class PageLoadCallback<T> {
/**
* Callback for new page load, which terminates the loading
* controller if it's the last one.
*
* @param newPage The new page.
*/
public final void onPageLoaded(Page<T> newPage) {
onPageLoaded(newPage.getResults(), newPage.hasNext());
}
/**
* Callback for new page load, which assumes that there
* are more to follow.
*
* @param newItems A list of the items in the new page.
*/
public final void onPageLoaded(List<T> newItems) {
onPageLoaded(newItems, true);
}
/**
* Callback for new page load, which terminates the loading
* controller if it's the last one.
*
* @param newItems A list of the items in the new page.
* @param hasMore Whether there are more pages to load.
*/
public abstract void onPageLoaded(List<T> newItems, boolean hasMore);
/**
* Callback for receiving an error during the page load.
*/
public abstract void onError();
/**
* Returns the user visibility status of the page load.
*
* @return <code>true</code> If a silent refresh is being performed,
* <code>false</code> if pagination is being done as usual.
*/
public abstract boolean isRefreshingSilently();
}
public interface InfiniteListController {
void reset();
void resetSilently();
}
public static class PageLoadController<T> implements InfiniteListController {
@NonNull
final ListContentController<T> adapter;
@NonNull
final PageLoader<T> pageLoader;
protected boolean hasMoreItems = true;
protected boolean loading = false;
final AtomicInteger activeLoadId = new AtomicInteger();
public PageLoadController(@NonNull ListContentController<T> adapter, @NonNull PageLoader<T> pageLoader) {
this.adapter = adapter;
this.pageLoader = pageLoader;
}
public void loadMore() {
if (!loading && hasMoreItems) {
loading = true;
onLoadMore();
}
}
/**
* This function simply shows a spinner while loading the next page.
*/
private void onLoadMore() {
onLoadMore(false);
}
/**
* This function allows us to control the visibility of progress and lazily clear adapter
* after a page has loaded.
*
* @param isRefreshingSilently <code>true</code> If we're doing a silent refresh,
* <code>false</code> if pagination is being done as usual.
*/
private void onLoadMore(final boolean isRefreshingSilently) {
final int instanceLoadId = activeLoadId.get();
if (!isRefreshingSilently) {
adapter.setProgressVisible(true);
}
pageLoader.loadNextPage(new PageLoadCallback<T>() {
@Override
public void onPageLoaded(List<T> newItems, boolean hasMore) {
if (isAbandoned()) {
return;
}
if (isRefreshingSilently) {
adapter.clear();
}
adapter.addAll(newItems);
hasMoreItems = hasMore;
if (!hasMoreItems) {
adapter.setProgressVisible(false);
}
loading = false;
}
@Override
public void onError() {
if (isAbandoned()) {
return;
}
adapter.setProgressVisible(false);
hasMoreItems = false;
loading = false;
}
@Override
public boolean isRefreshingSilently() {
return isRefreshingSilently;
}
/**
* Return whether this callback has been abandoned because of the controller being reset.
*
* @return <code>true</code> if the callback has been abandoned otherwise <code>false</code>
*/
private boolean isAbandoned() {
// Disregard result, since reset() was called
return instanceLoadId != activeLoadId.get();
}
});
}
@Override
public void reset() {
initLoading();
adapter.clear();
onLoadMore();
}
@Override
public void resetSilently() {
initLoading();
onLoadMore(true);
}
private void initLoading() {
activeLoadId.incrementAndGet(); // To disregard any in-progress loads
hasMoreItems = true;
loading = true;
}
}
public static class ListViewOnScrollListener implements AbsListView.OnScrollListener {
@NonNull
private final Runnable onScrollPastVisibilityThreshold;
public ListViewOnScrollListener(@NonNull Runnable onScrollPastVisibilityThreshold) {
this.onScrollPastVisibilityThreshold = onScrollPastVisibilityThreshold;
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
if (firstVisibleItem + visibleItemCount >= totalItemCount - VISIBILITY_THRESHOLD) {
onScrollPastVisibilityThreshold.run();
}
}
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
}
}
public static class RecyclerViewOnScrollListener extends RecyclerView.OnScrollListener {
@NonNull
private final LinearLayoutManager mLinearLayoutManager;
@NonNull
private final Runnable onScrollPastLoadThreshold;
public RecyclerViewOnScrollListener(@NonNull LinearLayoutManager linearLayoutManager, @NonNull Runnable onScrollPastLoadThreshold) {
this.mLinearLayoutManager = linearLayoutManager;
this.onScrollPastLoadThreshold = onScrollPastLoadThreshold;
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
final int visibleItemCount = recyclerView.getChildCount();
final int totalItemCount = mLinearLayoutManager.getItemCount();
final int firstVisibleItem = mLinearLayoutManager.findFirstVisibleItemPosition();
if ((totalItemCount - visibleItemCount)
<= (firstVisibleItem + VISIBILITY_THRESHOLD)) {
onScrollPastLoadThreshold.run();
}
}
}
}