140

I spent a moment trying to figure out a way to add a header to a RecyclerView, unsuccessfully.

This is what I got so far:

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...

    layouManager = new LinearLayoutManager(getActivity());
    recyclerView.setLayoutManager(layouManager);

    LayoutInflater inflater = (LayoutInflater) getActivity().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    headerPlaceHolder = inflater.inflate(R.layout.view_header_holder_medium, null, false);
    layouManager.addView(headerPlaceHolder, 0);

   ...
}

The LayoutManager seems to be the object handling the disposition of the RecyclerView items. As I couldn't find any addHeaderView(View view) method, I decided to go with the LayoutManager's addView(View view, int position) method and to add my header view in the first position to act as a header.

Aaand this is where things get uglier:

java.lang.NullPointerException: Attempt to read from field 'android.support.v7.widget.RecyclerView$ViewHolder android.support.v7.widget.RecyclerView$LayoutParams.mViewHolder' on a null object reference
    at android.support.v7.widget.RecyclerView.getChildViewHolderInt(RecyclerView.java:2497)
    at android.support.v7.widget.RecyclerView$LayoutManager.addViewInt(RecyclerView.java:4807)
    at android.support.v7.widget.RecyclerView$LayoutManager.addView(RecyclerView.java:4803)
    at com.mathieumaree.showz.fragments.CategoryFragment.setRecyclerView(CategoryFragment.java:231)
    at com.mathieumaree.showz.fragments.CategoryFragment.access$200(CategoryFragment.java:47)
    at com.mathieumaree.showz.fragments.CategoryFragment$2.success(CategoryFragment.java:201)
    at com.mathieumaree.showz.fragments.CategoryFragment$2.success(CategoryFragment.java:196)
    at retrofit.CallbackRunnable$1.run(CallbackRunnable.java:41)
    at android.os.Handler.handleCallback(Handler.java:739)
    at android.os.Handler.dispatchMessage(Handler.java:95)
    at android.os.Looper.loop(Looper.java:135)
    at android.app.ActivityThread.main(ActivityThread.java:5221)
    at java.lang.reflect.Method.invoke(Native Method)
    at java.lang.reflect.Method.invoke(Method.java:372)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:899)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:694)

After getting several NullPointerExceptions trying to call the addView(View view) at different moments of the Activity creation (also tried adding the view once everything is set up, even the Adapter's data), I realized I have no idea if this is the right way to do it (and it doesn't look to be).

PS: Also, a solution that could handle the GridLayoutManager in addition to the LinearLayoutManager would be really appreciated!

Reaz Murshed
  • 23,691
  • 13
  • 78
  • 98
