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);
}
}
Business logic
那么,我该怎么做呢?
你不要自己选择 RecyclerView 的项目,只需停下来并坚持下去,除非你是自定义布局的大师,并且你知道12,000行的代码为 RecyclerView . 所以,因为它始终与UI设计一致,如果你不能做某事,那就假装它 . 你 just draw the header on top of everything 使用 Canvas . 您还应该知道用户目前可以看到哪些项目 . 它恰好发生, ItemDecoration 可以为您提供 Canvas 和有关可见项目的信息 . 有了这个,这里是基本步骤:
在 onDrawOver 的 onDrawOver 方法中获取用户可见的第一个(顶部)项目 .
View topChild = parent.getChildAt(0);
确定哪个 Headers 代表它 .
int topChildPosition = parent.getChildAdapterPosition(topChild);
View currentHeader = getHeaderViewForItem(topChildPosition, parent);
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);
}
<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);
}
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);
}
8 回答
最简单的方法是为RecyclerView创建一个Item Decoration .
}
recycleler_section_header.xml中标头的XML:
最后将项目装饰添加到RecyclerView:
同这个项目装饰你可以在创建项目装饰时使 Headers 固定/粘贴或仅使用布尔值 .
你可以在github上找到一个完整的工作示例:https://github.com/paetztm/recycler_view_headers
在这里,我将解释如何在没有外部库的情况下完成它 . 这将是一个非常长的帖子,所以支撑自己 .
首先,让我承认@tim.paetz的帖子激励我开始使用
ItemDecoration
来实现我自己的粘性 Headers . 我在实现中借用了代码的一些部分 .正如您可能已经经历过的那样,如果您自己尝试这样做,很难找到一个很好的解释 HOW 来实际使用
ItemDecoration
技术 . 我的意思是,步骤是什么?它背后的逻辑是什么?如何将 Headers 贴在列表顶部?不知道这些问题的答案是什么让其他人使用外部库,而使用ItemDecoration
自己做这件事很容易 .Initial conditions
您的数据集应该是
list
不同类型的项目(不是在"Java types"意义上,而是在"header/item"类型意义上) .您的清单应该已经排序 .
列表中的每个项目都应该是某种类型 - 应该有一个与之相关的 Headers 项 .
list
中的第一项必须是 Headers 项 .在这里,我提供了我的
RecyclerView.ItemDecoration
的完整代码,名为HeaderItemDecoration
. 然后我详细解释了所采取的步骤 .Business logic
那么,我该怎么做呢?
你不要自己选择
RecyclerView
的项目,只需停下来并坚持下去,除非你是自定义布局的大师,并且你知道12,000行的代码为RecyclerView
. 所以,因为它始终与UI设计一致,如果你不能做某事,那就假装它 . 你 just draw the header on top of everything 使用Canvas
. 您还应该知道用户目前可以看到哪些项目 . 它恰好发生,ItemDecoration
可以为您提供Canvas
和有关可见项目的信息 . 有了这个,这里是基本步骤:onDrawOver
的onDrawOver
方法中获取用户可见的第一个(顶部)项目 .drawHeader()
方法在RecyclerView上绘制相应的 Headers .我还想在新的即将到来的 Headers 遇到顶级 Headers 时实现这种行为:它应该看起来像即将到来的 Headers 轻轻地将顶部当前 Headers 推出视图并最终取代他的位置 .
同样适用于“绘制所有内容”的技术也适用于此 .
Canvas
的translate()
方法实现此目的 . 结果,顶部 Headers 的起点将超出可见区域,并且它将显示为"being pushed out by the upcoming header" . 当它完全消失时,在顶部绘制新 Headers .其余部分通过我提供的代码中的注释和详尽注释来解释 .
用法很简单:
您的
mAdapter
必须实现StickyHeaderInterface
才能正常工作 . 实施取决于您拥有的数据 .最后,在这里我提供了一个带有半透明 Headers 的gif,这样你就可以掌握这个想法并实际看到底层发生了什么 .
这里是“只是绘制一切”的概念 . 您可以看到有两个项目“ Headers 1” - 一个我们绘制并保持在卡住位置的顶部,另一个来自数据集并与所有其余项目一起移动 . 用户将看不到它的内部工作原理,因为您将不会有半透明的 Headers .
在这里“推出”阶段会发生什么:
希望它有所帮助 .
Edit
这是我在RecyclerView的适配器中实际实现的
getHeaderPositionForItem()
方法:您可以在我的FlexibleAdapter项目中检查并执行类
StickyHeaderHelper
的实现,并使其适应您的用例 .但是,我建议使用该库,因为它简化并重新组织了您通常实现RecyclerView适配器的方式:不要重新发明轮子 .
我也会说,不要使用装饰器或不推荐使用的库,也不要使用只做1或3件事的库,你必须自己合并其他库的实现 .
我已经在上面制作了我自己的Sevastyan解决方案
...这里是StickyHeaderInterface的实现(我直接在Recycler适配器中完成):
因此,在这种情况下, Headers 不仅仅是在画布上绘制,而是使用选择器或波纹,clicklistener等进行查看 .
另一种解决方案,基于滚动侦听器 . 初始条件与Sevastyan answer相同
ViewHolder和粘贴 Headers 的布局 .
item_header.xml
RecyclerView的布局
HeaderItem的类 .
这都是有用的 . 适配器,ViewHolder和其他东西的实现对我们来说并不重要 .
绑定标头视图的接口 .
答案已经在这里了 . 如果您不想使用任何库,可以按照以下步骤操作:
按名称对数据进行排序
通过列表和数据迭代,当前的项目第一个字母!=下一个项目的第一个字母时,插入"special"种类的对象 .
当项目为"special"时,在适配器内部放置特殊视图 .
说明:
在
onCreateViewHolder
方法中,我们可以检查viewType
并根据值(我们的"special"种类)膨胀一个特殊的布局 .例如:
其中
class ItemElement
和class TitleElement
看起来像普通ViewHolder
:因此,所有这一切的想法都很有趣 . 但我感兴趣的是它是否有效,因为我们需要对数据列表进行排序 . 而且我认为这会降低速度 . 如果有任何想法,请写信给我:)
还有一个悬而未决的问题:如何将"special"布局放在顶部,而物品则是回收利用 . 也许将所有这些与
CoordinatorLayout
结合起来 .对于那些可能关心的人 . 根据Sevastyan的回答,你想要让它成为横向滚动 . 只需将所有
getBottom()
更改为getRight()
和getTop()
更改为getLeft()
| * |经过一整天的奋斗,我开发了Session Adopter或Manager Class
只需复制粘贴即可轻松使用,并指定列表和值 .
这是为了帮助所有我不想像我一样挣扎的人 .
| * |在SsnHdrAryVar中指定 Headers 名称:
| * |在SsnItmCwtAryVar中分别指定要在每个会话下显示的子项目数:
| * |在SsnHdrHytVar中指定会话标头的高度:
| * |指定 Headers 背景颜色和文本颜色:
| * |如果需要粘性会话标头,请指定true:
| * |完整列表活动类代码(不包括列表采用者和获取ArrayList)