1.概述
recyclerView的要实现分割线要继承RecyclerView.ItemDecoration
抽象类。谷歌官方为我们提供实例DividerItemDecoration
。ItmDecoration有三个重要的方法,如果只是想要设置分割线,只要getItemOffsets
就可以。
方法 | 作用 |
---|---|
getItemOffsets | 设置item上下左右的距离 |
onDraw | 可以通过它绘制分割线的颜色图片等,但它的区域超过item时会被item覆盖 |
onDrawOver | 同样可以通过它绘制颜色和图片等,但它的区域会覆盖item |
看看源码的注释:
public abstract static class ItemDecoration {
/**
*将任何适当的装饰画到提供给RecyclerView的Canvas中。
*用这种方法绘制的任何内容都将在绘制项目视图之前绘制,因此将出现在视图下
*/
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull State state) {
onDraw(c, parent);
}
/**
*将任何适当的装饰画到提供给RecyclerView的Canvas中。
* 用这种方法绘制的任何内容都将在绘制项目视图之后绘制,并因此出现在视图上方。
*/
public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent,
@NonNull State state) {
onDrawOver(c, parent);
}
/**
*检索给定项目的所有偏移量。
* outRect的每个字段指定插入项目视图的像素数,类似于填充或边距。
*默认实现将outRect的边界设置为0并返回。
*
* <p>
*如果需要访问适配器以获取其他数据,
*则可以调用{@link RecyclerView#getChildAdapterPosition(View)}
*以获取视图的适配器位置。
*/
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view,
@NonNull RecyclerView parent, @NonNull State state) {
getItemOffsets(outRect, ((LayoutParams) view.getLayoutParams()).getViewLayoutPosition(),
parent);
}
}
2.自定义ItemDecoration
自定一个ItemDecoration来实现一个可以吸顶效果的列表,看看效果图:
要实现这个效果,首先用onDraw
绘制一个红色背景的分割线,第二就是用onDrawOver
绘制吸顶的红色头部。
public class RecyclerViewDecoration extends RecyclerView.ItemDecoration {
private int groupHeaderHeight;
private Paint headPaint;
private Paint textPaint;
private Rect textRect;
public StarDecoration(Context context) {
groupHeaderHeight = dp2px(context, 100);
//画吸顶的画笔
headPaint = new Paint();
headPaint.setColor(Color.RED);
// 画文字的画笔
textPaint = new Paint();
textPaint.setTextSize(50);
textPaint.setColor(Color.WHITE);
textRect = new Rect();
}
@Override
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.onDraw(c, parent, state);
if (parent.getAdapter() instanceof StarAdapter) {
StarAdapter adapter = (StarAdapter) parent.getAdapter();
int left = parent.getPaddingLeft();
int right = parent.getWidth() - parent.getPaddingRight();
int count = parent.getChildCount(); // 当前屏幕的item个数
for (int i = 0; i < count; i++) {
// 获取对应i的View
View view = parent.getChildAt(i);
// 获取View的布局位置
int position = parent.getChildLayoutPosition(view);
// 是否绘制红色的头部分割线
boolean isGroupHeader = adapter.isGourpHeader(position);
if (isGroupHeader && view.getTop() - groupHeaderHeight - parent.getPaddingTop() >= 0) {
//在view的顶部画红色头部分割线
c.drawRect(left, view.getTop() - groupHeaderHeight, right, view.getTop(), headPaint);
//绘制文字
String groupName = adapter.getGroupName(position);
//得到文字需要的rect
textPaint.getTextBounds(groupName, 0, groupName.length(), textRect);
c.drawText(groupName, left + 20, view.getTop() -
groupHeaderHeight / 2 + textRect.height() / 2, textPaint);
} else if (view.getTop() - groupHeaderHeight - parent.getPaddingTop() >= 0) {
// 绘制红色分割线
c.drawRect(left, view.getTop() - 4, right, view.getTop(), headPaint);
}
}
}
}
@Override
public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.onDrawOver(c, parent, state);
if (parent.getAdapter() instanceof StarAdapter) {
StarAdapter adapter = (StarAdapter) parent.getAdapter();
// 返回可见区域内的第一个item的position
int position = ((LinearLayoutManager) parent.getLayoutManager()).findFirstVisibleItemPosition();
// 获取对应position的View
View itemView = parent.findViewHolderForAdapterPosition(position).itemView;
int left = parent.getPaddingLeft();
int right = parent.getWidth() - parent.getPaddingRight();
int top = parent.getPaddingTop();
// 当第二个是组的头部的时候
boolean isGroupHeader = adapter.isGourpHeader(position + 1);
//下一个红色吸顶头部位于第一个位置
if (isGroupHeader) {
int bottom = Math.min(groupHeaderHeight, itemView.getBottom() - parent.getPaddingTop());
c.drawRect(left, top, right, top + bottom, headPaint);//上一个不断缩短
String groupName = adapter.getGroupName(position);
c.clipRect(left, top, right, top + bottom);
textPaint.getTextBounds(groupName, 0, groupName.length(), textRect);
c.drawText(groupName, left + 20, top + bottom
- groupHeaderHeight / 2 + textRect.height() / 2, textPaint);
} else {
//绘制一个吸顶的红色头部
c.drawRect(left, top, right, top + groupHeaderHeight, headPaint);
//绘制文字
String groupName = adapter.getGroupName(position);
textPaint.getTextBounds(groupName, 0, groupName.length(), textRect);
c.drawText(groupName, left + 20, top + groupHeaderHeight / 2 + textRect.height() / 2, textPaint);
}
}
}
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
if (parent.getAdapter() instanceof StarAdapter) {
StarAdapter adapter = (StarAdapter) parent.getAdapter();
int position = parent.getChildLayoutPosition(view);
boolean isGroupHeader = adapter.isGourpHeader(position);
// 怎么判断 itemView是头部
if (isGroupHeader) {
// 如果是头部,预留更大的地方
outRect.set(0, groupHeaderHeight, 0, 0);
} else {
// 1像素
outRect.set(0, 4, 0, 0);
}
}
}
private int dp2px(Context context, float dpValue) {
float scale = context.getResources().getDisplayMetrics().density;
return (int) (dpValue * scale * 0.5f);
}
}
通过代码可以分为三步
- 通过
getItemOffsets
设置Item大和小的分割线- 在
onDraw
中为每个大小分割线绘制成红色,有文字的绘制文字- 在
onDrawOver
绘制一个吸顶红色头部,然后在下一个红色头部的前一个item变成第一个可见时,吸顶红色头部在[100,0]区间不断的缩小。
3.ItemDecoration的原理
要搞懂ItemDecoration的原理,主要有两方面:
1.
ItemDecoration
是怎样实现ItemView
之间的间距的
2.ItemDecoration
是如何绘制的
3.1 ItemDecoration如何实现间距
要理解ItemDecoration的间距,还要从RecyclerView
的测量和ItemView
的布局开始。测量主要是通过LayoutManager
的measureChildWithMargins
方法
public void measureChildWithMargins(@NonNull View child, int widthUsed, int heightUsed) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
//拿到全部分割线的左右上下全部总和
final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
//将需要的分割线总和大小加到宽高中
widthUsed += insets.left + insets.right;
heightUsed += insets.top + insets.bottom;
//确定总宽高
final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(),
getPaddingLeft() + getPaddingRight()
+ lp.leftMargin + lp.rightMargin + widthUsed, lp.width,
canScrollHorizontally());
final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(),
getPaddingTop() + getPaddingBottom()
+ lp.topMargin + lp.bottomMargin + heightUsed, lp.height,
canScrollVertically());
if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
child.measure(widthSpec, heightSpec);
}
}
这里有个getItemDecorInsetsForChild
方法,总要是拿到全部分割线上下左右全部相加的距离。 第二步是,把分割线的宽高总大小和ItemView宽高总大小进行相加
,并跟可滑动的方向确定RecyclerView的宽高。所以,全部分割线所需要宽高已经加到RecyclerView的宽高中
Rect getItemDecorInsetsForChild(View child) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (!lp.mInsetsDirty) {
return lp.mDecorInsets;
}
final boolean positionWillChange =
(lp.mViewHolder.getLayoutPosition() != lp.mViewHolder.getAbsoluteAdapterPosition());
if (mState.isPreLayout()
&& (lp.isItemChanged() || lp.isViewInvalid() || positionWillChange)) {
// changed/invalid items should not be updated until they are rebound.
return lp.mDecorInsets;
}
final Rect insets = lp.mDecorInsets;
insets.set(0, 0, 0, 0);
final int decorCount = mItemDecorations.size();
for (int i = 0; i < decorCount; i++) {
mTempRect.set(0, 0, 0, 0);
mItemDecorations.get(i).getItemOffsets(mTempRect, child, this, mState);
insets.left += mTempRect.left;
insets.top += mTempRect.top;
insets.right += mTempRect.right;
insets.bottom += mTempRect.bottom;
}
lp.mInsetsDirty = false;
return insets;
}
接下来,看看ItemView的布局。代码主要在LayoutManager
的layoutDecoratedWithMargins
中
public void layoutDecoratedWithMargins(@NonNull View child, int left, int top, int right,
int bottom) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
//拿到View的分割线大小
final Rect insets = lp.mDecorInsets;
//item布局时会在上下左右留出分割线的所需的大小
child.layout(left + insets.left + lp.leftMargin, top + insets.top + lp.topMargin,
right - insets.right - lp.rightMargin,
bottom - insets.bottom - lp.bottomMargin);
}
代码很简单,itemview在布局的时候,就已经留出分割线的大小了。所以我们的分割线的并不是onDraw
onDrawOver
绘制出来的,而是在ItemView
布局时就根据getItemOffsets
方法设置上下左右距离开始留出间距。所以我们自定义分割线时,只写getItemOffsets
方法也可以绘制分割线效果。
3.2 ItemDecoration的绘制
RecyclerView
的draw
绘制流程,主要就是绘制ItemDecoration
的onDraw
onDrawOver
,和绘制itemView
。
@Override
public void draw(Canvas c) {
//通过调用onDraw绘制item和ItemDecoration的onDraw()
super.draw(c);
//绘制ItemDecoration的onDrawOver()
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
mItemDecorations.get(i).onDrawOver(c, this, mState);
}
//padding滑动的处理
// TODO If padding is not 0 and clipChildrenToPadding is false, to draw glows properly, we
// need find children closest to edges. Not sure if it is worth the effort.
}
@Override
public void onDraw(Canvas c) {
super.onDraw(c);
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
mItemDecorations.get(i).onDraw(c, this, mState);
}
}
代码比较简单,不作分析。