Comment créer des en-têtes collants dans RecyclerView? (Sans lib externe)

120

Je souhaite corriger mes vues d'en-tête en haut de l'écran comme dans l'image ci-dessous et sans utiliser de bibliothèques externes.

entrez la description de l'image ici

Dans mon cas, je ne veux pas le faire par ordre alphabétique. J'ai deux types de vues différents (en-tête et normal). Je veux seulement fixer le haut, le dernier en-tête.

Jaume Colom
la source
17
la question portait sur RecyclerView, cette ^ lib est basée sur ListView
Max Ch

Réponses:

319

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 ItemDecorations. 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 ItemDecorationtechnique. 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 ItemDecorationest assez facile.

Conditions initiales

  1. Votre ensemble de données doit être un listdes éléments de type différent (pas dans un sens de "types Java", mais dans un sens de types "en-tête / élément").
  2. Votre liste devrait déjà être triée.
  3. Chaque élément de la liste doit être d'un certain type - il doit y avoir un élément d'en-tête associé.
  4. Le tout premier élément du listdoit être un élément d'en-tête.

Ici, je fournit le code complet pour mon RecyclerView.ItemDecorationappelé 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 RecyclerViewpar 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 ItemDecorationpeut vous fournir à la fois des Canvasinformations sur les éléments visibles. Avec cela, voici les étapes de base:

  1. Dans la onDrawOverméthode d' RecyclerView.ItemDecorationobtention du tout premier élément (supérieur) visible par l'utilisateur.

        View topChild = parent.getChildAt(0);
  2. Déterminez quel en-tête le représente.

            int topChildPosition = parent.getChildAdapterPosition(topChild);
        View currentHeader = getHeaderViewForItem(topChildPosition, parent);
  3. 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.

  1. Déterminez le moment où l'en-tête «bloqué» supérieur rencontre le nouveau à venir.

            View childInContact = getChildInContact(parent, contactPoint);
  2. 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();
  3. 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 mAdapterdevez mettre en œuvre StickyHeaderInterfacepour 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.

Concept "dessiner au-dessus de tout"

Et voici ce qui se passe lors de la phase "expulsion":

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

Sébastyan Savanyuk
la source
4
@Sevastyan Juste génial! J'ai vraiment aimé la façon dont vous avez résolu ce défi. Rien à dire, sauf peut-être une question: existe-t-il un moyen de définir un OnClickListener sur "l'en-tête collant", ou du moins de consommer le clic empêchant l'utilisateur de cliquer dessus?
Denis
17
Ce serait génial si vous mettez un exemple d'adaptateur de cette implémentation
SolidSnake
1
Je l'ai finalement fait fonctionner avec quelques ajustements ici et là. bien que si vous ajoutez un rembourrage à vos éléments, il continuera à clignoter chaque fois que vous faites défiler jusqu'à la zone rembourrée. la solution dans la mise en page de votre élément crée une mise en page parent avec 0 remplissage et une mise en page enfant avec le remplissage souhaité.
SolidSnake
8
Merci. Solution intéressante, mais un peu chère pour gonfler la vue d'en-tête à chaque événement de défilement. Je viens de changer la logique et d'utiliser ViewHolder et de les conserver dans un HashMap de WeakReferences pour réutiliser des vues déjà gonflées.
Michael
4
@Sevastyan, excellent travail. J'ai une suggestion. Pour éviter de créer de nouveaux en-têtes à chaque fois. Enregistrez simplement l'en-tête et modifiez-le uniquement lorsqu'il change. private View getHeaderViewForItem(int itemPosition, RecyclerView parent) { int headerPosition = mListener.getHeaderPositionForItem(itemPosition); if(headerPosition != mCurrentHeaderIndex) { mCurrentHeader = mListener.createHeaderView(headerPosition, parent); mCurrentHeaderIndex = headerPosition; } return mCurrentHeader; }
Vera Rivotti
27

Le moyen le plus simple consiste simplement à créer une décoration d'article pour votre RecyclerView.

import android.graphics.Canvas;
import android.graphics.Rect;
import android.support.annotation.NonNull;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