MathieuMaree
  • 7,453
  • 6
  • 26
  • 31
  • take a look at this http://stackoverflow.com/a/26573338/2127203 – EC84B4 Oct 29 '14 at 06:17
  • The problem is in Adapter code. It means, somehow you are returning null viewholder in onCreateViewHolder function. – Neo May 14 '16 at 09:41
  • There is a good way how to add header to StaggeredGridLayout http://stackoverflow.com/questions/42202735/how-to-put-main-cell-on-the-top-of-staggeredgridlayoutrecycler-view/42203237#42203237 – Sirop4ik Feb 13 '17 at 12:33
  • take a look at this [article](https://www.loopwiki.com/ui-ux-design/recyclerview-with-header-and-footer-android-example/) – Amar Yadav Sep 09 '20 at 06:28
  • Now I think the best solution is concatAdapter! take a loot at this https://developer.android.com/reference/androidx/recyclerview/widget/ConcatAdapter – hamid Mahmoodi Sep 09 '21 at 16:25

14 Answers14

129

I had to add a footer to my RecyclerView and here I'm sharing my code snippet as I thought it might be useful. Please check the comments inside the code for better understanding of the overall flow.

import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import java.util.ArrayList;

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

    private static final int FOOTER_VIEW = 1;
    private ArrayList<String> data; // Take any list that matches your requirement.
    private Context context;

    // Define a constructor
    public RecyclerViewWithFooterAdapter(Context context, ArrayList<String> data) {
        this.context = context;
        this.data = data;
    }

    // Define a ViewHolder for Footer view
    public class FooterViewHolder extends ViewHolder {
        public FooterViewHolder(View itemView) {
            super(itemView);
            itemView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    // Do whatever you want on clicking the item
                }
            });
        }
    }

    // Now define the ViewHolder for Normal list item
    public class NormalViewHolder extends ViewHolder {
        public NormalViewHolder(View itemView) {
            super(itemView);

            itemView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    // Do whatever you want on clicking the normal items
                }
            });
        }
    }

    // And now in onCreateViewHolder you have to pass the correct view
    // while populating the list item.

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {

        View v;

        if (viewType == FOOTER_VIEW) {
            v = LayoutInflater.from(context).inflate(R.layout.list_item_footer, parent, false);
            FooterViewHolder vh = new FooterViewHolder(v);
            return vh;
        }

        v = LayoutInflater.from(context).inflate(R.layout.list_item_normal, parent, false);

        NormalViewHolder vh = new NormalViewHolder(v);

        return vh;
    }

    // Now bind the ViewHolder in onBindViewHolder
    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {

        try {
            if (holder instanceof NormalViewHolder) {
                NormalViewHolder vh = (NormalViewHolder) holder;

                vh.bindView(position);
            } else if (holder instanceof FooterViewHolder) {
                FooterViewHolder vh = (FooterViewHolder) holder;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // Now the critical part. You have return the exact item count of your list
    // I've only one footer. So I returned data.size() + 1
    // If you've multiple headers and footers, you've to return total count
    // like, headers.size() + data.size() + footers.size()

    @Override
    public int getItemCount() {
        if (data == null) {
            return 0;
        }

        if (data.size() == 0) {
            //Return 1 here to show nothing
            return 1;
        }

        // Add extra view to show the footer view
        return data.size() + 1;
    }

    // Now define getItemViewType of your own.

    @Override
    public int getItemViewType(int position) {
        if (position == data.size()) {
            // This is where we'll add footer.
            return FOOTER_VIEW;
        }

        return super.getItemViewType(position);
    }

    // So you're done with adding a footer and its action on onClick.
    // Now set the default ViewHolder for NormalViewHolder

    public class ViewHolder extends RecyclerView.ViewHolder {
        // Define elements of a row here
        public ViewHolder(View itemView) {
            super(itemView);
            // Find view by ID and initialize here
        }

        public void bindView(int position) {
            // bindView() method to implement actions
        }
    }
}

The above code snippet adds a footer to the RecyclerView. You can check this GitHub repository for checking the implementation of adding both header and a footer.

Reaz Murshed
  • 23,691
  • 13
  • 78
  • 98
  • 2
    Works well. This should be marked as correct answer. – Naga Mallesh Maddali Oct 09 '15 at 12:03
  • 1
    This is exactly what I did. But what if I wanted my RecyclerView to adapt staggered list? The first element (The header) will be staggered as well. :( – Neon Warge Jul 16 '16 at 10:48
  • This is a tutorial about how you can populate your `RecyclerView` dynamically. You can have control on each of the elements of your `RecyclerView`. Please look at the code section for a working project. Hope it might help. https://github.com/comeondude/dynamic-recyclerview/wiki – Reaz Murshed Jul 16 '16 at 14:43
  • This is throwing NPE – kgandroid Jul 29 '16 at 10:37
  • Post your logcat in a different question and tag me there. It'll be easier to resolve. – Reaz Murshed Jul 29 '16 at 19:14
  • very nice! just a thought though, how can we be so sure that `return super.getItemViewType(position);` won't return the same value as `FOOTER_VIEW`? – Couitchy Sep 04 '17 at 13:47
  • @Couitchy you can have a look at the documentation here https://developer.android.com/reference/android/widget/BaseAdapter.html#getItemViewType%28int%29. Hope this explains your question. – Reaz Murshed Sep 04 '17 at 17:42
  • 2
    `int getItemViewType (int position)` - Return the view type of the item at position for the purposes of view recycling. The default implementation of this method returns 0, making the assumption of a single view type for the adapter. Unlike `ListView` adapters, types need not be contiguous. Consider using id resources to uniquely identify item view types. - From documentation. https://developer.android.com/reference/android/support/v7/widget/RecyclerView.Adapter.html – Reaz Murshed Sep 05 '17 at 02:12
  • 9
    So much manual work for something people want to do always. I can't believe it... – JohnyTex May 03 '19 at 10:08
32

Very simple to solve!!

I don't like an idea of having logic inside adapter as a different view type because every time it checks for the view type before returning the view. Below solution avoids extra checks.

Just add LinearLayout (vertical) header view + recyclerview + footer view inside android.support.v4.widget.NestedScrollView.

Check this out:

 <android.support.v4.widget.NestedScrollView
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

       <View
            android:id="@+id/header"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>

        <android.support.v7.widget.RecyclerView
            android:id="@+id/list"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:layoutManager="LinearLayoutManager"/>

        <View
            android:id="@+id/footer"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>
    </LinearLayout>
</android.support.v4.widget.NestedScrollView>

Add this line of code for smooth scrolling

RecyclerView v = (RecyclerView) findViewById(...);
v.setNestedScrollingEnabled(false);

This will lose all RV performance and RV will try to lay out all view holders regardless of the layout_height of RV

Recommended using for the small size list like Nav drawer or settings etc.

Nishant Shah
  • 2,022
  • 22
  • 20
  • 1
    Worked for me fairly simple – Hitesh Sahu Jul 10 '16 at 11:57
  • 1
    This is a very simple way to go about adding headers and footers to a recycler view when the headers and footers do not have to repeat within a list. – user1841702 Jul 10 '16 at 12:43
  • 54
    This is a very simple way of losing all the advantages that `RecyclerView` brings - you lose the actual recycling and the optimisation it brings. – Marcin Koziński Jul 13 '16 at 09:56
  • This solution is the best for a simple recyclerViews. Thanks. So simple and fast to implement – Andrii Kovalchuk Aug 30 '16 at 11:17
  • 1
    I tried this code , scrolling is not working properly... it became scrolling too slow .. plz suggest if could do something for that – Nibha Jain Aug 31 '16 at 07:09
  • 1
    @NibhaJain you may try https://gist.github.com/mheras/0908873267def75dc746 (from Mato's answer). Apparently Marcin's comment is right, This is not the solution for long list as we may loose all advantages of RecycerView of recycling. – Nishant Shah Sep 01 '16 at 04:00
  • This is very simple and useful – blueware Mar 07 '17 at 12:18
  • 2
    using nested scrollview will cause recyclerview to cache all the view and if size of recycler view is more scrolling and loading time will increase. I suggest not to use this code – Tushar Saha Oct 11 '17 at 08:32
  • @MarcinKoziński You won't lose if your recyclerview has wrap_content height. See my twitter thread! – Nishant Shah Feb 05 '18 at 07:27
  • 1
    So in the twitter thread Nick Butcher (who is great and knows his stuff ) says it might not be a problem if the list is very small. That's fair, I agree with that. But it still means that you're losing on the optimisations and that the recycling doesn't happen. But if it's just a small list, like a navigation drawer (to use the same example you added to your answer) then it's probably ok, it won't cause performance problems. (btw, for navigation drawer you can also use [Navigation View](https://developer.android.com/reference/android/support/design/widget/NavigationView.html)) – Marcin Koziński Feb 05 '18 at 15:41
25

I had the same problem on Lollipop and created two approaches to wrap the Recyclerview adapter. One is pretty easy to use, but I'm not sure how it will behave with a changing dataset. Because it wraps your adapter and you need to make yourself sure to call methods like notifyDataSetChanged on the right adapter-object.

The other shouldn't have such problems. Just let your regular adapter extend the class, implement the abstract methods and you should be ready. And here they are:

gists

HeaderRecyclerViewAdapterV1

import android.support.v7.widget.RecyclerView;
import android.view.ViewGroup;

/**
 * Created by sebnapi on 08.11.14.
 * <p/>
 * This is a Plug-and-Play Approach for adding a Header or Footer to
 * a RecyclerView backed list
 * <p/>
 * Just wrap your regular adapter like this
 * <p/>
 * new HeaderRecyclerViewAdapterV1(new RegularAdapter())
 * <p/>
 * Let RegularAdapter implement HeaderRecyclerView, FooterRecyclerView or both
 * and you are ready to go.
 * <p/>
 * I'm absolutely not sure how this will behave with changes in the dataset.
 * You can always wrap a fresh adapter and make sure to not change the old one or
 * use my other approach.
 * <p/>
 * With the other approach you need to let your Adapter extend HeaderRecyclerViewAdapterV2
 * (and therefore change potentially more code) but possible omit these shortcomings.
 * <p/>
 * TOTALLY UNTESTED - USE WITH CARE - HAVE FUN :)
 */
public class HeaderRecyclerViewAdapterV1 extends RecyclerView.Adapter {
    private static final int TYPE_HEADER = Integer.MIN_VALUE;
    private static final int TYPE_FOOTER = Integer.MIN_VALUE + 1;
    private static final int TYPE_ADAPTEE_OFFSET = 2;

    private final RecyclerView.Adapter mAdaptee;


    public HeaderRecyclerViewAdapterV1(RecyclerView.Adapter adaptee) {
        mAdaptee = adaptee;
    }

    public RecyclerView.Adapter getAdaptee() {
        return mAdaptee;
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        if (viewType == TYPE_HEADER && mAdaptee instanceof HeaderRecyclerView) {
            return ((HeaderRecyclerView) mAdaptee).onCreateHeaderViewHolder(parent, viewType);
        } else if (viewType == TYPE_FOOTER && mAdaptee instanceof FooterRecyclerView) {
            return ((FooterRecyclerView) mAdaptee).onCreateFooterViewHolder(parent, viewType);
        }
        return mAdaptee.onCreateViewHolder(parent, viewType - TYPE_ADAPTEE_OFFSET);
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        if (position == 0 && holder.getItemViewType() == TYPE_HEADER && useHeader()) {
            ((HeaderRecyclerView) mAdaptee).onBindHeaderView(holder, position);
        } else if (position == mAdaptee.getItemCount() && holder.getItemViewType() == TYPE_FOOTER && useFooter()) {
            ((FooterRecyclerView) mAdaptee).onBindFooterView(holder, position);
        } else {
            mAdaptee.onBindViewHolder(holder, position - (useHeader() ? 1 : 0));
        }
    }

    @Override
    public int getItemCount() {
        int itemCount = mAdaptee.getItemCount();
        if (useHeader()) {
            itemCount += 1;
        }
        if (useFooter()) {
            itemCount += 1;
        }
        return itemCount;
    }

    private boolean useHeader() {
        if (mAdaptee instanceof HeaderRecyclerView) {
            return true;
        }
        return false;
    }

    private boolean useFooter() {
        if (mAdaptee instanceof FooterRecyclerView) {
            return true;
        }
        return false;
    }

    @Override
    public int getItemViewType(int position) {
        if (position == 0 && useHeader()) {
            return TYPE_HEADER;
        }
        if (position == mAdaptee.getItemCount() && useFooter()) {
            return TYPE_FOOTER;
        }
        if (mAdaptee.getItemCount() >= Integer.MAX_VALUE - TYPE_ADAPTEE_OFFSET) {
            new IllegalStateException("HeaderRecyclerViewAdapter offsets your BasicItemType by " + TYPE_ADAPTEE_OFFSET + ".");
        }
        return mAdaptee.getItemViewType(position) + TYPE_ADAPTEE_OFFSET;
    }


    public static interface HeaderRecyclerView {
        public RecyclerView.ViewHolder onCreateHeaderViewHolder(ViewGroup parent, int viewType);

        public void onBindHeaderView(RecyclerView.ViewHolder holder, int position);
    }

    public static interface FooterRecyclerView {
        public RecyclerView.ViewHolder onCreateFooterViewHolder(ViewGroup parent, int viewType);

        public void onBindFooterView(RecyclerView.ViewHolder holder, int position);
    }

}

HeaderRecyclerViewAdapterV2

import android.support.v7.widget.RecyclerView;
import android.view.ViewGroup;

/**
 * Created by sebnapi on 08.11.14.
 * <p/>
 * If you extend this Adapter you are able to add a Header, a Footer or both
 * by a similar ViewHolder pattern as in RecyclerView.
 * <p/>
 * If you want to omit changes to your class hierarchy you can try the Plug-and-Play
 * approach HeaderRecyclerViewAdapterV1.
 * <p/>
 * Don't override (Be careful while overriding)
 * - onCreateViewHolder
 * - onBindViewHolder
 * - getItemCount
 * - getItemViewType
 * <p/>
 * You need to override the abstract methods introduced by this class. This class
 * is not using generics as RecyclerView.Adapter make yourself sure to cast right.
 * <p/>
 * TOTALLY UNTESTED - USE WITH CARE - HAVE FUN :)
 */
public abstract class HeaderRecyclerViewAdapterV2 extends RecyclerView.Adapter {
    private static final int TYPE_HEADER = Integer.MIN_VALUE;
    private static final int TYPE_FOOTER = Integer.MIN_VALUE + 1;
    private static final int TYPE_ADAPTEE_OFFSET = 2;

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        if (viewType == TYPE_HEADER) {
            return onCreateHeaderViewHolder(parent, viewType);
        } else if (viewType == TYPE_FOOTER) {
            return onCreateFooterViewHolder(parent, viewType);
        }
        return onCreateBasicItemViewHolder(parent, viewType - TYPE_ADAPTEE_OFFSET);
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        if (position == 0 && holder.getItemViewType() == TYPE_HEADER) {
            onBindHeaderView(holder, position);
        } else if (position == getBasicItemCount() && holder.getItemViewType() == TYPE_FOOTER) {
            onBindFooterView(holder, position);
        } else {
            onBindBasicItemView(holder, position - (useHeader() ? 1 : 0));
        }
    }

    @Override
    public int getItemCount() {
        int itemCount = getBasicItemCount();
        if (useHeader()) {
            itemCount += 1;
        }
        if (useFooter()) {
            itemCount += 1;
        }
        return itemCount;
    }

    @Override
    public int getItemViewType(int position) {
        if (position == 0 && useHeader()) {
            return TYPE_HEADER;
        }
        if (position == getBasicItemCount() && useFooter()) {
            return TYPE_FOOTER;
        }
        if (getBasicItemType(position) >= Integer.MAX_VALUE - TYPE_ADAPTEE_OFFSET) {
            new IllegalStateException("HeaderRecyclerViewAdapter offsets your BasicItemType by " + TYPE_ADAPTEE_OFFSET + ".");
        }
        return getBasicItemType(position) + TYPE_ADAPTEE_OFFSET;
    }

    public abstract boolean useHeader();

    public abstract RecyclerView.ViewHolder onCreateHeaderViewHolder(ViewGroup parent, int viewType);

    public abstract void onBindHeaderView(RecyclerView.ViewHolder holder, int position);

    public abstract boolean useFooter();

    public abstract RecyclerView.ViewHolder onCreateFooterViewHolder(ViewGroup parent, int viewType);

    public abstract void onBindFooterView(RecyclerView.ViewHolder holder, int position);

    public abstract RecyclerView.ViewHolder onCreateBasicItemViewHolder(ViewGroup parent, int viewType);

    public abstract void onBindBasicItemView(RecyclerView.ViewHolder holder, int position);

    public abstract int getBasicItemCount();

    /**
     * make sure you don't use [Integer.MAX_VALUE-1, Integer.MAX_VALUE] as BasicItemViewType
     *
     * @param position
     * @return
     */
    public abstract int getBasicItemType(int position);

}

Feedback and forks appreciated. I will use HeaderRecyclerViewAdapterV2 by my self and evolve, test and post the changes in the future.

EDIT: @OvidiuLatcu Yes I had some problems. Actually I stopped offsetting the Header implicitly by position - (useHeader() ? 1 : 0) and instead created a public method int offsetPosition(int position) for it. Because if you set an OnItemTouchListener on Recyclerview, you can intercept the touch, get the x,y coordinates of the touch, find the according child view and then call recyclerView.getChildPosition(...)and you will always get the non-offsetted position in the adapter! This is a shortcomming in the RecyclerView Code, I don't see an easy method to overcome this. This is why I now offset the positions explicit when I need to by my own code.

seb
  • 4,279
  • 2
  • 25
  • 36
  • looks good ! having any troubles with it ? or we can safely use it ? :D – Ovidiu Latcu Nov 12 '14 at 10:49
  • 1
    @OvidiuLatcu see post – seb Nov 12 '14 at 15:23
  • In these implementations, it seems that you have assumed the number of headers and footers to be only 1 each? – rishabhmhjn Mar 28 '15 at 11:19
  • @seb [Version 2](https://gist.github.com/sebnapi/fde648c17616d9d3bcde) works like charm!! the only thing i needed to modify is the condition for getting the footer on both onBindViewHolder and getItemViewType methods. The issue is that if you get the position using position == getBasicItemCount() it won't return true for the actual last position, but last position - 1. It ended up placing the FooterView there (not at the bottom). We fixed it changing the condition to position == getBasicItemCount() + 1 and it worked great! – mmark May 07 '15 at 20:03
  • @seb version 2 works so great. thanks so much. i am using it. one small thing i suggest is to add 'final' keyword for the override function. – RyanShao Aug 28 '15 at 01:51
  • That won't work properly if you any of the `notifyXXX`-methods from within the adaptee. I would recommend to register to all notify events of the adaptee in the constructor via `RecyclerView.Adapter.registerAdapterDataObserver` – Flo Nov 08 '15 at 04:30
  • Since the days I created this post, I have a way more sophisticated version (V2) running and I had no problems with the notifyXXX methods. But yes probably it won't work, it depends how u gonna use it. – seb Nov 10 '15 at 16:02
  • Great solution, thanks! I improved a bit with generics. – fox Nov 26 '15 at 12:59
  • Made the following corrections in order the code to work correcly: in `onBindViewHolder` and `getItemViewType` replaced `getBasicItemCount()` by `getItemCount() - 1` and it worked as expected. Hope this helps! – fox Nov 26 '15 at 13:44
10

I haven't tried this, but I would simply add 1 (or 2, if you want both a header and footer) to the integer returned by getItemCount in your adapter. You can then override getItemViewType in your adapter to return a different integer when i==0: https://developer.android.com/reference/android/support/v7/widget/RecyclerView.Adapter.html#getItemViewType(int)

createViewHolder is then passed the integer you returned from getItemViewType, allowing you to create or configure the view holder differently for the header view: https://developer.android.com/reference/android/support/v7/widget/RecyclerView.Adapter.html#createViewHolder(android.view.ViewGroup, int)

Don't forget to subtract one from the position integer passed to bindViewHolder.

Ian Newson
  • 7,679
  • 2
  • 47
  • 80
  • I do not think that's the job of recyclerview. Recyclerviews job is to simply recycle views. Writing a layoutmanager with a header or footer implementation is the way to go. – IZI_Shadow_IZI Oct 19 '14 at 12:36
  • Thanks @IanNewson for your reply. First, this solution seems to be working, even using only `getItemViewType(int position) { return position == 0 ? 0 : 1; }` (`RecyclerView` doesn't have a `getViewTypeCount()` method). On the other hand, I do agree with @IZI_Shadow_IZI, I really have the feeling the LayoutManager should be the one handling this kind of things. Any other idea? – MathieuMaree Oct 19 '14 at 16:51
  • @VieuMa you're probably both right, but I don't know how to do that currently and I was pretty sure my solution would work. A suboptimal solution is better than no solution, which is what you had before. – Ian Newson Oct 19 '14 at 18:19
  • @VieuMa also, abstracting the header and footer into the adapter means it should handle both types of layout requested, writing your own layout manager means reimplementing for both types of layout. – Ian Newson Oct 19 '14 at 18:23
  • just create an adapter that wraps any adapter and adds support for header view at index 0. HeaderView in listview creates lots of edge cases and the added value is minimal given that it is an easy problem to solve using a wrapper adapter. – yigit Oct 19 '14 at 18:23
  • @VieuMa yep, wrapping the adapter is an excellent idea. That allows you to use this solution easily with any of your existing RecyclerViews easily. For that reason I now think this solution is superior to implementing your own layout manager. – Ian Newson Oct 19 '14 at 18:34
  • @yigit Alright, if I understood correctly what you said, the idea would be to create a HeaderBaseAdapter that all the other adapters that need to have a headerView would extend? And add the required methods to implement the header? – MathieuMaree Oct 19 '14 at 18:49
  • That is one option, cleaner. Be sure to offset notify* methods accordingly. Depending on your codebase, you may also prefer to create a HeaderAdapter that gets a normal adapter as a constructor parameter and forwards API calls to that one. In that case, you can add your HeaderAdapter as a listener to the wrapped adapter so that you can override and dispatch notify events. It all depends on your use case. Both solutions are fine. – yigit Oct 19 '14 at 19:21
  • Alright, thanks for your time yigit and Ian Newson, I'll give it a try tomorrow. I may come back here for further details... – MathieuMaree Oct 19 '14 at 20:51
  • Can u post some code example when you'll have some kind of solution? I am interested in doing this also, but don't have much experience.. – Sandra Oct 27 '14 at 13:14
  • To add to this answer: http://stackoverflow.com/questions/26530685/is-there-an-addheaderview-equivalent-for-recyclerview/26573338#26573338 – John Shelley Oct 29 '14 at 03:54
10

You can used this GitHub library allowing to add Header and/or Footer in your RecyclerView in the simplest way possible.

You need to add HFRecyclerView library in your project or you can also grab it from Gradle:

compile 'com.mikhaellopez:hfrecyclerview:1.0.0'

This is a result in image:

Preview

EDIT:

If you just want to add a margin at the top and/or bottom with this library: SimpleItemDecoration:

int offsetPx = 10;
recyclerView.addItemDecoration(new StartOffsetItemDecoration(offsetPx));
recyclerView.addItemDecoration(new EndOffsetItemDecoration(offsetPx));
lopez.mikhael
  • 9,943
  • 19
  • 67
  • 110
  • This library add view properly on header in LinearLayoutManager but I want to set view as header in GridLayoutManager, which take place entire width of screen. Can it is possible with this library. – Ved Mar 09 '16 at 09:14
  • No, this library allows you to change the first and last element of a recyclerView at adapting (RecyclerView.Adapter). You can use this adapter to a GridView without problems. So I think this library allows to do what you want. – lopez.mikhael Mar 09 '16 at 09:25
7

recyclerview:1.2.0 introduces ConcatAdapter class which concatenates multiple adapters into a single one. So it allows to create separate header/footer adapters and reuse them across multiple lists.

myRecyclerView.adapter = ConcatAdapter(headerAdapter, listAdapter, footerAdapter)

Take a look at the announcement article. It contains a sample how to display a loading progress in header and footer using ConcatAdapter.

For the moment when I post this answer the version 1.2.0 of the library is in alpha stage, so the api might change. You can check the status here.

Valeriy Katkov
  • 33,616
  • 20
  • 100
  • 123
  • There is a problem with this solution, that if you use ListAdapter submitList method, it scrolls all the way down to the bottom. Not recommended. – mtsahakis Mar 16 '21 at 20:33
6

I ended up implementing my own adapter to wrap any other adapter and provide methods to add header and footer views.

Created a gist here: HeaderViewRecyclerAdapter.java

The main feature I wanted was a similar interface to a ListView, so I wanted to be able to inflate the views in my Fragment and add them to the RecyclerView in onCreateView. This is done by creating a HeaderViewRecyclerAdapter passing the adapter to be wrapped, and calling addHeaderView and addFooterView passing your inflated views. Then set the HeaderViewRecyclerAdapter instance as the adapter on the RecyclerView.

An extra requirement was that I needed to be able to easily swap out adapters while keeping the headers and footers, I didn't want to have multiple adapters with multiple instances of these headers and footers. So you can call setAdapter to change the wrapped adapter leaving the headers and footers intact, with the RecyclerView being notified of the change.

darnmason
  • 2,672
  • 1
  • 17
  • 23
3

my "keep it simple stupid" way ...it waste some resources , i know , but i dont care as my code keep simple so... First, add a footer with visibility GONE to your item_layout

<LinearLayout
        android:id="@+id/footer"
        android:layout_width="match_parent"
        android:layout_height="80dp"
        android:orientation="vertical"
        android:visibility="gone">
</LinearLayout>

Then, set it visible on the last item

public void onBindViewHolder(ChannelAdapter.MyViewHolder holder, int position) {
        boolean last = position==data.size()-1;
        //....
        holder.footer.setVisibility(View.GONE);
        if (last && showFooter){
            holder.footer.setVisibility(View.VISIBLE);
        }
    }

do the opposite for header

Shihab Uddin
  • 6,699
  • 2
  • 59
  • 74
Luca Rocchi
  • 6,272
  • 1
  • 23
  • 23
  • Other than this solution waste resources, I had a functional problem. When using ListAdapter and submitList() method with a List of data with one less item, and that one less item was the last one that displayed the footer, the footer never displayed. I think it is because itemCount (in combination with DiffUtil) remains the same. Beware. I think the best solution is this one https://medium.com/androiddevelopers/merge-adapters-sequentially-with-mergeadapter-294d2942127a – mtsahakis Feb 25 '21 at 16:34
1

Based on @seb's solution, I created a subclass of RecyclerView.Adapter that supports an arbitrary number of headers and footers.

https://gist.github.com/mheras/0908873267def75dc746

Although it seems to be a solution, I also think this thing should be managed by the LayoutManager. Unfortunately, I need it now and I don't have time to implement a StaggeredGridLayoutManager from scratch (nor even extend from it).

I'm still testing it, but you can try it out if you want. Please let me know if you find any issues with it.

mato
  • 1,461
  • 10
  • 17
1

You can use viewtype to solve this problem, here is my demo: https://github.com/yefengfreedom/RecyclerViewWithHeaderFooterLoadingEmptyViewErrorView

  1. you can define some recycler view display mode:

    public static final int MODE_DATA = 0, MODE_LOADING = 1, MODE_ERROR = 2, MODE_EMPTY = 3, MODE_HEADER_VIEW = 4, MODE_FOOTER_VIEW = 5;

2.override the getItemViewType mothod

 @Override
public int getItemViewType(int position) {
    if (mMode == RecyclerViewMode.MODE_LOADING) {
        return RecyclerViewMode.MODE_LOADING;
    }
    if (mMode == RecyclerViewMode.MODE_ERROR) {
        return RecyclerViewMode.MODE_ERROR;
    }
    if (mMode == RecyclerViewMode.MODE_EMPTY) {
        return RecyclerViewMode.MODE_EMPTY;
    }
    //check what type our position is, based on the assumption that the order is headers > items > footers
    if (position < mHeaders.size()) {
        return RecyclerViewMode.MODE_HEADER_VIEW;
    } else if (position >= mHeaders.size() + mData.size()) {
        return RecyclerViewMode.MODE_FOOTER_VIEW;
    }
    return RecyclerViewMode.MODE_DATA;
}

3.override the getItemCount method

@Override
public int getItemCount() {
    if (mMode == RecyclerViewMode.MODE_DATA) {
        return mData.size() + mHeaders.size() + mFooters.size();
    } else {
        return 1;
    }
}

4.override the onCreateViewHolder method. create view holder by viewType

@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    if (viewType == RecyclerViewMode.MODE_LOADING) {
        RecyclerView.ViewHolder loadingViewHolder = onCreateLoadingViewHolder(parent);
        loadingViewHolder.itemView.setLayoutParams(
                new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, parent.getHeight() - mToolBarHeight)
        );
        return loadingViewHolder;
    }
    if (viewType == RecyclerViewMode.MODE_ERROR) {
        RecyclerView.ViewHolder errorViewHolder = onCreateErrorViewHolder(parent);
        errorViewHolder.itemView.setLayoutParams(
                new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, parent.getHeight() - mToolBarHeight)
        );
        errorViewHolder.itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(final View v) {
                if (null != mOnErrorViewClickListener) {
                    new Handler().postDelayed(new Runnable() {
                        @Override
                        public void run() {
                            mOnErrorViewClickListener.onErrorViewClick(v);
                        }
                    }, 200);
                }
            }
        });
        return errorViewHolder;
    }
    if (viewType == RecyclerViewMode.MODE_EMPTY) {
        RecyclerView.ViewHolder emptyViewHolder = onCreateEmptyViewHolder(parent);
        emptyViewHolder.itemView.setLayoutParams(
                new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, parent.getHeight() - mToolBarHeight)
        );
        emptyViewHolder.itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(final View v) {
                if (null != mOnEmptyViewClickListener) {
                    new Handler().postDelayed(new Runnable() {
                        @Override
                        public void run() {
                            mOnEmptyViewClickListener.onEmptyViewClick(v);
                        }
                    }, 200);
                }
            }
        });
        return emptyViewHolder;
    }
    if (viewType == RecyclerViewMode.MODE_HEADER_VIEW) {
        RecyclerView.ViewHolder headerViewHolder = onCreateHeaderViewHolder(parent);
        headerViewHolder.itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(final View v) {
                if (null != mOnHeaderViewClickListener) {
                    new Handler().postDelayed(new Runnable() {
                        @Override
                        public void run() {
                            mOnHeaderViewClickListener.onHeaderViewClick(v, v.getTag());
                        }
                    }, 200);
                }
            }
        });
        return headerViewHolder;
    }
    if (viewType == RecyclerViewMode.MODE_FOOTER_VIEW) {
        RecyclerView.ViewHolder footerViewHolder = onCreateFooterViewHolder(parent);
        footerViewHolder.itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(final View v) {
                if (null != mOnFooterViewClickListener) {
                    new Handler().postDelayed(new Runnable() {
                        @Override
                        public void run() {
                            mOnFooterViewClickListener.onFooterViewClick(v, v.getTag());
                        }
                    }, 200);
                }
            }
        });
        return footerViewHolder;
    }
    RecyclerView.ViewHolder dataViewHolder = onCreateDataViewHolder(parent);
    dataViewHolder.itemView.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(final View v) {
            if (null != mOnItemClickListener) {
                new Handler().postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        mOnItemClickListener.onItemClick(v, v.getTag());
                    }
                }, 200);
            }
        }
    });
    dataViewHolder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
        @Override
        public boolean onLongClick(final View v) {
            if (null != mOnItemLongClickListener) {
                new Handler().postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        mOnItemLongClickListener.onItemLongClick(v, v.getTag());
                    }
                }, 200);
                return true;
            }
            return false;
        }
    });
    return dataViewHolder;
}

