前言
RecyclerView
在Android
开发中非常常用,如果能结合ItemDecoration
类使用,那么将大大提高RecyclerView
的表现效果- 本文全面解析了
ItemDecoration
类,包括ItemDecoration
类简介、使用方法 & 实例讲解,希望你们会喜欢。ItemDecoration
类属于RecyclerView
的高级用法,阅读本文前请先学习RecyclerView
的使用:
目录
1. ItemDecoration类 简介
1.1 定义
RecyclerView
类的静态内部类
###1.2 作用
向RecyclerView
中的 ItemView
添加装饰 即绘制更多内容,丰富
ItemView
的UI
效果
2. 具体使用
ItemDecoration
类中仅有3个方法,具体如下:
public class TestDividerItemDecoration extends RecyclerView.ItemDecoration { // 方法1:getItemOffsets() // 作用:设置ItemView的内嵌偏移长度(inset) @Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { ... } // 方法2:onDraw() // 作用:在子视图上设置绘制范围,并绘制内容 // 类似平时自定义View时写onDraw()一样 // 绘制图层在ItemView以下,所以如果绘制区域与ItemView区域相重叠,会被遮挡 @Override public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { ... } // 方法3:onDrawOver() // 作用:同样是绘制内容,但与onDraw()的区别是:绘制在图层的最上层 @Override public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) { ...}复制代码
下面,我将详细介绍这3个方法。
2.1 getItemOffsets()
2.1.1 作用
设置ItemView的内嵌偏移长度(inset)
- 如图,其实
RecyclerView
中的 ItemView 外面会包裹着一个矩形(outRect
) - 内嵌偏移长度 是指:该矩形(
outRect
)与ItemView
的间隔
- 内嵌偏移长度分为4个方向:上、下、左、右,并由
outRect
中的top、left、right、bottom
参数 控制top、left、right、bottom
参数默认 = 0,即矩形和Item重叠,所以看起来矩形就消失了
2.1.2 具体使用
@Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { // 参数说明: // 1. outRect:全为 0 的 Rect(包括着Item) // 2. view:RecyclerView 中的 视图Item // 3. parent:RecyclerView 本身 // 4. state:状态 outRect.set(50, 0, 0,50); // 4个参数分别对应左(Left)、上(Top)、右(Right)、下(Bottom) // 上述语句代表:左&下偏移长度=50px,右 & 上 偏移长度 = 0 ... }复制代码
2.1.3 源码分析
RecyclerView
本质上是一个自定义ViewGroup
,子视图child
= 每个ItemView
- 其通过
LayoutManager
测量并布局ItemView
public void measureChild(View child, int widthUsed, int heightUsed) {// 参数说明: // 1. child:要测量的子view(ItemView) // 2. widthUsed: 一个ItemView的所有ItemDecoration占用的宽度(px) // 3. heightUsed:一个ItemView的所有ItemDecoration占用的高度(px) final LayoutParams lp = (LayoutParams) child.getLayoutParams(); final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child); // 累加当前ItemDecoration 4个属性值->>分析1 widthUsed += insets.left + insets.right; // 计算每个ItemView的所有ItemDecoration的宽度 heightUsed += insets.top + insets.bottom; // 计算每个ItemView的所有ItemDecoration的高度 final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(), getPaddingLeft() + getPaddingRight() + widthUsed, lp.width, canScrollHorizontally()); // 测量child view(ItemView)的宽度 // 第三个参数设置 child view 的 padding,即ItemView的Padding // 而该参数把 insets 的值算进去,所以insets 值影响了每个 ItemView 的 padding值 // 高度同上 final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(), getPaddingTop() + getPaddingBottom() + heightUsed, lp.height, canScrollVertically()); if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) { child.measure(widthSpec, heightSpec); }}// 分析完毕,请跳出<-- 分析1:getItemDecorInsetsForChild()-->Rect getItemDecorInsetsForChild(View child) { final LayoutParams lp = (LayoutParams) child.getLayoutParams(); insets.set(0, 0, 0, 0); for (int i = 0; i < decorCount; i++) { mTempRect.set(0, 0, 0, 0); // 获取getItemOffsets() 中设置的值 mItemDecorations.get(i).getItemOffsets(mTempRect, child, this, mState); // 将getItemOffsets() 中设置的值添加到insets 变量中 insets.left += mTempRect.left; insets.top += mTempRect.top; insets.right += mTempRect.right; insets.bottom += mTempRect.bottom; } // 最终返回 return insets;}// insets介绍 // 1. 作用: // a. 把每个ItemView的所有 ItemDecoration 的 getItemOffsets 中设置的值累加起来,(每个ItemView可添加多个ItemDecoration) // 即把每个ItemDecoration的left, top, right, bottom 4个属性分别累加 // b. 记录上述结果 // c. inset就像padding和margin一样,会影响view的尺寸和位置 // 2. 使用场景:设置View的边界大小,使得其大小>View的背景大小 // 如 按钮图标(View的背景)较小,但是我们希望按钮有较大的点击热区(View的边界大小)// 返回到分析1进来的原处复制代码
总结
- 结论:
outRect
4个属性值影响着ItemView
的Padding值 - 具体过程:在
RecyclerView
进行子View
宽高测量时(measureChild()
),会将getItemOffsets()
里设置的outRect
4个属性值(Top、Bottom、Left、Right
)通过insert
值累加 ,并最终添加到子View
的Padding
属性中
2.2 onDraw()
2.2.1 作用
通过 Canvas
对象绘制内容
2.2.2 具体使用
- 使用方法类似自定义View时的
onDraw()
请看我写的自定义View文章:
@Override public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { .... // 使用类似自定义View时的 onDraw()}复制代码
2.2.3 特别注意
注意点1:Itemdecoration
的onDraw()
绘制会先于ItemView
的onDraw()
绘制,所以如果在Itemdecoration
的onDraw()
中绘制的内容在ItemView
边界内,就会被ItemView
遮挡住。如下图:
此现象称为
onDraw()
的OverDraw
现象
解决方案:配合前面的 getItemOffsets()
一起使用在outRect
矩形 与 ItemView
的间隔区域 绘制内容
即:通过
getItemOffsets()
设置与Item
的间隔区域,从而获得与ItemView
不重叠的绘制区域
注意点2: getItemOffsets()
针对是每一个 ItemView
的,而 onDraw()
针对 RecyclerView
本身
解决方案:在 使用onDraw()
绘制时,需要先遍历RecyclerView
的所有ItemView
分别获取它们的位置信息,然后再绘制内容
- 此处遍历的
RecyclerView
的ItemView
(即Child view
),并不是Adapter
设置的每一个item
,而是可见的item
- 因为只有可见的
Item
才是RecyclerView
的Child view
@Override public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { // RecyclerView 的左边界加上 paddingLeft距离 后的坐标位置 final int left = parent.getPaddingLeft(); // RecyclerView 的右边界减去 paddingRight 后的坐标位置 final int right = parent.getWidth() - parent.getPaddingRight(); // 即左右边界就是 RecyclerView 的 ItemView区域 // 获取RecyclerView的Child view的个数 final int childCount = parent.getChildCount(); // 设置布局参数 final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child .getLayoutParams(); // 遍历每个RecyclerView的Child view // 分别获取它们的位置信息,然后再绘制内容 for (int i = 0; i < childCount; i++) { final View child = parent.getChildAt(i); int index = parent.getChildAdapterPosition(view); // 第一个Item不需要绘制 if ( index == 0 ) { continue; } // ItemView的下边界:ItemView 的 bottom坐标 + 距离RecyclerView底部距离 +translationY final int top = child.getBottom() + params.bottomMargin + Math.round(ViewCompat.getTranslationY(child)); // 绘制分割线的下边界 = ItemView的下边界+分割线的高度 final int bottom = top + mDivider.getIntrinsicHeight(); mDivider.setBounds(left, top, right, bottom); mDivider.draw(c); }}}复制代码
2.2.4 应用场景
在丰富 ItemView
的显示效果,即在ItemView
的基础上绘制内容
如分割线等等
2.2.5 实例讲解
- 实例说明:在
ItemView
设计一个高度为10 px
的红色分割线 - 思路
- 通过
getItemOffsets()
设置与Item
的下间隔区域 =10 px
设置好
onDraw()
可绘制的区域 - 通过
onDraw()
绘制一个高度 =10px
的矩形(填充颜色=红色)
- 通过
- 具体实现
步骤1:自定义ItemDecoration类
ItemDecoration.java
public class DividerItemDecoration extends RecyclerView.ItemDecoration { private Paint mPaint; // 在构造函数里进行绘制的初始化,如画笔属性设置等 public DividerItemDecoration() { mPaint = new Paint(); mPaint.setColor(Color.RED); // 画笔颜色设置为红色 } // 重写getItemOffsets()方法 // 作用:设置矩形OutRect 与 Item 的间隔区域 @Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { super.getItemOffsets(outRect, view, parent, state); int itemPosition = parent.getChildAdapterPosition(view); // 获得每个Item的位置 // 第1个Item不绘制分割线 if (itemPosition != 0) { outRect.set(0, 0, 0, 10); // 设置间隔区域为10px,即onDraw()可绘制的区域为10px } } // 重写onDraw() // 作用:在间隔区域里绘制一个矩形,即分割线 @Override public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { super.onDraw(c, parent, state); // 获取RecyclerView的Child view的个数 int childCount = parent.getChildCount(); // 遍历每个Item,分别获取它们的位置信息,然后再绘制对应的分割线 for ( int i = 0; i < childCount; i++ ) { // 获取每个Item的位置 final View child = parent.getChildAt(i); int index = parent.getChildAdapterPosition(child); // 第1个Item不需要绘制 if ( index == 0 ) { continue; } // 获取布局参数 final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child .getLayoutParams(); // 设置矩形(分割线)的宽度为10px final int mDivider = 10; // 根据子视图的位置 & 间隔区域,设置矩形(分割线)的2个顶点坐标(左上 & 右下) // 矩形左上顶点 = (ItemView的左边界,ItemView的下边界) // ItemView的左边界 = RecyclerView 的左边界 + paddingLeft距离 后的位置 final int left = parent.getPaddingLeft(); // ItemView的下边界:ItemView 的 bottom坐标 + 距离RecyclerView底部距离 +translationY final int top = child.getBottom() + params.bottomMargin + Math.round(ViewCompat.getTranslationY(child)); // 矩形右下顶点 = (ItemView的右边界,矩形的下边界) // ItemView的右边界 = RecyclerView 的右边界减去 paddingRight 后的坐标位置 final int right = parent.getWidth() - parent.getPaddingRight(); // 绘制分割线的下边界 = ItemView的下边界+分割线的高度 final int bottom = top + mDivider; // 通过Canvas绘制矩形(分割线) c.drawRect(left,top,right,bottom,mPaint); } }}复制代码
步骤2:在设置RecyclerView时添加该分割线即可
Rv = (RecyclerView) findViewById(R.id.my_recycler_view); //使用线性布局 LinearLayoutManager layoutManager = new LinearLayoutManager(this); Rv.setLayoutManager(layoutManager); Rv.setHasFixedSize(true); // 通过自定义分割线类 添加分割线 Rv.addItemDecoration(new DividerItemDecoration()); //为ListView绑定适配器 myAdapter = new MyAdapter(this,listItem); Rv.setAdapter(myAdapter); myAdapter.setOnItemClickListener(this);复制代码
2.2.6 结果展示
2.2.7 源码地址
Carson_Ho的Github地址:
2.3 onDrawOver()
2.3.1 作用
- 与
onDraw()
类似,都是绘制内容 - 但与
onDraw()
的区别是:Itemdecoration
的onDrawOver()
绘制 是后于ItemView
的onDraw()
绘制- 即不需要考虑绘制内容被
ItemView
遮挡的问题,反而ItemView
会被onDrawOver()
绘制的内容遮挡 - 绘制时机比较:
Itemdecoration.onDraw()
>ItemView.onDraw()
>Itemdecoration.onDrawOver()
- 即不需要考虑绘制内容被
2.3.2 具体使用
- 使用方法类似自定义View时的
onDraw()
请看我写的自定义View文章:
@Override public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) { .... // 使用类似自定义View时的 onDraw()}复制代码
2.3.3 应用场景
在 RecyclerView
/ 特定的 ItemView
上绘制内容,如蒙层、重叠内容等等
2.3.4 实例讲解
- 实例说明:在
RecyclerView
上每个ItemView
上叠加一个角标
- 具体代码实现
步骤1:自定义 ItemDecoration
类
public class DividerItemDecoration extends RecyclerView.ItemDecoration { private Paint mPaint; private Bitmap mIcon; // 在构造函数里进行绘制的初始化,如画笔属性设置等 public DividerItemDecoration(Context context) { mPaint = new Paint(); mPaint.setColor(Color.RED); // 画笔颜色设置为红色 // 获取图片资源 mIcon = BitmapFactory.decodeResource(context.getResources(), R.mipmap.logo); } // 重写onDrawOver() // 将角度绘制到ItemView上 @Override public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) { super.onDrawOver(c, parent, state); // 获取Item的总数 int childCount = parent.getChildCount(); // 遍历Item for ( int i = 0; i < childCount; i++ ) { // 获取每个Item的位置 View view = parent.getChildAt(i); int index = parent.getChildAdapterPosition(view); // 设置绘制内容的坐标(ItemView的左边界,ItemView的上边界) // ItemView的左边界 = RecyclerView 的左边界 = paddingLeft距离 后的位置 final int left = parent.getWidth()/2; // ItemView的上边界 float top = view.getTop(); // 第1个ItemView不绘制 if ( index == 0 ) { continue; } // 通过Canvas绘制角标 c.drawBitmap(mIcon,left,top,mPaint); } }}复制代码
步骤2:在设置RecyclerView时添加即可
Rv = (RecyclerView) findViewById(R.id.my_recycler_view); //使用线性布局 LinearLayoutManager layoutManager = new LinearLayoutManager(this); Rv.setLayoutManager(layoutManager); Rv.setHasFixedSize(true); //用自定义分割线类设置分割线 Rv.addItemDecoration(new DividerItemDecoration()); //为ListView绑定适配器 myAdapter = new MyAdapter(this,listItem); Rv.setAdapter(myAdapter); myAdapter.setOnItemClickListener(this);复制代码
2.3.5 结果展示
2.3.6 源码地址
Carson_Ho的Github地址:
有一个非常使用的自定义View是基于RecyclerView ItemDecoration类的,具体请看文章
3. 总结
我用一张图总结
RecyclerView ItemDecoration
类的使用下一篇文章我将对讲解
Android
的相关知识,有兴趣可以继续关注