Як можна зробити липкі заголовки в RecyclerView? (Без зовнішньої губки)


120

Я хочу виправити свої заголовки у верхній частині екрана, як на зображенні внизу та без використання зовнішніх бібліотек.

введіть тут опис зображення

У моєму випадку я не хочу робити це за алфавітом. У мене є два різних види перегляду (Заголовок і нормальний). Я хочу лише виправити до верхнього, останнього заголовка.


17
питання стосувалося RecyclerView, цей ^ lib заснований на ListView
Max Ch

Відповіді:


319

Тут я поясню, як це зробити без зовнішньої бібліотеки. Це буде дуже довгий пост, тому підбадьорюйте себе.

Перш за все, дозвольте мені визнати @ tim.paetz, чий пост надихнув мене вирушити на шлях реалізації моїх власних липких заголовків за допомогою ItemDecorations. Я запозичив деякі частини його коду в моїй реалізації.

Як ви, можливо, вже відчували, якщо ви намагалися зробити це самостійно, дуже важко знайти хороше пояснення, ЯК насправді зробити це з 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предмет, який ви обрали, просто зупиніться і дотримуйтесь зверху, якщо ви не гуру користувальницьких макетів і не знаєте напам'ять 12000+ рядків коду RecyclerView. Отже, як це завжди стосується дизайну інтерфейсу, якщо ви щось не можете зробити, підробити це. Ви просто намалюєте заголовок зверху на все, що використовується 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 з напівпрозорими заголовками, щоб ви могли зрозуміти ідею і насправді побачити, що відбувається під капотом.

Ось ілюстрація концепції "просто намалюй зверху все". Ви можете бачити, що є два пункти "заголовок 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;
}

Трохи інша реалізація в Котліні


4
@Sevastyan Просто геніально! Мені дуже сподобалося, як ти вирішив цю задачу. Нічого не сказати, крім, можливо, одного питання: Чи є спосіб встановити OnClickListener на "липкому заголовку" або хоча б споживати клацання, що не дозволяє користувачеві натискати на нього?
Денис

17
Було б чудово, якщо ви прикладете адаптерний приклад цієї реалізації
SolidSnake

1
Нарешті я змусив її працювати з декількома твіками тут і там. хоча якщо ви додасте будь-які оббивки до ваших предметів, вона буде постійно мерехтіти, коли ви переходите до вкладеної області. рішення в макеті вашого елемента створіть батьківський макет із 0 прокладками та дочірній макет із будь-яким накладом.
SolidSnake

8
Дякую. Цікаве рішення, але трохи дорого завищене зображення заголовка на кожній події прокрутки. Я просто змінив логіку і використовую ViewHolder і зберігаю їх у HashMap WeakReferences, щоб повторно використовувати вже завищені види.
Майкл

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; }
Віра Рівотті

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

}

XML для вашого заголовка в reciler_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"
/>

І нарешті, щоб додати елемент прикраси до 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


Дякую. це працювало для мене, однак цей заголовок перекриває перегляд reciklier. Ви можете допомогти?
кашяп джимулія

Я не впевнений, що ви маєте на увазі під перекриттям RecyclerView. Для "липкого" булевого, якщо ви встановите це значення false, він помістить елемент прикраси між рядками і не залишиться у верхній частині RecyclerView.
tim.paetz

встановивши його на "липкий" на false, розміщує заголовок між рядками, але це не залишається застряглим (чого я не хочу) до верху. при встановленні його як істинного, він залишається застрягнутим у верхній частині, але він перекриває перший рядок у перегляді рециркулятора
Каш'яп Джимулія

Я можу бачити, що, як потенційно дві проблеми, одна - це зворотний виклик розділу, ви не встановлюєте перший елемент (0 позиція) для isSection в true. Інше - ви переходите в неправильній висоті. Висота xml для перегляду тексту повинна бути такою ж висотою, як і висота, яку ви передаєте конструктору декору елемента розділу.
tim.paetz

3
Я хотів би додати, що якщо макет заголовка має перегляд тексту заголовка в динамічному розмірі (наприклад, wrap_content), ви хочете запустити і fixLayoutSizeпісля встановлення тексту заголовка.
copolii

6

Я змінив рішення Севастяна вище

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)
Андрій Турковський,

Чи можете ви показати свою реалізацію у файлі класу? Як ви передали об'єкт слухача, який реалізований в адаптері.
Діпалі Шах

recyclerView.addItemDecoration(HeaderItemDecoration(recyclerView, adapter)). На жаль, не вдається знайти приклад реалізації, який я використав. Я відредагував відповідь - додав текст до коментарів
Андрій Турковський

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в моєму проекті гибкого адаптера та адаптувати його до вашого використання.

Але я пропоную використовувати бібліотеку, оскільки вона спрощує та реорганізує спосіб, як ви зазвичай реалізуєте Адаптери для RecyclerView: Не винаходити колесо.

Я б також сказав, що не використовуйте декоратори чи застарілі бібліотеки, а також не використовуйте бібліотеки, які виконують лише 1 або 3 речі, вам доведеться самостійно об'єднувати реалізації інших бібліотек.


Я провів 2 дні, щоб прочитати вікі та зразок, але досі не знаю, як створити збірний список за допомогою вашої lib. Зразок для новачків досить складний
Нгуен Мінь Бінь

1
Чому ви проти використання Decorators?
Севастьян Саванюк

1
@Sevastyan, оскільки ми дійдемо до того місця, коли нам потрібен слухач клацань на ньому та на дитячих переглядах. Ми прикрасити вас просто не можемо за визначенням.
Давідеас

@Davidea, ти маєш на увазі, що хочеш у майбутньому встановити слухачів кліків на заголовках? Якщо так, то це має сенс. Але все ж, якщо ви подаватимете заголовки як елементи набору даних, проблем не виникне. Навіть Ігіт Бояр рекомендує використовувати декоратори.
Севастьян Саванюк

@Sevastyan, так, у моїй бібліотеці заголовок є елементом, як інші у списку, тому користувачі можуть ним маніпулювати. У далекому майбутньому спеціальний менеджер планування замінить поточного помічника.
Давідеас

3

Ще одне рішення, засноване на прослуховуванні прокрутки. Початкові умови такі ж, як у відповіді Севастяна

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

Мені подобається це рішення. Невеликий друк у пошукуNearestHeader: for (int i = position; position >= 0; i--){ //should be i >= 0
Костянтин

3

Йо,

Це так, як ви це робите, якщо ви хочете, щоб лише один тип тримача тримався, коли він починає виходити з екрана (ми не піклуємося про жодні розділи). Є лише один спосіб, не порушуючи внутрішню логіку RecyclerView переробки елементів, і це надути додатковий вигляд на верхній частині заголовка recilerlerView та передати в нього дані. Я дозволю коду говорити.

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 , тип вашого, що повинен бути клейким тримачем.


2

Для тих, хто може турбувати. Виходячи з відповіді Севастяна, чи хочете ви зробити горизонтальну прокрутку. Просто змініть все 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.


чи можна зробити це за допомогою курсорадаптера
Йогешваран

10
це рішення нічого не говорить про заголовки STICKY, що є головним моментом цього поста
Siavash
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.