如何在RecyclerView中制作粘性标题? (没有外部库)

时间:2015-10-05 13:35:06

标签: android header android-recyclerview sticky

我想在屏幕顶部修复标题视图,如下图所示,而不使用外部库。

enter image description here

就我而言,我不想按字母顺序进行。我有两种不同类型的视图(标题和正常)。我只想修复顶部,最后一个标题。

12 个答案:

答案 0 :(得分:255)

在这里,我将解释如何在没有外部库的情况下执行此操作。这将是一个非常长的职位,所以鼓舞自己。

首先,让我感谢@tim.paetz,其帖子激励我开始使用ItemDecoration来实现我自己的粘性标题。我在实现中借用了部分代码。

正如您可能已经经历过的那样,如果您自己尝试这样做,很难找到 HOW 的良好解释来实际使用ItemDecoration技术。我的意思是,步骤是什么?它背后的逻辑是什么?如何将标题添加到列表顶部?不知道这些问题的答案是让其他人使用外部库的原因,而使用ItemDecoration自己完成它很容易。< / p>

初始条件

  1. 您的数据集应该是list不同类型的项目(不是在#34; Java类型&#34;意义上,而是在&#34;标题/项目&#34;类型意义上)。
  2. 您的列表应该已经排序。
  3. 列表中的每个项目都应该是某种类型 - 应该有与之相关的标题项。
  4. list中的第一项必须是标题项。
  5. 在这里,我提供了名为RecyclerView.ItemDecoration的{​​{1}}的完整代码。然后我详细解释了所采取的步骤。

    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); } } 项,只需停下来并坚持下去,除非您是自定义布局的大师,并且您知道{{1}的12,000多行代码用心。因此,因为它始终与UI设计一致,如果你不能做出某些东西,那就假装它。您只需使用RecyclerView在所有内容上绘制标题。您还应该知道用户目前可以看到哪些项目。恰好,RecyclerView可以为您提供Canvas和有关可见项目的信息。有了这个,这里有基本的步骤:

    1. ItemDecoration的{​​{1}}方法中获取用户可见的第一个(顶部)项目。

      Canvas
    2. 确定哪个标题代表它。

      onDrawOver
    3. 使用RecyclerView.ItemDecoration方法在RecyclerView上绘制相应的标题。

    4. 我还希望在即将到来的新标题符合最高标题时实现此行为:看起来即将出现的标题会轻轻地将当前最高标题推出视图并最终占据他的位置。

      同样的技术&#34;吸取所有东西&#34;适用于此。

      1. 确定顶部&#34;卡住&#34;标题符合即将推出的新标题。

            View topChild = parent.getChildAt(0);
        
      2. 获取此联系点(即您绘制的粘性标题的底部和即将出现的标题的顶部)。

                int topChildPosition = parent.getChildAdapterPosition(topChild);
            View currentHeader = getHeaderViewForItem(topChildPosition, parent);
        
      3. 如果列表中的项目是非法侵入此&#34;联系点&#34;,请重新绘制粘性标题,使其底部位于非法侵入项目的顶部。您可以使用drawHeader()的{​​{1}}方法实现此目的。结果,顶部标题的起点将超出可见区域,并且它将被即将到来的标题&#34;推出。当它完全消失时,在顶部绘制新标题。

                View childInContact = getChildInContact(parent, contactPoint);
        
      4. 其余部分通过我提供的代码中的注释和详尽注释来解释。

        用法很简单:

                int contactPoint = currentHeader.getBottom();
        

        您的translate()必须实施Canvas才能发挥作用。实施取决于您拥有的数据。

        最后,在这里,我提供了一个带有半透明标题的gif,这样你就可以掌握这个想法并实际看到底层发生了什么。

        以下是&#34的图示;只需绘制所有内容&#34;概念。您可以看到有两个项目&#34;标题1和#34; - 我们绘制并保持在卡住位置的顶部,另一个来自数据集并与所有其余项目一起移动。用户不会看到它的内部工作原因,因为您将不会拥有半透明的标题。

        "just draw on top of everything" concept

        在这里&#34;推出&#34;相:

        "pushing out" phase

        希望它有所帮助。

        修改

        以下是我在RecyclerView适配器中 if (childInContact != null) { if (mListener.isHeader(parent.getChildAdapterPosition(childInContact))) { moveHeader(c, currentHeader, childInContact); } else { drawHeader(c, currentHeader); } } 方法的实际实现:

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

        Slightly different implementation in Kotlin

答案 1 :(得分:21)

最简单的方法是为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);
}

}

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

