Wie kann ich in RecyclerView klebrige Header erstellen? (Ohne externe Bibliothek)

120

Ich möchte meine Header-Ansichten oben auf dem Bildschirm wie im Bild unten korrigieren, ohne externe Bibliotheken zu verwenden.

Geben Sie hier die Bildbeschreibung ein

In meinem Fall möchte ich es nicht alphabetisch machen. Ich habe zwei verschiedene Arten von Ansichten (Header und Normal). Ich möchte nur den letzten Header an der Spitze fixieren.

Jaume Colom
quelle
17
Die Frage war über RecyclerView, diese ^ lib basiert auf ListView
Max Ch

Antworten:

319

Hier werde ich erklären, wie es ohne eine externe Bibliothek geht. Es wird ein sehr langer Beitrag, also machen Sie sich bereit.

Lassen Sie mich zunächst @ tim.paetz bestätigen, dessen Beitrag mich dazu inspiriert hat, meine eigenen Sticky-Header mit ItemDecorations zu implementieren . Ich habe einige Teile seines Codes in meiner Implementierung ausgeliehen.

Wie Sie vielleicht schon erlebt, wenn Sie es selbst zu tun versucht, ist es sehr schwer , eine gute Erklärung zu finden , WIE es tatsächlich zu tun mit der ItemDecorationTechnik. Ich meine, was sind die Schritte? Was ist die Logik dahinter? Wie kann ich den Header ganz oben auf der Liste platzieren? Wenn Sie keine Antworten auf diese Fragen kennen, können andere externe Bibliotheken verwenden, während Sie dies selbst tunItemDecoration ziemlich einfach ist .

Anfangsbedingungen

  1. Ihr Datensatz sollte listaus Elementen unterschiedlichen Typs bestehen (nicht im Sinne von "Java-Typen", sondern im Sinne von "Header / Element" -Typen).
  2. Ihre Liste sollte bereits sortiert sein.
  3. Jedes Element in der Liste sollte von einem bestimmten Typ sein - es sollte ein zugehöriges Kopfelement vorhanden sein.
  4. Das allererste Element im listmuss ein Kopfelement sein.

Hier gebe ich den vollständigen Code für meinen RecyclerView.ItemDecorationAnruf an HeaderItemDecoration. Dann erkläre ich die Schritte im Detail.

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

Geschäftslogik

Also, wie mache ich es kleben?

Das tust du nicht. Sie können einen RecyclerViewArtikel Ihrer Wahl nicht einfach anhalten und oben bleiben, es sei denn, Sie sind ein Guru für benutzerdefinierte Layouts und kennen mehr als 12.000 Codezeilen RecyclerViewauswendig. Also, wie es immer mit dem UI-Design geht, wenn Sie etwas nicht machen können, fälschen Sie es. Sie zeichnen einfach den Header über alles mit Canvas. Sie sollten auch wissen, welche Elemente der Benutzer im Moment sehen kann. Es kommt einfach vor, dass ItemDecorationSie sowohl CanvasInformationen als auch Informationen zu sichtbaren Elementen erhalten. Hier sind die grundlegenden Schritte:

  1. Bei der onDrawOverMethode RecyclerView.ItemDecorationerhalten Sie das allererste (oberste) Element, das für den Benutzer sichtbar ist.

        View topChild = parent.getChildAt(0);
  2. Bestimmen Sie, welcher Header es darstellt.

            int topChildPosition = parent.getChildAdapterPosition(topChild);
        View currentHeader = getHeaderViewForItem(topChildPosition, parent);
  3. Zeichnen Sie mit der drawHeader()Methode den entsprechenden Header über die RecyclerView .

Ich möchte auch das Verhalten implementieren, wenn der neue kommende Header auf den obersten trifft: Es sollte so aussehen, als würde der bevorstehende Header den obersten aktuellen Header sanft aus der Ansicht schieben und schließlich seinen Platz einnehmen.

