Jau*_*lom 106 android header sticky android-recyclerview
我想在屏幕顶部修复我的标题视图,如下图所示,而不使用外部库.
就我而言,我不想按字母顺序进行.我有两种不同类型的视图(标题和正常).我只想修复顶部,最后一个标题.
Sev*_*yuk 286
在这里,我将解释如何在没有外部库的情况下完成它.这将是一个非常长的帖子,所以支撑自己.
首先,让我感谢@ tim.paetz,他的帖子激励我开始使用ItemDecorations 实现我自己的粘性标题.我在实现中借用了代码的一些部分.
正如您可能已经经历过的那样,如果您自己尝试这样做,很难找到一个很好的解释,如何实际使用该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'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);
}
}
Run Code Online (Sandbox Code Playgroud)
商业逻辑
那么,我该怎么做呢?
你没有.你不能让一个RecyclerView你的选择的项目只停留和贴在上面,除非你自定义布局的大师,你知道12000为代码行RecyclerView的心脏.所以,因为它始终与UI设计一致,如果你不能做某事,那就假装它.你只是借鉴一切顶部的标题使用Canvas.您还应该知道用户目前可以看到哪些项目.它恰好发生,它ItemDecoration可以为您提供Canvas有关可见项目的信息.有了这个,这里是基本步骤:
在onDrawOver方法RecyclerView.ItemDecoration获得的第一个(顶部)项目是对用户可见.
View topChild = parent.getChildAt(0);
Run Code Online (Sandbox Code Playgroud)确定哪个标头代表它.
int topChildPosition = parent.getChildAdapterPosition(topChild);
View currentHeader = getHeaderViewForItem(topChildPosition, parent);
Run Code Online (Sandbox Code Playgroud)使用drawHeader()方法在RecyclerView上绘制相应的标题.
我还想在新的即将到来的标题遇到顶级标题时实现这种行为:它应该看起来像即将到来的标题轻轻地将顶部当前标题推出视图并最终取代他的位置.
同样适用于"绘制所有内容"的技术也适用于此.
确定顶部"卡住"标题何时符合新即将发布的标题.
View childInContact = getChildInContact(parent, contactPoint);
Run Code Online (Sandbox Code Playgroud)获取此联系点(即您绘制的粘性标题的底部和即将出现的标题的顶部).
int contactPoint = currentHeader.getBottom();
Run Code Online (Sandbox Code Playgroud)如果列表中的项目侵入此"联系点",则重绘您的粘性标题,使其底部位于非法侵入项目的顶部.你用这个translate()方法实现了这一目标Canvas.结果,顶部标题的起点将超出可见区域,并且它将显示为"被即将到来的标题推出".当它完全消失时,在顶部绘制新标题.
if (childInContact != null) {
if (mListener.isHeader(parent.getChildAdapterPosition(childInContact))) {
moveHeader(c, currentHeader, childInContact);
} else {
drawHeader(c, currentHeader);
}
}
Run Code Online (Sandbox Code Playgroud)其余部分通过我提供的代码中的注释和详尽注释来解释.
用法很简单:
mRecyclerView.addItemDecoration(new HeaderItemDecoration((HeaderItemDecoration.StickyHeaderInterface) mAdapter));
Run Code Online (Sandbox Code Playgroud)
你mAdapter必须实现StickyHeaderInterface它才能工作.实施取决于您拥有的数据.
最后,在这里我提供了一个带有半透明标题的gif,这样你就可以掌握这个想法并实际看到底层发生了什么.
这里是"只是绘制一切"的概念.您可以看到有两个项目"标题1" - 一个我们绘制并保持在卡住位置的顶部,另一个来自数据集并与所有其余项目一起移动.用户将看不到它的内部工作原理,因为您将不会有半透明的标题.
在这里"推出"阶段会发生什么:
希望它有所帮助.
编辑
这是我getHeaderPositionForItem()在RecyclerView的适配器中实际实现的方法:
@Override
public int getHeaderPositionForItem(int itemPosition) {
int headerPosition = 0;
do {
if (this.isHeader(itemPosition)) {
headerPosition = itemPosition;
break;
}
itemPosition -= 1;
} while (itemPosition >= 0);
return headerPosition;
}
Run Code Online (Sandbox Code Playgroud)
tim*_*etz 25
最简单的方法是为RecyclerView创建一个Item Decoration.
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);
}
Run Code Online (Sandbox Code Playgroud)
}
recycleler_section_header.xml中标头的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"
/>
Run Code Online (Sandbox Code Playgroud)
最后将项目装饰添加到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);
Run Code Online (Sandbox Code Playgroud)
使用此项目装饰,您可以在创建项目装饰时使用布尔值固定或粘贴标题.
你可以在github上找到一个完整的工作示例:https://github.com/paetztm/recycler_view_headers
我在上面对Sevastyan的解决方案做了自己的修改
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
}
}
Run Code Online (Sandbox Code Playgroud)
...这是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
... */
}
Run Code Online (Sandbox Code Playgroud)
因此,在这种情况下,标题不仅是在画布上绘制,而且还具有选择器或波纹,单击侦听器等视图。
给正在寻找闪烁/闪烁问题解决方案的任何人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(...)
}
Run Code Online (Sandbox Code Playgroud)
这似乎有效,但谁能确认我没有破坏其他任何东西?
StickyHeaderHelper您可以在我的FlexibleAdapter项目中检查并获取该类的实现,并根据您的用例进行调整。
但是,我建议使用该库,因为它简化并重新组织了您通常实现 RecyclerView 适配器的方式:不要重新发明轮子。
我还要说,不要使用装饰器或已弃用的库,也不要使用只做 1 或 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)
}
}
Run Code Online (Sandbox Code Playgroud)
然后您只需在您的适配器中执行此操作:
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
super.onAttachedToRecyclerView(recyclerView)
recyclerView.addItemDecoration(StickyHeaderItemDecoration(R.layout.item_time_filter, YOUR_STICKY_VIEW_HOLDER_TYPE))
}
Run Code Online (Sandbox Code Playgroud)
其中YOUR_STICKY_VIEW_HOLDER_TYPE是您的粘性持有人的 viewType。
| 归档时间: |
|
| 查看次数: |
82598 次 |
| 最近记录: |