public class RecyclerSectionItemDecoration extends RecyclerView.ItemDecoration {

private final int             headerOffset;
private final boolean         sticky;
private final SectionCallback sectionCallback;

private View     headerView;
private TextView header;

public RecyclerSectionItemDecoration(int headerHeight, boolean sticky, @NonNull SectionCallback sectionCallback) {
    headerOffset = headerHeight;
    this.sticky = sticky;
    this.sectionCallback = sectionCallback;
}

@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
    super.getItemOffsets(outRect, view, parent, state);

    int pos = parent.getChildAdapterPosition(view);
    if (sectionCallback.isSection(pos)) {
        outRect.top = headerOffset;
    }
}

@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
    super.onDrawOver(c,
                     parent,
                     state);

    if (headerView == null) {
        headerView = inflateHeaderView(parent);
        header = (TextView) headerView.findViewById(R.id.list_item_section_text);
        fixLayoutSize(headerView,
                      parent);
    }

    CharSequence previousHeader = "";
    for (int i = 0; i < parent.getChildCount(); i++) {
        View child = parent.getChildAt(i);
        final int position = parent.getChildAdapterPosition(child);

        CharSequence title = sectionCallback.getSectionHeader(position);
        header.setText(title);
        if (!previousHeader.equals(title) || sectionCallback.isSection(position)) {
            drawHeader(c,
                       child,
                       headerView);
            previousHeader = title;
        }
    }
}

private void drawHeader(Canvas c, View child, View headerView) {
    c.save();
    if (sticky) {
        c.translate(0,
                    Math.max(0,
                             child.getTop() - headerView.getHeight()));
    } else {
        c.translate(0,
                    child.getTop() - headerView.getHeight());
    }
    headerView.draw(c);
    c.restore();
}

private View inflateHeaderView(RecyclerView parent) {
    return LayoutInflater.from(parent.getContext())
                         .inflate(R.layout.recycler_section_header,
                                  parent,
                                  false);
}

/**
 * Measures the header view to make sure its size is greater than 0 and will be drawn
 * https://yoda.entelect.co.za/view/9627/how-to-android-recyclerview-item-decorations
 */
private void fixLayoutSize(View view, ViewGroup parent) {
    int widthSpec = View.MeasureSpec.makeMeasureSpec(parent.getWidth(),
                                                     View.MeasureSpec.EXACTLY);
    int heightSpec = View.MeasureSpec.makeMeasureSpec(parent.getHeight(),
                                                      View.MeasureSpec.UNSPECIFIED);

    int childWidth = ViewGroup.getChildMeasureSpec(widthSpec,
                                                   parent.getPaddingLeft() + parent.getPaddingRight(),
                                                   view.getLayoutParams().width);
    int childHeight = ViewGroup.getChildMeasureSpec(heightSpec,
                                                    parent.getPaddingTop() + parent.getPaddingBottom(),
                                                    view.getLayoutParams().height);

    view.measure(childWidth,
                 childHeight);

    view.layout(0,
                0,
                view.getMeasuredWidth(),
                view.getMeasuredHeight());
}

public interface SectionCallback {

    boolean isSection(int position);

    CharSequence getSectionHeader(int position);
}

}

XML pour votre en-tête dans recycler_section_header.xml:

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/list_item_section_text"
    android:layout_width="match_parent"
    android:layout_height="@dimen/recycler_section_header_height"
    android:background="@android:color/black"
    android:paddingLeft="10dp"
    android:paddingRight="10dp"
    android:textColor="@android:color/white"
    android:textSize="14sp"
/>

Et enfin pour ajouter la décoration d'article à votre RecyclerView:

RecyclerSectionItemDecoration sectionItemDecoration =
        new RecyclerSectionItemDecoration(getResources().getDimensionPixelSize(R.dimen.recycler_section_header_height),
                                          true, // true for sticky, false for not
                                          new RecyclerSectionItemDecoration.SectionCallback() {
                                              @Override
                                              public boolean isSection(int position) {
                                                  return position == 0
                                                      || people.get(position)
                                                               .getLastName()
                                                               .charAt(0) != people.get(position - 1)
                                                                                   .getLastName()
                                                                                   .charAt(0);
                                              }

                                              @Override
                                              public CharSequence getSectionHeader(int position) {
                                                  return people.get(position)
                                                               .getLastName()
                                                               .subSequence(0,
                                                                            1);
                                              }
                                          });
    recyclerView.addItemDecoration(sectionItemDecoration);

