ฉันจะสร้างส่วนหัวแบบเหนียวใน RecyclerView ได้อย่างไร (ไม่มี lib ภายนอก)


120

ฉันต้องการแก้ไขมุมมองส่วนหัวที่ด้านบนของหน้าจอเหมือนในภาพด้านล่างและไม่ใช้ไลบรารีภายนอก

ใส่คำอธิบายภาพที่นี่

ในกรณีของฉันฉันไม่ต้องการทำตามตัวอักษร ฉันมีมุมมองสองประเภทที่แตกต่างกัน (ส่วนหัวและปกติ) ฉันต้องการแก้ไขเฉพาะด้านบนส่วนหัวสุดท้ายเท่านั้น


17
คำถามเกี่ยวกับ RecyclerView นี้ ^ lib อ้างอิงจาก ListView
Max Ch

คำตอบ:


319

ที่นี่ฉันจะอธิบายวิธีการทำโดยไม่มีไลบรารีภายนอก มันจะเป็นโพสต์ที่ยาวมากดังนั้นจงรั้งตัวเองไว้

ก่อนอื่นให้ฉันรับทราบ@ tim.paetzซึ่งโพสต์เป็นแรงบันดาลใจให้ฉันเริ่มต้นการเดินทางของการใช้ส่วนหัวแบบติดหนึบของตัวเองโดยใช้ItemDecorations ฉันยืมโค้ดบางส่วนของเขามาใช้ในการติดตั้ง

อย่างที่คุณเคยสัมผัสมาแล้วหากคุณพยายามที่จะทำด้วยตัวเองมันยากมากที่จะหาคำอธิบายที่ดีเกี่ยวกับวิธีการทำด้วยItemDecorationเทคนิคนี้ ฉันหมายถึงมีขั้นตอนอย่างไร? ตรรกะเบื้องหลังคืออะไร? ฉันจะทำให้ส่วนหัวติดด้านบนของรายการได้อย่างไร การไม่รู้คำตอบสำหรับคำถามเหล่านี้เป็นสิ่งที่ทำให้ผู้อื่นใช้ไลบรารีภายนอกในขณะที่การทำด้วยตัวเองด้วยการใช้งานItemDecorationนั้นค่อนข้างง่าย

เงื่อนไขเริ่มต้น

  1. ชุดข้อมูลของคุณควรเป็นlistรายการประเภทอื่น (ไม่ใช่ในความหมาย "Java types" แต่อยู่ในความหมายของประเภท "header / item")
  2. รายการของคุณควรจัดเรียงเรียบร้อยแล้ว
  3. ทุกรายการในรายการควรเป็นบางประเภท - ควรมีรายการส่วนหัวที่เกี่ยวข้อง
  4. รายการแรกในรายการlistต้องเป็นรายการส่วนหัว

นี่ผมให้เต็มรูปแบบสำหรับรหัสของฉันเรียกว่าRecyclerView.ItemDecoration HeaderItemDecorationจากนั้นฉันจะอธิบายขั้นตอนโดยละเอียด

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

ตรรกะทางธุรกิจ

แล้วฉันจะทำให้มันติดได้อย่างไร?

คุณไม่ทำ คุณไม่สามารถสร้างRecyclerViewรายการที่คุณเลือกได้เพียงแค่หยุดและยึดติดไว้ด้านบนเว้นแต่ว่าคุณจะเป็นกูรูด้านเลย์เอาต์ที่กำหนดเองและคุณรู้จักโค้ดมากกว่า 12,000 บรรทัดRecyclerViewด้วยใจจริง ดังนั้นเช่นเดียวกับการออกแบบ UI หากคุณทำอะไรไม่ได้ให้ปลอม คุณเพียงแค่วาดส่วนหัวด้านบนของทุกอย่างที่Canvasใช้ คุณควรทราบด้วยว่ารายการใดที่ผู้ใช้สามารถเห็นได้ในขณะนี้ มันเพิ่งเกิดขึ้นที่ItemDecorationสามารถให้ทั้งCanvasข้อมูลและข้อมูลเกี่ยวกับรายการที่มองเห็นได้ ด้วยขั้นตอนนี้ขั้นตอนพื้นฐานมีดังนี้

  1. ในonDrawOverวิธีการRecyclerView.ItemDecorationรับรายการแรก (บนสุด) ที่ผู้ใช้มองเห็นได้

        View topChild = parent.getChildAt(0);
  2. กำหนดส่วนหัวที่แสดงถึง

            int topChildPosition = parent.getChildAdapterPosition(topChild);
        View currentHeader = getHeaderViewForItem(topChildPosition, parent);
    
  3. วาดส่วนหัวที่เหมาะสมที่ด้านบนของ RecyclerView โดยใช้drawHeader()วิธีการ