Hier gilt die gleiche Technik des "Zeichnens auf alles".

  1. Bestimmen Sie, wann der oberste "feststeckende" Header auf den neuen anstehenden Header trifft.

            View childInContact = getChildInContact(parent, contactPoint);
  2. Holen Sie sich diesen Kontaktpunkt (das ist die Unterseite des von Ihnen gezeichneten klebrigen Headers und die Oberseite des bevorstehenden Headers).

            int contactPoint = currentHeader.getBottom();
  3. Wenn das Element in der Liste diesen "Kontaktpunkt" betritt, zeichnen Sie Ihren klebrigen Header neu, sodass sich seine Unterseite oben auf dem Element befindet. Dies erreichen Sie mit der translate()Methode der Canvas. Infolgedessen befindet sich der Startpunkt des oberen Headers außerhalb des sichtbaren Bereichs und scheint "vom kommenden Header herausgeschoben" zu werden. Wenn es vollständig verschwunden ist, zeichnen Sie den neuen Header oben.

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

Der Rest wird durch Kommentare und ausführliche Anmerkungen in dem von mir bereitgestellten Code erklärt.

Die Verwendung ist unkompliziert:

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

Sie mAdaptermüssen implementierenStickyHeaderInterface damit es funktioniert. Die Implementierung hängt von Ihren Daten ab.

Schließlich stelle ich hier ein GIF mit halbtransparenten Überschriften zur Verfügung, damit Sie die Idee erfassen und tatsächlich sehen können, was unter der Haube vor sich geht.

Hier ist die Illustration des Konzepts "Einfach auf alles zeichnen". Sie können sehen, dass es zwei Elemente "Kopfzeile 1" gibt - eines, das wir zeichnen und in einer festgefahrenen Position oben bleiben, und das andere, das aus dem Datensatz stammt und sich mit allen anderen Elementen bewegt. Der Benutzer wird das Innenleben nicht sehen, da Sie keine halbtransparenten Überschriften haben.

Konzept "Zeichnen Sie einfach auf alles"

Und hier, was passiert in der "Push-out" -Phase:

"Ausstoßen" -Phase

Hoffe es hat geholfen.

Bearbeiten

Hier ist meine tatsächliche Implementierung der getHeaderPositionForItem()Methode im RecyclerView-Adapter:

@Override
public int getHeaderPositionForItem(int itemPosition) {
    int headerPosition = 0;
    do {
        if (this.isHeader(itemPosition)) {
            headerPosition = itemPosition;
            break;
        }
        itemPosition -= 1;
    } while (itemPosition >= 0);
    return headerPosition;
}

Etwas andere Implementierung in Kotlin

Sevastyan Savanyuk
quelle
4
@Sevastyan Einfach genial! Es hat mir sehr gut gefallen, wie Sie diese Herausforderung gelöst haben. Nichts zu sagen, außer vielleicht einer Frage: Gibt es eine Möglichkeit, einen OnClickListener auf den "Sticky Header" zu setzen oder zumindest den Klick zu verbrauchen, der den Benutzer daran hindert, durch ihn zu klicken?
Denis
17
Wäre toll, wenn Sie Adapter Beispiel für diese Implementierung setzen
SolidSnake
1
Ich habe es endlich geschafft, hier und da mit ein paar Verbesserungen zu arbeiten. Wenn Sie Ihren Artikeln jedoch Polster hinzufügen, flackert diese weiter, wenn Sie zum gepolsterten Bereich scrollen. Die Lösung im Layout Ihres Elements erstellt ein übergeordnetes Layout mit 0 Auffüllungen und ein untergeordnetes Layout mit den gewünschten Auffüllungen.
Solid Snake
8
Vielen Dank. Interessante Lösung, aber etwas teuer, um die Header-Ansicht bei jedem Scroll-Ereignis aufzublasen. Ich habe gerade die Logik geändert und ViewHolder verwendet und sie in einer HashMap von WeakReferences gespeichert, um bereits aufgeblasene Ansichten wiederzuverwenden.
Michael
4
@Sevastyan, tolle Arbeit. Ich habe einen Vorschlag. Um zu vermeiden, dass jedes Mal neue Header erstellt werden. Speichern Sie einfach den Header und ändern Sie ihn nur, wenn er sich ändert. 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

Am einfachsten ist es, einfach eine Artikeldekoration für Ihre RecyclerView zu erstellen.

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 für Ihren Header 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"
/>

Und schließlich, um die Artikeldekoration zu Ihrer RecyclerView hinzuzufügen:

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

Mit dieser Artikeldekoration können Sie den Header beim Erstellen der Artikeldekoration entweder mit einem Booleschen Wert fixieren / kleben oder nicht.

