it-swarm.com.de

Wie kann ich in RecyclerView klebrige Überschriften erstellen? (Ohne externe lib)

Ich möchte die Kopfzeilenansichten oben auf dem Bildschirm wie in der Abbildung unten und ohne Verwendung externer Bibliotheken korrigieren.

 enter image description here

In meinem Fall möchte ich das nicht alphabetisch. Ich habe zwei verschiedene Arten von Ansichten (Header und Normal). Ich möchte nur den letzten Header nach oben fixieren.

83
Jaume Colom

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

Zunächst einmal möchte ich @ tim.paetz bestätigen, dessen Post mich dazu inspiriert hat, meine eigenen klebrigen Header mit ItemDecorations zu implementieren. Ich habe mir in meiner Implementierung einige Teile seines Codes ausgeliehen.

Wie Sie vielleicht bereits erlebt haben, ist es sehr schwierig, eine gute Erklärung dafür zu finden, dass WIE es tatsächlich mit der ItemDecoration-Technik macht. Ich meine, Was sind die Schritte? Was ist die Logik dahinter? Wie mache ich die Kopfzeile oben auf der Liste?} Das Wissen um Antworten auf diese Fragen bewirkt, dass andere externe Bibliotheken verwenden Es ist ziemlich einfach, es selbst mit ItemDecoration zu tun.

Anfangsbedingungen

  1. Ihr Dataset sollte eine list von Elementen eines anderen Typs sein (nicht im Sinne von "Java-Typen", sondern im Sinne eines "Kopf-/Elementtyps").
  2. Ihre Liste sollte bereits sortiert sein.
  3. Jedes Element in der Liste sollte einen bestimmten Typ haben - es sollte ein Kopfelement vorhanden sein.
  4. Das allererste Element in der list muss ein Kopfelement sein.

Hier gebe ich den vollständigen Code für meinen RecyclerView.ItemDecoration namens HeaderItemDecoration an. 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

Wie kann ich es kleben lassen?

Du nicht Sie können einen RecyclerView-Gegenstand Ihrer Wahl nicht einfach machen und an der Spitze bleiben, es sei denn, Sie sind ein Guru für benutzerdefinierte Layouts und kennen 12.000 Codezeilen für eine RecyclerView auswendig. So wie es immer zum UI-Design passt, fälschen Sie es, wenn Sie etwas nicht machen können. Sie zeichnen einfach die Kopfzeile über alles mit Canvas. Sie sollten auch wissen, welche Elemente der Benutzer im Moment sehen kann. Es kommt einfach vor, dass ItemDecoration Ihnen sowohl die Canvas als auch Informationen zu sichtbaren Elementen zur Verfügung stellt. Hier sind die grundlegenden Schritte:

  1. In onDrawOver Methode von RecyclerView.ItemDecoration wird das allererste (oberste) Element abgerufen, 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 die entsprechende Kopfzeile mit der drawHeader()-Methode ü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 kommende Header den obersten aktuellen Header aus der Ansicht herausschieben und schließlich seinen Platz einnehmen.

Hier gilt die gleiche Technik des "Zeichnens über alles".

  1. Bestimmen Sie, wann der oberste "steckenbleibende" Header den neuen kommenden trifft.

            View childInContact = getChildInContact(parent, contactPoint);
    
  2. Holen Sie sich diesen Kontaktpunkt (das ist der Boden des klebrigen Kopfes, den Sie gezeichnet haben, und den Kopf des bevorstehenden Kopfes).

            int contactPoint = currentHeader.getBottom();
    
  3. Wenn der Eintrag in der Liste diesen "Kontaktpunkt" überschreitet, zeichnen Sie den Sticky-Header so, dass sich der untere Punkt am oberen Rand des Eintrags befindet. Dies erreichen Sie mit der translate()-Methode der Canvas. Als Ergebnis befindet sich der Startpunkt des oberen Headers außerhalb des sichtbaren Bereichs und es scheint, als ob er vom kommenden Header herausgedrückt wird. Wenn es vollständig weg 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 einfach:

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

