Created
December 28, 2018 07:55
-
-
Save mengdd/c16b1588dbcf4ce453506a58463a5c36 to your computer and use it in GitHub Desktop.
Light weight Sticky Headers RecyclerView Decoration
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} | |
} |
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
Mainly inspired by answer at: https://stackoverflow.com/questions/32949971/how-can-i-make-sticky-headers-in-recyclerview-without-external-lib
And the touch event handling part is take reference from: https://github.com/timehop/sticky-headers-recyclerview