Tutaj wyjaśnię, jak to zrobić bez zewnętrznej biblioteki. To będzie bardzo długi post, więc przygotuj się.
Przede wszystkim chciałbym podziękować @ tim.paetz, którego post zainspirował mnie do wyruszenia w podróż polegającą na implementacji moich własnych lepkich nagłówków przy użyciu ItemDecoration
s. Pożyczyłem niektóre części jego kodu w mojej implementacji.
Jak mogłeś już doświadczyć, jeśli próbowałeś to zrobić samemu, bardzo trudno jest znaleźć dobre wyjaśnienie, JAK właściwie zrobić to za pomocą tej ItemDecoration
techniki. Mam na myśli, jakie są kroki? Jaka jest logika za tym? Jak ustawić nagłówek na górze listy? Brak znajomości odpowiedzi na te pytania sprawia, że inni korzystają z zewnętrznych bibliotek, podczas gdy robienie tego samemu przy użyciu ItemDecoration
jest całkiem proste.
Warunki początkowe
- Zbiór danych powinien składać się
list
z elementów innego typu (nie w sensie „typów Java”, ale w sensie typów „nagłówek / element”).
- Twoja lista powinna być już posortowana.
- Każda pozycja na liście powinna być określonego typu - powinien być związany z nią nagłówek.
- Pierwsza pozycja
list
musi być pozycją nagłówka.
Tutaj podaję pełny kod mojego RecyclerView.ItemDecoration
call HeaderItemDecoration
. Następnie szczegółowo wyjaśniam podjęte kroki.
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);
}
}
Logika biznesowa
Jak więc sprawić, by się przykleił?
Ty nie. Nie możesz zrobić wybranego RecyclerView
elementu, po prostu zatrzymaj się i trzymaj na górze, chyba że jesteś guru niestandardowych układów i znasz ponad 12 000 linii kodu na RecyclerView
pamięć. Tak więc, jak zawsze w przypadku projektu interfejsu użytkownika, jeśli nie możesz czegoś zrobić, udawaj. Po prostu rysujesz nagłówek na górze wszystkiego, używając Canvas
. Powinieneś także wiedzieć, które elementy użytkownik może w danej chwili zobaczyć. Tak się po prostu dzieje, że ItemDecoration
może dostarczyć zarówno Canvas
informacji, jak i widocznych przedmiotów. Oto podstawowe kroki:
W onDrawOver
metodzie RecyclerView.ItemDecoration
uzyskania pierwszego (górnego) elementu, który jest widoczny dla użytkownika.
View topChild = parent.getChildAt(0);
Określ, który nagłówek go reprezentuje.
int topChildPosition = parent.getChildAdapterPosition(topChild);
View currentHeader = getHeaderViewForItem(topChildPosition, parent);
Narysuj odpowiedni nagłówek na górze RecyclerView przy użyciu drawHeader()
metody.
Chcę również zaimplementować zachowanie, gdy nowy nadchodzący nagłówek spotyka się z górnym: powinien wyglądać na to, że nadchodzący nagłówek delikatnie wypycha aktualny górny nagłówek z widoku i ostatecznie zajmuje jego miejsce.
Ta sama technika „rysowania na wierzchu wszystkiego” ma tutaj zastosowanie.
Określ, kiedy górny „zablokowany” nagłówek spotyka się z nowym nadchodzącym.
View childInContact = getChildInContact(parent, contactPoint);
Zdobądź ten punkt kontaktowy (czyli dolną część lepkiego nagłówka, który narysowałeś, i górę nadchodzącego nagłówka).
int contactPoint = currentHeader.getBottom();
Jeśli element na liście przekracza ten „punkt kontaktowy”, przerysuj swój przyklejony nagłówek, tak aby jego dół znajdował się na górze elementu, dla którego nastąpiło naruszenie. Osiągasz to za pomocą translate()
metody Canvas
. W rezultacie punkt początkowy górnego nagłówka będzie poza widocznym obszarem i będzie wyglądał, jakby był „wypychany przez nadchodzący nagłówek”. Kiedy zniknie, narysuj nowy nagłówek na górze.
if (childInContact != null) {
if (mListener.isHeader(parent.getChildAdapterPosition(childInContact))) {
moveHeader(c, currentHeader, childInContact);
} else {
drawHeader(c, currentHeader);
}
}
Resztę wyjaśniają komentarze i dokładne adnotacje w podanym przeze mnie fragmencie kodu.
Użycie jest proste:
mRecyclerView.addItemDecoration(new HeaderItemDecoration((HeaderItemDecoration.StickyHeaderInterface) mAdapter));
Twój mAdapter
musi wdrożyćStickyHeaderInterface
go do pracy. Wdrożenie zależy od posiadanych danych.
Na koniec udostępniam tutaj gif z półprzezroczystymi nagłówkami, abyś mógł uchwycić pomysł i faktycznie zobaczyć, co się dzieje pod maską.
Oto ilustracja koncepcji „po prostu rysuj na wierzchu wszystkiego”. Możesz zobaczyć, że istnieją dwa elementy „nagłówek 1” - jeden, który rysujemy i pozostaje na górze w zablokowanej pozycji, a drugi, który pochodzi ze zbioru danych i przenosi się ze wszystkimi pozostałymi elementami. Użytkownik nie zobaczy jego wewnętrznego działania, ponieważ nie będziesz mieć półprzezroczystych nagłówków.
A oto co dzieje się w fazie „wypychania”:
Mam nadzieję, że to pomogło.
Edytować
Oto moja rzeczywista implementacja getHeaderPositionForItem()
metody w adapterze 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;
}
Nieco inna implementacja w Kotlinie