RecyclerView'da nasıl yapışkan başlıklar oluşturabilirim? (Dış kitap olmadan)


120

Başlık görünümlerimi ekranın üst kısmındaki aşağıdaki resimdeki gibi ve harici kütüphaneler kullanmadan düzeltmek istiyorum.

görüntü açıklamasını buraya girin

Benim durumumda bunu alfabetik olarak yapmak istemiyorum. İki farklı görünümüm var (Başlık ve normal). Ben sadece en üste, son başlığa sabitlemek istiyorum.


17
soru RecyclerView hakkındaydı, bu ^ lib ListView
Max Ch

Yanıtlar:


319

Burada harici bir kütüphane olmadan nasıl yapılacağını açıklayacağım. Çok uzun bir görev olacak, bu yüzden kendinizi destekleyin.

Her şeyden önce, gönderisiItemDecoration s kullanarak kendi yapışkan başlıklarımı uygulama yolculuğuna çıkmam için bana ilham veren @ tim.paetz'i kabul etmeme izin verin . Uygulamamda onun kodunun bazı kısımlarını ödünç aldım.

Zaten deneyimlemiş olabileceğiniz gibi, bunu kendiniz yapmaya kalkıştıysanız, bunu teknikle nasıl yapacağınıza dair iyi bir açıklama bulmak çok zordur ItemDecoration. Demek istediğim, adımlar nelerdir? Bunun arkasındaki mantık nedir? Başlığı listenin en üstüne nasıl yapıştırırım? Bu soruların cevaplarını bilmemek, başkalarını harici kitaplıkları kullanmaya iten şeydir, ancak bunu kendi başınıza yapmak ItemDecorationoldukça kolaydır.

Başlangıç ​​koşulları

  1. Veri kümeniz listfarklı türde öğelerden oluşmalıdır ("Java türleri" anlamında değil, "başlık / öğe" türleri anlamında).
  2. Listeniz önceden sıralanmış olmalıdır.
  3. Listedeki her öğe belirli türde olmalıdır - bununla ilgili bir başlık öğesi olmalıdır.
  4. İçindeki ilk öğe listbir başlık öğesi olmalıdır.

Benim için tam kodu sağlar Buraya RecyclerView.ItemDecorationdenir HeaderItemDecoration. Ardından atılan adımları detaylı bir şekilde anlatıyorum.

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

İş mantığı

Öyleyse, bunu nasıl yapıştırabilirim?

Yapmıyorsun. Bir yapamazsınız RecyclerViewözel düzenlerden bir guru olan ve bir için kod 12.000'den satırları bilmedikçe, üstte 'ın sadece durak seçtiğiniz öğeyi ve sopa RecyclerViewezbere. Yani, her zaman olduğu gibi UI tasarımında olduğu gibi, bir şey yapamıyorsanız, sahte yapın. Sen sadece her şeyin üstünde başlık çizmek kullanarak Canvas. Ayrıca kullanıcının o anda hangi öğeleri görebileceğini de bilmelisiniz. Sadece, olur ItemDecorationhem sağlayabilir Canvasgörünür öğeleri hakkında ve bilgiler. Bununla birlikte, işte temel adımlar:

  1. In onDrawOveryöntemine RecyclerView.ItemDecorationkullanıcının görebildiği ilk (üst) öğesini olsun.

        View topChild = parent.getChildAt(0);
  2. Hangi başlığın onu temsil ettiğini belirleyin.

            int topChildPosition = parent.getChildAdapterPosition(topChild);
        View currentHeader = getHeaderViewForItem(topChildPosition, parent);
    
  3. drawHeader()Yöntemi kullanarak RecyclerView'ın üstüne uygun başlığı çizin .

Ayrıca, yeni gelecek başlık en üstteki ile karşılaştığında davranışı uygulamak istiyorum: yaklaşan başlık nazikçe görünümün dışına itiyor ve sonunda yerini alıyor gibi görünmelidir.

