Làm cách nào để tạo tiêu đề cố định trong RecyclerView? (Không có lib bên ngoài)


120

Tôi muốn sửa các chế độ xem tiêu đề của mình ở đầu màn hình như trong hình bên dưới và không sử dụng thư viện bên ngoài.

nhập mô tả hình ảnh ở đây

Trong trường hợp của tôi, tôi không muốn làm theo thứ tự bảng chữ cái. Tôi có hai loại chế độ xem khác nhau (Tiêu đề và bình thường). Tôi chỉ muốn sửa phần đầu, phần đầu cuối cùng.


17
câu hỏi là về RecyclerView, ^ lib này dựa trên ListView
Tối đa

Câu trả lời:


319

Ở đâ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 ItemDecorations. 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 ItemDecorationkỹ 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 ItemDecorationkhá dễ dàng.

Điều kiện ban đầu

  1. Tập dữ liệu của bạn phải là một tập listhợ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").
  2. Danh sách của bạn đã được sắp xếp.
  3. 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ó.
  4. Mục đầu tiên listphải là mục tiêu đề.

Ở đây tôi cung cấp mã đầy đủ cho cuộc RecyclerView.ItemDecorationgọ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 RecyclerViewmụ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, ItemDecorationcó thể cung cấp cho bạn cả Canvasvà 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:

  1. Trong onDrawOverphương pháp RecyclerView.ItemDecorationlấy mục đầu tiên (trên cùng) hiển thị cho người dùng.

        View topChild = parent.getChildAt(0);
  2. Xác định tiêu đề đại diện cho nó.

            int topChildPosition = parent.getChildAdapterPosition(topChild);
        View currentHeader = getHeaderViewForItem(topChildPosition, parent);
    
  3. 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.

  1. 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);
  2. 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();
  3. 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 mAdapterphả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.

khái niệm "chỉ vẽ trên tất cả mọi thứ"

Và đây là những gì xảy ra trong giai đoạn "đẩy ra":

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


4
@Sevastyan Thật tuyệt vời! Tôi thực sự thích cách bạn giải quyết thử thách này. Không có gì để nói, ngoại trừ một câu hỏi: Có cách nào để đặt OnClickListener trên "tiêu đề cố định" hoặc ít nhất là sử dụng lần nhấp ngăn người dùng nhấp qua nó không?
Denis

17
Sẽ là tuyệt vời nếu bạn đặt bộ chuyển đổi ví dụ về thực hiện này
SolidSnake

1
Cuối cùng tôi đã làm cho nó hoạt động với một vài chỉnh sửa ở đây và ở đó. mặc dù nếu bạn thêm bất kỳ vùng đệm nào vào các mục của mình, nó sẽ tiếp tục nhấp nháy bất cứ khi nào bạn cuộn đến vùng đệm. giải pháp trong bố cục của mục của bạn tạo bố cục mẹ với 0 phần đệm và một bố cục con với bất kỳ phần đệm nào bạn muốn.
SolidSnake

8
Cảm ơn. Giải pháp thú vị, nhưng hơi tốn kém để tăng chế độ xem tiêu đề trên mọi sự kiện cuộn. Tôi vừa thay đổi logic và sử dụng ViewHolder và giữ chúng trong HashMap gồm các tài liệu tham khảo yếu để sử dụng lại các chế độ xem đã tăng cao.
Michael

4
@Sevastyan, tuyệt vời. Tôi có một gợi ý. Để tránh tạo tiêu đề mới mọi lúc. Chỉ cần lưu tiêu đề và chỉ thay đổi nó khi nó thay đổi. 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 vào

27

Cách dễ nhất là chỉ cần tạo một Trang trí Vật phẩm cho RecyclerView của bạn.

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 cho tiêu đề của bạn trong render_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"
/>

Và cuối cùng để thêm Trang trí vật phẩm vào RecyclerView của bạn:

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

Với Trang trí Vật phẩm này, bạn có thể làm cho tiêu đề được ghim / dính hoặc không chỉ bằng boolean khi tạo Trang trí Vật phẩm.

Bạn có thể tìm thấy một ví dụ hoạt động hoàn chỉnh trên github: https://github.com/paetztm/recycler_view_headers


Cảm ơn bạn. điều này làm việc cho tôi, tuy nhiên tiêu đề này chồng lên chế độ xem tái chế. bạn có thể giúp?
kashyap jimuliya

