Qui spiegherò come farlo senza una libreria esterna. Sarà un post molto lungo, quindi preparati.
Prima di tutto, permettimi di riconoscere @ tim.paetz il cui post mi ha ispirato a partire per un viaggio nell'implementazione delle mie intestazioni appiccicose usando ItemDecoration
s. Ho preso in prestito alcune parti del suo codice nella mia implementazione.
Come potresti aver già sperimentato, se hai tentato di farlo da solo, è molto difficile trovare una buona spiegazione di COME farlo effettivamente con la ItemDecoration
tecnica. Voglio dire, quali sono i passaggi? Qual è la logica che c'è dietro? Come faccio a mantenere l'intestazione in cima all'elenco? Non conoscere le risposte a queste domande è ciò che spinge gli altri a utilizzare le librerie esterne, mentre farlo da soli con l'uso di ItemDecoration
è piuttosto facile.
Condizioni iniziali
- Il tuo set di dati dovrebbe essere composto da
list
elementi di tipo diverso (non in senso "tipi Java", ma in senso "intestazione / elemento").
- Il tuo elenco dovrebbe essere già ordinato.
- Ogni elemento nell'elenco dovrebbe essere di un certo tipo: dovrebbe esserci un elemento di intestazione ad esso correlato.
- Il primo elemento nella
list
deve essere un elemento di intestazione.
Qui fornisco il codice completo per il mio RecyclerView.ItemDecoration
call HeaderItemDecoration
. Poi spiego dettagliatamente i passaggi effettuati.
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);
}
}
Logica di business
Quindi, come faccio a farlo aderire?
Non lo fai. Non puoi creare un RecyclerView
elemento di tua scelta, fermati e rimani sopra, a meno che tu non sia un guru dei layout personalizzati e conosci a memoria più di 12.000 righe di codice RecyclerView
. Quindi, come sempre con il design dell'interfaccia utente, se non puoi creare qualcosa, fingilo. Devi solo disegnare l'intestazione sopra tutto ciò che usi Canvas
. Dovresti anche sapere quali elementi l'utente può vedere al momento. Succede e ItemDecoration
può fornirti sia le Canvas
informazioni sugli elementi visibili. Con questo, ecco i passaggi di base:
Nel onDrawOver
metodo per RecyclerView.ItemDecoration
ottenere il primo elemento (in alto) visibile all'utente.
View topChild = parent.getChildAt(0);
Determina quale intestazione lo rappresenta.
int topChildPosition = parent.getChildAdapterPosition(topChild);
View currentHeader = getHeaderViewForItem(topChildPosition, parent);
Disegna l'intestazione appropriata sopra RecyclerView utilizzando il drawHeader()
metodo.
Voglio anche implementare il comportamento quando la nuova intestazione imminente incontra quella in alto: dovrebbe sembrare che l'intestazione imminente spinga delicatamente l'intestazione corrente superiore fuori dalla visualizzazione e alla fine prende il suo posto.
La stessa tecnica di "disegnare sopra tutto" si applica qui.
Determina quando l'intestazione "bloccata" in alto incontra quella nuova in arrivo.
View childInContact = getChildInContact(parent, contactPoint);
Ottieni questo punto di contatto (che è la parte inferiore dell'intestazione adesiva del tuo disegno e la parte superiore dell'intestazione imminente).
int contactPoint = currentHeader.getBottom();
Se l'elemento nell'elenco sta superando questo "punto di contatto", ridisegna la tua intestazione adesiva in modo che la sua parte inferiore si trovi nella parte superiore dell'elemento che ha oltrepassato. Raggiungi questo obiettivo con il translate()
metodo del Canvas
. Di conseguenza, il punto di partenza dell'intestazione superiore sarà fuori dall'area visibile e sembrerà "essere spinto fuori dall'intestazione imminente". Quando è completamente sparito, disegna la nuova intestazione in alto.
if (childInContact != null) {
if (mListener.isHeader(parent.getChildAdapterPosition(childInContact))) {
moveHeader(c, currentHeader, childInContact);
} else {
drawHeader(c, currentHeader);
}
}
Il resto è spiegato da commenti e annotazioni approfondite nella parte di codice che ho fornito.
L'utilizzo è semplice:
mRecyclerView.addItemDecoration(new HeaderItemDecoration((HeaderItemDecoration.StickyHeaderInterface) mAdapter));
Devi mAdapter
implementarlo StickyHeaderInterface
affinché funzioni. L'implementazione dipende dai dati che hai.
Infine, qui fornisco una gif con intestazioni semitrasparenti, così puoi cogliere l'idea e vedere effettivamente cosa sta succedendo sotto il cofano.
Ecco l'illustrazione del concetto "disegna sopra ogni cosa". Puoi vedere che ci sono due elementi "intestazione 1": uno che disegniamo e rimane in cima in una posizione bloccata, e l'altro che proviene dal set di dati e si sposta con tutti gli altri elementi. L'utente non ne vedrà il funzionamento interno, perché non avrai intestazioni semitrasparenti.
Ed ecco cosa succede nella fase di "push out":
Spero che abbia aiutato.
modificare
Ecco la mia attuale implementazione del getHeaderPositionForItem()
metodo nell'adattatore di 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;
}
Implementazione leggermente diversa in Kotlin