Come posso creare intestazioni adesive in RecyclerView? (Senza libreria esterna)


120

Voglio correggere le mie visualizzazioni di intestazione nella parte superiore dello schermo come nell'immagine qui sotto e senza utilizzare librerie esterne.

inserisci qui la descrizione dell'immagine

Nel mio caso, non voglio farlo in ordine alfabetico. Ho due diversi tipi di visualizzazioni (intestazione e normale). Voglio solo fissare in alto, l'ultima intestazione.


17
la domanda riguardava RecyclerView, questo ^ lib è basato su ListView
Max Ch

Risposte:


319

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 ItemDecorations. 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 ItemDecorationtecnica. 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

  1. Il tuo set di dati dovrebbe essere composto da listelementi di tipo diverso (non in senso "tipi Java", ma in senso "intestazione / elemento").
  2. Il tuo elenco dovrebbe essere già ordinato.
  3. Ogni elemento nell'elenco dovrebbe essere di un certo tipo: dovrebbe esserci un elemento di intestazione ad esso correlato.
  4. Il primo elemento nella listdeve essere un elemento di intestazione.

Qui fornisco il codice completo per il mio RecyclerView.ItemDecorationcall 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 RecyclerViewelemento 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 ItemDecorationpuò fornirti sia le Canvasinformazioni sugli elementi visibili. Con questo, ecco i passaggi di base:

  1. Nel onDrawOvermetodo per RecyclerView.ItemDecorationottenere il primo elemento (in alto) visibile all'utente.

        View topChild = parent.getChildAt(0);
  2. Determina quale intestazione lo rappresenta.

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

  1. Determina quando l'intestazione "bloccata" in alto incontra quella nuova in arrivo.

            View childInContact = getChildInContact(parent, contactPoint);
  2. 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();
  3. 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 mAdapterimplementarlo StickyHeaderInterfaceaffinché 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.

"disegna solo sopra tutto"

Ed ecco cosa succede nella fase di "push out":

fase di "spinta fuori"

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


4
@Sevastyan Semplicemente geniale! Mi è piaciuto molto il modo in cui hai risolto questa sfida. Niente da dire, tranne forse una domanda: c'è un modo per impostare un OnClickListener sull '"intestazione adesiva", o almeno per consumare il clic impedendo all'utente di fare clic su di esso?
Denis

17
Sarebbe fantastico se mettessi un esempio di adattatore di questa implementazione
SolidSnake

1
Finalmente sono riuscito a funzionare con alcune modifiche qua e là. sebbene se aggiungi del riempimento ai tuoi articoli, continuerà a lampeggiare ogni volta che scorri fino all'area imbottita. la soluzione nel layout del tuo articolo crea un layout genitore con 0 padding e un layout figlio con qualsiasi padding desideri.
SolidSnake

8
Grazie. Soluzione interessante, ma un po 'costosa per aumentare la visualizzazione dell'intestazione su ogni evento di scorrimento. Ho appena cambiato la logica e uso ViewHolder e li conservo in una HashMap di WeakReferences per riutilizzare le visualizzazioni già gonfiate.
Michael

4
@Sevastyan, ottimo lavoro. Ho un suggerimento. Per evitare di creare ogni volta nuove intestazioni. Basta salvare l'intestazione e modificarla solo quando cambia. 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

Il modo più semplice è creare semplicemente una decorazione oggetto per 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 per la tua intestazione in 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"
/>

E infine per aggiungere la decorazione dell'articolo a 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);

Con questa decorazione oggetto puoi rendere l'intestazione appuntata / appiccicosa o meno con un booleano quando crei la decorazione oggetto.

Puoi trovare un esempio funzionante completo su github: https://github.com/paetztm/recycler_view_headers


Grazie. questo ha funzionato per me, tuttavia questa intestazione si sovrappone al recyclerview. Puoi aiutare?
kashyap jimuliya

Non sono sicuro di cosa intendi per sovrapposizione a RecyclerView. Per il booleano "appiccicoso", se lo imposti su false, inserirà la decorazione dell'elemento tra le righe e non rimarrà nella parte superiore di RecyclerView.
tim.paetz

impostandolo su "sticky" su false inserisce l'intestazione tra le righe, ma ciò non rimane bloccato (cosa che non voglio) in alto. pur impostandolo su true, rimane bloccato in alto ma si sovrappone alla prima riga nella
recyclerview

Posso vedere che potenzialmente due problemi, uno è il callback della sezione, non stai impostando il primo elemento (posizione 0) per isSection su true. L'altro è che stai passando all'altezza sbagliata. L'altezza dell'xml per la visualizzazione del testo deve essere uguale all'altezza che passi nel costruttore della decorazione dell'elemento della sezione.
tim.paetz

3
Una cosa che aggiungerei è che se il layout dell'intestazione ha la vista del testo del titolo dimensionato dinamicamente (ad esempio wrap_content), vorresti eseguire anche fixLayoutSizedopo aver impostato il testo del titolo.
copolii

6

Ho fatto la mia variazione della soluzione di Sevastyan sopra

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
}
}

... ed ecco l'implementazione di StickyHeaderInterface (l'ho fatto direttamente nell'adattatore riciclatore):

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
    ... */
}

Quindi, in questo caso l'intestazione non è solo un disegno su tela, ma una visualizzazione con selettore o ripple, clicklistener, ecc.


Grazie per la condivisione! Perché hai finito per avvolgere RecyclerView in un nuovo RelativeLayout?
tmm1

