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

Je veux 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.

entrer 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 en haut, le dernier en-tête.

    Ici, je vais vous expliquer comment le faire sans une bibliothèque externe. Ce sera un très long message, alors préparez-vous.

    Tout d’abord, permettez-moi de reconnaître @ tim.paetz dont le poste m’a inspiré pour entreprendre un parcours d’implémentation de mes en-têtes à l’aide de ItemDecoration . J’ai emprunté certaines parties de son code dans mon implémentation.

    Comme vous l’avez peut-être déjà expérimenté, si vous tentez de le faire vous-même, il est très difficile de trouver une bonne explication de ItemDecoration le faire avec la technique ItemDecoration . Je veux dire, quelles sont les étapes? Quelle est la logique derrière cela? Comment puis-je 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 vous-même avec l’utilisation de ItemDecoration est assez simple.

    Conditions initiales

    1. Votre jeu de données doit être une list d’éléments de type différent (pas dans un sens “Java types”, mais dans un sens “en-tête / élément”).
    2. Votre liste devrait déjà être sortingé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. Tout premier élément de la list doit être un élément d’en-tête.

    Ici, je fournis le code complet pour mon RecyclerView.ItemDecoration appelé HeaderItemDecoration . Ensuite, j’explique les étapes sockets 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 je le fais coller?

    Vous ne le faites pas Vous ne pouvez pas faire en sorte qu'un article de RecyclerView de votre choix s'arrête et rest collé, sauf si vous êtes un gourou des mises en page personnalisées et que vous connaissez plus de 12 000 lignes de code pour un RecyclerView par cœur. Donc, comme cela se passe toujours avec le design de l'interface utilisateur, si vous ne pouvez pas faire quelque chose, faîtes-le. Vous venez de dessiner l'en-tête sur tout en utilisant Canvas . Vous devez également savoir quels éléments l'utilisateur peut voir pour le moment. Il se trouve que ItemDecoration peut vous fournir à la fois le Canvas et des informations sur les éléments visibles. Avec ceci, voici les étapes de base:

    1. Dans la méthode onDrawOver de RecyclerView.ItemDecoration obtenez le premier élément (supérieur) visible pour l'utilisateur.

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

        int topChildPosition = parent.getChildAdapterPosition(topChild); View currentHeader = getHeaderViewForItem(topChildPosition, parent); 
    3. Dessinez l'en-tête approprié en haut de RecyclerView en utilisant la méthode drawHeader() .

    Je veux également implémenter le comportement lorsque le nouvel en-tête à venir rencontre le premier: il devrait sembler que le prochain en-tête pousse doucement l’en-tête actuel hors de la vue et finit par prendre sa place.

    La même technique de "dessin sur tout" s'applique ici.

    1. Déterminez à quel moment le premier en-tête "coincé" rencontre le nouvel en-tête.

        View childInContact = getChildInContact(parent, contactPoint); 
    2. Obtenez ce sharepoint contact (c’est-à-dire le bas de l’en-tête collant que vous avez dessiné et le haut de l’en-tête à venir).

        int contactPoint = currentHeader.getBottom(); 
    3. Si l'élément de la liste ne respecte pas ce "sharepoint contact", redessinez l'en-tête collant de sorte que sa partie inférieure se trouve en haut de l'élément d'intrusion. Vous réalisez ceci avec la méthode translate() du Canvas . En conséquence, le sharepoint départ de l'en-tête supérieur sera hors de la zone visible, et il semblera être "sorti par l'en-tête à venir". Quand il est complètement parti, dessinez le nouvel en-tête.

        if (childInContact != null) { if (mListener.isHeader(parent.getChildAdapterPosition(childInContact))) { moveHeader(c, currentHeader, childInContact); } else { drawHeader(c, currentHeader); } } 

    Le rest est expliqué par des commentaires et des annotations approfondies dans un morceau de code que j'ai fourni.

    L'utilisation est simple:

     mRecyclerView.addItemDecoration(new HeaderItemDecoration((HeaderItemDecoration.StickyHeaderInterface) mAdapter)); 

    Votre mAdapter doit implémenter StickyHeaderInterface pour que cela fonctionne. L'implémentation dépend des données que vous avez.

    Enfin, je propose ici un gif avec des en-têtes à moitié transparents, de sorte que vous pouvez saisir l’idée et voir ce qui se passe sous le capot.

    Voici l'illustration du concept "juste dessiner par-dessus tout". Vous pouvez voir qu'il y a deux éléments "header 1" - l'un que nous dessinons et rest en haut dans une position bloquée, et l'autre qui vient du jeu de données et se déplace avec tous les éléments restants. L'utilisateur n'en verra pas le fonctionnement interne, car vous n'aurez pas d'en-têtes à moitié transparents.

    entrer la description de l'image ici

    Et voici ce qui se passe dans la phase de "repoussement":

    entrer la description de l'image ici

    J'espère que ça a aidé.

    modifier

    Voici mon implémentation réelle de la méthode getHeaderPositionForItem() 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; } 

    Le moyen le plus simple est de créer une décoration 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:

       

    Et enfin pour append 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'object, vous pouvez soit faire en sorte que l'en-tête soit épinglé / collé avec un booléen lors de la création de la décoration d'object.

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

    Vous pouvez vérifier et implémenter la classe StickyHeaderHelper dans 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 les bibliothèques Decorators ou désapprouvées, et n’utilisez pas de bibliothèques qui ne font que 1 ou 3 choses, vous devrez fusionner les implémentations des autres bibliothèques vous-même.

    J’ai fait ma propre variante de la solution de Sevastyan 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 de 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 { /* ... */ } override fun bindHeaderData(header: View, headerPosition: Int) { if (headerPosition == RecyclerView.NO_POSITION) header.layoutParams.height = 0 else /* ... */ } override fun isHeader(itemPosition: Int): Boolean { /* ... */ } 

    Ainsi, dans ce cas, l’en-tête n’est pas simplement dessiné sur la zone de travail, mais est affiché avec le sélecteur ou l’ondulation, le clicklistener, etc.

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

    1. Liste de sorting avec données par nom
    2. Itérer via la liste avec les données, et à la place lorsque l’élément en cours de la première lettre! = Première lettre de l’élément suivant, insérez le type d’object “spécial”.
    3. À l’intérieur de votre adaptateur, placez une vue spéciale lorsque l’élément est “spécial”.

    Explication:

    Dans la méthode onCreateViewHolder , nous pouvons vérifier viewType 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; } 

    où la class ItemElement et la class TitleElement peuvent ressembler à un ViewHolder ordinaire:

     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 sortinger la liste de données. Et je pense que cela va ralentir. Si des pensées à ce sujet, écrivez-moi s’il vous plaît 🙂

    Et aussi la question ouverte: comment maintenir la disposition “spéciale” sur le dessus, pendant que les articles sont recyclés. Peut-être combiner tout cela avec CoordinatorLayout .

    Une autre solution, basée sur le listener de défilement. Les conditions initiales sont les mêmes que dans Sevastyan

     RecyclerView recyclerView; TextView tvTitle; //sticky header view //... onCreate, initialize, etc... public void bindList(List items) { //All data in adapter. Item - just interface for different item types adapter = new YourAdapter(items); recyclerView.setAdapter(adapter); StickyHeaderViewManager 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); } 

    Mise en page pour ViewHolder et en-tête sticky.

    item_header.xml

      

    Mise en page pour RecyclerView

          

    Classe pour HeaderItem.

     public class HeaderItem implements Item { private Ssortingng title; public HeaderItem(Ssortingng title) { this.title = title; } public Ssortingng getTitle() { return title; } } 

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

     public class StickyHeaderViewManager { @Nonnull private View headerView; @Nonnull private RecyclerView recyclerView; @Nonnull private StickyHeaderViewWrapper viewWrapper; @Nonnull private Class headerDataClass; private List items; public StickyHeaderViewManager(@Nonnull View headerView, @Nonnull RecyclerView recyclerView, @Nonnull Class headerDataClass, @Nonnull StickyHeaderViewWrapper 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 || items.isEmpty() || position >= items.size()); } } 

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

     public interface StickyHeaderViewWrapper { void bindView(T data); } 

    Pour ceux qui peuvent être concernés. Sur la base de la réponse de Sevastyan, si vous voulez le faire défiler horizontalement. Changez tout getBottom() en getRight() et getTop() en getLeft()

    | * | Après une journée complète de lutte, j’ai développé ci-dessous Session Adopter ou Manager Class
    ce qui est simple à utiliser, il suffit de copier coller et de spécifier votre liste et vos valeurs.

    C’est pour aider tous ceux que je souhaite ne pas lutter comme moi.


    | * | Spécifiez les noms d’en-tête dans SsnHdrAryVar:

     Ssortingng SsnHdrAryVar[] = {"NamHdr1", "NamHdr2", "NamHdr3"}; 

    | * | Spécifiez le nombre d’éléments Sub à afficher sous chaque session dans SsnItmCwtAryVar respectivement:

     int SsnItmCwtAryVar[] = {2, 3, 5}; 

    | * | Spécifiez la hauteur de l’en-tête de session dans SsnHdrHytVar:

      int SsnHdrHytVar = 100; 

    | * | Spécifiez la couleur d’arrière-plan de l’en-tête et la couleur du texte:

      int SsnHdrBgdClr = Color.GREEN; int SsnHdrTxtClr = Color.MAGENTA; 

    | * | Indiquez true si vous avez besoin d’un en-tête de session sticky:

      boolean SsnStkVab = true; 

    | * | Code de classe d’activité de liste complète (à l’exclusion de List Adopter et Get ArrayList)

     public class NamLysSrnCls extends Activity { ArrayList ItmAryLysVar = new ArrayList<>(); // CodTdo :=> Specify all your requirement here : Ssortingng SsnHdrAryVar[] = {"NamHdr1", "NamHdr2", "NamHdr3"}; int SsnItmCwtAryVar[] = {2, 3, 5}; int LysItmHytVal = 200; int SsnHdrHytVar = 100; int SsnHdrBgdClr = Color.GREEN; int SsnHdrTxtClr = Color.MAGENTA; boolean SsnStkVab = true; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); RecyclerView NamLysLyoVav = new RecyclerView(this); NamLysAdrCls NamLysAdrVar = new NamLysAdrCls(GetItmAryLysFnc()); NamLysLyoVav.setAdapter(NamLysAdrVar); NamLysLyoVav.setLayoutManager(new LinearLayoutManager(this)); NamLysLyoVav.addItemDecoration(new LysSsnMgrCls()); // CodTdo :=> If you need Horizontal Lines : NamLysLyoVav.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL)); setContentView(NamLysLyoVav); } // TskTdo :=> Add Below List Seesion Manager or Adoptor Class to ur List activity class class LysSsnMgrCls extends RecyclerView.ItemDecoration { private RelativeLayout SsnHdrVav; private TextView SsnHdrTxtVav; private int SsnHdrRefIdxAryVar[]; public LysSsnMgrCls() { SsnHdrRefIdxAryVar = new int[ItmAryLysVar.size()]; int TmpSsnHdrIdxVar = 0; int TmpItmCwtAdnVar = SsnItmCwtAryVar[0]; for(int IdxVat = 1; IdxVat < ItmAryLysVar.size(); IdxVat++) { if(IdxVat < TmpItmCwtAdnVar) SsnHdrRefIdxAryVar[IdxVat] = TmpSsnHdrIdxVar; else { TmpSsnHdrIdxVar++; TmpItmCwtAdnVar += SsnItmCwtAryVar[TmpSsnHdrIdxVar]; SsnHdrRefIdxAryVar[IdxVat] = TmpSsnHdrIdxVar; } Log.d("TAG", "onCreate: " + SsnHdrRefIdxAryVar[IdxVat]); } } @Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { super.getItemOffsets(outRect, view, parent, state); int LysItmIdxVal = parent.getChildAdapterPosition(view); if (ChkSsnHasHdrFnc(LysItmIdxVal)) { outRect.top = SsnHdrHytVar; } } @Override public void onDrawOver(Canvas SsnCanvasPsgVal, RecyclerView SupLysLyoPsgVav, RecyclerView.State LysSttPsgVal) { super.onDrawOver(SsnCanvasPsgVal, SupLysLyoPsgVav, LysSttPsgVal); if (SsnHdrVav == null) { // TskTdo :=> Design Session Header : SsnHdrVav = new RelativeLayout(NamLysSrnCls.this); SsnHdrVav.setBackgroundColor(SsnHdrBgdClr); SsnHdrVav.setLayoutParams(new LinearLayoutCompat.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)); SsnHdrTxtVav = new TextView(NamLysSrnCls.this); SsnHdrTxtVav.setPadding(20,10,20,10); SsnHdrTxtVav.setLayoutParams(new LinearLayoutCompat.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)); SsnHdrTxtVav.setTextColor(SsnHdrTxtClr); SsnHdrTxtVav.setTextSize(20); SsnHdrTxtVav.setTextAlignment(View.TEXT_ALIGNMENT_CENTER); SsnHdrVav.addView(SsnHdrTxtVav); LyoSyzFixFnc(SsnHdrVav, SupLysLyoPsgVav); } for (int i = 0; i < SupLysLyoPsgVav.getChildCount(); i++) { View DspSubVyuVav = SupLysLyoPsgVav.getChildAt(i); final int LysItmIdxVal = SupLysLyoPsgVav.getChildAdapterPosition(DspSubVyuVav); if (ChkSsnHasHdrFnc(LysItmIdxVal)) { String title = GetSsnHdrTxtFnc(LysItmIdxVal); SsnHdrTxtVav.setText(title); DevSsnHdrFnc(SsnCanvasPsgVal, DspSubVyuVav, SsnHdrVav); } } } boolean ChkSsnHasHdrFnc(int LysItmIdxPsgVal) { return LysItmIdxPsgVal == 0 ? true : SsnHdrRefIdxAryVar[LysItmIdxPsgVal] != SsnHdrRefIdxAryVar[LysItmIdxPsgVal - 1]; } String GetSsnHdrTxtFnc(int LysItmIdxPsgVal) { return SsnHdrAryVar[SsnHdrRefIdxAryVar[LysItmIdxPsgVal]]; } private void DevSsnHdrFnc(Canvas HdrCanvasPsgVal, View DspSubItmPsgVav, View SsnHdrPsgVav) { HdrCanvasPsgVal.save(); if (SsnStkVab) HdrCanvasPsgVal.translate(0, Math.max(0, DspSubItmPsgVav.getTop() - SsnHdrPsgVav.getHeight())); else HdrCanvasPsgVal.translate(0, DspSubItmPsgVav.getTop() - SsnHdrPsgVav.getHeight()); SsnHdrPsgVav.draw(HdrCanvasPsgVal); HdrCanvasPsgVal.restore(); } private void LyoSyzFixFnc(View SsnHdrVyuPsgVal, ViewGroup SupLysLyoPsgVav) { int LysLyoWytVal = View.MeasureSpec.makeMeasureSpec(SupLysLyoPsgVav.getWidth(), View.MeasureSpec.EXACTLY); int LysLyoHytVal = View.MeasureSpec.makeMeasureSpec(SupLysLyoPsgVav.getHeight(), View.MeasureSpec.UNSPECIFIED); int SsnHdrWytVal = ViewGroup.getChildMeasureSpec(LysLyoWytVal, SupLysLyoPsgVav.getPaddingLeft() + SupLysLyoPsgVav.getPaddingRight(), SsnHdrVyuPsgVal.getLayoutParams().width); int SsnHdrHytVal = ViewGroup.getChildMeasureSpec(LysLyoHytVal, SupLysLyoPsgVav.getPaddingTop() + SupLysLyoPsgVav.getPaddingBottom(), SsnHdrVyuPsgVal.getLayoutParams().height); SsnHdrVyuPsgVal.measure(SsnHdrWytVal, SsnHdrHytVal); SsnHdrVyuPsgVal.layout(0, 0, SsnHdrVyuPsgVal.getMeasuredWidth(), SsnHdrVyuPsgVal.getMeasuredHeight()); } } }