public static final int TITLE = 0;
public static final int ITEM = 1;
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (context == null) {
context = parent.getContext();
}
if (viewType == TITLE) {
view = LayoutInflater.from(context).inflate(R.layout.recycler_adapter_title, parent,false);
return new TitleElement(view);
} else if (viewType == ITEM) {
view = LayoutInflater.from(context).inflate(R.layout.recycler_adapter_item, parent,false);
return new ItemElement(view);
}
return null;
}
其中 class ItemElement和 class TitleElement可以看起来像普通的 ViewHolder:
public class ItemElement extends RecyclerView.ViewHolder {
//TextView text;
public ItemElement(View view) {
super(view);
//text = (TextView) view.findViewById(R.id.text);
}
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 (Util.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 (Util.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());
}
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);
}
}
int topChildPosition = parent.getChildAdapterPosition(topChild);
View currentHeader = getHeaderViewForItem(topChildPosition, parent);
Draw the appropriate header on top of the RecyclerView by using drawHeader() method.
I also want to implement the behavior when the new upcoming header meets the top one: it should seem as the upcoming header gently pushes the top current header out of the view and takes his place eventually.
Same technique of "drawing on top of everything" applies here.
Determine when the top "stuck" header meets the new upcoming one.
Get this contact point (that is the bottom of the sticky header your drew and the top of the upcoming header).
int contactPoint = currentHeader.getBottom();
If the item in the list is trespassing this "contact point", redraw your sticky header so its bottom will be at the top of the trespassing item. You achieve this with translate() method of the Canvas. As the result, the starting point of the top header will be out of visible area, and it will seem as "being pushed out by the upcoming header". When it is completely gone, draw the new header on top.
if (childInContact != null) {
if (mListener.isHeader(parent.getChildAdapterPosition(childInContact))) {
moveHeader(c, currentHeader, childInContact);
} else {
drawHeader(c, currentHeader);
}
}
The rest is explained by comments and thorough annotations in piece of code I provided.
class HeaderItemDecoration(recyclerView: RecyclerView, private val listener: StickyHeaderInterface) : RecyclerView.ItemDecoration() {
private val headerContainer = FrameLayout(recyclerView.context)
private var stickyHeaderHeight: Int = 0
private var currentHeader: View? = null
private var currentHeaderPosition = 0
init {
val layout = RelativeLayout(recyclerView.context)
val params = recyclerView.layoutParams
val parent = recyclerView.parent as ViewGroup
val index = parent.indexOfChild(recyclerView)
parent.addView(layout, index, params)
parent.removeView(recyclerView)
layout.addView(recyclerView, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
layout.addView(headerContainer, LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
}
override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
super.onDrawOver(c, parent, state)
val topChild = parent.getChildAt(0) ?: return
val topChildPosition = parent.getChildAdapterPosition(topChild)
if (topChildPosition == RecyclerView.NO_POSITION) {
return
}
val currentHeader = getHeaderViewForItem(topChildPosition, parent)
fixLayoutSize(parent, currentHeader)
val contactPoint = currentHeader.bottom
val childInContact = getChildInContact(parent, contactPoint) ?: return
val nextPosition = parent.getChildAdapterPosition(childInContact)
if (listener.isHeader(nextPosition)) {
moveHeader(currentHeader, childInContact, topChildPosition, nextPosition)
return
}
drawHeader(currentHeader, topChildPosition)
}
private fun getHeaderViewForItem(itemPosition: Int, parent: RecyclerView): View {
val headerPosition = listener.getHeaderPositionForItem(itemPosition)
val layoutResId = listener.getHeaderLayout(headerPosition)
val header = LayoutInflater.from(parent.context).inflate(layoutResId, parent, false)
listener.bindHeaderData(header, headerPosition)
return header
}
private fun drawHeader(header: View, position: Int) {
headerContainer.layoutParams.height = stickyHeaderHeight
setCurrentHeader(header, position)
}
private fun moveHeader(currentHead: View, nextHead: View, currentPos: Int, nextPos: Int) {
val marginTop = nextHead.top - currentHead.height
if (currentHeaderPosition == nextPos && currentPos != nextPos) setCurrentHeader(currentHead, currentPos)
val params = currentHeader?.layoutParams as? MarginLayoutParams ?: return
params.setMargins(0, marginTop, 0, 0)
currentHeader?.layoutParams = params
headerContainer.layoutParams.height = stickyHeaderHeight + marginTop
}
private fun setCurrentHeader(header: View, position: Int) {
currentHeader = header
currentHeaderPosition = position
headerContainer.removeAllViews()
headerContainer.addView(currentHeader)
}
private fun getChildInContact(parent: RecyclerView, contactPoint: Int): View? =
(0 until parent.childCount)
.map { parent.getChildAt(it) }
.firstOrNull { it.bottom > contactPoint && it.top <= contactPoint }
private fun fixLayoutSize(parent: ViewGroup, view: View) {
val widthSpec = View.MeasureSpec.makeMeasureSpec(parent.width, View.MeasureSpec.EXACTLY)
val heightSpec = View.MeasureSpec.makeMeasureSpec(parent.height, View.MeasureSpec.UNSPECIFIED)
val childWidthSpec = ViewGroup.getChildMeasureSpec(widthSpec,
parent.paddingLeft + parent.paddingRight,
view.layoutParams.width)
val childHeightSpec = ViewGroup.getChildMeasureSpec(heightSpec,
parent.paddingTop + parent.paddingBottom,
view.layoutParams.height)
view.measure(childWidthSpec, childHeightSpec)
stickyHeaderHeight = view.measuredHeight
view.layout(0, 0, view.measuredWidth, stickyHeaderHeight)
}
interface StickyHeaderInterface {
fun getHeaderPositionForItem(itemPosition: Int): Int
fun getHeaderLayout(headerPosition: Int): Int
fun bindHeaderData(header: View, headerPosition: Int)
fun isHeader(itemPosition: Int): Boolean
}
}
下面是 SticyHeaderInterface 的实现(我直接在回收器适配器中完成的) :
override fun getHeaderPositionForItem(itemPosition: Int): Int =
(itemPosition downTo 0)
.map { Pair(isHeader(it), it) }
.firstOrNull { it.first }?.second ?: RecyclerView.NO_POSITION
override fun getHeaderLayout(headerPosition: Int): Int {
/* ...
return something like R.layout.view_header
or add conditions if you have different headers on different positions
... */
}
override fun bindHeaderData(header: View, headerPosition: Int) {
if (headerPosition == RecyclerView.NO_POSITION) header.layoutParams.height = 0
else /* ...
here you get your header and can change some data on it
... */
}
override fun isHeader(itemPosition: Int): Boolean {
/* ...
here have to be condition for checking - is item on this position header
... */
}
RecyclerView recyclerView;
TextView tvTitle; //sticky header view
//... onCreate, initialize, etc...
public void bindList(List<Item> items) { //All data in adapter. Item - just interface for different item types
adapter = new YourAdapter(items);
recyclerView.setAdapter(adapter);
StickyHeaderViewManager<HeaderItem> stickyHeaderViewManager = new StickyHeaderViewManager<>(
tvTitle,
recyclerView,
HeaderItem.class, //HeaderItem - subclass of Item, used to detect headers in list
data -> { // bind function for sticky header view
tvTitle.setText(data.getTitle());
});
stickyHeaderViewManager.attach(items);
}
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<!--it can be any view, but order important, draw over recyclerView-->
<include
layout="@layout/item_header"/>
</FrameLayout>
HeaderItem 的类。
public class HeaderItem implements Item {
private String title;
public HeaderItem(String title) {
this.title = title;
}
public String getTitle() {
return title;
}
}
适配器、 ViewHolder 和其他东西的实现对我们来说并不感兴趣。
public class StickyHeaderViewManager<T> {
@Nonnull
private View headerView;
@Nonnull
private RecyclerView recyclerView;
@Nonnull
private StickyHeaderViewWrapper<T> viewWrapper;
@Nonnull
private Class<T> headerDataClass;
private List<?> items;
public StickyHeaderViewManager(@Nonnull View headerView,
@Nonnull RecyclerView recyclerView,
@Nonnull Class<T> headerDataClass,
@Nonnull StickyHeaderViewWrapper<T> viewWrapper) {
this.headerView = headerView;
this.viewWrapper = viewWrapper;
this.recyclerView = recyclerView;
this.headerDataClass = headerDataClass;
}
public void attach(@Nonnull List<?> items) {
this.items = items;
if (ViewCompat.isLaidOut(headerView)) {
bindHeader(recyclerView);
} else {
headerView.post(() -> bindHeader(recyclerView));
}
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
bindHeader(recyclerView);
}
});
}
private void bindHeader(RecyclerView recyclerView) {
if (items.isEmpty()) {
headerView.setVisibility(View.GONE);
return;
} else {
headerView.setVisibility(View.VISIBLE);
}
View topView = recyclerView.getChildAt(0);
if (topView == null) {
return;
}
int topPosition = recyclerView.getChildAdapterPosition(topView);
if (!isValidPosition(topPosition)) {
return;
}
if (topPosition == 0 && topView.getTop() == recyclerView.getTop()) {
headerView.setVisibility(View.GONE);
return;
} else {
headerView.setVisibility(View.VISIBLE);
}
T stickyItem;
Object firstItem = items.get(topPosition);
if (headerDataClass.isInstance(firstItem)) {
stickyItem = headerDataClass.cast(firstItem);
headerView.setTranslationY(0);
} else {
stickyItem = findNearestHeader(topPosition);
int secondPosition = topPosition + 1;
if (isValidPosition(secondPosition)) {
Object secondItem = items.get(secondPosition);
if (headerDataClass.isInstance(secondItem)) {
View secondView = recyclerView.getChildAt(1);
if (secondView != null) {
moveViewFor(secondView);
}
} else {
headerView.setTranslationY(0);
}
}
}
if (stickyItem != null) {
viewWrapper.bindView(stickyItem);
}
}
private void moveViewFor(View secondView) {
if (secondView.getTop() <= headerView.getBottom()) {
headerView.setTranslationY(secondView.getTop() - headerView.getHeight());
} else {
headerView.setTranslationY(0);
}
}
private T findNearestHeader(int position) {
for (int i = position; position >= 0; i--) {
Object item = items.get(i);
if (headerDataClass.isInstance(item)) {
return headerDataClass.cast(item);
}
}
return null;
}
private boolean isValidPosition(int position) {
return !(position == RecyclerView.NO_POSITION || position >= items.size());
}
}
绑定头视图的接口。
public interface StickyHeaderViewWrapper<T> {
void bindView(T data);
}
override fun onDrawOver(...)
{
//code from before
//do NOT return on null
val childInContact = getChildInContact(recyclerView, currentHeader.bottom)
//add null check
if (childInContact != null && mHeaderListener.isHeader(recyclerView.getChildAdapterPosition(childInContact)))
{
moveHeader(...)
return
}
drawHeader(...)
}
//hide the image and the name, and draw only the alphabet
val headerView = getHeaderViewForItem(topChildPosition, parent) ?: return
headerView.findViewById<ShapeableImageView>(R.id.contactImageView).isVisible = false
headerView.findViewById<TextView>(R.id.nameTextView).isVisible = false