Perché la mia versione dell'intestazione adesiva è View, che ho inserito in questo RelativeLayout sopra RecyclerView (nel campo headerContainer)
Andrey Turkovsky

Puoi mostrare la tua implementazione nel file di classe? Come hai passato l'oggetto listener che è implementato nell'adattatore.
Dipali Shah

recyclerView.addItemDecoration(HeaderItemDecoration(recyclerView, adapter)). Spiacenti, non riesco a trovare un esempio di implementazione che ho utilizzato. Ho modificato la risposta - aggiunto del testo ai commenti
Andrey Turkovsky

6

a chiunque cerchi una soluzione al problema dello sfarfallio / lampeggiamento quando lo hai già DividerItemDecoration. mi sembra di averlo risolto in questo modo:

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(...)
}

questo sembra funzionare ma qualcuno può confermare che non ho rotto nient'altro?


Grazie, ha risolto il problema lampante anche per me.
Yamashiro Rion

3

Puoi controllare e prendere l'implementazione della classe StickyHeaderHelpernel mio progetto FlexibleAdapter e adattarla al tuo caso d'uso.

Tuttavia, suggerisco di utilizzare la libreria poiché semplifica e riorganizza il modo in cui di solito si implementano gli adattatori per RecyclerView: non reinventare la ruota.

Direi anche di non usare Decorator o librerie deprecate, oltre a non usare librerie che fanno solo 1 o 3 cose, dovrai unire tu stesso le implementazioni di altre librerie.


Ho passato 2 giorni a leggere il wiki e l'esempio, ma ancora non so come creare un elenco comprimibile utilizzando la tua libreria. Il campione è piuttosto complesso per i principianti
Nguyen Minh Binh

1
Perché sei contrario a usare Decorators?
Sevastyan Savanyuk

1
@Sevastyan, perché arriveremo al punto in cui abbiamo bisogno di fare clic su listener su di esso e anche sulle visualizzazioni figlio. Noi Decoratori non puoi per definizione.
Davideas

@Davidea, vuoi dire che vuoi impostare ascoltatori di clic sulle intestazioni in futuro? Se è così, ha senso. Tuttavia, se fornisci le tue intestazioni come elementi del set di dati, non ci saranno problemi. Anche Yigit Boyar consiglia di utilizzare Decoratori.
Sevastyan Savanyuk

@Sevastyan, sì nella mia libreria l'intestazione è un elemento come gli altri nell'elenco, quindi gli utenti possono manipolarlo. In un lontano futuro un gestore di layout personalizzato sostituirà l'attuale helper.
Davideas

3

Un'altra soluzione, basata sullo scroll listener. Le condizioni iniziali sono le stesse della risposta Sevastyan

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);
}

Layout per ViewHolder e intestazione fissa.

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"/>

Layout per 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 per HeaderItem.

public class HeaderItem implements Item {

    private String title;

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

    public String getTitle() {
        return title;
    }

}

È tutto uso. L'implementazione dell'adattatore, ViewHolder e altre cose, non è interessante per noi.

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());
    }
}

Interfaccia per la visualizzazione dell'intestazione di associazione.

public interface StickyHeaderViewWrapper<T> {

    void bindView(T data);
}

Mi piace questa soluzione. Piccolo errore di battitura in findNearestHeader: for (int i = position; position >= 0; i--){ //should be i >= 0
Konstantin

3

yo,

Questo è il modo in cui lo fai se vuoi un solo tipo di stick di supporto quando inizia a uscire dallo schermo (non ci interessano le sezioni). Esiste un solo modo senza rompere la logica interna di RecyclerView per il riciclaggio degli articoli, ovvero aumentare la visualizzazione aggiuntiva sopra l'elemento di intestazione di recyclerView e passarvi i dati. Lascio parlare il codice.

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)
}

}

E poi fai semplicemente questo nel tuo adattatore:

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

Dove YOUR_STICKY_VIEW_HOLDER_TYPE è viewType del tuo supporto appiccicoso.


2

Per coloro che possono interessare. Basato sulla risposta di Sevastyan, se vuoi farlo scorrere in orizzontale. Cambia semplicemente tutto getBottom()in getRight()e getTop()ingetLeft()


-1

La risposta è già stata qui. Se non desideri utilizzare alcuna libreria, puoi seguire questi passaggi:

  1. Ordina elenco con i dati per nome
  2. Scorri l'elenco con i dati e al suo posto quando la prima lettera dell'elemento corrente! = Prima lettera dell'elemento successivo, inserisci il tipo "speciale" di oggetto.
  3. All'interno dell'adattatore posizionare una visualizzazione speciale quando l'elemento è "speciale".

Spiegazione:

Nel onCreateViewHoldermetodo possiamo controllare viewTypeea seconda del valore (il nostro tipo "speciale") gonfiare un layout speciale.

Per esempio:

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;
}

dove class ItemElemente class TitleElementpuò sembrare normale ViewHolder:

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

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

}

Quindi l'idea di tutto ciò è interessante. Ma mi interessa se è efficace, perché dobbiamo ordinare l'elenco dei dati. E penso che questo ridurrà la velocità. Se hai qualche idea in merito, per favore scrivimi :)

E anche la domanda aperta: è come mantenere il layout "speciale" in alto, mentre gli articoli vengono riciclati. Forse combina tutto questo con CoordinatorLayout.


è possibile farlo con il cursoreadapter
M.Yogeshwaran

10
questa soluzione non dice nulla sugli header STICKY che è il punto principale di questo post
Siavash
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.