57

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?

Alex Facciorusso
  • 1,910
  • 3
  • 17
  • 30
  • I am also struggling with getting this to work. It is easy for single column rows, but does not look nice when you have multiple columns(as in a ``GridView``). Also, do we need to use ``RecyclerView`` or can it be accomplished with ``GridView``? – kgrevehagen Nov 25 '14 at 10:14
  • @kgrevehagen For the "do we need to use RecyclerView or can it be accomplished with GridView?" question I think I can answer almost surely: in [this video](https://www.youtube.com/watch?v=x5-ntYM_2UY), Chris Banes at 7:36 says that the RecyclerView "intended to replace ListView and GridView", so definitely yes, we should to update our apps to use this view, because "officially" it replaces other old views. The problem is that practically we can't add a footer and/or a header to it, without rewriting that View. I ask Chris or any other Google employer to answer to this question :/ – Alex Facciorusso Nov 26 '14 at 20:04
  • *rewriting = extending – Alex Facciorusso Nov 27 '14 at 01:05
  • 1
    https://github.com/pnikosis/materialish-progress this library can help you – Amrut Bidri Dec 11 '14 at 10:57
  • @AmrutBidri maybe you have misunderstood the problem: Is not the progress bar (or spinner, in this case), but the footer in the RecyclerView. Thanks for the comment, anyway :) – Alex Facciorusso Dec 11 '14 at 15:43
  • check out this post for adding header and footer http://stackoverflow.com/a/33274861/5439549 – Sadashiv Oct 22 '15 at 07:24

5 Answers5

69

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).

Vasily Kabunov
  • 5,179
  • 12
  • 41
  • 46
Bronx
  • 4,330
  • 4
  • 31
  • 43
  • Yes, this answers exactly to my question. The only thing I must notice is that this way to make headers and footers is valid only for GridLayoutManager and not Staggered ones. – Alex Facciorusso Mar 21 '15 at 23:55
  • In my case all is working fine but when I scroll recyler view Item at the end it will show progress bar at bottom but that progress bar infinite display till i scroll to up is this way to dismiss progress bar automatically after loading complete – Ganpat Kaliya Oct 25 '16 at 13:55
  • @GanpatKaliya your progressbar should be a temporary item, you should remove it when new items are loaded – Bronx Oct 25 '16 at 13:58
  • a combination of your answer with @Vasily Kabunov, working like charm – Hitesh Dhamshaniya Jan 25 '17 at 10:03
  • brilliant! very help – Serg Burlaka Oct 28 '17 at 03:55
57

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");
            }
        });

    }
}
Community
  • 1
  • 1
Vilen
  • 4,973
  • 3
  • 24
  • 38
  • 2
    This answer (I saw also in another question) can be ok only with a LinearLayoutManager. What I need to use, is a GridLayoutManager (and also Staggered one), so this system will not work in these cases :) – Alex Facciorusso Jan 22 '15 at 15:45
  • Why not ? there is very small modification you need to do is to make that progressbar item takes entire row, so you will need to add few empty items i.e. suppose you have 3 columns in your grid so possible number of empty items are: 1, 2, 3 then goes progress item then goes another empty item, I hope how to find count of empty items is clear (data.size%3 +1 ) – Vilen Jan 22 '15 at 16:02
  • Hi Vilen, I have a question on your code above - I have posted a new question here: http://stackoverflow.com/questions/30681905/adding-items-to-endless-scroll-recyclerview-with-progressbar-at-bottom – Simon Jun 06 '15 at 10:48
  • @Simon Hi. Thanks for putting attantion to my answer, i will take a look to your question once I'm near to a computer. – Vilen Jun 06 '15 at 14:40
  • @VilenMelkumyan The footer isn't taking match_parent as its width? – Atul O Holic Aug 25 '15 at 19:46
  • Hi @VilenMelkumyan. I used your solution and it works fine. I have a grid with 2 columns, I set the span size for the progress to 2. My problem is when I remove the progress, there is a weird animation that makes the progress view slide left or right a little. Is there a way to disable this animation? – syloc Aug 26 '15 at 08:12
  • hi @syloc this is a bit depricated solution and it has some limitations for revised solution please check this one http://stackoverflow.com/a/30691092/855843 – Vilen Sep 04 '15 at 14:23
  • I think people doesn't read the question correctly. Bronx's answer is correct answer and proper solution. – wonsuc Sep 17 '17 at 19:07
9

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!!

Vasily Kabunov
  • 5,179
  • 12
  • 41
  • 46
cass
  • 1,046
  • 12
  • 13
1

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.

Ramiro
  • 365
  • 5
  • 8
0

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;
            }
        });
    }
}
wonsuc
  • 2,284
  • 17
  • 25