我怎样才能使回收视图中的粘贴头? (没有外部库)

我想修复我的头部视图在屏幕的顶部,像下面的图片,没有使用外部库。

enter image description here

对我来说,我不想按字母顺序来。我有两种不同类型的视图(标题和正常)。我只想修复到顶部,最后的标题。

133643 次浏览

您可以在我的 FlexibleAdapter 灵活适配器项目中检查并获取类 StickyHeaderHelper的实现,并使其适应您的用例。

但是,我建议使用这个库,因为它简化并重新组织了您通常实现 Adapters for RevicleView 的方式: 不要重新发明轮子。

我还要说的是,不要使用 Decorator 或不推荐的库,也不要使用只做1或3件事的库,你必须自己合并其他库的实现。

答案已经在这里了。如果你不想使用任何库,你可以按照以下步骤:

  1. 按名称排序数据列表
  2. 通过数据列表迭代,并在当前的项目第一个字母到位!= 下一项的第一个字母,插入“特殊”类对象。
  3. 当项目是“特殊”时,在适配器内放置特殊视图。

说明:

onCreateViewHolder方法中,我们可以检查 viewType,并根据值(我们的“特殊”类型)膨胀一个特殊的布局。

例如:

public static final int TITLE = 0;
public static final int ITEM = 1;


@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (context == null) {
context = parent.getContext();
}
if (viewType == TITLE) {
view = LayoutInflater.from(context).inflate(R.layout.recycler_adapter_title, parent,false);
return new TitleElement(view);
} else if (viewType == ITEM) {
view = LayoutInflater.from(context).inflate(R.layout.recycler_adapter_item, parent,false);
return new ItemElement(view);
}
return null;
}

其中 class ItemElementclass TitleElement可以看起来像普通的 ViewHolder:

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


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


}

所以这些想法都很有趣。但我感兴趣的是它是否有效,因为我们需要排序的数据列表。我觉得这会降低速度。如果对此有任何想法,请写信给我:)

还有一个悬而未决的问题: 如何在物品回收的同时,在顶部保持“特殊”的布局。也许把这些和 CoordinatorLayout结合起来。

最简单的方法就是为回收视图创建一个项目装饰。

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 文件,请参考下面的文件:

<?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"
/>

最后,在回收视图中添加项目装饰:

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,它的文章激励我开始了使用 ItemDecoration实现我自己的粘贴头文件的旅程。我在实现中借用了他的一些代码。

正如您可能已经经历过的那样,如果您试图自己做这件事,那么很难找到一个关于 怎么做的很好的解释来实际使用 ItemDecoration技术来做这件事。我的意思是,步骤是什么?这背后的逻辑是什么?如何使标题粘贴在列表的顶部?不知道这些问题的答案是什么使其他人使用外部库,而自己使用 ItemDecoration做这件事是相当容易的。

初始条件

  1. 您的数据集应该是不同类型项目的 list(不是“ Java 类型”意义上的,而是“头/项目”意义上的)。
  2. 您的列表应该已经分类。
  3. 列表中的每个项目都应该是某种类型的——应该有一个与之相关的标题项目。
  4. list中的第一项必须是标题项。

在这里,我提供了完整的代码为我的 RecyclerView.ItemDecoration称为 HeaderItemDecoration。然后,我解释了所采取的步骤的详细信息。

public class HeaderItemDecoration extends RecyclerView.ItemDecoration {


private StickyHeaderInterface mListener;
private int mStickyHeaderHeight;


public HeaderItemDecoration(RecyclerView recyclerView, @NonNull StickyHeaderInterface listener) {
mListener = listener;


// On Sticky Header Click
recyclerView.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() {
public boolean onInterceptTouchEvent(RecyclerView recyclerView, MotionEvent motionEvent) {
if (motionEvent.getY() <= mStickyHeaderHeight) {
// Handle the clicks on the header here ...
return true;
}
return false;
}


public void onTouchEvent(RecyclerView recyclerView, MotionEvent motionEvent) {


}


public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {


}
});
}


@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDrawOver(c, parent, state);


View topChild = parent.getChildAt(0);
if (Util.isNull(topChild)) {
return;
}


int topChildPosition = parent.getChildAdapterPosition(topChild);
if (topChildPosition == RecyclerView.NO_POSITION) {
return;
}


View currentHeader = getHeaderViewForItem(topChildPosition, parent);
fixLayoutSize(parent, currentHeader);
int contactPoint = currentHeader.getBottom();
View childInContact = getChildInContact(parent, contactPoint);
if (Util.isNull(childInContact)) {
return;
}


if (mListener.isHeader(parent.getChildAdapterPosition(childInContact))) {
moveHeader(c, currentHeader, childInContact);
return;
}


drawHeader(c, currentHeader);
}


