Bagaimana cara membuat header lengket di RecyclerView? (Tanpa lib eksternal)

120

Saya ingin memperbaiki tampilan header saya di bagian atas layar seperti pada gambar di bawah ini dan tanpa menggunakan pustaka eksternal.

masukkan deskripsi gambar di sini

Dalam kasus saya, saya tidak ingin melakukannya menurut abjad. Saya memiliki dua jenis tampilan yang berbeda (Header dan normal). Saya hanya ingin memperbaiki ke atas, tajuk terakhir.

Jaume Colom
sumber
17
pertanyaannya adalah tentang RecyclerView, ^ lib ini didasarkan pada ListView
Max Ch

Jawaban:

319

Di sini saya akan menjelaskan bagaimana melakukannya tanpa perpustakaan eksternal. Ini akan menjadi posting yang sangat panjang, jadi persiapkan diri Anda.

Pertama-tama, izinkan saya mengakui @ tim.paetz yang posnya menginspirasi saya untuk memulai perjalanan menerapkan header tempel saya sendiri menggunakanItemDecoration s. Saya meminjam beberapa bagian kodenya dalam implementasi saya.

Seperti yang mungkin pernah Anda alami, jika Anda mencoba melakukannya sendiri, sangat sulit menemukan penjelasan yang baik tentang BAGAIMANA benar-benar melakukannya dengan ItemDecorationteknik ini. Maksud saya, apa saja langkah-langkahnya? Apa logika di baliknya? Bagaimana cara membuat tajuk menempel di bagian atas daftar? Tidak mengetahui jawaban atas pertanyaan-pertanyaan ini yang membuat orang lain menggunakan perpustakaan eksternal, sedangkan melakukannya sendiri dengan penggunaan ItemDecorationcukup mudah.

Kondisi awal

  1. Dataset Anda harus berupa listitem dengan jenis berbeda (bukan dalam arti "tipe Java", tetapi dalam arti jenis "header / item").
  2. Daftar Anda harus sudah diurutkan.
  3. Setiap item dalam daftar harus berjenis tertentu - harus ada item header yang terkait dengannya.
  4. Item pertama di listharus menjadi item header.

Di sini saya menyediakan kode penuh untuk saya RecyclerView.ItemDecorationdisebut HeaderItemDecoration. Kemudian saya menjelaskan langkah-langkah yang diambil secara rinci.

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 bisnis

Jadi, bagaimana cara membuatnya melekat?

Kamu tidak. Anda tidak dapat membuat RecyclerViewitem pilihan Anda hanya berhenti dan tetap di atas, kecuali Anda adalah ahli tata letak khusus dan Anda tahu 12.000+ baris kode untuk RecyclerViewhati. Jadi, seperti yang selalu terjadi dengan desain UI, jika Anda tidak bisa membuat sesuatu, palsukan. Anda hanya menggambar tajuk di atas semua yang menggunakan Canvas. Anda juga harus tahu item mana yang dapat dilihat pengguna saat ini. Itu terjadi begitu saja, yang ItemDecorationdapat memberi Anda Canvasdan informasi tentang item yang terlihat. Berikut langkah-langkah dasarnya:

  1. Dalam onDrawOvermetode RecyclerView.ItemDecorationmendapatkan item pertama (teratas) yang dapat dilihat oleh pengguna.

        View topChild = parent.getChildAt(0);
  2. Tentukan header mana yang mewakilinya.

            int topChildPosition = parent.getChildAdapterPosition(topChild);
        View currentHeader = getHeaderViewForItem(topChildPosition, parent);
  3. Gambar header yang sesuai di atas RecyclerView dengan menggunakan drawHeader()metode.

Saya juga ingin menerapkan perilaku tersebut ketika tajuk baru yang akan datang memenuhi tajuk teratas: seharusnya tajuk yang akan datang dengan lembut mendorong tajuk saat ini keluar dari tampilan dan pada akhirnya menggantikannya.