5.Override the onBindViewHolder method. bind data by viewType

@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
    if (mMode == RecyclerViewMode.MODE_LOADING) {
        onBindLoadingViewHolder(holder, position);
    } else if (mMode == RecyclerViewMode.MODE_ERROR) {
        onBindErrorViewHolder(holder, position);
    } else if (mMode == RecyclerViewMode.MODE_EMPTY) {
        onBindEmptyViewHolder(holder, position);
    } else {
        if (position < mHeaders.size()) {
            if (mHeaders.size() > 0) {
                onBindHeaderViewHolder(holder, position);
            }
        } else if (position >= mHeaders.size() + mData.size()) {
            if (mFooters.size() > 0) {
                onBindFooterViewHolder(holder, position - mHeaders.size() - mData.size());
            }
        } else {
            onBindDataViewHolder(holder, position - mHeaders.size());
        }
    }
}
yefeng
  • 111
  • 1
  • 14
1

You can use the library SectionedRecyclerViewAdapter to group your items in sections and add a header to each section, like on the image below:

enter image description here

First you create your section class:

class MySection extends StatelessSection {

    String title;
    List<String> list;

    public MySection(String title, List<String> list) {
        // call constructor with layout resources for this Section header, footer and items 
        super(R.layout.section_header, R.layout.section_item);

        this.title = title;
        this.list = list;
    }

