Android RecyclerView with sticky header using ItemDecoration

The sticky header layout, recycler_section_header.xml

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/list_item_section_text"
    android:layout_width="match_parent"
    android:layout_height="@dimen/recycler_section_header_height"
    android:background="#41cfef"
    android:paddingLeft="10dp"
    android:paddingRight="10dp"
    android:textColor="@android:color/white"
    android:textSize="18sp" />

The RecyclerView list item layout, list_item.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="10dp">
    <TextView
        android:id="@+id/tv_text"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="16dp"
        android:textSize="18sp"
        android:background="#afaeae"
        android:textColor="#fff"
        android:gravity="center"
        android:layout_gravity="center_horizontal"/>
</LinearLayout>

The main layout, activity_main.xml

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

        <android.support.v7.widget.RecyclerView
            android:id="@+id/rv_list"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>

</LinearLayout>

The activity class, MainActivity.java

public class MainActivity extends AppCompatActivity {

    RecyclerView recyclerView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        recyclerView = (RecyclerView) findViewById(R.id.rv_list);
        recyclerView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false));

        final List<Person> people = PeopleRepo.getPeopleSorted();
        recyclerView.setAdapter(new PersonAdapter(people, R.layout.list_item));

        RecyclerSectionItemDecoration sectionItemDecoration =
                new RecyclerSectionItemDecoration(getResources().getDimensionPixelSize(R.dimen.recycler_section_header_height),
                        true,
                        getSectionCallback(people));
        recyclerView.addItemDecoration(sectionItemDecoration);
    }
    private RecyclerSectionItemDecoration.SectionCallback getSectionCallback(final List<Person> people) {
        return new RecyclerSectionItemDecoration.SectionCallback() {
            @Override
            public boolean isSection(int position) {
                return position == 0
                        || people.get(position)
                        .getLastName()
                        .charAt(0) != people.get(position - 1)
                        .getLastName()
                        .charAt(0);
            }

            @Override
            public CharSequence getSectionHeader(int position) {
                return people.get(position)
                        .getLastName()
                        .subSequence(0,
                                1);
            }
        };
    }
}

The Adapter for the RecyclerView, PersonAdapter.java

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

    private final List<Person> people;
    private final int          rowLayout;

    public PersonAdapter(List<Person> people, @LayoutRes int rowLayout) {
        this.people = people;
        this.rowLayout = rowLayout;
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(rowLayout, parent, false);
        return new ViewHolder(view);
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        Person person = people.get(position);
        holder.fullName.setText(person.getFullName());
    }

    @Override
    public int getItemCount() {
        return people.size();
    }

    public static class ViewHolder extends RecyclerView.ViewHolder {
        private TextView fullName;

        public ViewHolder(View view) {
            super(view);
            fullName = (TextView) view.findViewById(R.id.tv_text);
        }
    }
}

The ItemDecoration class for the sticky header RecyclerSectionItemDecoration.java

import android.graphics.Canvas;
import android.graphics.Rect;
import android.support.annotation.NonNull;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

/**
 * https://github.com/paetztm/recycler_view_headers
 */

public class RecyclerSectionItemDecoration extends RecyclerView.ItemDecoration {

    private final int             headerOffset;
    private final boolean         sticky;
    private final SectionCallback sectionCallback;

    private View headerView;
    private TextView header;

    public RecyclerSectionItemDecoration(int headerHeight, boolean sticky, @NonNull SectionCallback sectionCallback) {
        headerOffset = headerHeight;
        this.sticky = sticky;
        this.sectionCallback = sectionCallback;
    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);