ฉันยังต้องการใช้พฤติกรรมนี้เมื่อส่วนหัวที่จะเกิดขึ้นใหม่ตรงกับส่วนบนสุด: ดูเหมือนว่าส่วนหัวที่จะเกิดขึ้นจะดันส่วนหัวปัจจุบันด้านบนออกจากมุมมองและเข้ามาแทนที่ในที่สุด

เทคนิคเดียวกับ "การวาดภาพบนทุกสิ่ง" ที่นี่

  1. ตรวจสอบว่าเมื่อใดที่ส่วนหัว "ติดอยู่" ด้านบนตรงกับส่วนที่จะเกิดขึ้นใหม่

            View childInContact = getChildInContact(parent, contactPoint);
  2. รับจุดติดต่อนี้ (นั่นคือด้านล่างของส่วนหัวแบบติดหนึบที่คุณวาดและด้านบนของส่วนหัวที่กำลังจะมาถึง)

            int contactPoint = currentHeader.getBottom();
  3. หากรายการในรายการละเมิด "จุดติดต่อ" นี้ให้วาดส่วนหัวแบบปักหมุดของคุณใหม่เพื่อให้ด้านล่างอยู่ที่ด้านบนของรายการที่บุกรุก คุณบรรลุสิ่งนี้ด้วยtranslate()วิธีการCanvas. ด้วยเหตุนี้จุดเริ่มต้นของส่วนหัวด้านบนจะอยู่นอกพื้นที่ที่มองเห็นได้และดูเหมือนว่า "ถูกดันออกโดยส่วนหัวที่กำลังจะมาถึง" เมื่อหมดแล้วให้วาดส่วนหัวใหม่ที่ด้านบน

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

ส่วนที่เหลืออธิบายโดยความคิดเห็นและคำอธิบายประกอบอย่างละเอียดในส่วนของรหัสที่ฉันให้ไว้

การใช้งานตรงไปตรงมา:

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

คุณmAdapterต้องดำเนินการStickyHeaderInterfaceเพื่อให้ได้ผล การใช้งานขึ้นอยู่กับข้อมูลที่คุณมี

สุดท้ายที่นี่ฉันให้ gif ที่มีส่วนหัวครึ่งโปร่งใสเพื่อให้คุณสามารถเข้าใจแนวคิดและดูว่าเกิดอะไรขึ้นภายใต้ประทุน

นี่คือภาพประกอบของแนวคิด "just draw on top of everything" คุณจะเห็นว่ามีสองรายการ "ส่วนหัว 1" - รายการหนึ่งที่เราวาดและอยู่ด้านบนในตำแหน่งที่ติดขัดและอีกรายการที่มาจากชุดข้อมูลและย้ายไปพร้อมกับรายการที่เหลือทั้งหมด ผู้ใช้จะไม่เห็นการทำงานภายในของมันเนื่องจากคุณจะไม่มีส่วนหัวที่โปร่งใสครึ่งหนึ่ง

"เพียงแค่วาดเหนือทุกสิ่ง" แนวคิด

และนี่คือสิ่งที่เกิดขึ้นในขั้นตอน "การผลักดันออก":

เฟส "ผลักออก"

หวังว่ามันจะช่วยได้

แก้ไข

นี่คือการนำgetHeaderPositionForItem()เมธอดไปใช้จริงของฉันในอะแดปเตอร์ของ 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;
}

การใช้งานที่แตกต่างกันเล็กน้อยใน Kotlin


4
@Sevastyan ยอดเยี่ยม! ฉันชอบวิธีที่คุณแก้ปัญหานี้มาก ไม่มีอะไรจะพูดนอกจากคำถามเดียว: มีวิธีตั้งค่า OnClickListener บน "ส่วนหัวแบบติดหนึบ" หรืออย่างน้อยก็ต้องใช้คลิกเพื่อป้องกันไม่ให้ผู้ใช้คลิกผ่าน
Denis

17
จะดีมากถ้าคุณใส่ตัวอย่างอะแดปเตอร์ของการใช้งานนี้
SolidSnake

