Last active
September 8, 2021 02:12
-
-
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
This file contains 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
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; | |
} | |
} |
This file contains 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
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); | |
} | |
} |
This file contains 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
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()); | |
} | |
} | |
} |
This file contains 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
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; | |
} | |
} |
@kasunn25, thanks a lot. I want to use PagedListAdapter + Room to do pagination, can you provide some idea?
header does not match parent
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
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.