最后将项目装饰添加到您的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

答案 2 :(得分:5)

我已经在上面制作了我自己的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
}
}

...这里是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
    ... */
}

因此,在这种情况下,标题不只是在画布上绘制,而是使用选择器或波纹,clicklistener等进行查看。

答案 3 :(得分:3)

您可以在我的FlexibleAdapter项目中检查并执行课程StickyHeaderHelper,并根据您的使用情况进行调整。

但是,我建议使用该库,因为它简化并重新组织了通常实现RecyclerView适配器的方式:不要重新发明轮子。

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

答案 4 :(得分:3)

另一种基于滚动侦听器的解决方案。初始条件与Sevastyan answer

中的相同
RecyclerView recyclerView;
TextView tvTitle; //sticky header view

//... onCreate, initialize, etc...

public void bindList(List<Item> items) { //All data in adapter. Item - just interface for different item types
    adapter = new YourAdapter(items);
    recyclerView.setAdapter(adapter);
    StickyHeaderViewManager<HeaderItem> stickyHeaderViewManager = new StickyHeaderViewManager<>(
            tvTitle,
            recyclerView,
            HeaderItem.class, //HeaderItem - subclass of Item, used to detect headers in list
            data -> { // bind function for sticky header view
                tvTitle.setText(data.getTitle());
            });
    stickyHeaderViewManager.attach(items);
}

ViewHolder和粘贴标题的布局。

item_header.xml

<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/tv_title"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"/>

RecyclerView的布局

<FrameLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

    <!--it can be any view, but order important, draw over recyclerView-->
    <include
        layout="@layout/item_header"/>

</FrameLayout>

HeaderItem的类。

public class HeaderItem implements Item {

    private String title;

    public HeaderItem(String title) {
        this.title = title;
    }

    public String getTitle() {
        return title;
    }

}

这一切都有用。适配器,ViewHolder和其他东西的实现对我们来说并不感兴趣。

public class StickyHeaderViewManager<T> {

    @Nonnull
    private View headerView;

    @Nonnull
    private RecyclerView recyclerView;

    @Nonnull
    private StickyHeaderViewWrapper<T> viewWrapper;

    @Nonnull
    private Class<T> headerDataClass;

    private List<?> items;

    public StickyHeaderViewManager(@Nonnull View headerView,
                                   @Nonnull RecyclerView recyclerView,
                                   @Nonnull Class<T> headerDataClass,
                                   @Nonnull StickyHeaderViewWrapper<T> viewWrapper) {
        this.headerView = headerView;
        this.viewWrapper = viewWrapper;
        this.recyclerView = recyclerView;
        this.headerDataClass = headerDataClass;
    }