1
ในที่สุดฉันก็ใช้งานได้โดยมีการปรับแต่งเล็กน้อยที่นี่และที่นั่น แม้ว่าคุณจะเพิ่มช่องว่างภายในรายการของคุณมันจะยังคงกะพริบทุกครั้งที่คุณเลื่อนไปที่บริเวณเบาะ โซลูชันในเลย์เอาต์ของรายการของคุณจะสร้างเลย์เอาต์ระดับบนสุดด้วย 0 padding และเลย์เอาต์ลูกที่มีช่องว่างภายในที่คุณต้องการ
SolidSnake

8
ขอบคุณ วิธีแก้ปัญหาที่น่าสนใจ แต่แพงไปหน่อยในการขยายมุมมองส่วนหัวในทุกเหตุการณ์ที่เลื่อน ฉันเพิ่งเปลี่ยนตรรกะและใช้ ViewHolder และเก็บไว้ใน HashMap ของ WeakReferences เพื่อใช้ซ้ำมุมมองที่สูงเกินจริงแล้ว
Michael

4
@Sevastyan งานเยี่ยม. ฉันมีข้อเสนอแนะ เพื่อหลีกเลี่ยงการสร้างส่วนหัวใหม่ทุกครั้ง เพียงบันทึกส่วนหัวและเปลี่ยนเฉพาะเมื่อมีการเปลี่ยนแปลง 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

วิธีที่ง่ายที่สุดคือสร้าง Item Decoration สำหรับ RecyclerView ของคุณ

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 สำหรับส่วนหัวของคุณใน 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"
/>

และสุดท้ายเพื่อเพิ่ม Item Decoration ใน RecyclerView ของคุณ:

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

ด้วยการตกแต่งไอเท็มนี้คุณสามารถสร้างส่วนหัวที่ตรึง / เหนียวหรือไม่ก็ได้ด้วยบูลีนเมื่อสร้างการตกแต่งไอเท็ม

คุณสามารถดูตัวอย่างการทำงานที่สมบูรณ์ได้ที่ github: https://github.com/paetztm/recycler_view_headers


ขอบคุณ. สิ่งนี้ใช้ได้ผลสำหรับฉันอย่างไรก็ตามส่วนหัวนี้ซ้อนทับมุมมองรีไซเคิล คุณสามารถช่วย?
kashyap jimuliya

ฉันไม่แน่ใจว่าคุณหมายถึงอะไรเมื่อซ้อนทับ RecyclerView สำหรับบูลีน "เหนียว" หากคุณตั้งค่าเป็นเท็จจะทำให้การตกแต่งรายการอยู่ระหว่างแถวและจะไม่อยู่ที่ด้านบนสุดของ RecyclerView
tim.paetz

การตั้งค่าเป็น "sticky" เป็น false จะทำให้ส่วนหัวอยู่ระหว่างแถว แต่ไม่ติดอยู่ (ซึ่งฉันไม่ต้องการ) ที่ด้านบน ในขณะที่ตั้งค่าเป็นจริงมันยังคงติดอยู่ด้านบน แต่มันทับซ้อนกันแถวแรกในมุมมองรีไซเคิล
kashyap jimuliya

ฉันเห็นว่าอาจเป็นปัญหาสองประการหนึ่งคือการเรียกกลับส่วนคุณไม่ได้ตั้งค่ารายการแรก (0 ตำแหน่ง) สำหรับ isSection เป็นจริง อีกประการหนึ่งคือคุณผ่านความสูงที่ไม่ถูกต้อง ความสูงของ xml สำหรับมุมมองข้อความต้องสูงเท่ากันกับความสูงที่คุณส่งเข้าไปในตัวสร้างของการตกแต่งรายการส่วน
tim.paetz

3
สิ่งหนึ่งที่ฉันจะเพิ่มก็คือถ้าเค้าโครงส่วนหัวของคุณมีมุมมองข้อความหัวเรื่องขนาดแบบไดนามิก (เช่นwrap_content) คุณต้องการเรียกใช้fixLayoutSizeหลังจากตั้งค่าข้อความหัวเรื่องเช่นกัน
copolii

6

ฉันได้สร้างโซลูชันของ Sevastyan ขึ้นมาเอง

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

... และนี่คือการใช้งาน StickyHeaderInterface (ฉันทำโดยตรงในอะแดปเตอร์รีไซเคิล):

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