Aynı "her şeyin üstüne çizim" tekniği burada da geçerlidir.

  1. En üstteki "sıkışmış" başlığın yeni gelecek olanla ne zaman karşılaştığını belirleyin.

            View childInContact = getChildInContact(parent, contactPoint);
  2. Bu temas noktasını alın (bu, çizdiğiniz yapışkan başlığın alt kısmı ve yaklaşan başlığın üstü).

            int contactPoint = currentHeader.getBottom();
  3. Listedeki öğe bu "temas noktasını" ihlal ediyorsa, yapışkan başlığınızı yeniden çizin, böylece alt kısmı izinsiz giren öğenin en üstünde olur. Sen ile bunu başarmak translate()yöntemi Canvas. Sonuç olarak, üst başlığın başlangıç ​​noktası görünür alanın dışında olacak ve "yaklaşan başlık tarafından dışarı atılmış" gibi görünecektir. Tamamen bittiğinde, yeni başlığı üstüne çizin.

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

Geri kalanı, sağladığım kod parçasındaki yorumlar ve ayrıntılı açıklamalarla açıklandı.

Kullanım basittir:

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

Sizin mAdapteruygulaması gereken StickyHeaderInterfaceçalışmalara bunun için. Uygulama, sahip olduğunuz verilere bağlıdır.

Son olarak, burada yarı saydam başlıklara sahip bir gif veriyorum, böylece fikri kavrayabilir ve kaputun altında neler olup bittiğini gerçekten görebilirsiniz.

İşte "her şeyin üstüne çizin" konseptinin örneği. İki öğe "başlık 1" olduğunu görebilirsiniz - biri çizip üstte takılı bir konumda kalır, diğeri ise veri kümesinden gelir ve geri kalan tüm öğelerle birlikte hareket eder. Yarı saydam başlıklara sahip olmayacağınız için kullanıcı onun iç işleyişini görmez.

"her şeyin üstüne çizin" kavramı

Ve burada "itme" aşamasında olanlar:

"dışarı itme" aşaması

Umarım yardımcı olmuştur.

Düzenle

İşte getHeaderPositionForItem()RecyclerView adaptöründeki gerçek yöntem uygulamam :

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

Kotlin'de biraz farklı uygulama


4
@Sevastyan Tek kelimeyle harika! Bu sorunu çözme şeklini gerçekten beğendim. Belki bir soru dışında söylenecek bir şey yok: "Yapışkan başlık" üzerinde bir OnClickListener ayarlamanın veya en azından kullanıcının tıklamasını engelleyen tıklamayı tüketmenin bir yolu var mı?
Denis

17
Bu uygulamanın adaptör örneğini koyarsanız harika olur
SolidSnake

1
Sonunda burada ve orada birkaç ince ayar yaparak çalışmasını sağladım. ancak öğelerinize herhangi bir dolgu eklerseniz, yastıklı alana her kaydırdığınızda titremeye devam edecektir. Öğenizin mizanpajındaki çözüm, 0 dolgulu bir ana düzen ve istediğiniz dolguya sahip bir alt düzen oluşturur.
SolidSnake

8
Teşekkürler. İlginç bir çözüm, ancak her kaydırma olayında başlık görünümünü şişirmek biraz pahalı. Mantığı değiştirdim ve ViewHolder'ı kullandım ve zaten şişirilmiş görünümleri yeniden kullanmak için bir HashMap of WeakReferences'da tuttum.
Michael

4
@Sevastyan, harika iş. Bir önerim var. Her seferinde yeni başlıklar oluşturmaktan kaçınmak için. Başlığı kaydedin ve yalnızca değiştiğinde değiştirin. 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

En kolay yol, RecyclerView'unuz için bir Öğe Dekorasyonu oluşturmaktır.

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

}

Recycler_section_header.xml'deki başlığınız için 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"
/>

Ve son olarak RecyclerView'unuza Öğe Dekorasyonunu eklemek için:

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

Bu Eşya Dekorasyonu ile, Eşya Dekorasyonunu oluştururken başlığı sabitleyebilir / yapıştırabilir veya sadece bir boole ile yapamazsınız.

Github'da eksiksiz bir çalışma örneği bulabilirsiniz: https://github.com/paetztm/recycler_view_headers


