Jak mogę tworzyć lepkie nagłówki w RecyclerView? (Bez biblioteki zewnętrznej)


120

Chcę naprawić moje widoki nagłówków u góry ekranu, jak na obrazku poniżej i bez korzystania z zewnętrznych bibliotek.

wprowadź opis obrazu tutaj

W moim przypadku nie chcę tego robić alfabetycznie. Mam dwa różne typy widoków (nagłówek i normalny). Chcę tylko naprawić górny, ostatni nagłówek.


17
pytanie dotyczyło RecyclerView, ten ^ lib jest oparty na ListView
Max Ch

Odpowiedzi:


319

Tutaj wyjaśnię, jak to zrobić bez zewnętrznej biblioteki. To będzie bardzo długi post, więc przygotuj się.

Przede wszystkim chciałbym podziękować @ tim.paetz, którego post zainspirował mnie do wyruszenia w podróż polegającą na implementacji moich własnych lepkich nagłówków przy użyciu ItemDecorations. Pożyczyłem niektóre części jego kodu w mojej implementacji.

Jak mogłeś już doświadczyć, jeśli próbowałeś to zrobić samemu, bardzo trudno jest znaleźć dobre wyjaśnienie, JAK właściwie zrobić to za pomocą tej ItemDecorationtechniki. Mam na myśli, jakie są kroki? Jaka jest logika za tym? Jak ustawić nagłówek na górze listy? Brak znajomości odpowiedzi na te pytania sprawia, że ​​inni korzystają z zewnętrznych bibliotek, podczas gdy robienie tego samemu przy użyciu ItemDecorationjest całkiem proste.

Warunki początkowe

  1. Zbiór danych powinien składać się listz elementów innego typu (nie w sensie „typów Java”, ale w sensie typów „nagłówek / element”).
  2. Twoja lista powinna być już posortowana.
  3. Każda pozycja na liście powinna być określonego typu - powinien być związany z nią nagłówek.
  4. Pierwsza pozycja listmusi być pozycją nagłówka.

Tutaj podaję pełny kod mojego RecyclerView.ItemDecorationcall HeaderItemDecoration. Następnie szczegółowo wyjaśniam podjęte kroki.

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

Logika biznesowa

Jak więc sprawić, by się przykleił?

Ty nie. Nie możesz zrobić wybranego RecyclerViewelementu, po prostu zatrzymaj się i trzymaj na górze, chyba że jesteś guru niestandardowych układów i znasz ponad 12 000 linii kodu na RecyclerViewpamięć. Tak więc, jak zawsze w przypadku projektu interfejsu użytkownika, jeśli nie możesz czegoś zrobić, udawaj. Po prostu rysujesz nagłówek na górze wszystkiego, używając Canvas. Powinieneś także wiedzieć, które elementy użytkownik może w danej chwili zobaczyć. Tak się po prostu dzieje, że ItemDecorationmoże dostarczyć zarówno Canvasinformacji, jak i widocznych przedmiotów. Oto podstawowe kroki:

  1. W onDrawOvermetodzie RecyclerView.ItemDecorationuzyskania pierwszego (górnego) elementu, który jest widoczny dla użytkownika.

        View topChild = parent.getChildAt(0);
  2. Określ, który nagłówek go reprezentuje.

            int topChildPosition = parent.getChildAdapterPosition(topChild);
        View currentHeader = getHeaderViewForItem(topChildPosition, parent);
  3. Narysuj odpowiedni nagłówek na górze RecyclerView przy użyciu drawHeader()metody.

Chcę również zaimplementować zachowanie, gdy nowy nadchodzący nagłówek spotyka się z górnym: powinien wyglądać na to, że nadchodzący nagłówek delikatnie wypycha aktualny górny nagłówek z widoku i ostatecznie zajmuje jego miejsce.

