Как я могу сделать липкие заголовки в RecyclerView? (Без внешней библиотеки)
Я хочу исправить мои заголовки в верхней части экрана, как на изображении ниже, и без использования внешних библиотек.
![введите описание изображения здесь]()
В моем случае я не хочу делать это в алфавитном порядке. У меня есть два разных типа просмотров (Header и normal). Я хочу только зафиксировать верхний, последний заголовок.
Ответы
Ответ 1
Здесь я объясню, как это сделать без внешней библиотеки. Это будет очень длинный пост, так что готовьтесь.
Прежде всего, позвольте мне поблагодарить @tim.paetz, чей пост вдохновил меня отправиться в путешествие по реализации моих собственных липких заголовков с использованием ItemDecoration
. Я заимствовал некоторые части его кода в моей реализации.
Как вы, возможно, уже испытали, если вы попытались сделать это самостоятельно, очень трудно найти хорошее объяснение того, как КАК сделать это с помощью техники ItemDecoration
. Я имею в виду, какие шаги? Какая логика стоит за этим? Как сделать заголовок в верхней части списка? Не зная ответов на эти вопросы, это то, что заставляет других использовать внешние библиотеки, а делать это самостоятельно с помощью ItemDecoration
довольно легко.
Начальные условия
- Ваш набор данных должен быть
list
элементов другого типа (не в смысле "типы Java", а в смысле "заголовок/элемент").
- Ваш список должен быть уже отсортирован.
- Каждый элемент в списке должен быть определенного типа - с ним должен быть связан элемент заголовка.
- Самый первый элемент в
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 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 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 position represents a header.
*/
boolean isHeader(int itemPosition);
}
}
Бизнес-логика
Итак, как мне заставить его придерживаться?
Вы не Вы не можете сделать элемент RecyclerView
по вашему выбору, просто остановитесь и держитесь сверху, если вы не гуру пользовательских макетов и не знаете 12, 000+ строк кода для RecyclerView
наизусть. Так что, как всегда с дизайном пользовательского интерфейса, если вы не можете что-то сделать, подделайте это. Вы просто рисуете заголовок поверх всего, используя Canvas
. Вы также должны знать, какие предметы пользователь может видеть в данный момент. Просто так получается, что ItemDecoration
может предоставить вам как Canvas
, так и информацию о видимых предметах. С этим вот основные шаги:
В методе onDrawOver
из RecyclerView.ItemDecoration
получают самый первый (верхний) элемент, который виден пользователю.
View topChild = parent.getChildAt(0);
Определите, какой заголовок представляет его.
int topChildPosition = parent.getChildAdapterPosition(topChild);
View currentHeader = getHeaderViewForItem(topChildPosition, parent);
Нарисуйте соответствующий заголовок поверх RecyclerView, используя метод drawHeader()
.
Я также хочу реализовать поведение, когда новый предстоящий заголовок встречается с верхним: он должен выглядеть так, как будто предстоящий заголовок осторожно выталкивает верхний текущий заголовок из представления и в конце концов занимает его место.
Та же техника "рисования поверх всего" применима и здесь.
Определите, когда верхний "застрявший" заголовок встречает новый предстоящий.
View childInContact = getChildInContact(parent, contactPoint);
Получите эту точку контакта (это нижняя часть липкого заголовка вашего рисунка и верхняя часть следующего заголовка).
int contactPoint = currentHeader.getBottom();
Если элемент в списке пересекает эту "точку контакта", перерисуйте свой липкий заголовок так, чтобы его нижняя часть находилась в верхней части пересекающего элемента. Это достигается с помощью метода 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" - один, который мы рисуем и остаемся сверху в застрявшем положении, а другой - из набора данных и перемещается со всеми остальными элементами. Пользователь не увидит его внутреннюю работу, потому что у вас не будет полупрозрачных заголовков.
!["just draw on top of everything" concept]()
И вот что происходит в фазе "выталкивания":
!["pushing out" phase]()
Надеюсь, это помогло.
Изменить
Вот моя фактическая реализация метода 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;
}
Немного другая реализация в Котлине
Ответ 2
Самый простой способ - просто создать украшения для вашего 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
Ответ 3
В то время как вам не нужно использовать внешнюю библиотеку, вы всегда можете взглянуть на существующие решения с открытым исходным кодом и использовать классы, которые имеют для вас значение, для создания пользовательского решения, которое вам необходимо:
Header-Decor
https://github.com/edubarr/header-decor
Super-Slim
https://github.com/TonicArtos/SuperSLiM
[Устаревший]
Recyclerview-stickyheaders
https://github.com/eowise/recyclerview-stickyheaders
Ответ 4
Я сделал свой собственный вариант решения Севастьяна выше
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 (я сделал это непосредственно в адаптере recycler):
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
... */
}
Итак, в этом случае заголовок не просто рисовать на холсте, а просматривать с помощью селектора или ряби, кликлайнера и т.д.
Ответ 5
Вы можете проверить и выполнить реализацию класса StickyHeaderHelper
в моем проекте FlexibleAdapter и адаптировать его к вашему прецеденту.
Но я предлагаю использовать библиотеку, поскольку она упрощает и реорганизует способ, которым вы обычно реализуете адаптеры для RecyclerView: не изобретайте велосипед.
Я бы также сказал: не используйте Decorators или устаревшие библиотеки, а также не используйте библиотеки, которые занимаются только 1 или 3 вещами, вам придется объединить реализации других библиотек самостоятельно.
Ответ 6
Другое решение, основанное на прослушивателе прокрутки. Начальные условия такие же, как в ответе Севастьяна
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>
Класс для заголовка.
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);
}
Ответ 7
любому, кто ищет решение проблемы мерцания/мигания, когда у вас уже есть 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(...)
}
Кажется, это работает, но кто-нибудь может подтвердить, что я больше ничего не сломал?
Ответ 8
Yo,
Это то, как вы делаете это, если вам нужен только один тип держателя, когда он начинает выходить из экрана (мы не заботимся о каких-либо разделах). Есть только один способ, не нарушая внутреннюю логику 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 вашего предполагаемого держателя.
Ответ 9
Для тех, кто может относиться. На основании ответа Севастьяна, если вы хотите сделать его горизонтальным прокруткой. Просто измените все getBottom()
на getRight()
и getTop()
на getLeft()
Ответ 10
У меня была такая же проблема, как у вас, но в более сложном случае.
Я хотел добавить несколько типов заголовков, которые остаются липкими в зависимости от его вида и разных разделителей с разной высотой.
Я опубликовал в github библиотеку андроида, которая может это сделать.
Проект Github: https://github.com/sokarcreative/BasicStuffRecyclerview
В вашем приложении build.gradle:
implementation 'com.github.sokarcreative:BasicStuffRecyclerview:0.1.0'
В вашем проекте build.gradle:
allprojects {
repositories {
maven { url "https://jitpack.io" }
}
}
![Android-пример]()
Ответ 11
Ответ уже был здесь. Если вы не хотите использовать какую-либо библиотеку, вы можете выполнить следующие действия:
- Список сортировки с данными по имени
- Итерации через список с данными и на месте, когда текущая первая буква позиции!= первая буква следующего элемента, вставьте "специальный" вид объекта.
- Внутри адаптера укажите специальный вид, когда элемент "особый".
Пояснение:
В 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
.
Ответ 12
| * | После одного полного дня борьбы, который я разработал ниже Семинар-приемник или класс менеджера
который прост в использовании, просто скопируйте вставку и укажите свой список и значения.
Это поможет всем, кого я не хочу бороться, как я.
| * | Укажите имена заголовков в SsnHdrAryVar:
String SsnHdrAryVar[] = {"NamHdr1", "NamHdr2", "NamHdr3"};
| * | Укажите количество подэлементов, отображаемых в каждом сеансе в SsnItmCwtAryVar, соответственно:
int SsnItmCwtAryVar[] = {2, 3, 5};
| * | Укажите высоту заголовка сеанса в SsnHdrHytVar:
int SsnHdrHytVar = 100;
| * | Укажите цвет фона заголовка и цвет текста:
int SsnHdrBgdClr = Color.GREEN;
int SsnHdrTxtClr = Color.MAGENTA;
| * | Укажите true, если вам нужен личный заголовок сеанса:
boolean SsnStkVab = true;
| * | Полный класс класса класса активности (исключая List Adopter и Get ArrayList)
public class NamLysSrnCls extends Activity
{
ArrayList<ItmLysCls> ItmAryLysVar = new ArrayList<>();
// CodTdo :=> Specify all your requirement here :
String SsnHdrAryVar[] = {"NamHdr1", "NamHdr2", "NamHdr3"};
int SsnItmCwtAryVar[] = {2, 3, 5};
int LysItmHytVal = 200;
int SsnHdrHytVar = 100;
int SsnHdrBgdClr = Color.GREEN;
int SsnHdrTxtClr = Color.MAGENTA;
boolean SsnStkVab = true;
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
RecyclerView NamLysLyoVav = new RecyclerView(this);
NamLysAdrCls NamLysAdrVar = new NamLysAdrCls(GetItmAryLysFnc());
NamLysLyoVav.setAdapter(NamLysAdrVar);
NamLysLyoVav.setLayoutManager(new LinearLayoutManager(this));
NamLysLyoVav.addItemDecoration(new LysSsnMgrCls());
// CodTdo :=> If you need Horizontal Lines :
NamLysLyoVav.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL));
setContentView(NamLysLyoVav);
}
// TskTdo :=> Add Below List Seesion Manager or Adoptor Class to ur List activity class
class LysSsnMgrCls extends RecyclerView.ItemDecoration
{
private RelativeLayout SsnHdrVav;
private TextView SsnHdrTxtVav;
private int SsnHdrRefIdxAryVar[];
public LysSsnMgrCls()
{
SsnHdrRefIdxAryVar = new int[ItmAryLysVar.size()];
int TmpSsnHdrIdxVar = 0;
int TmpItmCwtAdnVar = SsnItmCwtAryVar[0];
for(int IdxVat = 1; IdxVat < ItmAryLysVar.size(); IdxVat++)
{
if(IdxVat < TmpItmCwtAdnVar) SsnHdrRefIdxAryVar[IdxVat] = TmpSsnHdrIdxVar;
else
{
TmpSsnHdrIdxVar++;
TmpItmCwtAdnVar += SsnItmCwtAryVar[TmpSsnHdrIdxVar];
SsnHdrRefIdxAryVar[IdxVat] = TmpSsnHdrIdxVar;
}
Log.d("TAG", "onCreate: " + SsnHdrRefIdxAryVar[IdxVat]);
}
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state)
{
super.getItemOffsets(outRect, view, parent, state);
int LysItmIdxVal = parent.getChildAdapterPosition(view);
if (ChkSsnHasHdrFnc(LysItmIdxVal))
{
outRect.top = SsnHdrHytVar;
}
}
@Override
public void onDrawOver(Canvas SsnCanvasPsgVal, RecyclerView SupLysLyoPsgVav, RecyclerView.State LysSttPsgVal)
{
super.onDrawOver(SsnCanvasPsgVal, SupLysLyoPsgVav, LysSttPsgVal);
if (SsnHdrVav == null)
{
// TskTdo :=> Design Session Header :
SsnHdrVav = new RelativeLayout(NamLysSrnCls.this);
SsnHdrVav.setBackgroundColor(SsnHdrBgdClr);
SsnHdrVav.setLayoutParams(new LinearLayoutCompat.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT));
SsnHdrTxtVav = new TextView(NamLysSrnCls.this);
SsnHdrTxtVav.setPadding(20,10,20,10);
SsnHdrTxtVav.setLayoutParams(new LinearLayoutCompat.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT));
SsnHdrTxtVav.setTextColor(SsnHdrTxtClr);
SsnHdrTxtVav.setTextSize(20);
SsnHdrTxtVav.setTextAlignment(View.TEXT_ALIGNMENT_CENTER);
SsnHdrVav.addView(SsnHdrTxtVav);
LyoSyzFixFnc(SsnHdrVav, SupLysLyoPsgVav);
}
for (int i = 0; i < SupLysLyoPsgVav.getChildCount(); i++)
{
View DspSubVyuVav = SupLysLyoPsgVav.getChildAt(i);
final int LysItmIdxVal = SupLysLyoPsgVav.getChildAdapterPosition(DspSubVyuVav);
if (ChkSsnHasHdrFnc(LysItmIdxVal))
{
String title = GetSsnHdrTxtFnc(LysItmIdxVal);
SsnHdrTxtVav.setText(title);
DevSsnHdrFnc(SsnCanvasPsgVal, DspSubVyuVav, SsnHdrVav);
}
}
}
boolean ChkSsnHasHdrFnc(int LysItmIdxPsgVal)
{
return LysItmIdxPsgVal == 0 ? true : SsnHdrRefIdxAryVar[LysItmIdxPsgVal] != SsnHdrRefIdxAryVar[LysItmIdxPsgVal - 1];
}
String GetSsnHdrTxtFnc(int LysItmIdxPsgVal)
{
return SsnHdrAryVar[SsnHdrRefIdxAryVar[LysItmIdxPsgVal]];
}
private void DevSsnHdrFnc(Canvas HdrCanvasPsgVal, View DspSubItmPsgVav, View SsnHdrPsgVav)
{
HdrCanvasPsgVal.save();
if (SsnStkVab)
HdrCanvasPsgVal.translate(0,
Math.max(0, DspSubItmPsgVav.getTop() - SsnHdrPsgVav.getHeight()));
else
HdrCanvasPsgVal.translate(0,
DspSubItmPsgVav.getTop() - SsnHdrPsgVav.getHeight());
SsnHdrPsgVav.draw(HdrCanvasPsgVal);
HdrCanvasPsgVal.restore();
}
private void LyoSyzFixFnc(View SsnHdrVyuPsgVal, ViewGroup SupLysLyoPsgVav)
{
int LysLyoWytVal = View.MeasureSpec.makeMeasureSpec(SupLysLyoPsgVav.getWidth(), View.MeasureSpec.EXACTLY);
int LysLyoHytVal = View.MeasureSpec.makeMeasureSpec(SupLysLyoPsgVav.getHeight(), View.MeasureSpec.UNSPECIFIED);
int SsnHdrWytVal = ViewGroup.getChildMeasureSpec(LysLyoWytVal,
SupLysLyoPsgVav.getPaddingLeft() + SupLysLyoPsgVav.getPaddingRight(),
SsnHdrVyuPsgVal.getLayoutParams().width);
int SsnHdrHytVal = ViewGroup.getChildMeasureSpec(LysLyoHytVal,
SupLysLyoPsgVav.getPaddingTop() + SupLysLyoPsgVav.getPaddingBottom(),
SsnHdrVyuPsgVal.getLayoutParams().height);
SsnHdrVyuPsgVal.measure(SsnHdrWytVal, SsnHdrHytVal);
SsnHdrVyuPsgVal.layout(0, 0,
SsnHdrVyuPsgVal.getMeasuredWidth(),
SsnHdrVyuPsgVal.getMeasuredHeight());
}
}
}