首页 文章

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

提问于
浏览
76

我想在屏幕顶部修复我的 Headers 视图,如下图所示,而不使用外部库 .

enter image description here

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

8 回答

  • 0

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

    }

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

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

    同这个项目装饰你可以在创建项目装饰时使 Headers 固定/粘贴或仅使用布尔值 .

    你可以在github上找到一个完整的工作示例:https://github.com/paetztm/recycler_view_headers

  • 16

    在这里,我将解释如何在没有外部库的情况下完成它 . 这将是一个非常长的帖子,所以支撑自己 .

    首先,让我承认@tim.paetz的帖子激励我开始使用 ItemDecoration 来实现我自己的粘性 Headers . 我在实现中借用了代码的一些部分 .

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

    Initial conditions

    • 您的数据集应该是 list 不同类型的项目(不是在"Java types"意义上,而是在"header/item"类型意义上) .

    • 您的清单应该已经排序 .

    • 列表中的每个项目都应该是某种类型 - 应该有一个与之相关的 Headers 项 .

    • list 中的第一项必须是 Headers 项 .

    在这里,我提供了我的 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);
     }
    }
    

    Business logic

    那么,我该怎么做呢?

    你不要自己选择 RecyclerView 的项目,只需停下来并坚持下去,除非你是自定义布局的大师,并且你知道12,000行的代码为 RecyclerView . 所以,因为它始终与UI设计一致,如果你不能做某事,那就假装它 . 你 just draw the header on top of everything 使用 Canvas . 您还应该知道用户目前可以看到哪些项目 . 它恰好发生, ItemDecoration 可以为您提供 Canvas 和有关可见项目的信息 . 有了这个,这里是基本步骤:

    • onDrawOveronDrawOver 方法中获取用户可见的第一个(顶部)项目 .
    View topChild = parent.getChildAt(0);
    
    • 确定哪个 Headers 代表它 .
    int topChildPosition = parent.getChildAdapterPosition(topChild);
        View currentHeader = getHeaderViewForItem(topChildPosition, parent);
    
    • 使用 drawHeader() 方法在RecyclerView上绘制相应的 Headers .

    我还想在新的即将到来的 Headers 遇到顶级 Headers 时实现这种行为:它应该看起来像即将到来的 Headers 轻轻地将顶部当前 Headers 推出视图并最终取代他的位置 .

    同样适用于“绘制所有内容”的技术也适用于此 .

    • 确定顶部“卡住” Headers 何时符合新的 Headers .
    View childInContact = getChildInContact(parent, contactPoint);
    
    • 获取此联系人点(即您绘制的粘性 Headers 的底部和即将出现的 Headers 的顶部) .
    int contactPoint = currentHeader.getBottom();
    
    • 如果列表中的项目正在侵入此"contact point",则重绘您的粘性 Headers ,使其底部位于非法侵入项目的顶部 . 您可以使用 Canvastranslate() 方法实现此目的 . 结果,顶部 Headers 的起点将超出可见区域,并且它将显示为"being pushed out by the upcoming header" . 当它完全消失时,在顶部绘制新 Headers .
    if (childInContact != null) {
            if (mListener.isHeader(parent.getChildAdapterPosition(childInContact))) {
                moveHeader(c, currentHeader, childInContact);
            } else {
                drawHeader(c, currentHeader);
            }
        }
    

    其余部分通过我提供的代码中的注释和详尽注释来解释 .

    用法很简单:

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

    您的 mAdapter 必须实现 StickyHeaderInterface 才能正常工作 . 实施取决于您拥有的数据 .

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

    这里是“只是绘制一切”的概念 . 您可以看到有两个项目“ Headers 1” - 一个我们绘制并保持在卡住位置的顶部,另一个来自数据集并与所有其余项目一起移动 . 用户将看不到它的内部工作原理,因为您将不会有半透明的 Headers .

    enter image description here

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

    enter image description here

    希望它有所帮助 .

    Edit

    这是我在RecyclerView的适配器中实际实现的 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;
    }
    
  • 207

    您可以在我的FlexibleAdapter项目中检查并执行类 StickyHeaderHelper 的实现,并使其适应您的用例 .

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

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

  • 3

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

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

  • -1

    另一种解决方案,基于滚动侦听器 . 初始条件与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和粘贴 Headers 的布局 .

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

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

    • 按名称对数据进行排序

    • 通过列表和数据迭代,当前的项目第一个字母!=下一个项目的第一个字母时,插入"special"种类的对象 .

    • 当项目为"special"时,在适配器内部放置特殊视图 .

    说明:

    onCreateViewHolder 方法中,我们可以检查 viewType 并根据值(我们的"special"种类)膨胀一个特殊的布局 .

    例如:

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

    因此,所有这一切的想法都很有趣 . 但我感兴趣的是它是否有效,因为我们需要对数据列表进行排序 . 而且我认为这会降低速度 . 如果有任何想法,请写信给我:)

    还有一个悬而未决的问题:如何将"special"布局放在顶部,而物品则是回收利用 . 也许将所有这些与 CoordinatorLayout 结合起来 .

  • 2

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

  • 3

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

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


    | * |在SsnHdrAryVar中指定 Headers 名称:

    String SsnHdrAryVar[] = {"NamHdr1", "NamHdr2", "NamHdr3"};
    

    | * |在SsnItmCwtAryVar中分别指定要在每个会话下显示的子项目数:

    int SsnItmCwtAryVar[] = {2, 3, 5};
    

    | * |在SsnHdrHytVar中指定会话标头的高度:

    int SsnHdrHytVar = 100;
    

    | * |指定 Headers 背景颜色和文本颜色:

    int SsnHdrBgdClr = Color.GREEN;
        int SsnHdrTxtClr = Color.MAGENTA;
    

    | * |如果需要粘性会话标头,请指定true:

    boolean SsnStkVab = true;
    

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

    public class NamLysSrnCls extends Activity
    {
        ArrayList<ItmLysCls> ItmAryLysVar = new ArrayList<>();
    
    // CodTdo :=> Specify all your requirement here :    
        String SsnHdrAryVar[] = {"NamHdr1", "NamHdr2", "NamHdr3"};
        int SsnItmCwtAryVar[] = {2, 3, 5};
    
        int LysItmHytVal = 200;
        int SsnHdrHytVar = 100;
        int SsnHdrBgdClr = Color.GREEN;
        int SsnHdrTxtClr = Color.MAGENTA;
        boolean SsnStkVab = true;
    
        @Override
        protected void onCreate(Bundle savedInstanceState)
        {
            super.onCreate(savedInstanceState);
    
            RecyclerView NamLysLyoVav = new RecyclerView(this);
    
            NamLysAdrCls NamLysAdrVar = new NamLysAdrCls(GetItmAryLysFnc());
            NamLysLyoVav.setAdapter(NamLysAdrVar);
            NamLysLyoVav.setLayoutManager(new LinearLayoutManager(this));
    
            NamLysLyoVav.addItemDecoration(new LysSsnMgrCls());
    
        // CodTdo :=> If you need Horizontal Lines :
            NamLysLyoVav.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL));
    
            setContentView(NamLysLyoVav);
        }
    
    
    // TskTdo :=> Add Below List Seesion Manager or Adoptor Class to ur List activity class 
    
        class LysSsnMgrCls extends RecyclerView.ItemDecoration
        {
            private RelativeLayout SsnHdrVav;
            private TextView SsnHdrTxtVav;
    
            private int SsnHdrRefIdxAryVar[];
    
            public LysSsnMgrCls()
            {
                SsnHdrRefIdxAryVar = new int[ItmAryLysVar.size()];
                int TmpSsnHdrIdxVar = 0;
                int TmpItmCwtAdnVar = SsnItmCwtAryVar[0];
                for(int IdxVat = 1; IdxVat < ItmAryLysVar.size(); IdxVat++)
                {
                    if(IdxVat < TmpItmCwtAdnVar) SsnHdrRefIdxAryVar[IdxVat] = TmpSsnHdrIdxVar;
                    else
                    {
                        TmpSsnHdrIdxVar++;
                        TmpItmCwtAdnVar += SsnItmCwtAryVar[TmpSsnHdrIdxVar];
                        SsnHdrRefIdxAryVar[IdxVat] = TmpSsnHdrIdxVar;
                    }
                    Log.d("TAG", "onCreate: " + SsnHdrRefIdxAryVar[IdxVat]);
                }
            }
    
            @Override
            public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state)
            {
                super.getItemOffsets(outRect, view, parent, state);
    
                int LysItmIdxVal = parent.getChildAdapterPosition(view);
                if (ChkSsnHasHdrFnc(LysItmIdxVal))
                {
                    outRect.top = SsnHdrHytVar;
                }
            }
    
            @Override
            public void onDrawOver(Canvas SsnCanvasPsgVal, RecyclerView SupLysLyoPsgVav, RecyclerView.State LysSttPsgVal)
            {
                super.onDrawOver(SsnCanvasPsgVal, SupLysLyoPsgVav, LysSttPsgVal);
    
                if (SsnHdrVav == null)
                {
    // TskTdo :=> Design Session Header :
                    SsnHdrVav = new RelativeLayout(NamLysSrnCls.this);
                    SsnHdrVav.setBackgroundColor(SsnHdrBgdClr);
                    SsnHdrVav.setLayoutParams(new LinearLayoutCompat.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT));
                    SsnHdrTxtVav = new TextView(NamLysSrnCls.this);
                    SsnHdrTxtVav.setPadding(20,10,20,10);
                    SsnHdrTxtVav.setLayoutParams(new LinearLayoutCompat.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT));
                    SsnHdrTxtVav.setTextColor(SsnHdrTxtClr);
                    SsnHdrTxtVav.setTextSize(20);
                    SsnHdrTxtVav.setTextAlignment(View.TEXT_ALIGNMENT_CENTER);
                    SsnHdrVav.addView(SsnHdrTxtVav);
                    LyoSyzFixFnc(SsnHdrVav, SupLysLyoPsgVav);
                }
    
                for (int i = 0; i < SupLysLyoPsgVav.getChildCount(); i++)
                {
                    View DspSubVyuVav = SupLysLyoPsgVav.getChildAt(i);
                    final int LysItmIdxVal = SupLysLyoPsgVav.getChildAdapterPosition(DspSubVyuVav);
    
                    if (ChkSsnHasHdrFnc(LysItmIdxVal))
                    {
                        String title = GetSsnHdrTxtFnc(LysItmIdxVal);
                        SsnHdrTxtVav.setText(title);
    
                        DevSsnHdrFnc(SsnCanvasPsgVal, DspSubVyuVav, SsnHdrVav);
                    }
                }
            }
    
            boolean ChkSsnHasHdrFnc(int LysItmIdxPsgVal)
            {
                return LysItmIdxPsgVal == 0 ? true : SsnHdrRefIdxAryVar[LysItmIdxPsgVal] != SsnHdrRefIdxAryVar[LysItmIdxPsgVal - 1];
            }
    
            String GetSsnHdrTxtFnc(int LysItmIdxPsgVal)
            {
                return SsnHdrAryVar[SsnHdrRefIdxAryVar[LysItmIdxPsgVal]];
            }
    
            private void DevSsnHdrFnc(Canvas HdrCanvasPsgVal, View DspSubItmPsgVav, View SsnHdrPsgVav)
            {
                HdrCanvasPsgVal.save();
                if (SsnStkVab)
                    HdrCanvasPsgVal.translate(0,
                            Math.max(0, DspSubItmPsgVav.getTop() - SsnHdrPsgVav.getHeight()));
                else
                    HdrCanvasPsgVal.translate(0,
                            DspSubItmPsgVav.getTop() - SsnHdrPsgVav.getHeight());
    
                SsnHdrPsgVav.draw(HdrCanvasPsgVal);
                HdrCanvasPsgVal.restore();
            }
    
            private void LyoSyzFixFnc(View SsnHdrVyuPsgVal, ViewGroup SupLysLyoPsgVav)
            {
                int LysLyoWytVal = View.MeasureSpec.makeMeasureSpec(SupLysLyoPsgVav.getWidth(), View.MeasureSpec.EXACTLY);
                int LysLyoHytVal = View.MeasureSpec.makeMeasureSpec(SupLysLyoPsgVav.getHeight(), View.MeasureSpec.UNSPECIFIED);
    
                int SsnHdrWytVal = ViewGroup.getChildMeasureSpec(LysLyoWytVal,
                        SupLysLyoPsgVav.getPaddingLeft() + SupLysLyoPsgVav.getPaddingRight(),
                        SsnHdrVyuPsgVal.getLayoutParams().width);
                int SsnHdrHytVal = ViewGroup.getChildMeasureSpec(LysLyoHytVal,
                        SupLysLyoPsgVav.getPaddingTop() + SupLysLyoPsgVav.getPaddingBottom(),
                        SsnHdrVyuPsgVal.getLayoutParams().height);
    
                SsnHdrVyuPsgVal.measure(SsnHdrWytVal, SsnHdrHytVal);
    
                SsnHdrVyuPsgVal.layout(0, 0,
                        SsnHdrVyuPsgVal.getMeasuredWidth(),
                        SsnHdrVyuPsgVal.getMeasuredHeight());
            }
        }
    }
    

相关问题