Put an indeterminate progressbar as footer in a RecyclerView grid

AndroidFooterAndroid Recyclerview

Android Problem Overview


How to get indeterminate circular indicator for "Scroll up to load more" in a grid RecycleView?

The pattern is described there: http://www.google.com/design/spec/components/progress-activity.html#progress-activity-behavior in "Two-phased loads" and "Example 2: Scroll up to load more" example videos.

I'm trying to accomplish this using the new RecyclerView, but I can't find a "not-too-hackish" way to do that, firstly because there is not a way to add a footer that cover a full row in the grid. Any suggestions?

Android Solutions


Solution 1 - Android

It is very simple to do that.

The solution is to use the same approach of the LinearLayoutManager with a GridLayoutManager and then use the method setSpanSizeLookup on the LayoutManager like this:

mLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
        @Override
        public int getSpanSize(int position) {
            switch(myAdapter.getItemViewType(position)){
                case MyAdapter.VIEW_TYPES.Product:
                    return 1;
                case MyAdapter.VIEW_TYPES.Progress:
                    return 2; //number of columns of the grid
                default:
                    return -1;
            }
        }
    });

This will automatically make the item cover a full row of the grid (if the row is not totally empty this item goes to the next row).

Solution 2 - Android

Note solution below has some potential issues and limitation, for revised solution please check this one Adding items to Endless Scroll RecyclerView with ProgressBar at bottom

Here is solution I came up recently: the idea is to have RecyclerView with 2 type of items one is our usual items the second is progress bar, then we need to listen scroll event and decide are we going to load more and show progressbar or not. So from idea to the example code

progress_item.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:layout_width="match_parent"
              android:layout_height="match_parent">

    <ProgressBar
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/progressBar"
        android:indeterminate="true"
        style="@android:style/Widget.Holo.ProgressBar"
        android:layout_gravity="center_horizontal"/>
</LinearLayout>

activity_main.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                xmlns:tools="http://schemas.android.com/tools"
                xmlns:ring="http://schemas.android.com/apk/res-auto"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                tools:context=".MainActivity">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/my_recycler_view"
        android:scrollbars="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
</RelativeLayout>

EndlessRecyclerOnScrollListener.java

public abstract class EndlessRecyclerOnScrollListener extends RecyclerView.OnScrollListener {
	public static String TAG = EndlessRecyclerOnScrollListener.class.getSimpleName();

	private int previousTotal = 0; // The total number of items in the dataset after the last load
	private boolean loading = true; // True if we are still waiting for the last set of data to load.
	private int visibleThreshold = 1; // The minimum amount of items to have below your current scroll position before loading more.
	int firstVisibleItem, visibleItemCount, totalItemCount;

	private int current_page = 1;

	private LinearLayoutManager mLinearLayoutManager;

	public EndlessRecyclerOnScrollListener(LinearLayoutManager linearLayoutManager) {
		this.mLinearLayoutManager = linearLayoutManager;
	}

	@Override
	public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
		super.onScrolled(recyclerView, dx, dy);

		visibleItemCount = recyclerView.getChildCount();
		totalItemCount = mLinearLayoutManager.getItemCount();
		firstVisibleItem = mLinearLayoutManager.findFirstVisibleItemPosition();

		if (loading) {
			if (totalItemCount > previousTotal+1) {
				loading = false;
				previousTotal = totalItemCount;
			}
		}
		if (!loading && (totalItemCount - visibleItemCount) <= (firstVisibleItem + visibleThreshold)) {
		// End has been reached
		// Do something
			current_page++;
			onLoadMore(current_page);
			loading = true;
		}
	}

	public abstract void onLoadMore(int current_page);
}

MyAdapter.java

public class MyAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
	private final int VIEW_ITEM = 1;
	private final int VIEW_PROG = 0;

	private List<String> mDataset;

	public static class TextViewHolder extends RecyclerView.ViewHolder {
		public TextView mTextView;
		public TextViewHolder(View v) {
			super(v);
			mTextView = (TextView)v.findViewById(android.R.id.text1);
		}
	}
	public static class ProgressViewHolder extends RecyclerView.ViewHolder {
		public ProgressBar progressBar;
		public ProgressViewHolder(View v) {
			super(v);
			progressBar = (ProgressBar)v.findViewById(R.id.progressBar);
		}
	}

	public MyAdapter(List<String> myDataset) {
		mDataset = myDataset;
	}

	@Override
	public int getItemViewType(int position) {
		return mDataset.get(position)!=null? VIEW_ITEM: VIEW_PROG;
	}

	@Override
	public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
		RecyclerView.ViewHolder vh;
		if(viewType==VIEW_ITEM) {
			View v = LayoutInflater.from(parent.getContext())
					.inflate(android.R.layout.simple_list_item_1, parent, false);

			vh = new TextViewHolder(v);
		}else {
			View v = LayoutInflater.from(parent.getContext())
					.inflate(R.layout.progress_item, parent, false);

			vh = new ProgressViewHolder(v);
		}
		return vh;
	}

	@Override
	public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
		if(holder instanceof TextViewHolder){
			((TextViewHolder)holder).mTextView.setText(mDataset.get(position));
		}else{
			((ProgressViewHolder)holder).progressBar.setIndeterminate(true);
		}
	}

	@Override
	public int getItemCount() {
		return mDataset.size();
	}
}

and finally MainActivity.java

package virtoos.com.testapps;

import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;

import java.util.ArrayList;
import java.util.List;


