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 ItemDecoration
teknik 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 ItemDecoration
cukup mudah.
Kondisi awal
- Dataset Anda harus berupa
list
item dengan jenis berbeda (bukan dalam arti "tipe Java", tetapi dalam arti jenis "header / item").
- Daftar Anda harus sudah diurutkan.
- Setiap item dalam daftar harus berjenis tertentu - harus ada item header yang terkait dengannya.
- Item pertama di
list
harus menjadi item header.
Di sini saya menyediakan kode penuh untuk saya RecyclerView.ItemDecoration
disebut 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 RecyclerView
item pilihan Anda hanya berhenti dan tetap di atas, kecuali Anda adalah ahli tata letak khusus dan Anda tahu 12.000+ baris kode untuk RecyclerView
hati. 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 ItemDecoration
dapat memberi Anda Canvas
dan informasi tentang item yang terlihat. Berikut langkah-langkah dasarnya:
Dalam onDrawOver
metode RecyclerView.ItemDecoration
mendapatkan item pertama (teratas) yang dapat dilihat oleh pengguna.
View topChild = parent.getChildAt(0);
Tentukan header mana yang mewakilinya.
int topChildPosition = parent.getChildAdapterPosition(topChild);
View currentHeader = getHeaderViewForItem(topChildPosition, parent);
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.
Tentukan kapan header "macet" teratas bertemu dengan header baru yang akan datang.
View childInContact = getChildInContact(parent, contactPoint);
Dapatkan titik kontak ini (yaitu bagian bawah header tempel gambar Anda dan bagian atas header yang akan datang).
int contactPoint = currentHeader.getBottom();
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 mAdapter
harus menerapkannya StickyHeaderInterface
agar 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.
Dan inilah yang terjadi pada 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