Tôi không chắc ý của bạn khi chồng lấn RecyclerView. Đối với boolean "dính", nếu bạn đặt thành false, nó sẽ đặt trang trí vật phẩm ở giữa các hàng và sẽ không ở trên cùng của RecyclerView.
tim.paetz

đặt nó thành "dính" thành false đặt tiêu đề giữa các hàng, nhưng điều đó không bị kẹt (mà tôi không muốn) lên trên cùng. trong khi thiết lập nó là true, nó vẫn bị mắc kẹt trên đỉnh nhưng nó chồng lên hàng đầu tiên trong recyclerview
Kashyap jimuliya

Tôi có thể thấy rằng có hai vấn đề tiềm ẩn, một là phần gọi lại, bạn không đặt mục đầu tiên (vị trí 0) cho isSection thành true. Hai là bạn đang vượt sai độ cao. Chiều cao của xml cho chế độ xem văn bản phải bằng chiều cao mà bạn chuyển vào hàm tạo của phần trang trí mục.
tim.paetz

3
Tôi muốn thêm một điều, đó là nếu bố cục tiêu đề của bạn có chế độ xem văn bản tiêu đề có kích thước động (ví dụ wrap_content), thì Bạn cũng muốn chạy fixLayoutSizesau khi đặt văn bản tiêu đề.
copolii

6

Tôi đã tạo biến thể của riêng mình cho giải pháp Sevastyan ở trên

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

... và đây là việc triển khai StickyHeaderInterface (tôi đã thực hiện trực tiếp trong bộ điều hợp trình tái chế):

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

Vì vậy, trong trường hợp này, tiêu đề không chỉ vẽ trên canvas, mà là xem bằng bộ chọn hoặc gợn sóng, nhấp chuột, v.v.


Cám ơn vì đã chia sẻ! Tại sao bạn lại kết thúc RecyclerView trong một RelativeLayout mới?
tmm1

Bởi vì phiên bản tiêu đề cố định của tôi là Chế độ xem, tôi đã đưa vào RelativeLayout này bên trên RecyclerView (trong trường headerContainer)
Andrey Turkovsky 15/02/18

Bạn có thể hiển thị triển khai của bạn trong tệp lớp không? Cách bạn chuyển đối tượng của trình lắng nghe được triển khai trong bộ điều hợp.
Dipali Shah

recyclerView.addItemDecoration(HeaderItemDecoration(recyclerView, adapter)). Xin lỗi, không thể tìm thấy ví dụ về cách triển khai mà tôi đã sử dụng. Tôi đã chỉnh sửa câu trả lời - đã thêm một số văn bản vào nhận xét
Andrey Turkovsky.

6

cho bất kỳ ai đang tìm kiếm giải pháp cho vấn đề nhấp nháy / nhấp nháy khi bạn đã có DividerItemDecoration. Tôi dường như đã giải quyết nó như thế này:

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

Điều này dường như đang hoạt động nhưng bất cứ ai có thể xác nhận rằng tôi đã không phá vỡ bất cứ điều gì khác?


Cảm ơn bạn, nó cũng giải quyết vấn đề nhấp nháy cho tôi.
Yamashiro Rion

3

Bạn có thể kiểm tra và thực hiện việc triển khai lớp StickyHeaderHelpertrong dự án FlexAdapter của tôi và điều chỉnh nó cho phù hợp với trường hợp sử dụng của bạn.

Tuy nhiên, tôi khuyên bạn nên sử dụng thư viện vì nó đơn giản hóa và tổ chức lại cách bạn thường triển khai Bộ điều hợp cho RecyclerView: Đừng phát minh lại bánh xe.

Tôi cũng sẽ nói rằng, không sử dụng Decorator hoặc các thư viện không dùng nữa, cũng như không sử dụng các thư viện chỉ làm 1 hoặc 3 việc, bạn sẽ phải tự mình hợp nhất các triển khai của các thư viện khác.


Tôi đã dành 2 ngày để đọc wiki và mẫu, nhưng vẫn không biết cách tạo danh sách có thể thu gọn bằng cách sử dụng lib của bạn. Mẫu khá phức tạp đối với người mới
Nguyễn Minh Bình

1
Tại sao bạn không sử dụng Decorators?
Sevastyan Savanyuk

1
@Sevastyan, bởi vì chúng tôi sẽ đến thời điểm chúng tôi cần trình nghe nhấp chuột trên đó và cả các chế độ xem trẻ em. Chúng tôi Decorator đơn giản là bạn không thể theo định nghĩa.
Davideas