public class MainActivity extends Activity {
	private RecyclerView mRecyclerView;
	private LinearLayoutManager mLayoutManager;
	private MyAdapter mAdapter;
	private final List<String> myDataset = new ArrayList<>();
	private Handler handler;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);

		handler = new Handler();
		addItems(20);
		mRecyclerView = (RecyclerView) findViewById(R.id.my_recycler_view);

		// use this setting to improve performance if you know that changes
		// in content do not change the layout size of the RecyclerView
		mRecyclerView.setHasFixedSize(true);

		mLayoutManager = new LinearLayoutManager(this);
		mRecyclerView.setLayoutManager(mLayoutManager);

		mAdapter = new MyAdapter(myDataset);
		mRecyclerView.setAdapter(mAdapter);

		//mRecyclerView.setItemAnimator(new DefaultItemAnimator());

		mRecyclerView.setOnScrollListener(new EndlessRecyclerOnScrollListener(mLayoutManager) {
			@Override
			public void onLoadMore(int current_page) {
				//add progress item
				myDataset.add(null);
				mAdapter.notifyItemInserted(myDataset.size());
				handler.postDelayed(new Runnable() {
					@Override
					public void run() {
						//remove progress item
						myDataset.remove(myDataset.size() - 1);
						mAdapter.notifyItemRemoved(myDataset.size());
						//add items one by one
						for (int i = 0; i < 15; i++) {
							myDataset.add("Item"+(myDataset.size()+1));
							mAdapter.notifyItemInserted(myDataset.size());
						}
						//or you can add all at once but do not forget to call mAdapter.notifyDataSetChanged();
					}
				}, 2000);
				System.out.println("load");
			}
		});

	}
}

Solution 3 - Android

Here is a small modification to @Vilen Melkumyan answer on the RecyclerView.Adapter which worked better for me. And you can use your EndlessRecyclerOnScrollListener in any way you want for loading the data, also enabling or disabling the footer at any time.

PS: It worked with GridLayoutManager.

public class MyRecyclerViewAdapter  extends RecyclerView.Adapter<RecyclerView.ViewHolder> {

private final int VIEW_TYPE_ITEM = 1;
private final int VIEW_TYPE_PROGRESSBAR = 0;
private boolean isFooterEnabled = true;

private List<String> items;

public static class TextViewHolder extends RecyclerView.ViewHolder {
    public TextView mTextView;
    public TextViewHolder(View v) {
        super(v);
        mTextView = (TextView)v.findViewById(android.R.id.text1);
    }
}
public static class ProgressViewHolder extends RecyclerView.ViewHolder {
    public ProgressBar progressBar;
    public ProgressViewHolder(View v) {
        super(v);
        progressBar = (ProgressBar)v.findViewById(R.id.progressBar);
    }
}

public MyRecyclerViewAdapter(List<String> myDataset) {
    items = myDataset;
}

@Override
public int getItemCount() {
    return  (isFooterEnabled) ? items.size() + 1 : items.size();
}

@Override
public int getItemViewType(int position) {
    return (isFooterEnabled && position >= items.size() ) ? VIEW_TYPE_PROGRESSBAR : VIEW_TYPE_ITEM;
}

/**
 * Enable or disable footer (Default is true)
 *
 * @param isEnabled boolean to turn on or off footer.
 */
public void enableFooter(boolean isEnabled){
    this.isFooterEnabled = isEnabled;
}

@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    RecyclerView.ViewHolder vh;
    if(viewType== VIEW_TYPE_ITEM) {
        View v = LayoutInflater.from(parent.getContext())
                .inflate(android.R.layout.simple_list_item_1, parent, false);
        vh = new TextViewHolder(v);
    }else {
        View v = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.progressbar, parent, false);

        vh = new ProgressViewHolder(v);
    }
    return vh;
}

@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
    if(holder instanceof ProgressViewHolder){
        ((ProgressViewHolder)holder).progressBar.setIndeterminate(true);
    } else if(items.size() > 0 && position < items.size()) {
        ((TextViewHolder)holder).mTextView.setText(items.get(position));            
    }
}
}

My 2 cents, peace!!

Solution 4 - Android

Check out my solution in https://github.com/ramirodo/endless-recycler-view-adapter or https://bintray.com/ramiro/android/endless-recycler-view-adapter. There is an example there and also the steps to set up the library in your project.

You just need to extend your recycler view adapter by implementing the required methods. Also you can set up the layout of the progress footer.

Solution 5 - Android

You can simplify Bronx's answer with inserting the code inside an adapter.

public class ArticleGridAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
    private final int VIEW_ITEM = 0;
    private final int VIEW_LOADING = 1;

    private Context mContext;
    private List<Article> mArticles = new ArrayList<>();
    private RecyclerView mRecyclerView;
    private GridLayoutManager mManager;

    public ArticleGridAdapter(Context context, List<Article> articles, RecyclerView recyclerView) {
        this.mContext = context;
        this.mArticles = articles;
        this.mRecyclerView = recyclerView;
        this.mManager = (GridLayoutManager) recyclerView.getLayoutManager();

        mManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
            @Override
            public int getSpanSize(int position) {
                return getItemViewType(position) == VIEW_LOADING ? mManager.getSpanCount() : 1;
            }
        });
    }
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionAlex FacciorussoView Question on Stackoverflow
Solution 1 - AndroidBronxView Answer on Stackoverflow
Solution 2 - AndroidVilenView Answer on Stackoverflow
Solution 3 - AndroidcassView Answer on Stackoverflow
Solution 4 - AndroidRamiroView Answer on Stackoverflow
Solution 5 - AndroidwonsucView Answer on Stackoverflow