Avec cette décoration d'article, vous pouvez soit rendre l'en-tête épinglé / collé ou non avec juste un booléen lors de la création de la décoration d'article.

Vous pouvez trouver un exemple de travail complet sur github: https://github.com/paetztm/recycler_view_headers

tim.paetz
la source
Je vous remercie. cela a fonctionné pour moi, mais cet en-tête chevauche la vue de recyclage. pouvez-vous aider?
kashyap jimuliya
Je ne suis pas sûr de ce que vous entendez par chevauchement de RecyclerView. Pour le booléen "collant", si vous définissez cela sur false, il placera la décoration de l'élément entre les lignes et ne restera pas en haut de RecyclerView.
tim.paetz
le définir sur "sticky" sur false place l'en-tête entre les lignes, mais cela ne reste pas bloqué (ce que je ne veux pas) en haut. tout en le définissant sur true, il reste coincé sur le dessus mais il chevauche la première ligne dans le recyclerview
kashyap jimuliya
Je peux voir que comme potentiellement deux problèmes, l'un est le rappel de section, vous ne définissez pas le premier élément (position 0) pour isSection sur true. L'autre est que vous passez à la mauvaise hauteur. La hauteur du xml pour la vue de texte doit être la même que la hauteur que vous passez dans le constructeur de la décoration de l'élément de section.
tim.paetz
3
Une chose que j'ajouterais, c'est que si votre mise en page d'en-tête a la vue de texte de titre dimensionnée dynamiquement (par exemple wrap_content), vous voudrez également exécuter fixLayoutSizeaprès avoir défini le texte du titre.
copolii
6

J'ai fait ma propre variante de la solution de Sébastyan ci-dessus

