Здесь я объясню, как это сделать без внешней библиотеки. Это будет очень длинный пост, так что приготовьтесь.
Прежде всего, позвольте мне поблагодарить @ tim.paetz, чей пост вдохновил меня на путешествие по реализации моих собственных липких заголовков с использованием ItemDecoration
s. Я позаимствовал некоторые части его кода в своей реализации.
Как вы, возможно, уже испытали, если вы попытались сделать это самостоятельно, очень трудно найти хорошее объяснение того, КАК на самом деле делать это с помощью этой ItemDecoration
техники. Я имею в виду, какие шаги? Какая логика за этим? Как сделать так, чтобы заголовок находился в верхней части списка? Незнание ответов на эти вопросы заставляет других использовать внешние библиотеки, а сделать это самостоятельно с помощью ItemDecoration
довольно легко.
Первоначальные условия
- Ваш набор данных должен состоять
list
из элементов разного типа (не в смысле «типов Java», а в смысле типов «заголовок / элемент»).
- Ваш список должен быть уже отсортирован.
- Каждый элемент в списке должен быть определенного типа - с ним должен быть связан элемент заголовка.
- Самый первый элемент в списке
list
должен быть заголовком.
Здесь я предоставляю полный код для RecyclerView.ItemDecoration
вызываемого HeaderItemDecoration
. Затем я подробно объясняю предпринятые шаги.
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);
}
}
Бизнес-логика
Итак, как мне заставить его приклеиться?
Вы этого не сделаете. Вы не можете сделать RecyclerView
элемент по своему выбору, просто остановившись и придерживаясь его, если только вы не являетесь гуру пользовательских макетов и не знаете более 12 000 строк кода RecyclerView
наизусть. Итак, как всегда бывает с дизайном пользовательского интерфейса, если вы не можете что-то сделать, подделайте это. Вы просто рисуете заголовок поверх всего, используя Canvas
. Вы также должны знать, какие элементы пользователь может видеть в данный момент. Просто так получилось, что ItemDecoration
вы можете получить как Canvas
информацию о видимых элементах, так и информацию. При этом вот основные шаги:
В onDrawOver
методе RecyclerView.ItemDecoration
получения самый первый (верхний) элемент, видимый пользователю.
View topChild = parent.getChildAt(0);
Определите, какой заголовок его представляет.
int topChildPosition = parent.getChildAdapterPosition(topChild);
View currentHeader = getHeaderViewForItem(topChildPosition, parent);
Нарисуйте соответствующий заголовок поверх RecyclerView с помощью drawHeader()
метода.
Я также хочу реализовать поведение, когда новый предстоящий заголовок встречается с верхним: должно казаться, что предстоящий заголовок мягко выталкивает верхний текущий заголовок из представления и в конечном итоге занимает его место.
Здесь применяется та же техника «рисования поверх всего».
Определите, когда верхний «застрявший» заголовок встречается с новым, предстоящим.
View childInContact = getChildInContact(parent, contactPoint);
Получите эту точку контакта (это нижняя часть прикрепленного заголовка, который вы нарисовали, и верхняя часть будущего заголовка).
int contactPoint = currentHeader.getBottom();
Если элемент в списке нарушает эту «точку контакта», перерисуйте липкий заголовок так, чтобы его нижняя часть находилась наверху элемента, нарушающего границу. Вы добиваетесь этого с помощью translate()
метода Canvas
. В результате начальная точка верхнего заголовка будет вне видимой области и будет казаться «выталкиваемой предстоящим заголовком». Когда он полностью исчезнет, нарисуйте новый заголовок сверху.
if (childInContact != null) {
if (mListener.isHeader(parent.getChildAdapterPosition(childInContact))) {
moveHeader(c, currentHeader, childInContact);
} else {
drawHeader(c, currentHeader);
}
}
Остальное объясняется комментариями и подробными аннотациями в предоставленном мной фрагменте кода.
Использование простое:
mRecyclerView.addItemDecoration(new HeaderItemDecoration((HeaderItemDecoration.StickyHeaderInterface) mAdapter));
Вы mAdapter
должны реализовать, StickyHeaderInterface
чтобы он работал. Реализация зависит от имеющихся у вас данных.
Наконец, здесь я предоставляю гифку с полупрозрачными заголовками, чтобы вы могли понять идею и действительно увидеть, что происходит под капотом.
Вот иллюстрация концепции «рисовать поверх всего». Вы можете видеть, что есть два элемента «header 1» - один, который мы рисуем и остаемся наверху в застрявшем положении, а другой, который берется из набора данных и перемещается вместе со всеми остальными элементами. Пользователь не увидит его внутреннего устройства, потому что у вас не будет полупрозрачных заголовков.
А вот что происходит в фазе «выталкивания»:
Надеюсь, это помогло.
редактировать
Вот моя фактическая реализация getHeaderPositionForItem()
метода в адаптере RecyclerView:
@Override
public int getHeaderPositionForItem(int itemPosition) {
int headerPosition = 0;
do {
if (this.isHeader(itemPosition)) {
headerPosition = itemPosition;
break;
}
itemPosition -= 1;
} while (itemPosition >= 0);
return headerPosition;
}
Немного другая реализация в Котлине