Тут я поясню, як це зробити без зовнішньої бібліотеки. Це буде дуже довгий пост, тому підбадьорюйте себе.
Перш за все, дозвольте мені визнати @ 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
предмет, який ви обрали, просто зупиніться і дотримуйтесь зверху, якщо ви не гуру користувальницьких макетів і не знаєте напам'ять 12000+ рядків коду 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
щоб він працював. Реалізація залежить від ваших даних.
Нарешті, тут я надаю gif з напівпрозорими заголовками, щоб ви могли зрозуміти ідею і насправді побачити, що відбувається під капотом.
Ось ілюстрація концепції "просто намалюй зверху все". Ви можете бачити, що є два пункти "заголовок 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;
}
Трохи інша реалізація в Котліні