    public void attach(@Nonnull List<?> items) {
        this.items = items;
        if (ViewCompat.isLaidOut(headerView)) {
            bindHeader(recyclerView);
        } else {
            headerView.post(() -> bindHeader(recyclerView));
        }

        recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {

            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);
                bindHeader(recyclerView);
            }
        });
    }

    private void bindHeader(RecyclerView recyclerView) {
        if (items.isEmpty()) {
            headerView.setVisibility(View.GONE);
            return;
        } else {
            headerView.setVisibility(View.VISIBLE);
        }

        View topView = recyclerView.getChildAt(0);
        if (topView == null) {
            return;
        }
        int topPosition = recyclerView.getChildAdapterPosition(topView);
        if (!isValidPosition(topPosition)) {
            return;
        }
        if (topPosition == 0 && topView.getTop() == recyclerView.getTop()) {
            headerView.setVisibility(View.GONE);
            return;
        } else {
            headerView.setVisibility(View.VISIBLE);
        }

        T stickyItem;
        Object firstItem = items.get(topPosition);
        if (headerDataClass.isInstance(firstItem)) {
            stickyItem = headerDataClass.cast(firstItem);
            headerView.setTranslationY(0);
        } else {
            stickyItem = findNearestHeader(topPosition);
            int secondPosition = topPosition + 1;
            if (isValidPosition(secondPosition)) {
                Object secondItem = items.get(secondPosition);
                if (headerDataClass.isInstance(secondItem)) {
                    View secondView = recyclerView.getChildAt(1);
                    if (secondView != null) {
                        moveViewFor(secondView);
                    }
                } else {
                    headerView.setTranslationY(0);
                }
            }
        }

        if (stickyItem != null) {
            viewWrapper.bindView(stickyItem);
        }
    }

    private void moveViewFor(View secondView) {
        if (secondView.getTop() <= headerView.getBottom()) {
            headerView.setTranslationY(secondView.getTop() - headerView.getHeight());
        } else {
            headerView.setTranslationY(0);
        }
    }

    private T findNearestHeader(int position) {
        for (int i = position; position >= 0; i--) {
            Object item = items.get(i);
            if (headerDataClass.isInstance(item)) {
                return headerDataClass.cast(item);
            }
        }
        return null;
    }

    private boolean isValidPosition(int position) {
        return !(position == RecyclerView.NO_POSITION || position >= items.size());
    }
}

绑定标头视图的接口。

public interface StickyHeaderViewWrapper<T> {

    void bindView(T data);
}

答案 5 :(得分:2)

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

这似乎可行,但是谁能确认我没有破坏其他任何东西?

答案 6 :(得分:1)

是的

如果您只想要一种类型的支架棒,当它离开屏幕时(我们不在乎任何部分),这是您要执行的操作。在不破坏回收项目的内部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的位置。

答案 7 :(得分:1)

您可以通过 copying these 2 files into your project 获得粘性标题功能。我对这个实现没有任何问题:

  • 可以与粘性标题交互(点击/长按/滑动)
  • 粘性标题会正确隐藏并显示自己......即使每个视图持有者具有不同的高度(此处的其他一些答案无法正确处理,导致显示错误的标题,或标题上下跳跃)

see an example of the 2 files being used in this small github project i whipped up

答案 8 :(得分:0)

对于那些可能关心的人。根据Sevastyan的回答,你想要让它成为横向滚动。 只需将所有getBottom()更改为getRight(),将getTop()更改为getLeft()

答案 9 :(得分:0)

如果您希望标题位于像这样的 recyclerview 项目旁边 enter image description here 然后使用相同的代码 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

在这里,您基本上是重新绘制了 recyclerview 项目,但隐藏了右侧的所有元素。
如果您想知道如何创建这样的 recyclerview 项目,那么方法如下:
your recyclerview item 然后您将像这样创建数据列表:

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

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

这边

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

最后你用左边的字母构建这个rec​​yclerview,但是为了让它们变得粘稠,你膨胀并移动第一个元素,即标题一直向下直到下一个标题,上面提到的技巧是隐藏所有recyclerview 项目中除字母表之外的其他元素。
要使第一个元素可点击,您应该在 itemDecorat 中返回 false 在 init blockparent.addOnItemTouchListene{} 内 返回 false 时,您将点击侦听器传递给波纹管视图,在这种情况下,这是您的可见回收器视图项。

答案 10 :(得分:-1)

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

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

    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结合起来。

答案 11 :(得分:-2)

| * |经过一整天的奋斗,我开发了Session Adopter或Manager Class 只需复制粘贴即可轻松使用,并指定列表和值。

这是为了帮助所有我不想像我一样挣扎的人。

| * |在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;

| * |完整列表活动类代码(不包括列表采用者和获取数组列表)

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