Skip to content

Instantly share code, notes, and snippets.

@gabrielemariotti
Last active March 2, 2024 23:10
Show Gist options
  • Save gabrielemariotti/4c189fb1124df4556058 to your computer and use it in GitHub Desktop.
Save gabrielemariotti/4c189fb1124df4556058 to your computer and use it in GitHub Desktop.
A SimpleSectionedRecyclerViewAdapter: use this class to realize a simple sectioned `RecyclerView.Adapter`.

You can use this class to realize a simple sectioned RecyclerView.Adapter without changing your code.

The RecyclerView should use a LinearLayoutManager. You can use this code also with the TwoWayView with the ListLayoutManager (https://github.com/lucasr/twoway-view)

This is a porting of the class SimpleSectionedListAdapter provided by Google

Screen

Example:

        //Your RecyclerView
        mRecyclerView = (RecyclerView) findViewById(R.id.list);
        mRecyclerView.setHasFixedSize(true);
        mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
        mRecyclerView.addItemDecoration(new DividerItemDecoration(this,LinearLayoutManager.VERTICAL));
        
        //Your RecyclerView.Adapter
        mAdapter = new SimpleAdapter(this,sCheeseStrings);


        //This is the code to provide a sectioned list
        List<SimpleSectionedRecyclerViewAdapter.Section> sections =
                new ArrayList<SimpleSectionedRecyclerViewAdapter.Section>();
        
        //Sections
        sections.add(new SimpleSectionedRecyclerViewAdapter.Section(0,"Section 1"));
        sections.add(new SimpleSectionedRecyclerViewAdapter.Section(5,"Section 2"));
        sections.add(new SimpleSectionedRecyclerViewAdapter.Section(12,"Section 3"));
        sections.add(new SimpleSectionedRecyclerViewAdapter.Section(14,"Section 4"));
        sections.add(new SimpleSectionedRecyclerViewAdapter.Section(20,"Section 5"));

        //Add your adapter to the sectionAdapter
        SimpleSectionedRecyclerViewAdapter.Section[] dummy = new SimpleSectionedRecyclerViewAdapter.Section[sections.size()];
        SimpleSectionedRecyclerViewAdapter mSectionedAdapter = new
                  SimpleSectionedRecyclerViewAdapter(this,R.layout.section,R.id.section_text,mAdapter);
        mSectionedAdapter.setSections(sections.toArray(dummy));

        //Apply this adapter to the RecyclerView
        mRecyclerView.setAdapter(mSectionedAdapter);

You can customize the section layout, changing the layout section.xml and changing the code in the SimpleSectionedRecyclerViewAdapter.SectionViewHolder class and SimpleSectionedRecyclerViewAdapter#onBindViewHolder method.

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="48dp"
android:gravity="center_vertical"
android:paddingLeft="16dp"
android:singleLine="true"
android:textAllCaps="true"
android:textColor="@color/demo_theme_status_bar_color"
android:background="@android:color/transparent"
android:textSize="16sp"
android:id="@+id/section_text"
android:textStyle="bold" />
public class SimpleAdapter extends RecyclerView.Adapter<SimpleAdapter.SimpleViewHolder> {
private final Context mContext;
private List<String> mData;
public void add(String s,int position) {
position = position == -1 ? getItemCount() : position;
mData.add(position,s);
notifyItemInserted(position);
}
public void remove(int position){
if (position < getItemCount() ) {
mData.remove(position);
notifyItemRemoved(position);
}
}
public static class SimpleViewHolder extends RecyclerView.ViewHolder {
public final TextView title;
public SimpleViewHolder(View view) {
super(view);
title = (TextView) view.findViewById(R.id.simple_text);
}
}
public SimpleAdapter(Context context, String[] data) {
mContext = context;
if (data != null)
mData = new ArrayList<String>(Arrays.asList(data));
else mData = new ArrayList<String>();
}
public SimpleViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
final View view = LayoutInflater.from(mContext).inflate(R.layout.simple_item, parent, false);
return new SimpleViewHolder(view);
}
@Override
public void onBindViewHolder(SimpleViewHolder holder, final int position) {
holder.title.setText(mData.get(position));
holder.title.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Toast.makeText(mContext,"Position ="+position,Toast.LENGTH_SHORT).show();
}
});
}
@Override
public int getItemCount() {
return mData.size();
}
}
public class SimpleSectionedRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private final Context mContext;
private static final int SECTION_TYPE = 0;
private boolean mValid = true;
private int mSectionResourceId;
private int mTextResourceId;
private LayoutInflater mLayoutInflater;
private RecyclerView.Adapter mBaseAdapter;
private SparseArray<Section> mSections = new SparseArray<Section>();
public SimpleSectionedRecyclerViewAdapter(Context context, int sectionResourceId, int textResourceId,
RecyclerView.Adapter baseAdapter) {
mLayoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
mSectionResourceId = sectionResourceId;
mTextResourceId = textResourceId;
mBaseAdapter = baseAdapter;
mContext = context;
mBaseAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
@Override
public void onChanged() {
mValid = mBaseAdapter.getItemCount()>0;
notifyDataSetChanged();
}
@Override
public void onItemRangeChanged(int positionStart, int itemCount) {
mValid = mBaseAdapter.getItemCount()>0;
notifyItemRangeChanged(positionStart, itemCount);
}
@Override
public void onItemRangeInserted(int positionStart, int itemCount) {
mValid = mBaseAdapter.getItemCount()>0;
notifyItemRangeInserted(positionStart, itemCount);
}
@Override
public void onItemRangeRemoved(int positionStart, int itemCount) {
mValid = mBaseAdapter.getItemCount()>0;
notifyItemRangeRemoved(positionStart, itemCount);
}
});
}
public static class SectionViewHolder extends RecyclerView.ViewHolder {
public TextView title;
public SectionViewHolder(View view,int mTextResourceid) {
super(view);
title = (TextView) view.findViewById(mTextResourceid);
}
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int typeView) {
if (typeView == SECTION_TYPE) {
final View view = LayoutInflater.from(mContext).inflate(mSectionResourceId, parent, false);
return new SectionViewHolder(view,mTextResourceId);
}else{
return mBaseAdapter.onCreateViewHolder(parent, typeView -1);
}
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder sectionViewHolder, int position) {
if (isSectionHeaderPosition(position)) {
((SectionViewHolder)sectionViewHolder).title.setText(mSections.get(position).title);
}else{
mBaseAdapter.onBindViewHolder(sectionViewHolder,sectionedPositionToPosition(position));
}
}
@Override
public int getItemViewType(int position) {
return isSectionHeaderPosition(position)
? SECTION_TYPE
: mBaseAdapter.getItemViewType(sectionedPositionToPosition(position)) +1 ;
}
public static class Section {
int firstPosition;
int sectionedPosition;
CharSequence title;
public Section(int firstPosition, CharSequence title) {
this.firstPosition = firstPosition;
this.title = title;
}
public CharSequence getTitle() {
return title;
}
}
public void setSections(Section[] sections) {
mSections.clear();
Arrays.sort(sections, new Comparator<Section>() {
@Override
public int compare(Section o, Section o1) {
return (o.firstPosition == o1.firstPosition)
? 0
: ((o.firstPosition < o1.firstPosition) ? -1 : 1);
}
});
int offset = 0; // offset positions for the headers we're adding
for (Section section : sections) {
section.sectionedPosition = section.firstPosition + offset;
mSections.append(section.sectionedPosition, section);
++offset;
}
notifyDataSetChanged();
}
public int positionToSectionedPosition(int position) {
int offset = 0;
for (int i = 0; i < mSections.size(); i++) {
if (mSections.valueAt(i).firstPosition > position) {
break;
}
++offset;
}
return position + offset;
}
public int sectionedPositionToPosition(int sectionedPosition) {
if (isSectionHeaderPosition(sectionedPosition)) {
return RecyclerView.NO_POSITION;
}
int offset = 0;
for (int i = 0; i < mSections.size(); i++) {
if (mSections.valueAt(i).sectionedPosition > sectionedPosition) {
break;
}
--offset;
}
return sectionedPosition + offset;
}
public boolean isSectionHeaderPosition(int position) {
return mSections.get(position) != null;
}
@Override
public long getItemId(int position) {
return isSectionHeaderPosition(position)
? Integer.MAX_VALUE - mSections.indexOfKey(position)
: mBaseAdapter.getItemId(sectionedPositionToPosition(position));
}
@Override
public int getItemCount() {
return (mValid ? mBaseAdapter.getItemCount() + mSections.size() : 0);
}
}
@saket
Copy link

