Ici, je vais vous expliquer comment le faire sans bibliothèque externe. Ce sera un très long post, alors préparez-vous.
Tout d'abord, permettez-moi de remercier @ tim.paetz dont le message m'a inspiré à entreprendre un voyage d'implémentation de mes propres en-têtes collants en utilisant ItemDecoration
s. J'ai emprunté certaines parties de son code dans mon implémentation.
Comme vous l'avez déjà fait l' expérience, si vous essayez de le faire vous - même, il est très difficile de trouver une bonne explication de COMMENT faire réellement avec la ItemDecoration
technique. Je veux dire, quelles sont les étapes? Quelle est la logique derrière cela? Comment faire coller l'en-tête en haut de la liste? Ne pas connaître les réponses à ces questions est ce qui incite les autres à utiliser des bibliothèques externes, alors que le faire soi-même avec l'utilisation de ItemDecoration
est assez facile.
Conditions initiales
- Votre ensemble de données doit être un
list
des éléments de type différent (pas dans un sens de "types Java", mais dans un sens de types "en-tête / élément").
- Votre liste devrait déjà être triée.
- Chaque élément de la liste doit être d'un certain type - il doit y avoir un élément d'en-tête associé.
- Le tout premier élément du
list
doit être un élément d'en-tête.
Ici, je fournit le code complet pour mon RecyclerView.ItemDecoration
appelé HeaderItemDecoration
. Ensuite, j'explique les étapes suivies en détail.
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);
}
}
Logique métier
Alors, comment puis-je le faire tenir?
Vous ne le faites pas. Vous ne pouvez pas créer un RecyclerView
élément de votre choix, arrêtez-vous et restez au top, à moins que vous ne soyez un gourou des mises en page personnalisées et que vous connaissiez plus de 12 000 lignes de code RecyclerView
par cœur. Donc, comme cela va toujours avec la conception de l'interface utilisateur, si vous ne pouvez pas faire quelque chose, faites semblant. Vous venez de dessiner l'en-tête au-dessus de tout ce que vous utilisez Canvas
. Vous devez également savoir quels éléments l'utilisateur peut voir pour le moment. Cela se produit simplement, cela ItemDecoration
peut vous fournir à la fois des Canvas
informations sur les éléments visibles. Avec cela, voici les étapes de base:
Dans la onDrawOver
méthode d' RecyclerView.ItemDecoration
obtention du tout premier élément (supérieur) visible par l'utilisateur.
View topChild = parent.getChildAt(0);
Déterminez quel en-tête le représente.
int topChildPosition = parent.getChildAdapterPosition(topChild);
View currentHeader = getHeaderViewForItem(topChildPosition, parent);
Dessinez l'en-tête approprié au-dessus de RecyclerView à l'aide de la drawHeader()
méthode.
Je souhaite également implémenter le comportement lorsque le nouvel en-tête à venir rencontre celui du haut: il devrait sembler que l'en-tête à venir pousse doucement l'en-tête actuel supérieur hors de la vue et prend finalement sa place.
La même technique de «dessiner par-dessus tout» s'applique ici.
Déterminez le moment où l'en-tête «bloqué» supérieur rencontre le nouveau à venir.
View childInContact = getChildInContact(parent, contactPoint);
Obtenez ce point de contact (qui est le bas de l'en-tête collant de votre dessin et le haut de l'en-tête à venir).
int contactPoint = currentHeader.getBottom();
Si l'élément de la liste empiète sur ce "point de contact", redessinez votre en-tête collant de sorte que son bas soit en haut de l'élément d'intrusion. Vous y parvenez avec la translate()
méthode du Canvas
. En conséquence, le point de départ de l'en-tête supérieur sera hors de la zone visible, et il semblera "être poussé par l'en-tête à venir". Quand il est complètement parti, dessinez le nouvel en-tête en haut.
if (childInContact != null) {
if (mListener.isHeader(parent.getChildAdapterPosition(childInContact))) {
moveHeader(c, currentHeader, childInContact);
} else {
drawHeader(c, currentHeader);
}
}
Le reste est expliqué par des commentaires et des annotations approfondies dans le morceau de code que j'ai fourni.
L'utilisation est simple:
mRecyclerView.addItemDecoration(new HeaderItemDecoration((HeaderItemDecoration.StickyHeaderInterface) mAdapter));
Vous mAdapter
devez mettre en œuvre StickyHeaderInterface
pour que cela fonctionne. La mise en œuvre dépend des données dont vous disposez.
Enfin, je fournis ici un gif avec des en-têtes semi-transparents, afin que vous puissiez saisir l'idée et voir réellement ce qui se passe sous le capot.
Voici l'illustration du concept «dessiner au-dessus de tout». Vous pouvez voir qu'il y a deux éléments "en-tête 1" - l'un que nous dessinons et reste en haut dans une position bloquée, et l'autre qui provient de l'ensemble de données et se déplace avec tous les autres éléments. L'utilisateur ne verra pas le fonctionnement interne de celui-ci, car vous n'aurez pas d'en-têtes semi-transparents.
Et voici ce qui se passe lors de la phase "expulsion":
J'espère que cela a aidé.
Éditer
Voici mon implémentation réelle de la getHeaderPositionForItem()
méthode dans l'adaptateur de 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;
}
Implémentation légèrement différente dans Kotlin