Ta sama technika „rysowania na wierzchu wszystkiego” ma tutaj zastosowanie.

  1. Określ, kiedy górny „zablokowany” nagłówek spotyka się z nowym nadchodzącym.

            View childInContact = getChildInContact(parent, contactPoint);
  2. Zdobądź ten punkt kontaktowy (czyli dolną część lepkiego nagłówka, który narysowałeś, i górę nadchodzącego nagłówka).

            int contactPoint = currentHeader.getBottom();
  3. Jeśli element na liście przekracza ten „punkt kontaktowy”, przerysuj swój przyklejony nagłówek, tak aby jego dół znajdował się na górze elementu, dla którego nastąpiło naruszenie. Osiągasz to za pomocą translate()metody Canvas. W rezultacie punkt początkowy górnego nagłówka będzie poza widocznym obszarem i będzie wyglądał, jakby był „wypychany przez nadchodzący nagłówek”. Kiedy zniknie, narysuj nowy nagłówek na górze.

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

Resztę wyjaśniają komentarze i dokładne adnotacje w podanym przeze mnie fragmencie kodu.

Użycie jest proste:

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

Twój mAdaptermusi wdrożyćStickyHeaderInterface go do pracy. Wdrożenie zależy od posiadanych danych.

Na koniec udostępniam tutaj gif z półprzezroczystymi nagłówkami, abyś mógł uchwycić pomysł i faktycznie zobaczyć, co się dzieje pod maską.

Oto ilustracja koncepcji „po prostu rysuj na wierzchu wszystkiego”. Możesz zobaczyć, że istnieją dwa elementy „nagłówek 1” - jeden, który rysujemy i pozostaje na górze w zablokowanej pozycji, a drugi, który pochodzi ze zbioru danych i przenosi się ze wszystkimi pozostałymi elementami. Użytkownik nie zobaczy jego wewnętrznego działania, ponieważ nie będziesz mieć półprzezroczystych nagłówków.

koncepcja „po prostu narysuj wszystko na wierzchu”

A oto co dzieje się w fazie „wypychania”:

faza „wypychania”

Mam nadzieję, że to pomogło.

Edytować

Oto moja rzeczywista implementacja getHeaderPositionForItem()metody w adapterze 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;
}

Nieco inna implementacja w Kotlinie


4
@Sevastyan Po prostu genialny! Naprawdę podobał mi się sposób, w jaki rozwiązałeś to wyzwanie. Nie mam nic do powiedzenia, może poza jednym pytaniem: czy istnieje sposób, aby ustawić OnClickListener na „lepkim nagłówku” lub przynajmniej wykorzystać kliknięcie, uniemożliwiając użytkownikowi kliknięcie go?
Denis

17
Byłoby wspaniale, gdybyś umieścił przykład adaptera tej implementacji
SolidSnake

1
W końcu udało mi się zadziałać z kilkoma poprawkami tu i tam. chociaż jeśli dodasz jakąkolwiek wyściółkę do swoich przedmiotów, będzie migotać za każdym razem, gdy przewiniesz do wyściełanego obszaru. rozwiązanie w układzie elementu stwórz układ nadrzędny z dopełnieniem 0 i układ podrzędny z dowolnym wypełnieniem.
SolidSnake

8
Dzięki. Ciekawe rozwiązanie, ale trochę drogie, aby zawyżać widok nagłówka przy każdym przewijaniu. Po prostu zmieniłem logikę i używam ViewHolder i przechowuję je w HashMap of WeakReferences, aby ponownie wykorzystać już zawyżone widoki.
Michael

4
@Sevastyan, świetna robota. Mam propozycję. Aby uniknąć tworzenia nowych nagłówków za każdym razem. Po prostu zapisz nagłówek i zmień go tylko wtedy, gdy się zmieni. 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

Najłatwiejszym sposobem jest po prostu utworzenie dekoracji przedmiotu dla Twojego 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 dla Twojego nagłówka w pliku recycling_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"
/>

I na koniec, aby dodać dekorację przedmiotu do swojego 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);

Dzięki tej dekoracji przedmiotu możesz albo przypiąć / przykleić nagłówek, albo nie, używając tylko wartości logicznej podczas tworzenia dekoracji przedmiotu.