saket commented Sep 8, 2015

@sevar83 You're right. That definitely looks like a bug

@sevar83
Copy link

sevar83 commented Sep 11, 2015

@Saketme The code to transform from base adapter positions to sectioned is not complicated:

int sectionedPositionStart = positionToSectionedPosition(positionStart);
int sectionedItemCount = positionToSectionedPosition(positionStart + itemCount) - sectionedPositionStart;
notifyItemRange****(sectionedPositionStart, sectionedItemCount);

But the trouble comes when you try to animate the section changes. The difficulty with sections is that they are not usual items. They are dynamic and depend on the data in base adapter. Their appearance and disappearance depends on it. After data item is inserted/removed all section positions after must be recalculated in linear manner. This makes pretty hard to maintain section animation in the general case.
The only working approach I've found is: before any changes to the base adapter, i remove all sections with notify (for animations to take place) and after the change I rebuild the sections again and add them with notify.

Anyone had some success with section animations?

@mobiRic
Copy link

mobiRic commented Sep 21, 2015

@RoundSparrow I can confirm what you see. I am wrapping a SelectableAdapter from enoent.fr.

And unfortunately the inner adapter will get its position from RecyclerView.getAdapterPosition() which has been declared final.

Without the inner adapter being coupled to the outer SimpleSectionedRecyclerViewAdapter I can see no way around this issue.

