RecyclerView에서 고정 헤더를 어떻게 만들 수 있습니까? (외부 라이브러리없이)


120

아래 이미지와 같이 외부 라이브러리를 사용하지 않고 화면 상단의 헤더보기를 수정하고 싶습니다.

여기에 이미지 설명 입력

제 경우에는 알파벳순으로하고 싶지 않습니다. 두 가지 유형의 뷰 (헤더 및 일반)가 있습니다. 마지막 헤더 인 맨 위에 만 수정하고 싶습니다.


17
질문은 RecyclerView에 관한 것이 었습니다.이 ^ lib는 ListView를 기반으로합니다
Max Ch

답변:


319

여기에서는 외부 라이브러리없이 수행하는 방법을 설명합니다. 매우 긴 게시물이 될 것입니다.

우선, s를 사용하여 고정 헤더를 구현하는 여정을 시작하게 된 게시물의 영감을받은 @ tim.paetz 를 인정하겠습니다 ItemDecoration. 내 구현에서 그의 코드의 일부를 빌 렸습니다.

이미 경험 하셨겠지만, 직접 시도해 본다면 실제로 어떻게해야하는지 에 대한 좋은 설명을 찾기가 매우 어렵 습니다.ItemDecoration 기술 . 내 말 은, 단계는 무엇입니까? 그 뒤에있는 논리는 무엇입니까? 헤더를 목록 상단에 고정하려면 어떻게해야합니까? 이러한 질문에 대한 답을 모르기 때문에 다른 사람들이 외부 라이브러리를 사용하고ItemDecoration 것은 매우 쉽습니다.

초기 조건

  1. 데이터 세트는 list 다른 유형의 항목 ( "Java 유형"의미가 아니라 "헤더 / 항목"유형 의미).
  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. drawHeader()메서드 를 사용하여 RecyclerView 위에 적절한 헤더를 그 립니다.

또한 새로운 다가오는 헤더가 맨 위 헤더를 만날 때 동작을 구현하고 싶습니다. 다가오는 헤더가 뷰에서 현재 맨 위 헤더를 부드럽게 밀어 내고 결국 그의 자리를 차지하는 것처럼 보일 것입니다.

"모든 것 위에 그리기"라는 동일한 기술이 여기에 적용됩니다.

  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를 제공하므로 아이디어를 파악하고 실제로 어떤 일이 일어나는지 확인할 수 있습니다.

다음은 "모든 것 위에 그냥 그려라"개념의 삽화입니다. 두 개의 항목 "header 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를 설정하는 방법이 있습니까? 아니면 적어도 사용자가 클릭하지 못하도록 클릭을 소비하는 방법이 있습니까?
데니스

17
이 구현의 어댑터 예제를 넣으면 좋을 것입니다
SolidSnake

1
나는 마침내 여기저기서 약간의 조정으로 작업하도록 만들었습니다. 항목에 패딩을 추가하면 패딩 된 영역으로 스크롤 할 때마다 계속 깜박입니다. 항목 레이아웃의 솔루션은 패딩이 0 인 상위 레이아웃과 원하는 패딩이있는 하위 레이아웃을 만듭니다.
SolidSnake

8
감사. 흥미로운 솔루션이지만 모든 스크롤 이벤트에서 헤더 뷰를 확장하는 데 약간의 비용이 듭니다. 방금 논리를 변경하고 ViewHolder를 사용하고 이미 부풀린 뷰를 재사용하기 위해 WeakReferences의 HashMap에 보관했습니다.
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

가장 쉬운 방법은 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);
}

}

recycler_section_header.xml의 헤더에 대한 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"
/>

마지막으로 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


감사합니다. 이것은 나를 위해 일했지만이 헤더는 recyclerview와 겹칩니다. 도울 수 있니?
kashyap jimuliya

RecyclerView와 겹치는 것이 무엇을 의미하는지 잘 모르겠습니다. "sticky"부울의 경우 false로 설정하면 항목 장식이 행 사이에 배치되고 RecyclerView의 맨 위에 유지되지 않습니다.
tim.paetz

"sticky"를 false로 설정하면 행 사이에 헤더가 배치되지만 맨 위에 고정되어 있지는 않습니다. true로 설정하는 동안 상단에 고정되어 있지만 recyclerview의 첫 번째 행과 겹칩니다
kashyap jimuliya

잠재적으로 두 가지 문제가 있음을 알 수 있습니다. 하나는 섹션 콜백이며 isSection의 첫 번째 항목 (0 위치)을 true로 설정하지 않습니다. 다른 하나는 잘못된 높이로 지나가고 있다는 것입니다. 텍스트 뷰에 대한 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
    ... */
}

따라서이 경우 헤더는 캔버스에 그리는 것만이 아니라 선택기 또는 잔물결, 클릭 리스너 등으로보기입니다.


공유해 주셔서 감사합니다! 새로운 RelativeLayout에서 RecyclerView를 래핑 한 이유는 무엇입니까?
tmm1

고정 헤더의 버전이 View이기 때문에 RecyclerView
위에이

클래스 파일에서 구현을 보여줄 수 있습니까? 어댑터에 구현 된 리스너의 객체를 어떻게 전달했는지.
Dipali Shah

recyclerView.addItemDecoration(HeaderItemDecoration(recyclerView, adapter)). 죄송합니다. 제가 사용한 구현 예를 찾을 수 없습니다. 나는 대답을 편집 한 - 추가 의견에 일부 텍스트를
안드레이 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

StickyHeaderHelperFlexibleAdapter 에서 클래스 구현을 확인하고 취할 수 있습니다. 프로젝트 사용 사례에 맞게 조정할 수 있습니다.

하지만 일반적으로 RecyclerView 용 어댑터를 구현하는 방식을 단순화하고 재구성하므로 라이브러리를 사용하는 것이 좋습니다. 바퀴를 재발 명하지 마십시오.

또한 데코레이터 나 더 이상 사용되지 않는 라이브러리를 사용하지 말고 1 ~ 3 가지 작업 만 수행하는 라이브러리를 사용하지 마세요. 다른 라이브러리의 구현을 직접 병합해야합니다.


위키와 샘플을 읽는 데 이틀을 보냈지 만 여전히 lib를 사용하여 축소 가능한 목록을 만드는 방법을 모릅니다. 샘플은 초보자를위한 매우 복잡하다
응우 엔 민 빈

1
Decorators 를 사용하지 않습니까?
Sevastyan Savanyuk

1
@Sevastyan, 클릭 리스너가 필요한 시점에 도달 할 것이기 때문에 자식 뷰에서도 마찬가지입니다. 우리 데코레이터는 정의에 따라 할 수 없습니다.
Davideas

@Davidea, 앞으로 헤더에 클릭 리스너를 설정 하시겠습니까? 그렇다면 의미가 있습니다. 그러나 여전히 헤더를 데이터 세트 항목으로 제공하면 문제가 없습니다. Yigit Boyar조차도 데코레이터 사용을 권장합니다.
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
콘스탄틴

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입니다.


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 ItemElementclass 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.