ดังนั้นในกรณีนี้ส่วนหัวไม่ได้เป็นเพียงแค่การวาดบนผืนผ้าใบ แต่ดูด้วยตัวเลือกหรือระลอกตัวเลือกคลิกและอื่น ๆ


ขอบคุณสำหรับการแบ่งปัน! ทำไมคุณถึงจบ RecyclerView ใน RelativeLayout ใหม่
tmm1

เนื่องจากรุ่นของส่วนหัวแบบเหนียวของฉันคือ View ซึ่งฉันใส่ไว้ใน RelativeLayout นี้ด้านบน RecyclerView (ในฟิลด์ headerContainer)
Andrey Turkovsky

คุณสามารถแสดงการใช้งานของคุณในไฟล์คลาสได้หรือไม่? วิธีที่คุณส่งผ่านวัตถุของผู้ฟังซึ่งนำไปใช้ในอะแด็ปเตอร์
Dipali Shah

recyclerView.addItemDecoration(HeaderItemDecoration(recyclerView, adapter)). ขออภัยไม่พบตัวอย่างการใช้งานที่ฉันใช้ ฉันได้แก้ไขคำตอบ - เพิ่มข้อความในความคิดเห็น
Andrey Turkovsky

6

ให้กับทุกคนที่กำลังมองหาวิธีการแก้ริบหรี่ / DividerItemDecorationกระพริบปัญหาเมื่อคุณมีอยู่แล้ว ฉันดูเหมือนจะแก้ไขได้ดังนี้:

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

ดูเหมือนจะใช้งานได้ แต่มีใครยืนยันได้หรือไม่ว่าฉันไม่ได้ทำผิดอะไร


ขอบคุณมันช่วยแก้ปัญหาการกะพริบให้ฉันด้วย
Yamashiro Rion

3

คุณสามารถตรวจสอบและนำคลาสไปใช้StickyHeaderHelperในโปรเจ็กต์FlexibleAdapterของฉันและปรับให้เข้ากับกรณีการใช้งานของคุณ

แต่ฉันขอแนะนำให้ใช้ไลบรารีเนื่องจากจะช่วยลดความซับซ้อนและจัดระเบียบวิธีที่คุณใช้งาน Adapters for RecyclerView ตามปกติ: อย่าสร้างวงล้อใหม่

ฉันจะบอกด้วยว่าอย่าใช้ Decorators หรือไลบรารีที่เลิกใช้แล้วรวมทั้งอย่าใช้ไลบรารีที่ทำเพียง 1 หรือ 3 อย่างคุณจะต้องผสานการใช้งานไลบรารีอื่นด้วยตัวเอง


ฉันใช้เวลา 2 วันในการอ่านวิกิและตัวอย่าง แต่ยังไม่รู้วิธีสร้างรายการที่ยุบได้โดยใช้ lib ของคุณ ตัวอย่างค่อนข้างซับซ้อนสำหรับมือใหม่
เหงียนมินห์บินห์

1
ทำไมคุณถึงต่อต้านการใช้Decorators?
Sevastyan Savanyuk

1
@Sevastyan เพราะเราจะมาถึงจุดที่เราต้องการผู้ฟังคลิกและมุมมองของเด็กด้วย เรามัณฑนากรคุณไม่สามารถทำได้ตามคำจำกัดความ
Davideas

@Davidea คุณหมายความว่าคุณต้องการตั้งค่าผู้ฟังคลิกในส่วนหัวในอนาคตหรือไม่? ถ้าเป็นเช่นนั้นก็สมเหตุสมผล แต่ถึงกระนั้นหากคุณระบุส่วนหัวของคุณเป็นรายการชุดข้อมูลก็จะไม่มีปัญหา แม้แต่ Yigit Boyar ก็แนะนำให้ใช้ Decorators
Sevastyan Savanyuk

@Sevastyan ใช่ในไลบรารีของฉันส่วนหัวเป็นรายการเช่นเดียวกับรายการอื่น ๆ ในรายการดังนั้นผู้ใช้จึงสามารถจัดการได้ ในอนาคตอันไกลผู้จัดการโครงร่างที่กำหนดเองจะเข้ามาแทนที่ผู้ช่วยปัจจุบัน
Davideas

3

อีกวิธีหนึ่งขึ้นอยู่กับฟังก์ชั่นการเลื่อน เงื่อนไขเริ่มต้นเหมือนกับในคำตอบของ 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);
}