    @Override
    public int getContentItemsTotal() {
        return list.size(); // number of items of this section
    }

    @Override
    public RecyclerView.ViewHolder getItemViewHolder(View view) {
        // return a custom instance of ViewHolder for the items of this section
        return new MyItemViewHolder(view);
    }

    @Override
    public void onBindItemViewHolder(RecyclerView.ViewHolder holder, int position) {
        MyItemViewHolder itemHolder = (MyItemViewHolder) holder;

        // bind your view here
        itemHolder.tvItem.setText(list.get(position));
    }

    @Override
    public RecyclerView.ViewHolder getHeaderViewHolder(View view) {
        return new SimpleHeaderViewHolder(view);
    }

    @Override
    public void onBindHeaderViewHolder(RecyclerView.ViewHolder holder) {
        MyHeaderViewHolder headerHolder = (MyHeaderViewHolder) holder;

        // bind your header view here
        headerHolder.tvItem.setText(title);
    }
}

Then you set up the RecyclerView with your sections and change the SpanSize of the headers with a GridLayoutManager:

// Create an instance of SectionedRecyclerViewAdapter 
SectionedRecyclerViewAdapter sectionAdapter = new SectionedRecyclerViewAdapter();

// Create your sections with the list of data
MySection section1 = new MySection("My Section 1 title", dataList1);
MySection section2 = new MySection("My Section 2 title", dataList2);