class HeaderItemDecoration(recyclerView: RecyclerView, private val listener: StickyHeaderInterface) : RecyclerView.ItemDecoration() {

private val headerContainer = FrameLayout(recyclerView.context)
private var stickyHeaderHeight: Int = 0
private var currentHeader: View? = null
private var currentHeaderPosition = 0

init {
    val layout = RelativeLayout(recyclerView.context)
    val params = recyclerView.layoutParams
    val parent = recyclerView.parent as ViewGroup
    val index = parent.indexOfChild(recyclerView)
    parent.addView(layout, index, params)
    parent.removeView(recyclerView)
    layout.addView(recyclerView, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
    layout.addView(headerContainer, LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
}

override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
    super.onDrawOver(c, parent, state)

    val topChild = parent.getChildAt(0) ?: return

    val topChildPosition = parent.getChildAdapterPosition(topChild)
    if (topChildPosition == RecyclerView.NO_POSITION) {
        return
    }

    val currentHeader = getHeaderViewForItem(topChildPosition, parent)
    fixLayoutSize(parent, currentHeader)
    val contactPoint = currentHeader.bottom
    val childInContact = getChildInContact(parent, contactPoint) ?: return

    val nextPosition = parent.getChildAdapterPosition(childInContact)
    if (listener.isHeader(nextPosition)) {
        moveHeader(currentHeader, childInContact, topChildPosition, nextPosition)
        return
    }

    drawHeader(currentHeader, topChildPosition)
}

private fun getHeaderViewForItem(itemPosition: Int, parent: RecyclerView): View {
    val headerPosition = listener.getHeaderPositionForItem(itemPosition)
    val layoutResId = listener.getHeaderLayout(headerPosition)
    val header = LayoutInflater.from(parent.context).inflate(layoutResId, parent, false)
    listener.bindHeaderData(header, headerPosition)
    return header
}

private fun drawHeader(header: View, position: Int) {
    headerContainer.layoutParams.height = stickyHeaderHeight
    setCurrentHeader(header, position)
}

private fun moveHeader(currentHead: View, nextHead: View, currentPos: Int, nextPos: Int) {
    val marginTop = nextHead.top - currentHead.height
    if (currentHeaderPosition == nextPos && currentPos != nextPos) setCurrentHeader(currentHead, currentPos)

    val params = currentHeader?.layoutParams as? MarginLayoutParams ?: return
    params.setMargins(0, marginTop, 0, 0)
    currentHeader?.layoutParams = params

    headerContainer.layoutParams.height = stickyHeaderHeight + marginTop
}

private fun setCurrentHeader(header: View, position: Int) {
    currentHeader = header
    currentHeaderPosition = position
    headerContainer.removeAllViews()
    headerContainer.addView(currentHeader)
}

private fun getChildInContact(parent: RecyclerView, contactPoint: Int): View? =
        (0 until parent.childCount)
            .map { parent.getChildAt(it) }
            .firstOrNull { it.bottom > contactPoint && it.top <= contactPoint }

private fun fixLayoutSize(parent: ViewGroup, view: View) {

    val widthSpec = View.MeasureSpec.makeMeasureSpec(parent.width, View.MeasureSpec.EXACTLY)
    val heightSpec = View.MeasureSpec.makeMeasureSpec(parent.height, View.MeasureSpec.UNSPECIFIED)

    val childWidthSpec = ViewGroup.getChildMeasureSpec(widthSpec,
            parent.paddingLeft + parent.paddingRight,
            view.layoutParams.width)
    val childHeightSpec = ViewGroup.getChildMeasureSpec(heightSpec,
            parent.paddingTop + parent.paddingBottom,
            view.layoutParams.height)

    view.measure(childWidthSpec, childHeightSpec)

    stickyHeaderHeight = view.measuredHeight
    view.layout(0, 0, view.measuredWidth, stickyHeaderHeight)
}

interface StickyHeaderInterface {

    fun getHeaderPositionForItem(itemPosition: Int): Int

    fun getHeaderLayout(headerPosition: Int): Int

    fun bindHeaderData(header: View, headerPosition: Int)

    fun isHeader(itemPosition: Int): Boolean
}
}

... et voici l'implémentation de StickyHeaderInterface (je l'ai fait directement dans l'adaptateur recycleur):

override fun getHeaderPositionForItem(itemPosition: Int): Int =
    (itemPosition downTo 0)
        .map { Pair(isHeader(it), it) }
        .firstOrNull { it.first }?.second ?: RecyclerView.NO_POSITION

override fun getHeaderLayout(headerPosition: Int): Int {
    /* ... 
      return something like R.layout.view_header
      or add conditions if you have different headers on different positions
    ... */
}

override fun bindHeaderData(header: View, headerPosition: Int) {
    if (headerPosition == RecyclerView.NO_POSITION) header.layoutParams.height = 0
    else /* ...
      here you get your header and can change some data on it
    ... */
}

override fun isHeader(itemPosition: Int): Boolean {
    /* ...
      here have to be condition for checking - is item on this position header
    ... */
}

Ainsi, dans ce cas, l'en-tête ne consiste pas seulement à dessiner sur un canevas, mais à afficher avec un sélecteur ou une ondulation, un écouteur de clics, etc.

Andrey Turkovsky
la source
Merci d'avoir partagé! Pourquoi avez-vous fini par envelopper le RecyclerView dans un nouveau RelativeLayout?
tmm1
Parce que ma version de l'en-tête collant est View, que j'ai mis dans ce RelativeLayout au-dessus de RecyclerView (dans le champ headerContainer)
Andrey Turkovsky
Pouvez-vous montrer votre implémentation dans un fichier de classe? Comment vous avez transmis l'objet de l'écouteur qui est implémenté dans l'adaptateur.
Dipali Shah
recyclerView.addItemDecoration(HeaderItemDecoration(recyclerView, adapter)). Désolé, je ne trouve pas d'exemple d'implémentation que j'ai utilisé. J'ai modifié la réponse - ajouté du texte aux commentaires
Andrey Turkovsky
6

à tous ceux qui recherchent une solution au problème de scintillement / clignotement lorsque vous en avez déjà DividerItemDecoration. il semble que je l'ai résolu comme ceci:

override fun onDrawOver(...)
    {
        //code from before

       //do NOT return on null
        val childInContact = getChildInContact(recyclerView, currentHeader.bottom)
        //add null check
        if (childInContact != null && mHeaderListener.isHeader(recyclerView.getChildAdapterPosition(childInContact)))
        {
            moveHeader(...)
            return
        }
    drawHeader(...)
}

cela semble fonctionner mais quelqu'un peut-il confirmer que je n'ai rien cassé d'autre?

or_dvir
la source
Merci, cela a également résolu le problème du clignotement pour moi.
Yamashiro Rion le
3

Vous pouvez vérifier et prendre l'implémentation de la classe StickyHeaderHelperdans mon projet FlexibleAdapter et l'adapter à votre cas d'utilisation.

Mais, je suggère d'utiliser la bibliothèque car elle simplifie et réorganise la façon dont vous implémentez habituellement les adaptateurs pour RecyclerView: ne réinventez pas la roue.

Je dirais aussi, n'utilisez pas de décorateurs ou de bibliothèques obsolètes, et n'utilisez pas de bibliothèques qui ne font que 1 ou 3 choses, vous devrez fusionner vous-même les implémentations d'autres bibliothèques.

Davideas
la source
J'ai passé 2 jours à lire le wiki et l'exemple, mais je ne sais toujours pas comment créer une liste réductible en utilisant votre bibliothèque. L'échantillon est assez complexe pour les débutants
Nguyen Minh Binh
1
Pourquoi êtes-vous contre l'utilisation de Decorators?
Sevastyan Savanyuk
1
@Sevastyan, car nous arriverons au point où nous avons besoin d'un écouteur de clic dessus et sur les vues enfants également. Nous décorateur, vous ne pouvez tout simplement pas par définition.
Davideas
@Davidea, voulez-vous dire que vous souhaitez définir des écouteurs de clic sur les en-têtes à l'avenir? Si tel est le cas, cela a du sens. Mais quand même, si vous fournissez vos en-têtes en tant qu'éléments de jeu de données, il n'y aura aucun problème. Même Yigit Boyar recommande d'utiliser des décorateurs.
Sevastyan Savanyuk
@Sevastyan, oui dans ma bibliothèque l'en-tête est un élément comme d'autres dans la liste, donc les utilisateurs peuvent le manipuler. Dans un futur lointain, un gestionnaire de mise en page personnalisé remplacera l'assistant actuel.
Davideas
3

Une autre solution, basée sur l'écouteur scroll. Les conditions initiales sont les mêmes que dans la réponse de Sébastyan

RecyclerView recyclerView;
TextView tvTitle; //sticky header view

//... onCreate, initialize, etc...

public void bindList(List<Item> items) { //All data in adapter. Item - just interface for different item types
    adapter = new YourAdapter(items);
    recyclerView.setAdapter(adapter);
    StickyHeaderViewManager<HeaderItem> stickyHeaderViewManager = new StickyHeaderViewManager<>(
            tvTitle,
            recyclerView,
            HeaderItem.class, //HeaderItem - subclass of Item, used to detect headers in list
            data -> { // bind function for sticky header view
                tvTitle.setText(data.getTitle());
            });
    stickyHeaderViewManager.attach(items);
}

Disposition pour ViewHolder et en-tête collant.

item_header.xml

<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/tv_title"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"/>

Disposition pour RecyclerView

<FrameLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