@Davidea, ý bạn là bạn muốn đặt trình nghe nhấp chuột trên tiêu đề trong tương lai? Nếu vậy, nó có ý nghĩa. Tuy nhiên, nếu bạn cung cấp tiêu đề của mình dưới dạng các mục tập dữ liệu, sẽ không có vấn đề gì. Ngay cả Yigit Boyar cũng khuyên bạn nên sử dụng Decorator.
Sevastyan Savanyuk

@Sevastyan, vâng trong thư viện của tôi, tiêu đề là một mục giống như những mục khác trong danh sách, vì vậy người dùng có thể thao tác với nó. Trong tương lai xa, trình quản lý bố cục tùy chỉnh sẽ thay thế trình trợ giúp hiện tại.
Davideas

3

Một giải pháp khác, dựa trên trình nghe cuộn. Các điều kiện ban đầu giống như trong câu trả lời 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);
}

Bố cục cho ViewHolder và tiêu đề cố định.

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

Bố cục cho 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>

Lớp cho HeaderItem.

public class HeaderItem implements Item {

    private String title;

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

    public String getTitle() {
        return title;
    }

}

Tất cả đều được sử dụng. Việc triển khai bộ điều hợp, ViewHolder và những thứ khác, không thú vị đối với chúng tôi.

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

Giao diện cho chế độ xem tiêu đề liên kết.

public interface StickyHeaderViewWrapper<T> {

    void bindView(T data);
}

Tôi thích giải pháp này. Typo nhỏ trong findNearestHeader: for (int i = position; position >= 0; i--){ //should be i >= 0
Konstantin

3

Yo,

Đây là cách bạn thực hiện nếu bạn chỉ muốn một loại giá đỡ khi nó bắt đầu ra khỏi màn hình (chúng tôi không quan tâm đến bất kỳ phần nào). Chỉ có một cách mà không phá vỡ logic RecyclerView nội bộ của các mục tái chế và đó là tăng chế độ xem bổ sung lên trên mục tiêu đề của RecyclerView và chuyển dữ liệu vào đó. Tôi sẽ để mã nói chuyện.

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

}

Và sau đó bạn chỉ cần làm điều này trong bộ điều hợp của mình:

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

Nơi YOUR_STICKY_VIEW_HOLDER_TYPE là chế độ xem Loại của những gì được cho là người giữ cố định của bạn.


2

Đối với những người có thể quan tâm. Dựa trên câu trả lời của Sevastyan, nếu bạn muốn làm cho nó cuộn ngang. Đơn giản chỉ cần thay đổi tất cả getBottom()để getRight()getTop()đểgetLeft()


-1

Câu trả lời đã có ở đây. Nếu bạn không muốn sử dụng bất kỳ thư viện nào, bạn có thể làm theo các bước sau:

  1. Sắp xếp danh sách với dữ liệu theo tên
  2. Lặp lại qua danh sách với dữ liệu, và ở vị trí khi chữ cái đầu tiên của mục hiện tại! = Chữ cái đầu tiên của mục tiếp theo, hãy chèn loại đối tượng "đặc biệt".
  3. Bên trong Bộ điều hợp của bạn đặt chế độ xem đặc biệt khi mục là "đặc biệt".

Giải trình:

Trong onCreateViewHolderphương pháp này, chúng tôi có thể kiểm tra viewTypevà tùy thuộc vào giá trị (loại "đặc biệt" của chúng tôi) sẽ thổi phồng một bố cục đặc biệt.

Ví dụ:

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

ở đâu class ItemElementclass TitleElementcó thể trông giống như bình thường ViewHolder:

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

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

}

Vì vậy, ý tưởng về tất cả những điều đó thật thú vị. Nhưng tôi quan tâm nếu nó hiệu quả, vì chúng tôi cần sắp xếp danh sách dữ liệu. Và tôi nghĩ điều này sẽ làm giảm tốc độ. Nếu có bất kỳ suy nghĩ về nó, xin vui lòng viết cho tôi :)

Và cũng là câu hỏi mở: là làm thế nào để giữ bố cục "đặc biệt" trên cùng, trong khi các vật dụng đang tái chế. Có thể kết hợp tất cả những điều đó với CoordinatorLayout.


là nó có thể làm cho nó với CursorAdapter
M.Yogeshwaran

10
giải pháp này không nói bất cứ điều gì về tiêu đề STICKY đó là điểm chính của bài viết này
Siavash
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.