Hier werde ich erklären, wie es ohne eine externe Bibliothek geht. Es wird ein sehr langer Beitrag, also machen Sie sich bereit.
Lassen Sie mich zunächst @ tim.paetz bestätigen, dessen Beitrag mich dazu inspiriert hat, meine eigenen Sticky-Header mit ItemDecoration
s zu implementieren . Ich habe einige Teile seines Codes in meiner Implementierung ausgeliehen.
Wie Sie vielleicht schon erlebt, wenn Sie es selbst zu tun versucht, ist es sehr schwer , eine gute Erklärung zu finden , WIE es tatsächlich zu tun mit der ItemDecoration
Technik. Ich meine, was sind die Schritte? Was ist die Logik dahinter? Wie kann ich den Header ganz oben auf der Liste platzieren? Wenn Sie keine Antworten auf diese Fragen kennen, können andere externe Bibliotheken verwenden, während Sie dies selbst tunItemDecoration
ziemlich einfach ist .
Anfangsbedingungen
- Ihr Datensatz sollte
list
aus Elementen unterschiedlichen Typs bestehen (nicht im Sinne von "Java-Typen", sondern im Sinne von "Header / Element" -Typen).
- Ihre Liste sollte bereits sortiert sein.
- Jedes Element in der Liste sollte von einem bestimmten Typ sein - es sollte ein zugehöriges Kopfelement vorhanden sein.
- Das allererste Element im
list
muss ein Kopfelement sein.
Hier gebe ich den vollständigen Code für meinen RecyclerView.ItemDecoration
Anruf an HeaderItemDecoration
. Dann erkläre ich die Schritte im Detail.
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);
}
}
Geschäftslogik
Also, wie mache ich es kleben?
Das tust du nicht. Sie können einen RecyclerView
Artikel Ihrer Wahl nicht einfach anhalten und oben bleiben, es sei denn, Sie sind ein Guru für benutzerdefinierte Layouts und kennen mehr als 12.000 Codezeilen RecyclerView
auswendig. Also, wie es immer mit dem UI-Design geht, wenn Sie etwas nicht machen können, fälschen Sie es. Sie zeichnen einfach den Header über alles mit Canvas
. Sie sollten auch wissen, welche Elemente der Benutzer im Moment sehen kann. Es kommt einfach vor, dass ItemDecoration
Sie sowohl Canvas
Informationen als auch Informationen zu sichtbaren Elementen erhalten. Hier sind die grundlegenden Schritte:
Bei der onDrawOver
Methode RecyclerView.ItemDecoration
erhalten Sie das allererste (oberste) Element, das für den Benutzer sichtbar ist.
View topChild = parent.getChildAt(0);
Bestimmen Sie, welcher Header es darstellt.
int topChildPosition = parent.getChildAdapterPosition(topChild);
View currentHeader = getHeaderViewForItem(topChildPosition, parent);
Zeichnen Sie mit der drawHeader()
Methode den entsprechenden Header über die RecyclerView .
Ich möchte auch das Verhalten implementieren, wenn der neue kommende Header auf den obersten trifft: Es sollte so aussehen, als würde der bevorstehende Header den obersten aktuellen Header sanft aus der Ansicht schieben und schließlich seinen Platz einnehmen.
Hier gilt die gleiche Technik des "Zeichnens auf alles".
Bestimmen Sie, wann der oberste "feststeckende" Header auf den neuen anstehenden Header trifft.
View childInContact = getChildInContact(parent, contactPoint);
Holen Sie sich diesen Kontaktpunkt (das ist die Unterseite des von Ihnen gezeichneten klebrigen Headers und die Oberseite des bevorstehenden Headers).
int contactPoint = currentHeader.getBottom();
Wenn das Element in der Liste diesen "Kontaktpunkt" betritt, zeichnen Sie Ihren klebrigen Header neu, sodass sich seine Unterseite oben auf dem Element befindet. Dies erreichen Sie mit der translate()
Methode der Canvas
. Infolgedessen befindet sich der Startpunkt des oberen Headers außerhalb des sichtbaren Bereichs und scheint "vom kommenden Header herausgeschoben" zu werden. Wenn es vollständig verschwunden ist, zeichnen Sie den neuen Header oben.
if (childInContact != null) {
if (mListener.isHeader(parent.getChildAdapterPosition(childInContact))) {
moveHeader(c, currentHeader, childInContact);
} else {
drawHeader(c, currentHeader);
}
}
Der Rest wird durch Kommentare und ausführliche Anmerkungen in dem von mir bereitgestellten Code erklärt.
Die Verwendung ist unkompliziert:
mRecyclerView.addItemDecoration(new HeaderItemDecoration((HeaderItemDecoration.StickyHeaderInterface) mAdapter));
Sie mAdapter
müssen implementierenStickyHeaderInterface
damit es funktioniert. Die Implementierung hängt von Ihren Daten ab.
Schließlich stelle ich hier ein GIF mit halbtransparenten Überschriften zur Verfügung, damit Sie die Idee erfassen und tatsächlich sehen können, was unter der Haube vor sich geht.
Hier ist die Illustration des Konzepts "Einfach auf alles zeichnen". Sie können sehen, dass es zwei Elemente "Kopfzeile 1" gibt - eines, das wir zeichnen und in einer festgefahrenen Position oben bleiben, und das andere, das aus dem Datensatz stammt und sich mit allen anderen Elementen bewegt. Der Benutzer wird das Innenleben nicht sehen, da Sie keine halbtransparenten Überschriften haben.
Und hier, was passiert in der "Push-out" -Phase:
Hoffe es hat geholfen.
Bearbeiten
Hier ist meine tatsächliche Implementierung der getHeaderPositionForItem()
Methode im RecyclerView-Adapter:
@Override
public int getHeaderPositionForItem(int itemPosition) {
int headerPosition = 0;
do {
if (this.isHeader(itemPosition)) {
headerPosition = itemPosition;
break;
}
itemPosition -= 1;
} while (itemPosition >= 0);
return headerPosition;
}
Etwas andere Implementierung in Kotlin