Ein vollständiges Arbeitsbeispiel finden Sie auf github: https://github.com/paetztm/recycler_view_headers

tim.paetz
quelle
Danke dir. Das hat bei mir funktioniert, aber dieser Header überlappt die Recycling-Ansicht. kannst du helfen?
Kashyap Jimuliya
Ich bin mir nicht sicher, was Sie unter Überlappung der RecyclerView verstehen. Wenn Sie für den "klebrigen" Booleschen Wert den Wert "false" setzen, wird die Elementdekoration zwischen die Zeilen eingefügt und bleibt nicht oben in der RecyclerView.
tim.paetz
Wenn Sie "sticky" auf "false" setzen, wird der Header zwischen die Zeilen gesetzt, aber das bleibt nicht oben hängen (was ich nicht möchte). während es auf true gesetzt wird, bleibt es
oben hängen,
Ich kann sehen, dass als möglicherweise zwei Probleme, eines ist der Abschnittsrückruf, Sie nicht das erste Element (0-Position) für isSection auf true setzen. Das andere ist, dass Sie in der falschen Höhe passieren. Die Höhe der XML-Datei für die Textansicht muss mit der Höhe übereinstimmen, die Sie an den Konstruktor der Dekoration des Abschnittselements übergeben.
tim.paetz
3
Eine Sache, die ich hinzufügen möchte, ist, dass Sie, wenn Ihr Header-Layout eine dynamisch dimensionierte Titeltextansicht hat (z. B. wrap_content), diese auch fixLayoutSizenach dem Festlegen des Titeltextes ausführen möchten .
Copolii
6

Ich habe oben meine eigene Variation von Sevastyans Lösung gemacht

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

... und hier ist die Implementierung von StickyHeaderInterface (ich habe es direkt im Recycler-Adapter gemacht):

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

In diesem Fall wird der Header also nicht nur auf Leinwand gezeichnet, sondern mit Selektor oder Ripple, Clicklistener usw. angezeigt.

Andrey Turkovsky
quelle
Danke für das Teilen! Warum haben Sie die RecyclerView in ein neues RelativeLayout verpackt?
tmm1
Weil meine Version des Sticky-Headers View ist, die ich in dieses RelativeLayout über RecyclerView (im Feld headerContainer)
eingefügt habe
Können Sie Ihre Implementierung in einer Klassendatei anzeigen? Wie Sie das Objekt des Listeners übergeben haben, das im Adapter implementiert ist.
Dipali s.
recyclerView.addItemDecoration(HeaderItemDecoration(recyclerView, adapter)). Leider kann ich kein Beispiel für eine Implementierung finden, die ich verwendet habe. Ich habe die Antwort bearbeitet - Text zu Kommentaren hinzugefügt
Andrey Turkovsky
6

an alle, die nach einer Lösung für das flackernde / blinkende Problem suchen, wenn Sie dies bereits getan haben DividerItemDecoration. Ich scheine es so gelöst zu haben:

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

Das scheint zu funktionieren, aber kann jemand bestätigen, dass ich nichts anderes kaputt gemacht habe?

or_dvir
quelle
Vielen Dank, es hat das blinkende Problem auch für mich gelöst.
Yamashiro Rion
3

Sie können die Implementierung der Klasse StickyHeaderHelperin meinem FlexibleAdapter überprüfen und übernehmen Projekt und an Ihren Anwendungsfall anpassen.

Ich empfehle jedoch, die Bibliothek zu verwenden, da sie die Art und Weise vereinfacht und neu organisiert, wie Sie normalerweise die Adapter für RecyclerView implementieren: Erfinden Sie das Rad nicht neu.

Ich würde auch sagen, verwenden Sie keine Decorators oder veralteten Bibliotheken und verwenden Sie keine Bibliotheken, die nur 1 oder 3 Dinge tun. Sie müssen die Implementierungen anderer Bibliotheken selbst zusammenführen.