เค้าโครงสำหรับ ViewHolder และส่วนหัวแบบเหนียว

item_header.xml

<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/tv_title"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"/>

เค้าโครงสำหรับ RecyclerView

<FrameLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

    <!--it can be any view, but order important, draw over recyclerView-->
    <include
        layout="@layout/item_header"/>

</FrameLayout>

คลาสสำหรับ HeaderItem

public class HeaderItem implements Item {

    private String title;

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

    public String getTitle() {
        return title;
    }

}

มันใช้ได้ทั้งหมด การใช้งานอะแดปเตอร์ ViewHolder และสิ่งอื่น ๆ ไม่น่าสนใจสำหรับเรา

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

อินเทอร์เฟซสำหรับการผูกมุมมองส่วนหัว

public interface StickyHeaderViewWrapper<T> {

    void bindView(T data);
}

ฉันชอบวิธีนี้ พิมพ์ผิดเล็กน้อยใน findNearestHeader: for (int i = position; position >= 0; i--){ //should be i >= 0
Konstantin

3

โย่

นี่คือวิธีที่คุณทำหากคุณต้องการไม้ยึดเพียงชนิดเดียวเมื่อมันเริ่มออกจากหน้าจอ (เราไม่สนใจส่วนใด ๆ ) มีวิธีเดียวเท่านั้นที่จะไม่ทำลายตรรกะ RecyclerView ภายในของรายการรีไซเคิลและนั่นคือการขยายมุมมองเพิ่มเติมที่ด้านบนของรายการส่วนหัวของ RecyclerView และส่งข้อมูลเข้าไป ฉันจะปล่อยให้รหัสพูด

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

}

จากนั้นคุณก็ทำสิ่งนี้ในอะแดปเตอร์ของคุณ:

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

โดยที่YOUR_STICKY_VIEW_HOLDER_TYPEคือ viewType ของสิ่งที่คุณควรจะเป็น Sticky Holder


2

สำหรับผู้ที่อาจกังวล จากคำตอบของ Sevastyan คุณควรจะเลื่อนในแนวนอน เพียงแค่เปลี่ยนทั้งหมดgetBottom()เป็นgetRight()และgetTop()เป็นgetLeft()


-1

คำตอบอยู่ที่นี่แล้ว หากคุณไม่ต้องการใช้ไลบรารีใด ๆ คุณสามารถทำตามขั้นตอนเหล่านี้:

  1. จัดเรียงรายการด้วยข้อมูลตามชื่อ
  2. วนซ้ำผ่านรายการด้วยข้อมูลและในตำแหน่งเมื่ออักษรตัวแรกของรายการปัจจุบัน! = อักษรตัวแรกของรายการถัดไปให้แทรกวัตถุชนิด "พิเศษ"
  3. ภายในอะแดปเตอร์ของคุณมีมุมมองพิเศษเมื่อรายการเป็น "พิเศษ"

คำอธิบาย:

ในonCreateViewHolderวิธีการเราสามารถตรวจสอบviewTypeและขึ้นอยู่กับค่า (ชนิด "พิเศษ" ของเรา) ขยายเลย์เอาต์พิเศษ

ตัวอย่างเช่น:

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

ที่ไหนclass ItemElementและclass TitleElementดูเหมือนธรรมดาViewHolder:

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

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

}

ดังนั้นแนวคิดทั้งหมดนี้น่าสนใจ แต่ฉันสนใจถ้ามันมีประสิทธิภาพเพราะเราต้องเรียงลำดับรายการข้อมูล และฉันคิดว่านี่จะทำให้ความเร็วลดลง หากมีความคิดเห็นเกี่ยวกับเรื่องนี้โปรดเขียนถึงฉัน :)

และคำถามที่เปิดอยู่: วิธีการจัดเค้าโครง "พิเศษ" ที่ด้านบนในขณะที่รายการกำลังรีไซเคิล อาจจะรวมทั้งหมดเข้าด้วยCoordinatorLayoutกัน


เป็นไปได้ไหมที่จะทำด้วย cursoradapter
M.Yogeshwaran

10
วิธีแก้ปัญหานี้ไม่ได้พูดอะไรเกี่ยวกับส่วนหัวของ STICKY ซึ่งเป็นประเด็นหลักของโพสต์นี้
Siavash
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.