    <!--it can be any view, but order important, draw over recyclerView-->
    <include
        layout="@layout/item_header"/>

</FrameLayout>

Classe pour HeaderItem.

public class HeaderItem implements Item {

    private String title;

    public HeaderItem(String title) {
        this.title = title;
    }

    public String getTitle() {
        return title;
    }

}

Tout est utile. L'implémentation de l'adaptateur, ViewHolder et autres, n'est pas intéressante pour nous.

public class StickyHeaderViewManager<T> {

    @Nonnull
    private View headerView;

    @Nonnull
    private RecyclerView recyclerView;

    @Nonnull
    private StickyHeaderViewWrapper<T> viewWrapper;

    @Nonnull
    private Class<T> headerDataClass;

    private List<?> items;

    public StickyHeaderViewManager(@Nonnull View headerView,
                                   @Nonnull RecyclerView recyclerView,
                                   @Nonnull Class<T> headerDataClass,
                                   @Nonnull StickyHeaderViewWrapper<T> viewWrapper) {
        this.headerView = headerView;
        this.viewWrapper = viewWrapper;
        this.recyclerView = recyclerView;
        this.headerDataClass = headerDataClass;
    }

    public void attach(@Nonnull List<?> items) {
        this.items = items;
        if (ViewCompat.isLaidOut(headerView)) {
            bindHeader(recyclerView);
        } else {
            headerView.post(() -> bindHeader(recyclerView));
        }

        recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {

            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);
                bindHeader(recyclerView);
            }
        });
    }

    private void bindHeader(RecyclerView recyclerView) {
        if (items.isEmpty()) {
            headerView.setVisibility(View.GONE);
            return;
        } else {
            headerView.setVisibility(View.VISIBLE);
        }

        View topView = recyclerView.getChildAt(0);
        if (topView == null) {
            return;
        }
        int topPosition = recyclerView.getChildAdapterPosition(topView);
        if (!isValidPosition(topPosition)) {
            return;
        }
        if (topPosition == 0 && topView.getTop() == recyclerView.getTop()) {
            headerView.setVisibility(View.GONE);
            return;
        } else {
            headerView.setVisibility(View.VISIBLE);
        }

        T stickyItem;
        Object firstItem = items.get(topPosition);
        if (headerDataClass.isInstance(firstItem)) {
            stickyItem = headerDataClass.cast(firstItem);
            headerView.setTranslationY(0);
        } else {
            stickyItem = findNearestHeader(topPosition);
            int secondPosition = topPosition + 1;
            if (isValidPosition(secondPosition)) {
                Object secondItem = items.get(secondPosition);
                if (headerDataClass.isInstance(secondItem)) {
                    View secondView = recyclerView.getChildAt(1);
                    if (secondView != null) {
                        moveViewFor(secondView);
                    }
                } else {
                    headerView.setTranslationY(0);
                }
            }
        }

        if (stickyItem != null) {
            viewWrapper.bindView(stickyItem);
        }
    }

    private void moveViewFor(View secondView) {
        if (secondView.getTop() <= headerView.getBottom()) {
            headerView.setTranslationY(secondView.getTop() - headerView.getHeight());
        } else {
            headerView.setTranslationY(0);
        }
    }

    private T findNearestHeader(int position) {
        for (int i = position; position >= 0; i--) {
            Object item = items.get(i);
            if (headerDataClass.isInstance(item)) {
                return headerDataClass.cast(item);
            }
        }
        return null;
    }

    private boolean isValidPosition(int position) {
        return !(position == RecyclerView.NO_POSITION || position >= items.size());
    }
}

