Ở đây tôi sẽ giải thích cách thực hiện mà không cần thư viện bên ngoài. Nó sẽ là một bài rất dài, vì vậy hãy chuẩn bị tinh thần.
Trước hết, hãy để tôi thừa nhận @ tim.paetz có bài đăng đã truyền cảm hứng cho tôi bắt đầu hành trình triển khai tiêu đề cố định của riêng tôi bằng cách sử dụng ItemDecoration
s. Tôi đã mượn một số phần mã của anh ấy trong quá trình triển khai của mình.
Như bạn có thể đã có kinh nghiệm, nếu bạn cố gắng tự mình làm điều đó, sẽ rất khó để tìm ra lời giải thích tốt về CÁCH thực sự làm điều đó với ItemDecoration
kỹ thuật này. Ý tôi là, các bước là gì? Logic đằng sau nó là gì? Làm cách nào để tôi đặt tiêu đề lên đầu danh sách? Không biết câu trả lời cho những câu hỏi này là điều khiến người khác sử dụng các thư viện bên ngoài, trong khi việc tự làm với việc sử dụng ItemDecoration
khá dễ dàng.
Điều kiện ban đầu
- Tập dữ liệu của bạn phải là một tập
list
hợp các mục thuộc loại khác nhau (không phải theo nghĩa "các loại Java", mà theo nghĩa các loại "tiêu đề / mục").
- Danh sách của bạn đã được sắp xếp.
- Mọi mục trong danh sách phải thuộc loại nhất định - phải có mục tiêu đề liên quan đến nó.
- Mục đầu tiên
list
phải là mục tiêu đề.
Ở đây tôi cung cấp mã đầy đủ cho cuộc RecyclerView.ItemDecoration
gọi của tôi HeaderItemDecoration
. Sau đó, tôi giải thích các bước thực hiện chi tiết.
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);
}
}
Logic kinh doanh
Vì vậy, làm thế nào để tôi làm cho nó dính?
Bạn không. Bạn không thể làm cho một RecyclerView
mục mà bạn chọn chỉ dừng lại và dán lên trên, trừ khi bạn là chuyên gia về bố cục tùy chỉnh và bạn biết thuộc lòng hơn 12.000 dòng mã RecyclerView
. Vì vậy, nó luôn đi kèm với thiết kế giao diện người dùng, nếu bạn không thể tạo ra thứ gì đó, hãy giả mạo nó. Bạn chỉ cần vẽ tiêu đề trên đầu mọi thứ bằng cách sử dụng Canvas
. Bạn cũng nên biết những mục nào người dùng có thể nhìn thấy vào lúc này. Nó chỉ xảy ra, ItemDecoration
có thể cung cấp cho bạn cả Canvas
và thông tin về các mục hiển thị. Với điều này, đây là các bước cơ bản:
Trong onDrawOver
phương pháp RecyclerView.ItemDecoration
lấy mục đầu tiên (trên cùng) hiển thị cho người dùng.
View topChild = parent.getChildAt(0);
Xác định tiêu đề đại diện cho nó.
int topChildPosition = parent.getChildAdapterPosition(topChild);
View currentHeader = getHeaderViewForItem(topChildPosition, parent);
Vẽ tiêu đề thích hợp trên đầu RecyclerView bằng drawHeader()
phương pháp sử dụng .
Tôi cũng muốn thực hiện hành vi khi tiêu đề mới sắp tới gặp tiêu đề trên cùng: có vẻ như tiêu đề sắp tới sẽ nhẹ nhàng đẩy tiêu đề hiện tại trên cùng ra khỏi chế độ xem và cuối cùng sẽ chiếm vị trí của anh ta.
Kỹ thuật "vẽ trên đầu mọi thứ" cũng được áp dụng ở đây.
Xác định thời điểm tiêu đề "bị kẹt" trên cùng gặp tiêu đề mới sắp ra mắt.
View childInContact = getChildInContact(parent, contactPoint);
Lấy điểm tiếp xúc này (đó là phần cuối của tiêu đề cố định mà bạn đã vẽ và phần đầu của tiêu đề sắp tới).
int contactPoint = currentHeader.getBottom();
Nếu mục trong danh sách đang xâm phạm "điểm tiếp xúc" này, hãy vẽ lại tiêu đề cố định của bạn để phần cuối của nó nằm ở đầu mục xâm phạm. Bạn đạt được điều này với translate()
phương pháp của Canvas
. Do đó, điểm bắt đầu của tiêu đề trên cùng sẽ nằm ngoài vùng có thể nhìn thấy và nó sẽ có vẻ như "bị đẩy ra bởi tiêu đề sắp tới". Khi nó biến mất hoàn toàn, hãy vẽ tiêu đề mới lên trên.
if (childInContact != null) {
if (mListener.isHeader(parent.getChildAdapterPosition(childInContact))) {
moveHeader(c, currentHeader, childInContact);
} else {
drawHeader(c, currentHeader);
}
}
Phần còn lại được giải thích bằng các nhận xét và chú thích kỹ lưỡng trong đoạn mã tôi đã cung cấp.
Việc sử dụng là thẳng về phía trước:
mRecyclerView.addItemDecoration(new HeaderItemDecoration((HeaderItemDecoration.StickyHeaderInterface) mAdapter));
Bạn mAdapter
phải triển khai StickyHeaderInterface
để nó hoạt động. Việc triển khai phụ thuộc vào dữ liệu bạn có.
Cuối cùng, ở đây tôi cung cấp một ảnh gif với tiêu đề nửa trong suốt, vì vậy bạn có thể nắm bắt ý tưởng và thực sự xem những gì đang diễn ra.
Dưới đây là minh họa của khái niệm "chỉ vẽ trên tất cả mọi thứ". Bạn có thể thấy rằng có hai mục "tiêu đề 1" - một mục mà chúng tôi vẽ và ở trên cùng ở vị trí bị mắc kẹt, và mục còn lại đến từ tập dữ liệu và di chuyển cùng với tất cả các mục còn lại. Người dùng sẽ không nhìn thấy hoạt động bên trong của nó, vì bạn sẽ không có tiêu đề nửa trong suốt.
Và đây là những gì xảy ra trong giai đoạn "đẩy ra":
Hy vọng nó sẽ giúp.
Biên tập
Đây là cách triển khai thực tế của tôi về getHeaderPositionForItem()
phương thức trong bộ điều hợp của 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;
}
Cách triển khai hơi khác trong Kotlin