// Add your Sections to the adapter
sectionAdapter.addSection(section1);
sectionAdapter.addSection(section2);

// Set up a GridLayoutManager to change the SpanSize of the header
GridLayoutManager glm = new GridLayoutManager(getContext(), 2);
glm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
    @Override
    public int getSpanSize(int position) {
        switch(sectionAdapter.getSectionItemViewType(position)) {
            case SectionedRecyclerViewAdapter.VIEW_TYPE_HEADER:
                return 2;
            default:
                return 1;
        }
    }
});

// Set up your RecyclerView with the SectionedRecyclerViewAdapter
RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recyclerview);
recyclerView.setLayoutManager(glm);
recyclerView.setAdapter(sectionAdapter);
Gustavo Pagani
  • 6,583
  • 5
  • 40
  • 71
1

I would just add an alternative to all those HeaderRecyclerViewAdapter implementation. CompoundAdapter:

https://github.com/negusoft/CompoundAdapter-android

It is a more flexible approach, since you can create a AdapterGroup out of Adapters. For the header example, use your adapter as it is, along with an adapter containing one item for the header:

AdapterGroup adapterGroup = new AdapterGroup();
adapterGroup.addAdapter(SingleAdapter.create(R.layout.header));
adapterGroup.addAdapter(new MyAdapter(...));