Kompletny przykład roboczy można znaleźć na github: https://github.com/paetztm/recycler_view_headers


Dziękuję Ci. to zadziałało dla mnie, jednak ten nagłówek pokrywa się z widokiem recyklingu. możesz pomóc?
kashyap jimuliya

Nie jestem pewien, co masz na myśli, mówiąc, że pokrywa się z RecyclerView. W przypadku wartości logicznej „sticky”, jeśli ustawisz ją na false, dekoracja elementu zostanie umieszczona między wierszami i nie pozostanie na górze RecyclerView.
tim.paetz

ustawienie jej na „sticky” na false umieszcza nagłówek między wierszami, ale to nie zostaje zablokowane (czego nie chcę) na górze. ustawiając go na wartość true, utknie na górze, ale zachodzi na pierwszy rząd w widoku recyklingu
kashyap jimuliya

Widzę, że jako potencjalnie dwa problemy, jeden to wywołanie zwrotne sekcji, nie ustawiasz pierwszego elementu (pozycja 0) dla isSection na true. Po drugie, mijasz niewłaściwą wysokość. Wysokość XML dla widoku tekstu musi być taka sama, jak wysokość przekazana do konstruktora dekoracji elementu sekcji.
tim.paetz

3
Jedną rzeczą, którą chciałbym dodać, jest to, że jeśli układ nagłówka ma dynamiczny rozmiar widoku tekstu tytułu (np. wrap_content), Chciałbyś również uruchomić fixLayoutSizepo ustawieniu tekstu tytułu.
copolii

6

Zrobiłem własną odmianę rozwiązania Sevastyana powyżej

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

... a oto implementacja StickyHeaderInterface (zrobiłem to bezpośrednio w adapterze recyklera):

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

Tak więc w tym przypadku nagłówek nie jest tylko rysowaniem na płótnie, ale widokiem z selektorem lub ripple, nasłuchiwaniem kliknięć itp.


Dzięki za udostępnienie! Dlaczego zakończyłeś zawijanie RecyclerView w nowym RelativeLayout?
tmm1

Ponieważ moja wersja lepkiego nagłówka to View, którą umieściłem w tym RelativeLayout powyżej RecyclerView (w polu headerContainer)
Andrey Turkovsky

Czy możesz pokazać swoją implementację w pliku klasy? Sposób przekazania obiektu nasłuchiwania zaimplementowanego w adapterze.
Dipali Shah

recyclerView.addItemDecoration(HeaderItemDecoration(recyclerView, adapter)). Przepraszamy, nie mogę znaleźć przykładu realizacji, z którego korzystałem. Zredagowałem odpowiedź - dodałem tekst do komentarzy
Andrey Turkovsky

6

każdemu, kto szuka rozwiązania problemu migotania / migania, gdy już masz DividerItemDecoration. wydaje się, że rozwiązałem to w następujący sposób:

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

wydaje się, że działa, ale czy ktoś może potwierdzić, że nic innego nie zepsułem?


Dziękuję, rozwiązało to również dla mnie migający problem.
Yamashiro Rion

3

Możesz sprawdzić i podjąć implementację klasy StickyHeaderHelperw moim projekcie FlexibleAdapter i dostosować ją do swojego przypadku użycia.

Ale sugeruję użycie biblioteki, ponieważ upraszcza i reorganizuje sposób, w jaki zwykle implementujesz adaptery dla RecyclerView: nie wymyślaj ponownie koła.

Powiedziałbym również, nie używaj dekoratorów ani przestarzałych bibliotek, a także nie używaj bibliotek, które robią tylko 1 lub 3 rzeczy, będziesz musiał sam scalić implementacje innych bibliotek.


Spędziłem 2 dni na przeczytaniu wiki i próbki, ale nadal nie wiem, jak stworzyć zwijaną listę za pomocą twojego lib. Próbka jest dość złożona dla początkujących
Nguyen Minh Binh

1
Dlaczego jesteście przeciwni używaniu Decorators?
Sevastyan Savanyuk