2 ways forward as I see it:

  • combine my 2 adapters into 1 SectionedSelectableAdapter
  • tightly couple the 2 adapters so the inner knows about the outer (bad code, but quicker to implement/hack)

@huteri
Copy link

huteri commented Oct 20, 2015

Hi, you have issue with onClickListener in base adapter, if I add the data with position 0, then the on click trigger will return position 0 for that new data but also the data with position 1. OnClickListener is giving wrong position here.

And yes, I know I need to implement onClickListener on the viewholder instead, and use getAdapterPosition() to get the position, but it can't be used with section adapter, since it will return the position including the sections position, and my arraylist does not contain the sections.

Any workaround?

@krishnameena
Copy link

Thanks, It's very useful.

@pruthvirajha
Copy link

Thanks a lot. But how can I dynamically add or remove item to any section in the list?

@SolveSoul
Copy link

Great implementation for a sectioned adapter but I also have the issue of the getAdapterPosition() in the OnClickListener which returns the wrong position unfortunately :/

@jeremypiednoel
Copy link

Use "sectionedPositionToPosition" to get the right index

@Override
public void onClick(View v) {
     mSectionedAdapter.sectionedPositionToPosition(getLayoutPosition());
}

@ranjitzade
Copy link

@gabrielemariotti. Can you suggest what sould i do to make the header view sticky?

@davideas
Copy link

I've implemented a new way to create Sections with Sticky headers, basically you can handle them by simply assigning the IHeader object to a ISectionable object, all is coherent: add, delete and drag, automatically relink the header to the next ISectionable object.
You can check the project https://github.com/davideas/FlexibleAdapter which contains much more features than only sections.

Not using ItemDecoration, sticky headers are now clickable.

You should also read my answer at this question: http://stackoverflow.com/questions/33018788/how-can-i-set-a-listener-inside-a-recyclerview-header-decor

@kz
Copy link

kz commented Mar 20, 2016

Using this now in https://github.com/kz/balances-android and works well. Thanks!

@zuchetto
Copy link

zuchetto commented Apr 3, 2016

hi guys i need help here please i tried a lot of things without succes so my probleme is :

here is my code
` protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_articles_grid);

    MenuDBHelper menuDBHelper = new MenuDBHelper(this);
    ArrayList<Card> cards = new ArrayList<>();
    mCardArrayAdapter = new CardGridArrayAdapter(this, cards);

    categoriesIds = menuDBHelper.getAllCategoriesIds();
    ArrayList <Integer> separatorPosition=new ArrayList<>();

    CardGridView gridView = (CardGridView) findViewById(R.id.articles_grid);
    for (int j = 0; j < categoriesIds.size(); j++) {
        articles = menuDBHelper.getArticlesObjByCategorie(categoriesIds.get(j).toString());

        // i need to put a subheader here with the name of the category


        for (int i = 0; i < articles.size(); i++) {
            Card card = new Card(this);
            CardHeader header = new CardHeader(this);
            Article current = articles.get(i);
            header.setTitle(current.getName());
            card.setTitle(current.getPrix());
            card.addCardHeader(header);

            CardThumbnail thumb = new CardThumbnail(this);
            thumb.setDrawableResource(R.drawable.victor);
            card.addCardThumbnail(thumb);

            card.setClickable(true);
            card.setOnClickListener(new Card.OnCardClickListener() {
                @Override
                public void onClick(Card card, View view) {

                    int position = mCardArrayAdapter.getPosition(card);
                    Article clickedArticle = articles.get(position);

                    Intent intent = new Intent(getApplicationContext(), ArticleDetailsActivity.class);
                    intent.putExtra("article", clickedArticle);
                    startActivity(intent);

                }
            });
            Log.d("card", card.getTitle());
            cards.add(card);
        }

        CardGridArrayAdapter mCardArrayAdapter = new CardGridArrayAdapter(this, cards);


        gridView.setAdapter(mCardArrayAdapter);


    }



}

}
`