Davideas
quelle
Ich habe 2 Tage damit verbracht, das Wiki und das Beispiel zu lesen, weiß aber immer noch nicht, wie ich mit Ihrer Bibliothek eine reduzierbare Liste erstellen kann. Die Probe ist für Neuling ziemlich komplex
Nguyen Minh Binh
1
Warum bist du gegen die Verwendung von Decorators?
Sevastyan Savanyuk
1
@Sevastyan, da wir an dem Punkt ankommen, an dem wir einen Klick-Listener darauf und auch auf untergeordnete Ansichten benötigen. Wir Dekorateur können Sie einfach nicht per Definition.
Davideas
@Davidea, meinst du, du möchtest in Zukunft Klick-Listener auf die Header setzen? Wenn ja, macht es Sinn. Wenn Sie Ihre Header jedoch als Dataset-Elemente angeben, treten keine Probleme auf. Sogar Yigit Boyar empfiehlt die Verwendung von Dekorateuren.
Sevastyan Savanyuk
@Sevastyan, ja, in meiner Bibliothek ist der Header ein Element wie andere in der Liste, sodass Benutzer ihn bearbeiten können. In ferner Zukunft wird ein benutzerdefinierter Layout-Manager den aktuellen Helfer ersetzen.
Davideas
3

Eine weitere Lösung, die auf dem Scroll Listener basiert. Die Anfangsbedingungen sind die gleichen wie in der Antwort von 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 für ViewHolder und Sticky Header.

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 für 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>

Klasse für HeaderItem.

public class HeaderItem implements Item {

    private String title;

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

    public String getTitle() {
        return title;
    }

}

Es ist alles nützlich. Die Implementierung des Adapters ViewHolder und anderer Dinge ist für uns nicht interessant.

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

Schnittstelle für die Bindungskopfansicht.

public interface StickyHeaderViewWrapper<T> {

    void bindView(T data);
}
Anrimian
quelle
Ich mag diese Lösung. Kleiner Tippfehler in findNearestHeader:for (int i = position; position >= 0; i--){ //should be i >= 0
Konstantin
3

Yo,

So machen Sie es, wenn Sie nur eine Art von Halter-Stick möchten, wenn dieser aus dem Bildschirm herauskommt (wir kümmern uns nicht um Abschnitte). Es gibt nur einen Weg, ohne die interne RecyclerView-Logik des Recyclings von Elementen zu brechen, nämlich die zusätzliche Ansicht über dem Header-Element von recyclerView aufzublasen und Daten an dieses zu übergeben. Ich werde den Code sprechen lassen.

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

}

Und dann machen Sie das einfach in Ihrem Adapter:

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

Wobei YOUR_STICKY_VIEW_HOLDER_TYPE viewType Ihres angeblich klebrigen Halters ist.

Stanislav Kinzl
quelle
2

Für diejenigen, die sich Sorgen machen können. Basierend auf Sevastyans Antwort, sollten Sie es horizontal scrollen lassen wollen. Ändern Sie einfach alle getBottom()zu getRight()und getTop()zugetLeft()

Guster
quelle
-1

Die Antwort war bereits hier. Wenn Sie keine Bibliothek verwenden möchten, können Sie die folgenden Schritte ausführen:

  1. Liste mit Daten nach Namen sortieren
  2. Durch die Liste mit Daten iterieren und an Ort und Stelle, wenn der erste Buchstabe des aktuellen Elements! = Erster Buchstabe des nächsten Elements, "spezielle" Art von Objekt einfügen.
  3. Platzieren Sie in Ihrem Adapter eine spezielle Ansicht, wenn das Element "speziell" ist.

Erläuterung:

In der onCreateViewHolderMethode können wir überprüfenviewType und je nach Wert (unsere "spezielle" Art) ein spezielles Layout aufblasen.

Beispielsweise:

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

wo class ItemElementund class TitleElementkann wie gewöhnlich aussehen ViewHolder:

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

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

}

Die Idee von all dem ist also interessant. Aber ich bin interessiert, ob es effektiv ist, weil wir die Datenliste sortieren müssen. Und ich denke, das wird die Geschwindigkeit verringern. Wenn Sie darüber nachdenken, schreiben Sie mir bitte :)

Und auch die offene Frage: Wie soll das "spezielle" Layout oben gehalten werden, während die Artikel recycelt werden? Vielleicht kombinieren Sie das alles mit CoordinatorLayout.

Valeria
quelle
ist es möglich, es mit Cursoradapter zu machen
M.Yogeshwaran
10
Diese Lösung sagt nichts über STICKY-Header aus, was der Hauptpunkt dieses Beitrags ist
Siavash