Teşekkür ederim. bu benim için çalıştı, ancak bu başlık geri dönüşüm görünümü ile örtüşüyor. Yardım edebilir misin?
kashyap jimuliya

RecyclerView ile ne demek istediğinizden emin değilim. "Yapışkan" boole için, bunu yanlış olarak ayarlarsanız, öğe dekorasyonunu satırların arasına yerleştirir ve RecyclerView’un üstünde kalmaz.
tim.paetz

yanlış olarak "yapışkan" olarak ayarlamak, satırlar arasına başlık koyar, ancak bu (istemediğim) üstte kalmaz. doğru olarak ayarlarken, en üstte kalıyor ancak geri dönüşüm görünümündeki ilk satırla örtüşüyor
kashyap jimuliya

Potansiyel olarak iki sorun olarak görebiliyorum, biri bölüm geri aramasıdır, isSection için ilk öğeyi (0 konumu) true olarak ayarlamıyorsunuz. Diğeri ise yanlış yükseklikten geçiyorsun. Metin görünümü için xml yüksekliği, bölüm öğesi dekorasyonunun yapıcısına ilettiğiniz yükseklik ile aynı yükseklikte olmalıdır.
tim.paetz

3
Ekleyeceğim bir şey, başlık düzeniniz dinamik olarak boyutlandırılmış başlık metin görünümüne sahipse (örneğin wrap_content), fixLayoutSizebaşlık metnini ayarladıktan sonra da çalıştırmak isteyeceksiniz .
copolii

6

Yukarıda Sevastyan'ın çözümünün kendi varyasyonunu yaptım

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

... ve işte StickyHeaderInterface uygulaması (bunu doğrudan geri dönüşüm adaptöründe yaptım):

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

Dolayısıyla, bu durumda başlık yalnızca tuval üzerine değil, seçici veya dalgalanma, tıklama dinleyici vb. İle görüntülemedir.


Paylaşım için teşekkürler! Neden RecyclerView'ı yeni bir RelativeLayout ile sarmaya başladınız?
tmm1

Çünkü yapışkan başlık sürümüm, RecyclerView üzerindeki bu RelativeLayout'a koyduğum View (başlıkContainer alanında)
Andrey Turkovsky

Uygulamanızı sınıf dosyasında gösterebilir misiniz? Bağdaştırıcıda uygulanan dinleyici nesnesini nasıl geçirdiğiniz.
Dipali Shah

recyclerView.addItemDecoration(HeaderItemDecoration(recyclerView, adapter)). Maalesef kullandığım uygulama örneği bulamıyorum. Cevabı düzenledim - yorumlara bazı metinler ekledim
Andrey Turkovsky

6

Zaten sahip olduğunuzda titreyen / yanıp sönen soruna çözüm arayan herkese DividerItemDecoration. sanırım bunu şu şekilde çözdüm:

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

Bu çalışıyor gibi görünüyor ama kimse başka bir şey kırmadığımı onaylayabilir mi?


Teşekkürler, yanıp sönen sorunu benim için de çözdü.
Yamashiro Rion

3

Sen kontrol etmek ve sınıfın uygulanmasını alabilir StickyHeaderHelperbenim de FlexibleAdapter proje ve kullanımı durumda uyarlanması.

Ancak, RecyclerView için Adaptörleri genellikle uygulama şeklinizi basitleştirdiği ve yeniden düzenlediği için kitaplığı kullanmanızı öneririm: Tekerleği yeniden icat etmeyin.

Ayrıca, Dekoratörleri veya kullanımdan kaldırılmış kitaplıkları kullanmayın, ayrıca yalnızca 1 veya 3 şey yapan kitaplıkları kullanmayın, diğer kitaplıkların uygulamalarını kendiniz birleştirmeniz gerekecek.


Wiki'yi ve örneği okumak için 2 gün harcadım, ancak hala kütüphanenizi kullanarak kapatılabilir listeyi nasıl oluşturacağımı bilmiyorum. Örnek acemi için oldukça karmaşık
Nguyen Minh Binh

1
Neden Decorators kullanmaya karşısınız ?
Sevastyan Savanyuk