1
@Sevastyan, ponieważ dojdziemy do punktu, w którym potrzebujemy odbiornika kliknięć na nim, a także na widokach podrzędnych. My Dekorator, po prostu nie możesz z definicji.
Davideas

@Davidea, czy masz na myśli, że w przyszłości chcesz ustawić odbiorców kliknięć w nagłówkach? Jeśli tak, to ma sens. Jeśli jednak podasz swoje nagłówki jako elementy zestawu danych, nie będzie problemów. Nawet Yigit Boyar zaleca używanie dekoratorów.
Sevastyan Savanyuk

@Sevastyan, tak, w mojej bibliotece nagłówek jest pozycją, tak jak inne na liście, więc użytkownicy mogą nim manipulować. W dalekiej przyszłości menedżer układu niestandardowego zastąpi obecnego pomocnika.
Davideas

3

Kolejne rozwiązanie oparte na nasłuchiwaniu przewijania. Warunki początkowe są takie same jak w odpowiedzi Sewastiańskiej

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

Układ dla ViewHolder i lepkiego nagłówka.

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

Układ dla 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>

Klasa dla HeaderItem.

public class HeaderItem implements Item {

    private String title;

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

    public String getTitle() {
        return title;
    }

}

To wszystko jest przydatne. Implementacja adaptera ViewHolder i innych rzeczy nie jest dla nas interesująca.

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

Interfejs do widoku nagłówka powiązania.

public interface StickyHeaderViewWrapper<T> {

    void bindView(T data);
}

Podoba mi się to rozwiązanie. Mała literówka w findNearestHeader: for (int i = position; position >= 0; i--){ //should be i >= 0
Konstantin

3

Siema,

Tak to się robi, jeśli chcesz mieć tylko jeden rodzaj uchwytu, który zaczyna wysuwać się z ekranu (nie dbamy o żadne sekcje). Jest tylko jeden sposób bez łamania wewnętrznej logiki RecyclerView recyklingu elementów, a to polega na zawyżaniu dodatkowego widoku na górze elementu nagłówka recyklingu i przekazaniu do niego danych. Niech przemówi kod.

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

}

A potem po prostu zrób to w swoim adapterze:

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

Gdzie YOUR_STICKY_VIEW_HOLDER_TYPE jest viewType twojego, co ma być lepkim uchwytem.


2

Dla tych, którzy mogą się martwić. Opierając się na odpowiedzi Sevastyana, jeśli chcesz, aby był on przewijany w poziomie. Po prostu zmień wszystko getBottom()na getRight()i getTop()nagetLeft()


-1

Odpowiedź już tu jest. Jeśli nie chcesz korzystać z żadnej biblioteki, możesz wykonać następujące kroki:

  1. Sortuj listę z danymi według nazwy
  2. Iteruj po liście z danymi i na miejscu, gdy pozycja bieżącego elementu to pierwsza litera! = Pierwsza litera następnego elementu, wstaw "specjalny" rodzaj obiektu.
  3. Wewnątrz adaptera umieszcza się specjalny widok, gdy przedmiot jest „specjalny”.

Wyjaśnienie:

W onCreateViewHoldermetodzie możemy sprawdzićviewType iw zależności od wartości (nasz "specjalny" rodzaj) nadmuchać specjalny układ.

Na przykład:

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

gdzie class ItemElementi class TitleElementmoże wyglądać jak zwykły ViewHolder:

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

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

}

A więc idea tego wszystkiego jest interesująca. Ale jestem zainteresowany, czy jest to skuteczne, ponieważ musimy posortować listę danych. Myślę, że to zmniejszy prędkość. Jeśli masz jakieś uwagi na ten temat, napisz do mnie :)

A także otwarte pytanie: jak utrzymać „specjalny” układ na górze, podczas gdy przedmioty są poddawane recyklingowi. Może połączyć to wszystko z CoordinatorLayout.


czy można to zrobić za pomocą adaptera kursora
M.Yogeshwaran

10
to rozwiązanie nie mówi nic o nagłówkach STICKY, co jest głównym punktem tego postu
Siavash
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.