private View getHeaderViewForItem(int itemPosition, RecyclerView parent) {
int headerPosition = mListener.getHeaderPositionForItem(itemPosition);
int layoutResId = mListener.getHeaderLayout(headerPosition);
View header = LayoutInflater.from(parent.getContext()).inflate(layoutResId, parent, false);
mListener.bindHeaderData(header, headerPosition);
return header;
}


private void drawHeader(Canvas c, View header) {
c.save();
c.translate(0, 0);
header.draw(c);
c.restore();
}


private void moveHeader(Canvas c, View currentHeader, View nextHeader) {
c.save();
c.translate(0, nextHeader.getTop() - currentHeader.getHeight());
currentHeader.draw(c);
c.restore();
}


private View getChildInContact(RecyclerView parent, int contactPoint) {
View childInContact = null;
for (int i = 0; i < parent.getChildCount(); i++) {
View child = parent.getChildAt(i);
if (child.getBottom() > contactPoint) {
if (child.getTop() <= contactPoint) {
// This child overlaps the contactPoint
childInContact = child;
break;
}
}
}
return childInContact;
}


/**
* Properly measures and layouts the top sticky header.
* @param parent ViewGroup: RecyclerView in this case.
*/
private void fixLayoutSize(ViewGroup parent, View view) {


// Specs for parent (RecyclerView)
int widthSpec = View.MeasureSpec.makeMeasureSpec(parent.getWidth(), View.MeasureSpec.EXACTLY);
int heightSpec = View.MeasureSpec.makeMeasureSpec(parent.getHeight(), View.MeasureSpec.UNSPECIFIED);


// Specs for children (headers)
int childWidthSpec = ViewGroup.getChildMeasureSpec(widthSpec, parent.getPaddingLeft() + parent.getPaddingRight(), view.getLayoutParams().width);
int childHeightSpec = ViewGroup.getChildMeasureSpec(heightSpec, parent.getPaddingTop() + parent.getPaddingBottom(), view.getLayoutParams().height);


view.measure(childWidthSpec, childHeightSpec);


view.layout(0, 0, view.getMeasuredWidth(), mStickyHeaderHeight = view.getMeasuredHeight());
}


public interface StickyHeaderInterface {


/**
* This method gets called by {@link HeaderItemDecoration} to fetch the position of the header item in the adapter
* that is used for (represents) item at specified position.
* @param itemPosition int. Adapter's position of the item for which to do the search of the position of the header item.
* @return int. Position of the header item in the adapter.
*/
int getHeaderPositionForItem(int itemPosition);


/**
* This method gets called by {@link HeaderItemDecoration} to get layout resource id for the header item at specified adapter's position.
* @param headerPosition int. Position of the header item in the adapter.
* @return int. Layout resource id.
*/
int getHeaderLayout(int headerPosition);


/**
* This method gets called by {@link HeaderItemDecoration} to setup the header View.
* @param header View. Header to set the data on.
* @param headerPosition int. Position of the header item in the adapter.
*/
void bindHeaderData(View header, int headerPosition);


/**
* This method gets called by {@link HeaderItemDecoration} to verify whether the item represents a header.
* @param itemPosition int.
* @return true, if item at the specified adapter's position represents a header.
*/
boolean isHeader(int itemPosition);
}
}

业务逻辑

那么,我怎么才能让这事成真呢?

你不知道。你不能让你选择的 RecyclerView的项目只是停止和坚持在顶部,除非你是一个自定义布局的大师,你知道12,000多行代码的 RecyclerView的心。因此,正如它总是与用户界面设计,如果你不能做一些东西,假装它。你使用 Canvas 只要把标题画在所有东西的上面。您还应该知道用户此时可以看到哪些项目。碰巧,ItemDecoration可以为您提供 Canvas和关于可见项目的信息。基本步骤如下:

  1. RecyclerView.ItemDecorationonDrawOver方法中,获取用户可见的第一个(顶部)项。

        View topChild = parent.getChildAt(0);
    
  2. Determine which header represents it.

            int topChildPosition = parent.getChildAdapterPosition(topChild);
    View currentHeader = getHeaderViewForItem(topChildPosition, parent);
    
  3. Draw the appropriate header on top of the RecyclerView by using drawHeader() method.

I also want to implement the behavior when the new upcoming header meets the top one: it should seem as the upcoming header gently pushes the top current header out of the view and takes his place eventually.

Same technique of "drawing on top of everything" applies here.

  1. Determine when the top "stuck" header meets the new upcoming one.

            View childInContact = getChildInContact(parent, contactPoint);
    
  2. Get this contact point (that is the bottom of the sticky header your drew and the top of the upcoming header).

            int contactPoint = currentHeader.getBottom();
    
  3. If the item in the list is trespassing this "contact point", redraw your sticky header so its bottom will be at the top of the trespassing item. You achieve this with translate() method of the Canvas. As the result, the starting point of the top header will be out of visible area, and it will seem as "being pushed out by the upcoming header". When it is completely gone, draw the new header on top.

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