Teknik yang sama untuk "menggambar di atas segalanya" berlaku di sini.

  1. Tentukan kapan header "macet" teratas bertemu dengan header baru yang akan datang.

            View childInContact = getChildInContact(parent, contactPoint);
  2. Dapatkan titik kontak ini (yaitu bagian bawah header tempel gambar Anda dan bagian atas header yang akan datang).

            int contactPoint = currentHeader.getBottom();
  3. Jika item dalam daftar melanggar "titik kontak" ini, gambar ulang header tempel Anda sehingga bagian bawahnya berada di atas item yang masuk tanpa izin. Anda mencapai ini dengan translate()metode Canvas. Akibatnya, titik awal dari header teratas akan keluar dari area yang terlihat, dan akan tampak seperti "didorong keluar oleh header yang akan datang". Jika sudah benar-benar hilang, gambar tajuk baru di atas.

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

Sisanya dijelaskan oleh komentar dan anotasi menyeluruh dalam kode yang saya berikan.

Penggunaannya langsung:

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

Anda mAdapterharus menerapkannya StickyHeaderInterfaceagar berfungsi. Penerapannya bergantung pada data yang Anda miliki.

Terakhir, di sini saya memberikan gif dengan header setengah transparan, sehingga Anda dapat memahami idenya dan benar-benar melihat apa yang terjadi di balik terpal.

Berikut ilustrasi konsep "menggambar di atas segalanya". Anda dapat melihat bahwa ada dua item "header 1" - satu yang kami gambar dan tetap di atas dalam posisi macet, dan yang lainnya berasal dari kumpulan data dan bergerak dengan semua item lainnya. Pengguna tidak akan melihat cara kerjanya, karena Anda tidak akan memiliki header setengah transparan.

Konsep "hanya menggambar di atas segalanya"

Dan inilah yang terjadi pada fase "mendorong keluar":

fase "mendorong keluar"

Semoga membantu.

Sunting

Berikut adalah implementasi sebenarnya dari getHeaderPositionForItem()metode saya di adaptor 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;
}

Implementasi yang sedikit berbeda di Kotlin

Sevastyan Savanyuk
sumber
4
@Sevastyan Hanya brilian! Saya sangat menyukai cara Anda memecahkan tantangan ini. Tidak ada yang perlu dikatakan, kecuali mungkin satu pertanyaan: Adakah cara untuk menyetel OnClickListener pada "tajuk lengket", atau setidaknya mengonsumsi klik yang mencegah pengguna untuk mengekliknya?
Denis
17
Akan lebih bagus jika Anda meletakkan contoh adaptor dari implementasi ini
SolidSnake
1
Saya akhirnya berhasil dengan sedikit perubahan di sana-sini. meskipun jika Anda menambahkan padding ke item Anda, itu akan terus berkedip setiap kali Anda menggulir ke area empuk. solusi dalam tata letak item Anda buat tata letak induk dengan 0 padding dan tata letak anak dengan padding apa pun yang Anda inginkan.
SolidSnake
8
Terima kasih. Solusi menarik, tapi agak mahal untuk memperbesar tampilan header pada setiap acara gulir. Saya baru saja mengubah logika dan menggunakan ViewHolder dan menyimpannya di HashMap dari WeakReferences untuk menggunakan kembali tampilan yang sudah digelembungkan.
Michael
4
@Sevastyan, kerja bagus. Saya punya saran. Untuk menghindari membuat header baru setiap saat. Simpan saja header dan ubah hanya jika berubah. 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

Cara termudah adalah dengan membuat Dekorasi Item untuk RecyclerView Anda.

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 untuk header Anda di 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"
/>

Dan terakhir untuk menambahkan Dekorasi Item ke RecyclerView Anda:

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

Dengan Dekorasi Item ini Anda dapat membuat tajuk disematkan / lengket atau tidak hanya dengan boolean saat membuat Dekorasi Item.

Anda dapat menemukan contoh kerja lengkap di github: https://github.com/paetztm/recycler_view_headers

