GridView
shares a lot of features with ListView
, with one noticeable difference: no headers or footers. Now they are unified under RecyclerView
, and I want to see how I can add a header to a grid.
GridLayoutManager
I created a RecylcerView
using a GridLayoutManager
with a spanCount
of 2.
RecyclerView recyclerView = (RecyclerView) findViewById( R.id.recycler_view); recyclerView.addItemDecoration(new MarginDecoration(this)); recyclerView.setHasFixedSize(true); recyclerView.setLayoutManager(new GridLayoutManager(this, 2)); recyclerView.setAdapter(new NumberedAdapter(30));
NumberedAdapter
shows the position of the item as string, and toasts when clicked.
Variable span size
In the basic setup I have a spanCount
of 2, each item with a span size of 1. A header will need a span size of 2 instead. Before I try to add a header, I want to see how I can change the span size. Turns out to be fairly easy.
GridLayoutManager manager = new GridLayoutManager(this, 3); manager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { @Override public int getSpanSize(int position) { return (3 - position % 3); } }); recyclerView.setLayoutManager(manager);
setSpanSizeLookup
lets you change the span size according to the position. This formula gives me span sizes 3, 2, 1, 3, 2, 1...
Header
Now let's add a header! We will need an adapter that providers two view types, one the header and one for the items. Take a look at HeaderNumberedAdapter
, which takes a View
as the header in the constructor and stash it away in a member variable.
public boolean isHeader(int position) { return position == 0; } @Override public TextViewHolder onCreateViewHolder( ViewGroup parent, int viewType) { if (viewType == ITEM_VIEW_TYPE_HEADER) { return new TextViewHolder(header); } View view = LayoutInflater.from(parent.getContext()) .inflate(R.layout.item, parent, false); return new TextViewHolder(view); } @Override public void onBindViewHolder( final TextViewHolder holder, final int position) { if (isHeader(position)) { return; } // Subtract 1 for header final String label = labels.get(position - 1); holder.textView.setText(label); holder.textView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Toast.makeText(holder.textView.getContext(), label, Toast.LENGTH_SHORT).show(); } }); } @Override public int getItemViewType(int position) { return isHeader(position) ? ITEM_VIEW_TYPE_HEADER : ITEM_VIEW_TYPE_ITEM; } @Override public int getItemCount() { return labels.size() + 1; }
When the RecyclerView
creates a view, if we are at the header position we wrap the stashed header with the view holder. On bind does not need to do anything for the header since the logic is done in the activity. However, we need to subtract the position by 1 when we bind the remaining items.
Back to the activity. We need to initialize the HeaderNumberedAdapter
with a header, and also override the setSpanSizeLookup
to have the header span all the columns.
final GridLayoutManager manager = new GridLayoutManager(this, 2); recyclerView.setLayoutManager(manager); View header = LayoutInflater.from(this).inflate( R.layout.header, recyclerView, false); header.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Toast.makeText(v.getContext(), R.string.grid_layout_header, Toast.LENGTH_SHORT).show(); } }); final HeaderNumberedAdapter adapter = new HeaderNumberedAdapter(header, 30); recyclerView.setAdapter(adapter); manager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { @Override public int getSpanSize(int position) { return adapter.isHeader(position) ? manager.getSpanCount() : 1; } });
We inflate the header, define its click behavior, and use it to construct the adapter. Then, in setSpanSizeLookup
, we return the span count as the span size if we are at the header position.
Summary
To create a grid with header using RecyclerView
:
- Define an adapter with two view types, one for the header and one of the items.
- Inflate a header and pass it to the adapter.
- Override
setSpanSizeLookup
inGridLayoutManager
to return the span count as the span size for the header.
Source: https://github.com/chiuki/android-recyclerview
P.S.: I got a comment on my Google+ post that you can also copy HeaderGridView.java out of AOSP if you don't need other RecyclerView
functionalities such as animation, reordering and staggering.
Inline coding questions will not be answsered. Instead, ask on StackOverflow and put the link in the comment.
Thanks!! Very useful
ReplyDeleteThanks, I was able to add a Viewpager as the header to a recyclerView using your post as a starting point.
ReplyDeleteThanks. It helped me.
ReplyDeleteThe best solution. I used the code to make a footer. Thanks a lot.
ReplyDeleteThanks a lot
ReplyDeleteThank You~
ReplyDelete:) Works grate (y) :)
ReplyDeleteThanks very Much
ReplyDeleteHelpful. Thank
ReplyDeleteSaved my life! ;) thank you!
ReplyDeleteThank you very much. Took me a minute to get the span aspect of it but finally got it.
ReplyDeleteThank you , I couldn't imagine it was so easy to implement
ReplyDeleteThanks! Very useful and quite easy to implement!
ReplyDeleteThank you very much..
ReplyDeleteThank you so much Chiuki! You are the best! Love you!
ReplyDeleteThank you very much..
ReplyDeleteThanks so much ^^!
ReplyDeleteThanks a lot
ReplyDeleteSpend a lot of time to find this. Thank you!
ReplyDeleteLife saver!
ReplyDeleteInstead of setting a new click listener each time in onBindViewHolder(), I suggest you set it once in onCreateViewHolder(), then you fetch the label using getAdapterPosition() in onClick() (or you retrieve it from the TextView).
ReplyDeleteThe documentation states that if you override getSpanSize() without overriding getSpanIndex() in your own SpanSizeLookup like you do here, you should enable the span index cache using setSpanIndexCacheEnabled(true).
ReplyDeleteThank you so much; this was very informative. I also liked your Pluralsight courses.
ReplyDelete