Interface pour la vue d'en-tête de liaison.

public interface StickyHeaderViewWrapper<T> {

    void bindView(T data);
}
Anrimien
la source
J'aime cette solution. Petite faute de frappe dans findNearestHeader: for (int i = position; position >= 0; i--){ //should be i >= 0
Konstantin
3

Yo,

Voici comment procéder si vous ne voulez qu'un seul type de bâton de support lorsqu'il commence à sortir de l'écran (nous ne nous soucions d'aucune section). Il n'y a qu'une seule façon de ne pas casser la logique interne de RecyclerView des articles de recyclage et c'est de gonfler une vue supplémentaire au-dessus de l'élément d'en-tête de recyclerView et d'y passer des données. Je vais laisser parler le code.

import android.graphics.Canvas
import android.graphics.Rect
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.LayoutRes
import androidx.recyclerview.widget.RecyclerView

class StickyHeaderItemDecoration(@LayoutRes private val headerId: Int, private val HEADER_TYPE: Int) : RecyclerView.ItemDecoration() {

private lateinit var stickyHeaderView: View
private lateinit var headerView: View

private var sticked = false

// executes on each bind and sets the stickyHeaderView
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
    super.getItemOffsets(outRect, view, parent, state)

    val position = parent.getChildAdapterPosition(view)

    val adapter = parent.adapter ?: return
    val viewType = adapter.getItemViewType(position)

    if (viewType == HEADER_TYPE) {
        headerView = view
    }
}