tim.paetz
sumber
Terima kasih. ini berhasil untuk saya, namun tajuk ini tumpang tindih dengan recyclerview. Bisakah kamu menolong?
kashyap jimuliya
Saya tidak yakin apa yang Anda maksud dengan tumpang tindih dengan RecyclerView. Untuk boolean "lengket", jika Anda menyetelnya ke false, dekorasi item akan ditempatkan di antara baris dan tidak akan tetap berada di bagian atas RecyclerView.
tim.paetz
mengaturnya ke "sticky" menjadi false menempatkan header di antara baris, tetapi itu tidak tetap macet (yang tidak saya inginkan) ke atas. sementara menyetelnya ke true, itu tetap macet di atas tetapi tumpang tindih dengan baris pertama di recyclerview
kashyap jimuliya
Saya dapat melihat bahwa sebagai dua kemungkinan masalah, satu adalah bagian callback, Anda tidak menyetel item pertama (0 posisi) untuk isSection menjadi true. Yang lainnya adalah Anda melewati ketinggian yang salah. Tinggi xml untuk tampilan teks harus sama tingginya dengan tinggi yang Anda berikan ke konstruktor dekorasi item bagian.
tim.paetz
3
Satu hal yang akan saya tambahkan, adalah jika tata letak tajuk Anda memiliki tampilan teks judul yang berukuran dinamis (misalnya wrap_content), Anda juga ingin menjalankan fixLayoutSizesetelah menyetel teks judul.
copolii
6

Saya telah membuat variasi sendiri dari solusi Sevastyan di atas

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

... dan berikut adalah implementasi StickyHeaderInterface (saya melakukannya langsung di adaptor pendaur ulang):

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

Jadi, dalam hal ini header tidak hanya menggambar di atas kanvas, tetapi tampilan dengan selector atau ripple, clicklistener, dll.

Andrey Turkovsky
sumber
Terima kasih telah berbagi! Mengapa Anda akhirnya membungkus RecyclerView dalam RelativeLayout baru?
tmm1
Karena versi sticky header saya adalah View, yang saya masukkan ke RelativeLayout ini di atas RecyclerView (di kolom headerContainer)
Andrey Turkovsky
Bisakah Anda menunjukkan implementasi Anda dalam file kelas? Bagaimana Anda meneruskan objek listener yang diimplementasikan di adapter.
Dipali Shah
recyclerView.addItemDecoration(HeaderItemDecoration(recyclerView, adapter)). Maaf, tidak dapat menemukan contoh implementasi, yang saya gunakan. Saya telah mengedit jawaban - menambahkan beberapa teks ke komentar
Andrey Turkovsky
6

kepada siapa pun yang mencari solusi untuk masalah berkedip / berkedip saat Anda sudah memilikinya DividerItemDecoration. sepertinya saya telah menyelesaikannya seperti ini:

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

ini tampaknya berfungsi tetapi adakah yang bisa memastikan bahwa saya tidak merusak apa pun?

or_dvir
sumber
Terima kasih, ini juga menyelesaikan masalah berkedip untuk saya.
Yamashiro Rion
3

Anda dapat memeriksa dan mengambil implementasi kelas StickyHeaderHelperdalam proyek FlexibleAdapter saya , dan menyesuaikannya dengan kasus penggunaan Anda.

Tapi, saya menyarankan untuk menggunakan pustaka karena ini menyederhanakan dan mengatur ulang cara Anda biasanya mengimplementasikan Adapters for RecyclerView: Jangan menemukan kembali roda.

Saya juga akan mengatakan, jangan gunakan Dekorator atau perpustakaan usang, serta jangan gunakan perpustakaan yang hanya melakukan 1 atau 3 hal, Anda harus menggabungkan sendiri implementasi perpustakaan lain.

Davideas
sumber
Saya menghabiskan 2 hari untuk membaca wiki dan sampel, tetapi masih tidak tahu cara membuat daftar yang dapat diciutkan dengan menggunakan lib Anda. Sampelnya cukup rumit untuk pemula
Nguyen Minh Binh
1
Mengapa Anda menentang penggunaan Decorators?
Sevastyan Savanyuk
1
@Sevastyan, karena kita akan sampai pada titik yang kita perlukan pemroses klik di atasnya dan juga pada tampilan anak. Kami Dekorator Anda tidak bisa menurut definisi.
Davideas
@Davidea, maksud Anda ingin menyetel pendengar klik di header di masa mendatang? Jika demikian, itu masuk akal. Namun tetap saja, jika Anda menyediakan header Anda sebagai item set data, tidak akan ada masalah. Bahkan Yigit Boyar merekomendasikan penggunaan Dekorator.
Sevastyan Savanyuk
@Sevastyan, ya di perpustakaan saya, tajuknya adalah item seperti orang lain dalam daftar, sehingga pengguna dapat memanipulasinya. Di masa mendatang, pengelola tata letak khusus akan menggantikan helper saat ini.
Davideas
3

