使用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三个方法,设计流程大致为:
- 在getItemOffsets中为第一个item增加上边距值,距离为header的高度,使header不会跟第一个item重叠
- 在onDrawOver中绘制header,使用onDrawOver意味着header会绘制在子view的上层
- 使用canvas.translate()对绘制的header做平移处理,使其始终保持在第一个item的上面
ps:
- 这里使用了onDrawOver,意味着Header会绘制在子View的上面,如果不对canvas做平移处理则Header会叠加到子View上层,可以使用这种方式做始终悬停在顶部的Header。
- 在上面有两处位置做了initHeader()的操作。如果Recycler View里面有子View,则初始化会在getItemOffsets中进行,因为增加item的边距需要先初始化并计算出header的宽高。如果Recycler View里面没有子View,则getItemOffsets是不会执行的,所以只能放到onDrawOver中进行初始化,这样做的效果是即使没有子View,也能显示Header。
最后上两张效果图:
以上是关于使用ItemDecoration为RecyclerView添加header的主要内容,如果未能解决你的问题,请参考以下文章
使用ItemDecoration为RecyclerView添加header
深入RecyclerView-为什么要使用ItemDecoration
Android 仿微信通讯录 导航分组列表-上使用ItemDecoration为RecyclerView打造带悬停头部的分组列表
markdown 使用自定义ItemDecoration为Android RecyclerView GridLayoutManager提供相等的列间距