The rest is explained by comments and thorough annotations in piece of code I provided.

The usage is straight forward:

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

您的 mAdapter必须实现 StickyHeaderInterface才能工作。实现取决于您拥有的数据。

最后,在这里我提供了一个半透明头部的 gif,这样您就可以理解这个想法,并实际看到底发生了什么。

这里是“只是在上面画一切”的概念的说明。您可以看到有两个项目“头1”-一个是我们绘制并停留在顶部的一个卡住的位置,另一个来自数据集和移动的所有其余项目。用户不会看到它的内部工作原理,因为不会有半透明的头。

"just draw on top of everything" concept

在“推出”阶段会发生什么:

"pushing out" phase

希望有帮助。

剪辑

下面是我在珊瑚礁视图的适配器中对 getHeaderPositionForItem()方法的实际实现:

@Override
public int getHeaderPositionForItem(int itemPosition) {
int headerPosition = 0;
do {
if (this.isHeader(itemPosition)) {
headerPosition = itemPosition;
break;
}
itemPosition -= 1;
} while (itemPosition >= 0);
return headerPosition;
}

Kotlin 的实施略有不同

我在上面做了我自己的塞瓦斯蒂安解法的变体

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

下面是 SticyHeaderInterface 的实现(我直接在回收器适配器中完成的) :

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 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"/>

回收视图的布局

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

对于那些谁可能会关心。根据塞瓦斯蒂安的答案,如果你想让它水平滚动。 只需将所有 getBottom()改为 getRight(),将 getTop()改为 getLeft()

当你已经有 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(...)
}

这似乎是工作,但有人能证实我没有打破任何其他?

哟,

这就是你如何做到这一点,如果你想只有一种类型的持有人坚持时,开始走出屏幕(我们不关心任何部分)。只有一种方法可以避免破坏回收项的回收视图内部逻辑,那就是在回收视图的头部项之上增加额外的视图,并将数据传递给它。我会让代码说话。

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

其中 你的粘性视图持有者类型是视图类型的什么是应该是粘贴持有人。

你可以通过 将这两个文件复制到您的项目中获得粘性头部功能。我对这个实现没有任何问题:

  • 可以与粘性头(点击/长按/滑动)交互
  • 即使每个视图持有者有不同的高度(这里的一些其他答案没有正确处理这个问题,导致显示错误的标题,或者标题上下跳动)

看一个在这个小的 github 项目中使用的2个文件的例子

如果您希望标题位于您的回收视图项目的旁边,可以这样做 enter image description here 然后使用相同的代码 给你onDrawOver中加入这两行

//hide the image and the name, and draw only the alphabet
val headerView = getHeaderViewForItem(topChildPosition, parent) ?: return
headerView.findViewById<ShapeableImageView>(R.id.contactImageView).isVisible = false
headerView.findViewById<TextView>(R.id.nameTextView).isVisible = false

在这里,你基本上是重新绘制,回收视图的项目,但隐藏所有的元素,在右边。
如果你想知道如何创建这样的回收视图项目,那么这里是如何:
your recyclerview item 然后创建如下数据列表:

class ContactRecyclerDataItem(val contact: SimpleContact, val alphabet: String? = null)

这样,当您收到数据列表时,就可以构建 Contact迴收数据项列表

这边

list?.let {
val adapterDataList = mutableListOf<ContactRecyclerDataItem>()
if (it.isNotEmpty()) {
var prevChar = (it[0].name[0].code + 1).toChar()
it.forEach { contact ->
if (contact.name[0] != prevChar) {
prevChar = contact.name[0]
adapterDataList.add(ContactRecyclerDataItem(contact, prevChar.toString()))
} else {
adapterDataList.add(ContactRecyclerDataItem(contact))
}
}
}
contactsAdapter.data = adapterDataList
}

然后在 viewHolder内部的回收适配器中检查字母表是否为空,

        if (itemRecycler.alphabet != null) {
alphabetTextView.text = itemRecycler.alphabet
} else {
alphabetTextView.text = ""
}

最后,你在左边构建了这个回收视图,但是为了让它们更加粘稠,你需要充气并移动第一个元素,这个元素就是页眉,一直向下直到下一个页眉,上面提到的技巧就是隐藏除了字母之外的所有其他元素。
要使第一个元素可单击 ,应该在 itemDecorat 中返回 false 在 parent.addOnItemTouchListene{}init block内 当返回 false 时,您将单击监听器传递给下面的视图,在这种情况下,下面的视图就是可见的回收视图项。