@mrThinBone
Copy link

I have same question as @pruthvirajha

@davideas
Copy link

davideas commented Jun 6, 2016

@mrThinBone, @pruthvirajha, @zuchetto, @niravkhunt1, @ranjitzade
You can actually interact with the Adapter I made, and animate every single change, also you can enable sticky headers and do lot of cool stuff.

@bellus93
Copy link

Hi i have to use the CheckedTextView in recycler view, but if i user selects a section I have to select all items inside that section, how i can do it?

@asoni960
Copy link

asoni960 commented Aug 9, 2016

I'm calling mcontext in onCreateViewHolder ,Simplesectionedrecyclerview from a fragment which giving an error and no getActivity() option there, can you help me with this

@ChristopheVersieux
Copy link

Hello,
I currently tried this code and have issue with the position of the clicked item.
Is there a way to access sectionedPositionToPosition from the baseAdapter?

@seasox
Copy link

seasox commented Oct 27, 2016

@ChristopheVersieux I attached a RecyclerView.OnItemTouchListener to my recyclerView for touch detection, from my Activity class.

drawerList.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() {
@Override
public boolean onInterceptTouchEvent(RecyclerView recyclerView, MotionEvent motionEvent) {
  View child = recyclerView.findChildViewUnder(motionEvent.getX(),motionEvent.getY());
  int position = recyclerView.getChildAdapterPosition(child);

  if (position != RecyclerView.NO_POSITION 
    && !sectionedAdapter.isSectionHeaderPosition(position) 
    && gestureDetector.onTouchEvent(motionEvent)) {

    position = sectionedAdapter.sectionedPositionToPosition(position);
    /* do something with position */
    return true;
  }
  return false;
}
@Override
public void onTouchEvent(RecyclerView recyclerView, MotionEvent motionEvent) {
  // stub
}

@Override
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
  // stub
}

Where gestureDetector is defined as:

// touch listener
final GestureDetector gestureDetector = new GestureDetector(this, new GestureDetector.SimpleOnGestureListener() {
  @Override
  public boolean onSingleTapUp(MotionEvent evt) {
    return true;
  }
});

And sectionedAdapter is an instance of the adapter discussed here.

@Ankur008
Copy link

Ankur008 commented Dec 7, 2016

I think it have anomalies with the section position. There is no symmetric with the section position.

String[] sCheeseStrings=["one","two","three","four","five"];

sections.add(new SimpleSectionedRecyclerViewAdapter.Section(0,"Section 1"));
sections.add(new SimpleSectionedRecyclerViewAdapter.Section(1,"Section 2"));
sections.add(new SimpleSectionedRecyclerViewAdapter.Section(2,"Section 3"));
sections.add(new SimpleSectionedRecyclerViewAdapter.Section(3,"Section 4"));
sections.add(new SimpleSectionedRecyclerViewAdapter.Section(4,"Section 5"));

OUTPUT:

Section 1
one
Section 2
two
Section 3
three
Section 4
four
Section 5
five

when i have continuous section position, then how can item come in between.?

Copy link

ghost commented Dec 10, 2016

How to specify onBIndItemViewHOlder for multiple item holder and how we use section and position there?

@ahmedmoussa8
Copy link

How to access Header view when we click on a child ?

@benju69
Copy link

benju69 commented Jun 14, 2017

Any idea how to notify when sections have changed?

@fredriks
Copy link

@DoruAdryan
Copy link

According to this talk: [https://www.youtube.com/watch?v=KhLVD6iiZQs&t=39m11s](Yigit Boyar: Pro RecyclerView), there is a problem with the onBindViewHolder method. But then, using getAdapterPosition() inside the ViewHolder it returns the position of the item from the SectionedAdapter, which would be wrong to use with some notify** methods for example.
How would a nice solution for this look like?
PS. I'm trying to use these adapters with checkable items.

@jackyhieu1211
Copy link

How to loadmore?

@Hocuri
Copy link

Hocuri commented Jul 5, 2018

Great idea, but maybe you could add a license so that I can be copy it without the risk of a copyright infringement?

@rrifafauzikomara
Copy link

what it's the mean that code "sCheeseStrings" ?

@agueroveraalvaro
Copy link

How could I expand the sections? There is a way : I click in the section and remove all their items and viceversa

Is there a better way? Thanks!

@SAGARSURI
Copy link

Will this work for HORIZONTAL LinearLayoutManager?

@aurangzaibumer777
Copy link

aurangzaibumer777 commented Nov 21, 2019

how can we put a search feature into this? it is going to iterate over headers too?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment