Skip to content

5.x | Headers and Sections

Stanislav Koshutsky edited this page Dec 20, 2019 · 61 revisions

In this page


Section

A Section is composed by a Header item with IHeader interface and a Child item or a set of Children items of type ISectionable which internally hold the same instance of the header item. Read the tutorial of Item interfaces for more details about all item interfaces.

For simplicity, you can directly extend AbstractHeaderItem and AbstractSectionableItem respectively to quickly create a header item and a child item with its header. Optionally you can extend/collapse a section (please refer at the end of this page):

public abstract class AbstractHeaderItem<VH extends FlexibleViewHolder>
        extends AbstractFlexibleItem<VH>
        implements IHeader<VH> {

    /**
     * By default, header is hidden and not selectable
     */
    public AbstractHeaderItem() {
        setHidden(true);
        setSelectable(false);
    }
}
public abstract class AbstractSectionableItem<VH extends RecyclerView.ViewHolder, T extends IHeader>
        extends AbstractFlexibleItem<VH>
        implements ISectionable<VH, T> {

    /**
     * The header of this item
     */
    protected T header;

    public AbstractSectionableItem(T header) {
        this.header = header;
    }

    @Override
    public T getHeader() {
        return header;
    }

    @Override
    public void setHeader(T header) {
        this.header = header;
    }
}

Initialization

There are 2 use cases to represent sections with headers:

1. Use case similar to Instagram or Contacts

Main items are defined as ISectionable that hold IHeader item, useful to display 1 or more items with the same Header linked.
The adapter list should be initialized only with ISectionable items, while, the same instance of the Header item, not added to the list, must be assigned to all the ISectionable items of the same group, using the constructor or the method setHeader().

Header items can be shown all together with a unique command showAllHeaders() OR at startup by calling setDisplayHeadersAtStartUp(true) which internally calls showAllHeaders(). Doing this, the hidden property is changed to false and the header items are inserted at the top of each ISectionable item, wherever the same header instance is still hidden.

For sectioned(headered) Grid you should pass a GrigLayoutManager to a RecyclerView and override getSpanSize(int spanCount, int position) of your IHeader implementation to return actual span (by default it returns 1 and thus your headers will look like grid items). You can pass span size in to your IHeader class constructor if it does not change at runtime. If span size changes dynamically please refer to a sample app.

⚠️ Warning:

  • By default, headers are NOT shown at startup.
  • As explained in the Item interfaces, it is important to well Override the equals method.

2. Use case to extend/collapse a section

Header item is defined as IExpandable combined with IHeader, containing sub items of type ISectionable. For simplicity, you can directly extend from AbstractExpandableHeaderItem. Here, on the other hand, you should add only the expandable/header items to the Adapter list. To initially expand the section at startup, you need to call expandItemsAtStartUp() in the Activity/Fragment creation phase.

ℹ️ Note: Headers items can be hidden, but be aware that sub items need the own parent (Header) currently shown.

Sticky headers

To make a header sticky, you must follow these 5 steps:

At design time

1. Always wrap the RecyclerView widget with a FrameLayout as following:

<!-- This FrameLayout is still mandatory ONLY IF sticky header will be enabled. This
     layout will help to receive the inflated layout at runtime and to display the
     Refresh circle AND the FastScroller on the top of sticky_header_layout. -->
<FrameLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:listitem="@layout/recycler_adapter_item"/>

    <!-- NOT NEEDED ANYMORE!! This layout is generated at runtime when
         sticky headers are enabled. So you can safely remove it. -->
    <!--<include layout="@layout/sticky_header_layout"/>-->

</FrameLayout>

2. Implement IHeader interface for the item that will be sticky.

3. Since itemView is declared final in Android SDK, you will need to pass true to the super() constructor of the Header ViewHolder only.
This parameter allows to insert a FrameLayout holding the real content and to avoid App crashing during scrolling (etc.) because the LayoutManager needs the itemView with a contentView filled. The technique adopted actually moves the contentView to the FrameLayout (@layout/sticky_header_layout) changing the parent view!
Therefore, to access to the header content you must call getContentView() and not itemView.

At runtime

4. Display headers with setDisplayHeadersAtStartUp(true).

5. Call setStickyHeaders(true), headers stay sticky if they are currently shown.

ℹ️ Note:

  • You can still customize your sticky_header_layout by calling:
setStickyHeaders(true, ViewGroup stickyContainer).
  • You should call getFlexibleAdapterPosition() instead of getAdapterPosition() only in Header ViewHolder, for 2 reasons:
    • getAdapterPosition() is declared final and we cannot override it;
    • When scrolling down the RecyclerView, the sticky ViewHolders are created out of the LayoutManager and if clicked in this particular situation, NO_POSITION is returned!

Transparency, Elevation and Offset

It is now super easy to add the Transparency and Elevation to the header item, the values are taken from the header item layout:

<RelativeLayout	xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="#aa7e57c2" <--- transparent background
    android:elevation="5dp">       <--- elevation for header item + sticky header
    ...
</RelativeLayout>

But if elevation is not configured in the layout or is Zero, then you have 1 new method to obtain the following effect: header item flat + sticky header elevated:

setStickyHeaderElevation(float stickyElevation);

🏅 Bonus from my researches:

  • Transparent Sticky Header with elevation at the same time.
  • Surgery precision on header swapping :-)
  • Double background not visible anymore on header swapping, if transparency is set.
  • Fade animation on sticky header container, when sticky is enabled or disabled.

Layout Linear Layout Grid

By applying custom Offset to the header item, it will be retained on Sticky Layout too:

mRecyclerView.addItemDecoration(new FlexibleItemDecoration(Context)
    .addItemViewType(R.layout.header_item, 8, 8, 8, 8) //values in dpi
    .withEdge(true));

Layout Linear Layout Grid

You can read more about offsets on Flexible Item Decoration wiki page.

Adding/removing a section

Following the use case 1:

  • To add new section:
    • You can invoke addSection(header). An empty section (the header) will be added to the top of the list;
    • You can invoke addSection(header, comparator*). To add the section in the middle of the list with your custom sorting rules.
  • To remove a section:
    • From 5.0.5, you can delete the whole section, header included, by invoking removeSection(header);.

Following the use case 2:

  • Add the new IExpandable+IHeader item to the Adapter list by simply calling addSection() or addItem(position, item). This section remains collapsed until you call expand(the new position), when you do, be sure to have the expanded property equals to false. Optionally, this property can remain true, but you have to call expand(the new position, init = true): read carefully the JavaDoc.
  • Deleting the IExpandable item, will automatically collapse the expandable and removed from the adapter list together with the sub-items too!

Adding/removing any item in a section

Always keep in mind that, the Adapter list must be manually synchronized with your Database list, meaning that the new item already exists in the Database list, then you call the Adapter methods to reflect the change and animate it for the user.

To add sub items:

Following the use case 1:

You can call addItemToSection(sectionable, header, comparator*) and to simplify the process calculatePositionFor(item, comparator*) has been introduced, this method accepts a custom Comparator object to extract the precise position where to add the new item in the Adapter list, then call addItem(position, item).

*️⃣ = The Comparator object should be customized to support ALL the types of items the Adapter is displaying or a ClassCastException will be raised. If the Comparator is null the returned position will be 0. See the demo App for a real example.

Following the use case 2:

Simply call addSubItem(parentPosition, subPosition, subItem). Section must be already expanded, otherwise it doesn't have any effect, if not, add true as 4th parameter it will be automatically expanded. Example:

public void addChild(AbstractExpandableHeaderItem header) {
    ChildItem childItem = ...
    // Add the child item to the end of the section
    // Note: you don't need the next line, if childItem is already part of the sublist
    header.addSubItem(childItem);//last position
    // Synchronize the Adapter,
    mAdapter.addSubItem(mAdapter.getGlobalPositionOf(header),
            header.getSubItemPosition(childItem),
            childItem);
}

To remove any items, simply call removeItem(the adapter global position). If you have the Undo functionality enabled, you then delete the item from the Database only when timeout is over (See UndoHelper wiki page).

Dragging with auto-linkage

We can drag an ISectionable item with a header inside or the IHeader item as well. Auto-linkage works in 6 ways:

  • Header item is dragged up.
  • Header item is dragged down.
  • The upper item is dragged below the header item.
  • The underlying item is dragged above the header.
  • The underlying item is dragged lower.
  • A lower item is dragged up and becomes a new underlying item.

The ISectionable item is updated with the following behaviors:

  • Item moved out of any section has a null header.
  • Item moved into another section updates the header.
  • Item moved inside the same section maintains the header.

The header item is notified about the change and if the underlying item is another header, then the section becomes empty and header remains unlinked.

Expand/Collapse a section

Using AbstractExpandableHeaderItem (in the library), an implementation of IExpandable and IHeader interfaces, it is possible to make a section expandable/collapsible by configuring the item signature as following (3 examples). This special item can be collapsed by clicking on it, also when the header of the section (aka parent) is sticky!

Check all details for expandable items.

Example 1 (preferable):

public class ExpandableHeaderItem
        extends AbstractExpandableHeaderItem<ExpandableHeaderItem.ExpandableHeaderViewHolder, SubItem> {

    public ExpandableHeaderItem() {
        super(); //Call super to auto-configure the section status as "shown, expanded, not selectable"
    }
}

Example 2:

public class ExpandableHeaderItem
        extends AbstractFlexibleItem<ExpandableHeaderItem.ExpandableHeaderViewHolder>
        implements IExpandable<ExpandableHeaderItem.ExpandableHeaderViewHolder, SubItem>,
        IHeader<ExpandableHeaderItem.ExpandableHeaderViewHolder> {

    public ExpandableHeaderItem() {
        // Configure the section status: shown and/or expanded
        setHidden(false);
        setExpanded(true);
        // NOT selectable, otherwise (if you have) the ActionMode will be activated on long click!
        setSelectable(false);
    }
}

Example 3:

public class ExpandableHeaderItem
        extends AbstractExpandableItem<ExpandableHeaderItem.ExpandableHeaderViewHolder, SubItem>,
        implements IHeader<ExpandableHeaderItem.ExpandableHeaderViewHolder> {

    public ExpandableHeaderItem() {
        setHidden(false);
        setExpanded(true);
        setSelectable(false);
    }
}
Clone this wiki locally