override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
    super.onDrawOver(c, parent, state)
    if (::headerView.isInitialized) {

        if (headerView.y <= 0 && !sticked) {
            stickyHeaderView = createHeaderView(parent)
            fixLayoutSize(parent, stickyHeaderView)
            sticked = true
        }

        if (headerView.y > 0 && sticked) {
            sticked = false
        }

        if (sticked) {
            drawStickedHeader(c)
        }
    }
}

private fun createHeaderView(parent: RecyclerView) = LayoutInflater.from(parent.context).inflate(headerId, parent, false)

private fun drawStickedHeader(c: Canvas) {
    c.save()
    c.translate(0f, Math.max(0f, stickyHeaderView.top.toFloat() - stickyHeaderView.height.toFloat()))
    headerView.draw(c)
    c.restore()
}

private fun fixLayoutSize(parent: ViewGroup, view: View) {

    // Specs for parent (RecyclerView)
    val widthSpec = View.MeasureSpec.makeMeasureSpec(parent.width, View.MeasureSpec.EXACTLY)
    val heightSpec = View.MeasureSpec.makeMeasureSpec(parent.height, View.MeasureSpec.UNSPECIFIED)

    // Specs for children (headers)
    val childWidthSpec = ViewGroup.getChildMeasureSpec(widthSpec, parent.paddingLeft + parent.paddingRight, view.getLayoutParams().width)
    val childHeightSpec = ViewGroup.getChildMeasureSpec(heightSpec, parent.paddingTop + parent.paddingBottom, view.getLayoutParams().height)

    view.measure(childWidthSpec, childHeightSpec)

    view.layout(0, 0, view.measuredWidth, view.measuredHeight)
}

}

Et puis vous faites cela dans votre adaptateur:

override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
    super.onAttachedToRecyclerView(recyclerView)
    recyclerView.addItemDecoration(StickyHeaderItemDecoration(R.layout.item_time_filter, YOUR_STICKY_VIEW_HOLDER_TYPE))
}

YOUR_STICKY_VIEW_HOLDER_TYPE est le type de vue de ce qui est censé être un support collant.

Stanislav Kinzl
la source
2

Pour ceux qui peuvent être concernés. Sur la base de la réponse de Sébastyan, si vous voulez le faire défiler horizontalement. Tout simplement changer getBottom()en getRight()et getTop()engetLeft()

Guster
la source
-1

La réponse est déjà là. Si vous ne souhaitez utiliser aucune bibliothèque, vous pouvez suivre ces étapes:

  1. Trier la liste avec les données par nom
  2. Itérer via une liste avec des données, et en place lorsque la première lettre de l'élément courant! = Première lettre de l'élément suivant, insérer le type d'objet "spécial".
  3. À l'intérieur de votre adaptateur, placez une vue spéciale lorsque l'élément est "spécial".

Explication:

Dans la onCreateViewHolderméthode, nous pouvons vérifierviewType et en fonction de la valeur (notre type "spécial") gonfler une mise en page spéciale.

Par exemple:

public static final int TITLE = 0;
public static final int ITEM = 1;

@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    if (context == null) {
        context = parent.getContext();
    }
    if (viewType == TITLE) {
        view = LayoutInflater.from(context).inflate(R.layout.recycler_adapter_title, parent,false);
        return new TitleElement(view);
    } else if (viewType == ITEM) {
        view = LayoutInflater.from(context).inflate(R.layout.recycler_adapter_item, parent,false);
        return new ItemElement(view);
    }
    return null;
}

class ItemElementet class TitleElementpeut ressembler à l'ordinaire ViewHolder:

public class ItemElement extends RecyclerView.ViewHolder {
//TextView text;

public ItemElement(View view) {
    super(view);
   //text = (TextView) view.findViewById(R.id.text);

}

Donc l'idée de tout cela est intéressante. Mais je suis intéressé si c'est efficace, car nous devons trier la liste des données. Et je pense que cela réduira la vitesse. Si vous en pensez, écrivez-moi :)

Et aussi la question ouverte: comment tenir la mise en page "spéciale" sur le dessus, pendant que les articles sont recyclés. Peut-être combiner tout cela avec CoordinatorLayout.

Valeria
la source
est-il possible de le faire avec un
adaptateur de curseur
10
cette solution ne dit rien sur les en-têtes STICKY qui est le point principal de cet article
Siavash