使用ItemDecoration为RecyclerView添加header

Posted 苦逼程序员_

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了使用ItemDecoration为RecyclerView添加header相关的知识,希望对你有一定的参考价值。

最近遇到了一个需求,在recycler view上面添加一个header view,可以随着recycler view一起滚动。咋一看起来这个需求好像没什么问题,搜一下recycler view header,一堆一堆”RecyclerView添加header正确方式”的博客文章。翻看了一下,也到gayhub找各种recycler view header相关的开源库,基本上实现方式都是使用ViewType区分header view和item view,在最上面放置一个不同于其他item的header view。

以上这种实现方式具体的示例就不写了,可以自行搜索。
在了解完这种实现方式后,再看详细需求,header view要固定第一个item上面不可点击不可被选中不可获得焦点
如果用常用的ViewType来做header view的话,由于其他item都需要可点击可选中可获得焦点,header要实现上面的需求就要做比较多的特殊处理,于是想到了ItemDecoration

ItemDecoration的功能是为Recycler View的item做装饰,item的边距就可以使用它做很好的控制,并且这种装饰本身就是不可点击不可选中不可获得焦点的,那只要做一个可以固定在第一个item上面的ItemDecoration即可,使用时很方便就能通过Recycler View addItemDecoration()方法添加,不需要在adapter中做更多的特殊处理。

RecyclerView.ItemDecoration

这个类包含三个方法:

  • onDraw(Canvas c, RecyclerView parent, State state)
  • onDrawOver(Canvas c, RecyclerView parent, State state)
  • getItemOffsets(Rect outRect, View view, RecyclerView parent, State state)

这里用尽量简单的总结描述帮助大家理解这三个方法。

getItemOffsets:

每一个子view在设置itemDecoration的时候都会进入这个回调,可以通过设置第一个参数Rect,为子view增加padding值,例如outRect.set(10,20,30,40),即为子view左边距增加10,上边距增加20,右边距增加30,下边距增加40。
为什么要增加边距?
是否增加边距可以由开发者自己决定。
如果绘制的装饰内容是在item的上下左右空白处,则为item增加边距再绘制装饰才不会遮盖住item原本显示的内容。
如果绘制的装饰内容是直接遮盖在item上面的,例如为item绘制半透明遮罩,这种情况则不需要为item增加边距。

onDraw 以及 onDrawOver:

关于这两个方法的原理,这里不会太具体的解释,只需要记住: decoration 的 onDraw,child view 的 onDraw,decoration 的 onDrawOver,这三者是依次发生的

也就是说,onDraw绘制的内容会在子item的下面,onDrawOver绘制的内容会在子item的上面,至于装饰内容要放在下面被子item覆盖,还是要放在上面覆盖子item由具体场景决定。


ItemDecoration详细原理请参考:
深入理解 RecyclerView 系列之一:ItemDecoration

HeaderDecoration代码

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;


