Skip to content

Instantly share code, notes, and snippets.

@kasunn25
Last active September 8, 2021 02:12
Show Gist options
  • Save kasunn25/256e2c6b8dc8d0953d6fc9f7d9861246 to your computer and use it in GitHub Desktop.
Save kasunn25/256e2c6b8dc8d0953d6fc9f7d9861246 to your computer and use it in GitHub Desktop.
Complete solution to implement a Recyclerview with sticky group headers. The ItemDecorations class is already given in this question https://stackoverflow.com/questions/32949971/how-can-i-make-sticky-headers-in-recyclerview-without-external-lib
public class Directory {
String name;
String branch;
boolean isHeader;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getBranch() {
return branch;
}
public void setBranch(String branch) {
this.branch = branch;
}
public boolean isHeader() {
return isHeader;
}
public void setHeader(boolean header) {
isHeader = header;
}
}
public class HeaderItemDecoration extends RecyclerView.ItemDecoration {
private StickyHeaderInterface mListener;
private int mStickyHeaderHeight;
public HeaderItemDecoration(RecyclerView recyclerView, @NonNull StickyHeaderInterface listener) {
mListener = listener;
// On Sticky Header Click
recyclerView.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() {
public boolean onInterceptTouchEvent(RecyclerView recyclerView, MotionEvent motionEvent) {
if (motionEvent.getY() <= mStickyHeaderHeight) {
// Handle the clicks on the header here ...
return true;
}
return false;
}
public void onTouchEvent(RecyclerView recyclerView, MotionEvent motionEvent) {
}
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
}
});
}
@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDrawOver(c, parent, state);
View topChild = parent.getChildAt(0);
if (isNull(topChild)) {
return;
}
int topChildPosition = parent.getChildAdapterPosition(topChild);
if (topChildPosition == RecyclerView.NO_POSITION) {
return;
}
View currentHeader = getHeaderViewForItem(topChildPosition, parent);
fixLayoutSize(parent, currentHeader);
int contactPoint = currentHeader.getBottom();
View childInContact = getChildInContact(parent, contactPoint);
if (isNull(childInContact)) {
return;
}
if (mListener.isHeader(parent.getChildAdapterPosition(childInContact))) {
moveHeader(c, currentHeader, childInContact);
return;
}
drawHeader(c, currentHeader);
}
private View getHeaderViewForItem(int itemPosition, RecyclerView parent) {
int headerPosition = mListener.getHeaderPositionForItem(itemPosition);
int layoutResId = mListener.getHeaderLayout(headerPosition);
View header = LayoutInflater.from(parent.getContext()).inflate(layoutResId, parent, false);
mListener.bindHeaderData(header, headerPosition);
return header;
}
private void drawHeader(Canvas c, View header) {
c.save();
c.translate(0, 0);
header.draw(c);
c.restore();
}
private void moveHeader(Canvas c, View currentHeader, View nextHeader) {
c.save();
c.translate(0, nextHeader.getTop() - currentHeader.getHeight());
currentHeader.draw(c);
c.restore();
}
private View getChildInContact(RecyclerView parent, int contactPoint) {
View childInContact = null;
for (int i = 0; i < parent.getChildCount(); i++) {
View child = parent.getChildAt(i);
if (child.getBottom() > contactPoint) {
if (child.getTop() <= contactPoint) {
// This child overlaps the contactPoint
childInContact = child;
break;
}
}
}
return childInContact;
}
/**
* Properly measures and layouts the top sticky header.
* @param parent ViewGroup: RecyclerView in this case.
*/
private void fixLayoutSize(ViewGroup parent, View view) {
// Specs for parent (RecyclerView)
int widthSpec = View.MeasureSpec.makeMeasureSpec(parent.getWidth(), View.MeasureSpec.EXACTLY);
int heightSpec = View.MeasureSpec.makeMeasureSpec(parent.getHeight(), View.MeasureSpec.UNSPECIFIED);
// Specs for children (headers)
int childWidthSpec = ViewGroup.getChildMeasureSpec(widthSpec, parent.getPaddingLeft() + parent.getPaddingRight(), view.getLayoutParams().width);
int childHeightSpec = ViewGroup.getChildMeasureSpec(heightSpec, parent.getPaddingTop() + parent.getPaddingBottom(), view.getLayoutParams().height);
view.measure(childWidthSpec, childHeightSpec);
view.layout(0, 0, view.getMeasuredWidth(), mStickyHeaderHeight = view.getMeasuredHeight());
}
private boolean isNull(View view) {
return (view == null);
}
public interface StickyHeaderInterface {
/**
* This method gets called by {@link HeaderItemDecoration} to fetch the position of the header item in the adapter
* that is used for (represents) item at specified position.
* @param itemPosition int. Adapter's position of the item for which to do the search of the position of the header item.
* @return int. Position of the header item in the adapter.
*/
int getHeaderPositionForItem(int itemPosition);
/**
* This method gets called by {@link HeaderItemDecoration} to get layout resource id for the header item at specified adapter's position.
* @param headerPosition int. Position of the header item in the adapter.
* @return int. Layout resource id.
*/
int getHeaderLayout(int headerPosition);
/**
* This method gets called by {@link HeaderItemDecoration} to setup the header View.
* @param header View. Header to set the data on.
* @param headerPosition int. Position of the header item in the adapter.
*/
void bindHeaderData(View header, int headerPosition);
/**
* This method gets called by {@link HeaderItemDecoration} to verify whether the item represents a header.
* @param itemPosition int.
* @return true, if item at the specified adapter's position represents a header.
*/
boolean isHeader(int itemPosition);
}
}
public class MainActivity extends AppCompatActivity {
private final String TAG = MainActivity.this.getClass().getSimpleName();
RecyclerView mRecyclerView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
try {
setContentView(R.layout.main_activity);
mRecyclerView = (RecyclerView) rootView.findViewById(R.id.recycler_view);
mRecyclerView.setHasFixedSize(true);
final LinearLayoutManager mLayoutManager = new LinearLayoutManager(getActivity());
mLayoutManager.setOrientation(LinearLayoutManager.VERTICAL);
mRecyclerView.setLayoutManager(mLayoutManager);
MyRecyclerAdapter mMyRecyclerAdapter = new MyRecyclerAdapter(getActivity(), yourDataArrayList);
mRecyclerView.setAdapter(mMyRecyclerAdapter);
mRecyclerView.addItemDecoration(new HeaderItemDecoration(mRecyclerView, mMyRecyclerAdapter));
}
catch (Exception ex) {
Log.e(TAG, "onCreate: " + ex.toString());
}
}
}
public class MyRecyclerAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
implements HeaderItemDecoration.StickyHeaderInterface {
private List<Directory> mList;
private Context mContext;
private final int VIEW_HEADER = 1;
private final int VIEW_DETAIL = 0;
public MyRecyclerAdapter(Context context, List<Directory> mList) {
this.mList = mList;
this.mContext = context;
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
if (viewType == VIEW_HEADER) {
View v = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.row_header, null);
return new HeaderViewHolder(v);
} else {
View v = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.row_detail, null);
return new DetailViewHolder(v);
}
}
@Override
public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, final int position) {
if (holder instanceof HeaderViewHolder) {
((HeaderViewHolder) holder).bind(getItem(position));
} else {
((DetailViewHolder) holder).bind(getItem(position));
}
}
public class HeaderViewHolder extends RecyclerView.ViewHolder {
TextView tvName;
HeaderViewHolder(@NonNull View itemView) {
super(itemView);
tvName = (TextView) itemView.findViewById(R.id.tv_name);
}
void bind(@NonNull Directory model) {
tvName.setText(model.getSubCategory());
}
}
public class DetailViewHolder extends RecyclerView.ViewHolder {
TextView tvName;
TextView tvBranch;
DetailViewHolder(@NonNull View view) {
super(view);
tvName = (TextView) view.findViewById(R.id.tv_name);
tvBranch = (TextView) view.findViewById(R.id.tv_branch);
}
void bind(@NonNull Directory child) {
tvName.setText(child.getName());
tvBranch.setText(child.getBranch());
}
}
@Override
public int getItemCount() {
return (null != mList ? mList.size() : 0);
}
public Directory getItem(int position) {
return mList.get(position);
}
@Override
public int getHeaderPositionForItem(int itemPosition) {
int headerPosition = 0;
do {
if (this.isHeader(itemPosition)) {
headerPosition = itemPosition;
break;
}
itemPosition -= 1;
} while (itemPosition >= 0);
return headerPosition;
}
@Override
public int getHeaderLayout(int headerPosition) {
return R.layout.row_header;
}
@Override
public void bindHeaderData(View header, int headerPosition) {
Directory child = mList.get(headerPosition);
TextView tvName = header.findViewById(R.id.tv_name);
tvName.setText(child.getSubCategory());
}
@Override
public boolean isHeader(int itemPosition) {
return mList.get(itemPosition).isHeader();
}
@Override
public int getItemViewType(int position) {
return (mList.get(position).isHeader()) ? VIEW_HEADER : VIEW_DETAIL;
}
}
@stonyz
Copy link

stonyz commented May 21, 2019

If there are thousands of Directory in database, how to handle it? so I can't load all in one time. If doesn't have "Head", I can use Page + Room to handle it. any suggestion?

@kasunn25
Copy link
Author

If you have thousands of records then the you need to use the pagination to fetch limited records one at a time and append it to the recyclerview. In order to implement sticky group header in recyclerview you need to write a logic to group all of your data and mark true for "Directory" "isHeader" parameter when you found a header item in your list.

@stonyz
Copy link

stonyz commented Jun 3, 2019

@kasunn25, thanks a lot. I want to use PagedListAdapter + Room to do pagination, can you provide some idea?

@tellwap
Copy link

tellwap commented Oct 2, 2020

header does not match parent

@chieuancucbo
Copy link

I'm sorry, but what is getSubCategory() ??

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