Ihre mAdapter muss StickyHeaderInterface implementieren, damit sie funktioniert. Die Implementierung hängt von den Daten ab, die Sie haben.

Zum Schluss gebe ich hier ein GIF mit halbdurchsichtigen Kopfzeilen, damit Sie die Idee verstehen und tatsächlich sehen können, was unter der Haube passiert.

Hier ist die Illustration des Konzepts "Über alles zeichnen". Sie sehen, dass es zwei Elemente gibt, "Header 1" - 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 kann die inneren Abläufe nicht sehen, da Sie keine halbdurchsichtigen Header haben. 

 enter image description here

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

 enter image description here

Hoffe es hat geholfen.

Bearbeiten

Hier ist meine tatsächliche Implementierung der getHeaderPositionForItem()-Methode im Adapter von 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;
}
233
Sevastyan

Am einfachsten erstellen Sie einfach eine Artikeldekoration für Ihre 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 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 fügen Sie Ihrem RecyclerView die Artikeldekoration hinzu: 

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 die Kopfzeile beim Erstellen der Artikeldekoration entweder mit einem Booleschen Stift fixieren/verkleben oder nicht.

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

19
tim.paetz

Ich habe meine eigene Variante 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 Recycleradapter 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 also nicht nur auf die Leinwand gezeichnet, sondern auch mit Selector oder Ripple, Clicklistener usw. angezeigt.

3

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

Ich schlage jedoch vor, die Bibliothek zu verwenden, da sie die Art und Weise, wie Sie die Adapter für RecyclerView normalerweise implementieren, vereinfacht und reorganisiert: Das Rad nicht neu erfinden.

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

3
Davideas

Eine andere Lösung, die auf dem Scroll-Listener basiert. Anfangsbedingungen sind die gleichen wie in Sevastyan Antwort

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 zu gebrauchen. 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);
}
2
Anrimian

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

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 gebrochen habe?

1
or_dvir

Yo,

So machen Sie es, wenn Sie nur einen Haltestab benötigen, 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 unterbrechen, nämlich die zusätzliche Ansicht über das Header-Element des RecyclerView aufzublähen und Daten an dieses zu übergeben. Ich lasse den Code sprechen.

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 machst du das einfach in deinem 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 angeblichen Klebehalters ist.

1
Stanislav Kinzl

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

  1. Liste mit Daten nach Namen sortieren
  2. Iteriere über Liste mit Daten und an Ort und Stelle, wenn der erste Buchstabe des aktuellen Eintrags vorhanden ist: = erster Buchstabe des nächsten Eintrags, "spezieller" Objekttyp einfügen.
  3. Platzieren Sie in Ihrem Adapter eine spezielle Ansicht, wenn der Artikel "besonders" ist.

Erläuterung:

In onCreateViewHolder können wir viewType überprüfen und abhängig vom Wert (unsere "spezielle" Art) ein spezielles Layout aufblasen.

Zum Beispiel:

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

wobei class ItemElement und class TitleElement wie gewöhnliche ViewHolder aussehen können:

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, wenn es effektiv ist, weil wir die Datenliste sortieren müssen. Und ich denke, das wird die Geschwindigkeit verringern. Wenn Sie irgendwelche Gedanken dazu haben, schreiben Sie mir bitte :)

Und auch die offene Frage ist, wie man das "spezielle" Layout oben hält, während die Gegenstände recycelt werden. Vielleicht kombinieren Sie all das mit CoordinatorLayout.

0
Valeria

Für diejenigen, die sich Sorgen machen können. Basierend auf der Antwort von Sevastyan, sollten Sie die horizontale Bildlauffunktion ausführen lassen. __ Ändern Sie einfach alle getBottom() in getRight() und getTop() in getLeft().

0
Guster