1
@Sevastyan, çünkü tıklama dinleyicisine ihtiyacımız olan noktaya ve çocuk görünümlerine de ulaşacağız. Biz Dekoratör, tanım gereği yapamazsınız.
Davideas

@Davidea, gelecekte başlıklarda tıklama dinleyicileri ayarlamak istediğinizi mi söylüyorsunuz? Eğer öyleyse, mantıklı. Ancak yine de başlıklarınızı veri kümesi öğeleri olarak sağlarsanız hiçbir sorun olmayacaktır. Yiğit Boyar bile Dekoratörlerin kullanılmasını tavsiye ediyor.
Sevastyan Savanyuk

@Sevastyan, evet kütüphanemde başlık listedeki diğerleri gibi bir öğedir, böylece kullanıcılar onu değiştirebilir. Uzak bir gelecekte özel bir düzen yöneticisi mevcut yardımcının yerini alacak.
Davideas

3

Kaydırma dinleyicisine dayalı başka bir çözüm. Başlangıç ​​koşulları Sevastyan cevabındaki ile aynıdır

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

ViewHolder ve yapışkan başlık düzeni.

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

RecyclerView için Düzen

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

HeaderItem Sınıfı.

public class HeaderItem implements Item {

    private String title;

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

    public String getTitle() {
        return title;
    }

}

Hepsi kullanılıyor. Adaptörün, ViewHolder'ın ve diğer şeylerin uygulanması bizim için ilginç değil.

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

Bağlama başlığı görünümü için arayüz.

public interface StickyHeaderViewWrapper<T> {

    void bindView(T data);
}

Bu çözümü beğendim. FindNearestHeader'da küçük yazım hatası: for (int i = position; position >= 0; i--){ //should be i >= 0
Konstantin

3

Yo

Bu, ekrandan çıkmaya başladığında yalnızca bir tür tutucu çubuk istiyorsanız bunu nasıl yapacağınızdır (herhangi bir bölümle ilgilenmiyoruz). Geri dönüşüm öğelerinin dahili RecyclerView mantığını bozmadan tek bir yol vardır ve bu, recyclerView'in başlık öğesinin üstündeki ek görünümü şişirmek ve içine veri iletmektir. Kodun konuşmasına izin vereceğim.

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

}

Ve sonra bunu adaptörünüzde yaparsınız:

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

YOUR_STICKY_VIEW_HOLDER_TYPE nerede yapışkan tutucu olması gereken şeyin viewType'ı.


2

Endişelenebilecekler için. Sevastyan'ın cevabına göre, yatay kaydırma yapmak ister misin? Basitçe tüm değiştirmek getBottom()için getRight()ve getTop()içingetLeft()


-1

Cevap zaten buradaydı. Herhangi bir kitaplık kullanmak istemiyorsanız, şu adımları uygulayabilirsiniz:

  1. Verileri içeren listeyi ada göre sırala
  2. Veri ile liste yoluyla yineleyin ve geçerli öğenin ilk harfi! = Sonraki öğenin ilk harfi olduğunda, "özel" türden bir nesne ekleyin.
  3. Adaptörünüzün içine, öğe "özel" olduğunda özel bir görünüm yerleştirin.

Açıklama:

Gelen onCreateViewHolderyöntemle kontrol edebilmemiz viewTypeve değer (bizim "özel" tür) bağlı özel bir düzen şişirmek.

Örneğin:

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

nerede class ItemElementve class TitleElementsıradan gibi görünebilir ViewHolder:

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

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

}

Yani tüm bunların fikri ilginç. Ama etkili olup olmadığı ile ilgileniyorum, çünkü veri listesini sıralamamız gerekiyor. Ve bence bu hızı düşürecek. Bu konuda herhangi bir düşünceniz varsa, lütfen bana yazın :)

Ve ayrıca açık olan soru, öğeler geri dönüştürülürken "özel" düzenin üstte nasıl tutulacağıdır. Belki bunların hepsini birleştirin CoordinatorLayout.


cursoradapter ile yapmak mümkün mü
M.Yogeshwaran

10
Bu çözüm, bu yazının ana noktası olan YAPIŞKAN başlıkları hakkında hiçbir şey söylemiyor
Siavash
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.