Skip to content

Instantly share code, notes, and snippets.

@mengdd
Created December 28, 2018 07:55
Show Gist options
  • Save mengdd/c16b1588dbcf4ce453506a58463a5c36 to your computer and use it in GitHub Desktop.
Save mengdd/c16b1588dbcf4ce453506a58463a5c36 to your computer and use it in GitHub Desktop.
Light weight Sticky Headers RecyclerView Decoration
import android.view.View;
public interface StickyHeaderInterface {
/**
* This method gets called by {@link StickyHeaderItemDecoration} 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 StickyHeaderItemDecoration} 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 StickyHeaderItemDecoration} 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 StickyHeaderItemDecoration} 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);
}
import android.graphics.Canvas;
import android.support.annotation.NonNull;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
public class StickyHeaderItemDecoration extends RecyclerView.ItemDecoration {
private StickyHeaderInterface adapter;
private int stickyHeaderHeight;
public StickyHeaderItemDecoration(RecyclerView recyclerView, @NonNull StickyHeaderInterface adapter, StickyHeaderTouchListener.OnHeaderClickListener listener) {
this.adapter = adapter;
StickyHeaderTouchListener touchListener = new StickyHeaderTouchListener(recyclerView, this);
touchListener.setOnHeaderClickListener(listener);
recyclerView.addOnItemTouchListener(touchListener);
}
@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDrawOver(c, parent, state);
View topChild = parent.getChildAt(0);
if (topChild == null) {
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 (childInContact == null) {
return;
}
if (adapter.isHeader(parent.getChildAdapterPosition(childInContact))) {
moveHeader(c, currentHeader, childInContact);
return;
}
drawHeader(c, currentHeader);
}
public boolean isInStickyHeaderArea(int x, int y) {
return y <= stickyHeaderHeight;
}
private View getHeaderViewForItem(int itemPosition, RecyclerView parent) {
int headerPosition = adapter.getHeaderPositionForItem(itemPosition);
int layoutResId = adapter.getHeaderLayout(headerPosition);
View header = LayoutInflater.from(parent.getContext()).inflate(layoutResId, parent, false);
adapter.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(), stickyHeaderHeight = view.getMeasuredHeight());
}
}
import android.support.v7.widget.RecyclerView;
import android.view.GestureDetector;
import android.view.MotionEvent;
public class StickyHeaderTouchListener implements RecyclerView.OnItemTouchListener {
private final GestureDetector gestureDetector;
private final StickyHeaderItemDecoration decoration;
private OnHeaderClickListener onHeaderClickListener;
public interface OnHeaderClickListener {
void onStickyHeaderClick(int touchX, int touchY);
}
public StickyHeaderTouchListener(final RecyclerView recyclerView,
final StickyHeaderItemDecoration decor) {
gestureDetector = new GestureDetector(recyclerView.getContext(), new SingleTapDetector());
decoration = decor;
}
public void setOnHeaderClickListener(OnHeaderClickListener listener) {
onHeaderClickListener = listener;
}
@Override
public boolean onInterceptTouchEvent(RecyclerView view, MotionEvent e) {
if (this.onHeaderClickListener != null) {
boolean tapDetectorResponse = this.gestureDetector.onTouchEvent(e);
if (tapDetectorResponse) {
// Don't return false if a single tap is detected
return true;
}
if (e.getAction() == MotionEvent.ACTION_DOWN) {
return decoration.isInStickyHeaderArea((int) e.getX(), (int) e.getY());
}
}
return false;
}
@Override
public void onTouchEvent(RecyclerView view, MotionEvent e) {
}
@Override
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
}
private class SingleTapDetector extends GestureDetector.SimpleOnGestureListener {
@Override
public boolean onSingleTapUp(MotionEvent e) {
boolean inTouchArea = decoration.isInStickyHeaderArea((int) e.getX(), (int) e.getY());
if (inTouchArea) {
onHeaderClickListener.onStickyHeaderClick((int) e.getX(), (int) e.getY());
return true;
}
return false;
}
@Override
public boolean onDoubleTap(MotionEvent e) {
return true;
}
}
}
@mengdd
Copy link
Author

mengdd commented Dec 28, 2018

@mengdd
Copy link
Author

mengdd commented Jan 3, 2019

Note: do not use margin in the header layout, use padding instead.
Otherwise the decoration will not take margin, and the push up animation between two headers will look junky.

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