recyclerView.setAdapter(adapterGroup);

It is fairly simple and readable. You can implement more complex adapter easily using the same principle.

blurkidi
  • 165
  • 1
  • 3
1

Excellent answer by @reaz-murshed shared here . But I dont like the part where datasize is added with +1 and returning Footer View if the end is reached.
It tells that every last element is a Footer View and I had really hard time removing the footer view.

Instead did something like this for my case -

private List<RealResponse> addEmptyLoaderResponse(List<RealResponse> originalList){
    if(originalList == null){
        originalList= new ArrayList<>();
    }
    originalList.add(new EmptyRealResponse());
    return originalList;
}
private class EmptyRealResponse extends RealResponse{
    /**Just an Empty class as placeholder for loader at Footer View
     *
     */
}
public void setItems(List<InconcurPostResponse> items) {
    this.items = addEmptyLoaderResponse(items);
}
@Override
public int getItemCount() {
    return items.size();
}

@Override
public int getItemViewType(int position){
    if(this.items.get(position) instanceof  EmptyRealResponse){
        return ViewTypes.FOOTER_VIEW_TYPE.getViewType();
    }
    return super.getItemViewType(position);
}

This is way cleaner for me and It loads actual Object into Recycler View. Plus I did get the benefit of removing the Footer View when I dont need it Or If I want to add more Placeholder Footer View.

voucher_wolves
  • 565
  • 10
  • 20
0

I know I come late, but only recently I was able to implement such "addHeader" to the Adapter. In my FlexibleAdapter project you can call setHeader on a Sectionable item, then you call showAllHeaders. If you need only 1 header then the first item should have the header. If you delete this item, then the header is automatically linked to the next one.

Unfortunately footers are not covered (yet).

The FlexibleAdapter allows you to do much more than create headers/sections. You really should have a look: https://github.com/davideas/FlexibleAdapter.

Davideas
  • 3,226
  • 2
  • 33
  • 51