        int pos = parent.getChildAdapterPosition(view);
        if (sectionCallback.isSection(pos)) {
            outRect.top = headerOffset;
        }
    }

    @Override
    public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDrawOver(c, parent, state);

        if (headerView == null) {
            headerView = inflateHeaderView(parent);
            header = (TextView) headerView.findViewById(R.id.list_item_section_text);
            fixLayoutSize(headerView, parent);
        }

        CharSequence previousHeader = "";
        for (int i = 0; i < parent.getChildCount(); i++) {
            View child = parent.getChildAt(i);
            final int position = parent.getChildAdapterPosition(child);

            CharSequence title = sectionCallback.getSectionHeader(position);
            header.setText(title);
            if (!previousHeader.equals(title) || sectionCallback.isSection(position)) {
                drawHeader(c, child, headerView);
                previousHeader = title;
            }
        }
    }

    private void drawHeader(Canvas c, View child, View headerView) {
        c.save();
        if (sticky) {
            c.translate(0, Math.max(0, child.getTop() - headerView.getHeight()));
        } else {
            c.translate(0, child.getTop() - headerView.getHeight());
        }
        headerView.draw(c);
        c.restore();
    }

    private View inflateHeaderView(RecyclerView parent) {
        return LayoutInflater.from(parent.getContext())
                             .inflate(R.layout.recycler_section_header, parent, false);
    }

    /**
     * Measures the header view to make sure its size is greater than 0 and will be drawn
     * https://yoda.entelect.co.za/view/9627/how-to-android-recyclerview-item-decorations
     */
    private void fixLayoutSize(View view, ViewGroup parent) {
        int widthSpec = View.MeasureSpec.makeMeasureSpec(parent.getWidth(),
                                                         View.MeasureSpec.EXACTLY);
        int heightSpec = View.MeasureSpec.makeMeasureSpec(parent.getHeight(),
                                                          View.MeasureSpec.UNSPECIFIED);

        int childWidth = ViewGroup.getChildMeasureSpec(widthSpec,
                                                       parent.getPaddingLeft() + parent.getPaddingRight(),
                                                       view.getLayoutParams().width);
        int childHeight = ViewGroup.getChildMeasureSpec(heightSpec,
                                                        parent.getPaddingTop() + parent.getPaddingBottom(),
                                                        view.getLayoutParams().height);

        view.measure(childWidth, childHeight);

        view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
    }

    public interface SectionCallback {

        boolean isSection(int position);

        CharSequence getSectionHeader(int position);
    }
}

The dummy object class, Person.java

import android.support.annotation.NonNull;

import java.util.Locale;

public class Person implements Comparable<Person> {
    private final CharSequence firstName;
    private final CharSequence lastName;
    private static final String NAME_DISPLAY = "%s, %s";

    public Person(@NonNull CharSequence firstName, @NonNull CharSequence lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public CharSequence getLastName() {
        return lastName;
    }

    public CharSequence getFirstName() {
        return firstName;
    }

    public String getFullName() {
        return String.format(Locale.getDefault(),
                             NAME_DISPLAY,
                             getLastName(),
                             getFirstName());
    }

    @Override
    public int compareTo(@NonNull Person person) {
        return getLastName().toString()
                            .compareTo(person.getLastName()
                                             .toString());
    }
}

The data creation class, PeopleRepo.java

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class PeopleRepo {

    public static List<Person> getPeople() {
        List<Person> people = new ArrayList<>(45);
        people.add(new Person("George", "Washington"));
        people.add(new Person("John", "Adams"));
        people.add(new Person("Thomas", "Jefferson"));
        people.add(new Person("James", "Madison"));
        people.add(new Person("James", "Monroe"));
        people.add(new Person("John Quincy", "Adams"));
        people.add(new Person("Andrew", "Jackson"));
        people.add(new Person("Martin", "Van Buren"));
        people.add(new Person("William", "Harrison"));
        people.add(new Person("John", "Tyler"));
        people.add(new Person("Zachary", "Taylor"));
        people.add(new Person("Millard", "Fillmore"));
        people.add(new Person("Franklin", "Pierce"));
        people.add(new Person("James", "Buchanan"));
        people.add(new Person("Abraham", "Lincoln"));
        people.add(new Person("Andrew", "Johnson"));
        people.add(new Person("Ulysses", "Grant"));
        people.add(new Person("Rutherford", "Hayes"));
        people.add(new Person("James", "Garfield"));
        people.add(new Person("Chester", "Arthur"));
        people.add(new Person("Grover", "Cleveland"));
        people.add(new Person("Benjamin", "Harrison"));
        people.add(new Person("William", "McKinley"));
        people.add(new Person("Theodore", "Roosevelt"));
        people.add(new Person("William", "Taft"));
        people.add(new Person("Woodrow", "Wilson"));
        people.add(new Person("Warren", "Harding"));
        people.add(new Person("Calvin", "Coolidge"));
        people.add(new Person("Herbert", "Hoover"));
        people.add(new Person("Harry", "Truman"));
        people.add(new Person("Dwight", "Eisenhower"));
        people.add(new Person("John", "Kennedy"));
        people.add(new Person("Lyndon", "Johnson"));
        people.add(new Person("Richard", "Nixon"));
        people.add(new Person("Gerald", "Ford"));
        people.add(new Person("Jimmy", "Carter"));
        people.add(new Person("Ronald", "Reagan"));
        people.add(new Person("George H.W.", "Bush"));
        people.add(new Person("Bill", "Clinton"));
        people.add(new Person("George W.", "Bush"));
        people.add(new Person("Barack", "Obama"));
        people.add(new Person("Donald", "Trump"));
        return people;
    }

    public static List<Person> getPeopleSorted() {
        List<Person> people = getPeople();
        Collections.sort(people);
        return people;
    }
}

Complete example in Github
Reference:
https://github.com/paetztm/recycler_view_headers

Search within Codexpedia

Custom Search

Search the entire web

Custom Search