Как я могу сделать липкие заголовки в RecyclerView? (Без внешней библиотеки)

121

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

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

В моем случае я не хочу делать это по алфавиту. У меня есть два разных типа представлений (заголовок и нормальный). Хочу закрепить только вверху, последний заголовок.

Жауме Колом
источник
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элемент по своему выбору, просто остановившись и придерживаясь его, если только вы не являетесь гуру пользовательских макетов и не знаете более 12 000 строк кода 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чтобы он работал. Реализация зависит от имеющихся у вас данных.

Наконец, здесь я предоставляю гифку с полупрозрачными заголовками, чтобы вы могли понять идею и действительно увидеть, что происходит под капотом.

Вот иллюстрация концепции «рисовать поверх всего». Вы можете видеть, что есть два элемента «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;
}

Немного другая реализация в Котлине

Севастян Саванюк
источник
4
@ Севастян Просто молодец! Мне очень понравилось, как вы решили эту задачу. Нечего сказать, кроме, может быть, одного вопроса: есть ли способ установить OnClickListener на «липкий заголовок» или, по крайней мере, использовать щелчок, не позволяющий пользователю щелкнуть по нему?
Денис
17
Было бы здорово, если бы вы поместили пример адаптера этой реализации
SolidSnake
1
Я наконец-то заставил его работать с несколькими настройками здесь и там. хотя, если вы добавите какие-либо отступы к своим элементам, они будут продолжать мигать всякий раз, когда вы прокручиваете область с добавлением. решение в макете вашего элемента создает родительский макет с заполнением 0 и дочерний макет с любым заполнением, которое вы хотите.
SolidSnake
8
Спасибо. Интересное решение, но немного дорого, чтобы раздувать представление заголовка при каждом событии прокрутки. Я просто изменил логику и использую ViewHolder и сохраняю их в HashMap WeakReferences, чтобы повторно использовать уже завышенные представления.
Майкл
4
@ Севастян, отличная работа. У меня есть предложение. Чтобы не создавать каждый раз новые заголовки. Просто сохраните заголовок и меняйте его только при изменении. 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 для вашего заголовка в 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"
/>

И, наконец, чтобы добавить украшение элемента в ваш 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

tim.paetz
источник
Спасибо. это сработало для меня, однако этот заголовок перекрывает recyclerview. вы можете помочь?
кашьяп джимулия
Я не уверен, что вы имеете в виду под перекрытием RecyclerView. Для «липкого» логического значения, если вы установите для него значение false, он поместит украшение элемента между строками и не останется в верхней части RecyclerView.
tim.paetz
установка «липкого» значения в false помещает заголовок между строками, но он не остается застрявшим (чего я не хочу) наверху. при установке значения true, он остается наверху, но перекрывает первую строку в recyclerview
kashyap jimuliya
Я вижу, что потенциально две проблемы, одна из которых - обратный вызов раздела, вы не устанавливаете для первого элемента (позиция 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(...)
}

похоже, это работает, но может ли кто-нибудь подтвердить, что я больше ничего не сломал?

or_dvir
источник
Спасибо, проблема с миганием решена и для меня.
Ямасиро Рион,
3

Вы можете проверить и взять реализацию класса StickyHeaderHelperв моем проекте FlexibleAdapter и адаптировать его к вашему варианту использования.

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

Я бы также сказал, не используйте декораторы или устаревшие библиотеки, а также не используйте библиотеки, которые делают только 1 или 3 вещи, вам придется самостоятельно объединить реализации других библиотек.

Davideas
источник
Я потратил 2 дня на чтение вики и образца, но до сих пор не знаю, как создать сворачиваемый список с помощью вашей библиотеки. Пример довольно сложный для новичка
Нгуен Минь Бинь
1
Почему вы против использования Decorators?
Севастян Саванюк
1
@Sevastyan, потому что мы придем к тому моменту, когда нам понадобится прослушиватель кликов по нему, а также по дочерним представлениям. Мы Decorator просто не сможем по определению.
Davideas
@Davidea, вы имеете в виду, что хотите в будущем устанавливать прослушиватели кликов для заголовков? Если так, то в этом есть смысл. Но все же, если вы предоставите свои заголовки как элементы набора данных, проблем не будет. Даже Йигит Бояр рекомендует использовать декораторы.
Севастян Саванюк
@Sevastyan, да, в моей библиотеке заголовок - это такой же элемент, как и другие в списке, поэтому пользователи могут манипулировать им. В далеком будущем пользовательский менеджер компоновки заменит текущий помощник.
Davideas
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);
}
Anrimian
источник
Мне нравится это решение. Небольшая опечатка в 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 того, что должно быть липким держателем.

Станислав Кинзль
источник
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
это решение ничего не говорит о липких заголовках, что является основной
темой