Solusi lain, berdasarkan pendengar gulir. Kondisi awal sama dengan jawaban Sevastyan

RecyclerView recyclerView;
TextView tvTitle; //sticky header view

//... onCreate, initialize, etc...

public void bindList(List<Item> items) { //All data in adapter. Item - just interface for different item types
    adapter = new YourAdapter(items);
    recyclerView.setAdapter(adapter);
    StickyHeaderViewManager<HeaderItem> stickyHeaderViewManager = new StickyHeaderViewManager<>(
            tvTitle,
            recyclerView,
            HeaderItem.class, //HeaderItem - subclass of Item, used to detect headers in list
            data -> { // bind function for sticky header view
                tvTitle.setText(data.getTitle());
            });
    stickyHeaderViewManager.attach(items);
}

Tata letak untuk ViewHolder dan tajuk lengket.

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

Tata letak untuk 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>

Kelas untuk HeaderItem.

public class HeaderItem implements Item {

    private String title;

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

    public String getTitle() {
        return title;
    }

}

Itu semua berguna. Penerapan adaptor, ViewHolder, dan hal-hal lain, tidak menarik bagi kami.

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

Antarmuka untuk tampilan header bind.

public interface StickyHeaderViewWrapper<T> {

    void bindView(T data);
}
Anrimian
sumber
Saya suka solusi ini. Salah ketik kecil di findNearestHeader: for (int i = position; position >= 0; i--){ //should be i >= 0
Konstantin
3

Yo,

Beginilah cara Anda melakukannya jika Anda menginginkan hanya satu jenis dudukan tongkat saat mulai keluar dari layar (kami tidak peduli tentang bagian mana pun). Hanya ada satu cara tanpa merusak logika RecyclerView internal dari item daur ulang dan itu adalah dengan meluaskan tampilan tambahan di atas item header recyclerView dan meneruskan data ke dalamnya. Aku akan membiarkan kodenya berbicara.

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

}

Dan kemudian Anda cukup melakukan ini di adaptor Anda:

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

Di mana YOUR_STICKY_VIEW_HOLDER_TYPE adalah viewType dari Anda yang seharusnya menjadi pemegang tempel.

Stanislav Kinzl
sumber
2

Bagi mereka yang mungkin peduli. Berdasarkan jawaban Sevastyan, apakah Anda ingin membuatnya horizontal scroll. Cukup ubah semua getBottom()menjadi getRight()dan getTop()kegetLeft()

Guster
sumber
-1

Jawabannya sudah ada di sini. Jika Anda tidak ingin menggunakan pustaka apa pun, Anda dapat mengikuti langkah-langkah berikut:

  1. Urutkan daftar dengan data berdasarkan nama
  2. Iterasi melalui daftar dengan data, dan di tempat ketika item saat ini huruf pertama! = Huruf pertama dari item berikutnya, masukkan jenis objek "khusus".
  3. Di dalam Adaptor Anda, tempatkan tampilan khusus ketika item itu "khusus".

Penjelasan:

Dalam onCreateViewHoldermetode kami dapat memeriksaviewType dan bergantung pada nilai (jenis "khusus" kami) mengembangkan tata letak khusus.

Sebagai contoh:

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

dimana class ItemElementdan class TitleElementbisa terlihat seperti biasa ViewHolder:

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

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

}

Jadi gagasan tentang semua itu menarik. Tapi saya tertarik jika efektif, karena kita perlu mengurutkan daftar datanya. Dan saya pikir ini akan menurunkan kecepatan. Jika ada pemikiran tentang itu, tolong tulis saya :)

Dan juga pertanyaan terbuka: adalah bagaimana memegang tata letak "khusus" di atas, saat barang-barang didaur ulang. Mungkin menggabungkan semua itu dengan CoordinatorLayout.

Valeria
sumber
apakah mungkin membuatnya dengan cursoradapter
M.Yogeshwaran
10
solusi ini tidak mengatakan apa-apa tentang header STICKY yang merupakan poin utama dari posting ini
Siavash