Divide elements on groups in RecyclerView

For example you can:

  1. Use a TreeMap<Date,List<Event>> for splitting elements by date. This will be a collection for keeping your business objects. Of course if you already have a similar structure you can keep it. It's just important to have something for easily building list of items for populating UI with right elements order.

  2. Define a dedicated abstract type for List items (e.g. ListItem) to wrap your business objects. Its implementation could be something like this:

    public abstract class ListItem {
    
        public static final int TYPE_HEADER = 0;
        public static final int TYPE_EVENT = 1;
    
        abstract public int getType();
    } 
    
  3. Define a class for each of your List element type (here I added just two types but you can use many as you need):

    public class HeaderItem extends ListItem {
    
        private Date date;
    
        // here getters and setters 
        // for title and so on, built
        // using date
    
        @Override
        public int getType() {
            return TYPE_HEADER;
        }
    
    }
    
    public class EventItem extends ListItem {
    
        private Event event;
    
        // here getters and setters 
        // for title and so on, built 
        // using event
    
        @Override
        public int getType() {
            return TYPE_EVENT;
        }
    
    }
    
  4. Create a List as follows (where mEventsMap is map build at point 1):

    List<ListItem> mItems;
    // ...
    mItems = new ArrayList<>();
    for (Date date : mEventsMap.keySet()) {
        HeaderItem header = new HeaderItem();
        header.setDate(date); 
        mItems.add(header);
        for (Event event : mEventsMap.get(date)) {
            EventItem item = new EventItem();
            item.setEvent(event);
            mItems.add(item);
        }
    }
    
  5. Define an adapter for your RecyclerView, working on List defined at point 4. Here what is important is to override getItemViewType method as follows:

    @Override
    public int getItemViewType(int position) {
        return mItems.get(position).getType();
    }
    

    Then you need to have two layouts and ViewHolder for header and event items. Adapter methods should take care of this accordingly:

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        if (viewType == ListItem.TYPE_HEADER) {
            View itemView = mLayoutInflater.inflate(R.layout.view_list_item_header, parent, false);
            return new HeaderViewHolder(itemView);
        } else {
            View itemView = mLayoutInflater.inflate(R.layout.view_list_item_event, parent, false);
            return new EventViewHolder(itemView);
        }
    }
    
    
    @Override
    public void onBindViewHolder(final RecyclerView.ViewHolder viewHolder, final int position) {
        int type = getItemViewType(position);
        if (type == ListItem.TYPE_HEADER) {
            HeaderItem header = (HeaderItem) mItems.get(position);
            HeaderViewHolder holder = (HeaderViewHolder) viewHolder;
            // your logic here
        } else {            
            EventItem event = (EventItem) mItems.get(position);
            EventViewHolder holder = (EventViewHolder) viewHolder;
            // your logic here
        }
    }
    

Here it is a repository on GitHub providing an implementation of the approach explained above.


You can try to use the library I've wrote to solve this problem in my project. Gradle dependency (needs jcenter repo included):

dependencies {
    //your other dependencies
    compile 'su.j2e:rv-joiner:1.0.3'//latest version by now
}

Then, in your situation, you can do smth like this:

//init your RecyclerView as usual
RecyclerView rv = (RecyclerView) findViewById(R.id.rv);
rv.setLayoutManager(new LinearLayoutManager(this));

//construct a joiner
RvJoiner rvJoiner = new RvJoiner();
rvJoiner.add(new JoinableLayout(R.layout.today));
YourAdapter todayAdapter = new YourAdapter();
rvJoiner.add(new JoinableAdapter(todayAdapter));
rvJoiner.add(new JoinableLayout(R.layout.yesterday));
YourAdapter yesterdayAdapter = new YourAdapter();
rvJoiner.add(new JoinableAdapter(yesterdayAdapter));

//set join adapter to your RecyclerView
rv.setAdapter(rvJoiner.getAdapter());

When you need to add item, add it to appropriate adapter, like:

if (timeIsToday) {
    todayAdapter.addItem(item);//or other func you've written
} else if (timeIsYesterday) {
    yesterdayAdapter.addItem(item);
}

If you need to add new group to recycler view dynamically, you can use this methods:

rvJoiner.add(new JoinableLayout(R.layout.tomorrow));
YourAdapter tomorrowAdapter = new YourAdapter();
rvJoiner.add(new JoinableAdapter(tomorrowAdapter));

You can check this link for more library description. I can't say that it's surely the best way to achieve you goal, but it helps me sometimes.

UPD:

I've found the way to do this without using external libraries. Use RecyclerView.ItemDecoration class. For example, to group items by 3 item in group you can do this:

recyclerView.addItemDecoration(new RecyclerView.ItemDecoration() {

        private int textSize = 50;
        private int groupSpacing = 100;
        private int itemsInGroup = 3;

        private Paint paint = new Paint();
        {
            paint.setTextSize(textSize);
        }

        @Override
        public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
            for (int i = 0; i < parent.getChildCount(); i++) {
                View view = parent.getChildAt(i);
                int position = parent.getChildAdapterPosition(view);
                if (position % itemsInGroup == 0) {
                    c.drawText("Group " + (position / itemsInGroup + 1), view.getLeft(),
                            view.getTop() - groupSpacing / 2 + textSize / 3, paint);
                }
            }
        }

        @Override
        public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
            if (parent.getChildAdapterPosition(view) % itemsInGroup == 0) {
                outRect.set(0, groupSpacing, 0, 0);
            }
        }
    });

Hope it helps.