public class HeaderDecoration extends RecyclerView.ItemDecoration 
    private static final String TAG = HeaderDecoration.class.getSimpleName();
    private final int mOrientation;
    private View header;
    private Context mContext;
    private int mLayout;

    public HeaderDecoration(Context context, int orientation, int layout)
        mContext = context;
        mOrientation = orientation;
        mLayout = layout;
    

    @Override
    public void onDrawOver(Canvas canvas, RecyclerView parent, RecyclerView.State state) 

        if (parent.getChildCount() > 0) 
            final View child = parent.getChildAt(0);
            // 如果第一个item已经被回收,没有显示在recycler view上,则不需要draw header
            if (parent.getChildAdapterPosition(child) != 0) 
                return;
            
            // 使header始终画在第一个item上方
            if (mOrientation == LinearLayoutManager.VERTICAL) 
                // 垂直平移
                canvas.translate(0, child.getTop() - header.getHeight());
             else 
                // 水平平移
                canvas.translate(child.getLeft() - header.getWidth(), 0);
            
        else
            // 如果recycler view中没有item, getItemOffsets不会执行
            // 需要在此初始化header view
            if(header == null) 
                initHeader(parent);
            
        
        header.draw(canvas);
    

    // 初始化并计算header view的宽高以及位置
    public void initHeader(RecyclerView parent) 
        if (header == null) 
            //TODO - recycle views
            header = LayoutInflater.from(mContext).inflate(mLayout, parent, false);
            if (header.getLayoutParams() == null) 
                header.setLayoutParams(new ViewGroup.LayoutParams(
                        ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
            

            int widthSpec;
            int heightSpec;

            if (mOrientation == LinearLayoutManager.VERTICAL) 
                widthSpec = View.MeasureSpec.makeMeasureSpec(parent.getWidth(), View.MeasureSpec.EXACTLY);
                heightSpec = View.MeasureSpec.makeMeasureSpec(parent.getHeight(), View.MeasureSpec.UNSPECIFIED);
             else 
                widthSpec = View.MeasureSpec.makeMeasureSpec(parent.getWidth(), View.MeasureSpec.UNSPECIFIED);
                heightSpec = View.MeasureSpec.makeMeasureSpec(parent.getHeight(), View.MeasureSpec.EXACTLY);
            

            int childWidth = ViewGroup.getChildMeasureSpec(widthSpec,
                    parent.getPaddingLeft() + parent.getPaddingRight(), header.getLayoutParams().width);
            int childHeight = ViewGroup.getChildMeasureSpec(heightSpec,
                    parent.getPaddingTop() + parent.getPaddingBottom(), header.getLayoutParams().height);

            header.measure(childWidth, childHeight);
            header.layout(0, 0, header.getMeasuredWidth(), header.getMeasuredHeight());
        
    

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
                               RecyclerView.State state) 
        // 初始化header view
        if(header == null) 
            initHeader(parent);
        
        // 对第一个item最偏移
        if(parent.getChildAdapterPosition(view) == 0) 
            // 垂直布局则把item自顶部向下偏移,偏移距离为header的高度
            // 横向布局则把item自左向右偏移,偏移距离为header的宽度
            if (mOrientation == LinearLayoutManager.VERTICAL) 
                outRect.set(0, header.getMeasuredHeight(), 0, 0);
            else 
                outRect.set(header.getMeasuredWidth(), 0, 0, 0);
            
        
    

使用示例

recyclerView.addItemDecoration(new HeaderDecoration(this, LinearLayoutManager.VERTICAL, R.layout.item_top_decor));
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="#f00">
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:textSize="60sp"
        android:includeFontPadding="false"
        android:text="Header"/>
</LinearLayout>

代码中的注释已经写得比较清晰,结合上面讲到的ItemDecoration三个方法,设计流程大致为:

  1. 在getItemOffsets中为第一个item增加上边距值,距离为header的高度,使header不会跟第一个item重叠
  2. 在onDrawOver中绘制header,使用onDrawOver意味着header会绘制在子view的上层
  3. 使用canvas.translate()对绘制的header做平移处理,使其始终保持在第一个item的上面

ps:

  1. 这里使用了onDrawOver,意味着Header会绘制在子View的上面,如果不对canvas做平移处理则Header会叠加到子View上层,可以使用这种方式做始终悬停在顶部的Header。
  2. 在上面有两处位置做了initHeader()的操作。如果Recycler View里面有子View,则初始化会在getItemOffsets中进行,因为增加item的边距需要先初始化并计算出header的宽高。如果Recycler View里面没有子View,则getItemOffsets是不会执行的,所以只能放到onDrawOver中进行初始化,这样做的效果是即使没有子View,也能显示Header。

最后上两张效果图:


以上是关于使用ItemDecoration为RecyclerView添加header的主要内容,如果未能解决你的问题,请参考以下文章

为什么要使用ItemDecoration

为什么要使用ItemDecoration

使用ItemDecoration为RecyclerView添加header

深入RecyclerView-为什么要使用ItemDecoration

Android 仿微信通讯录 导航分组列表-上使用ItemDecoration为RecyclerView打造带悬停头部的分组列表

markdown 使用自定义ItemDecoration为Android RecyclerView GridLayoutManager提供相等的列间距