文章目录
ItemDecoration实现RecyclerView item吸顶效果
ItemDecoration
可以帮助我们绘制RecyclerView item
的分割线,吸顶效果就借助它来实现,效果如下:
再实战之前先弄清ItemDecoration
的原理
ItemDecoration的原理
首先定义一个CustomDecoration
类继承自ItemDecoration
,使用ItemDecoration
时,一般重写三个方法 getItemOffsets
,onDraw
,onDrawOver
对于RecyclerView
的使用不再赘述,其itemView
的样式如下:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black">
<TextView
android:id="@+id/tv"
android:layout_width="match_parent"
android:layout_height="60dp"
android:gravity="center"
android:textColor="@color/white"
android:textSize="20sp" />
</RelativeLayout>
未加入ItemDecoration
的实现效果
addItemDecoration
在使用ItemDecoration
肯定会去调用addItemDecoration
,看一下源码做了什么事情
RecyclerView#addItemDecoration
public void addItemDecoration(@NonNull ItemDecoration decor) {
addItemDecoration(decor, -1);
}
public void addItemDecoration(@NonNull ItemDecoration decor, int index) {
...
//往集合添加一个元素
mItemDecorations.add(decor);
...
}
getItemOffsets
给Item
绘制ItemDecoration
提供空间
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
//左,上,右,下分别代表给item多分配多大的空间,这里是给每个item的上面多要15个单位空间
outRect.set(0, 15, 0, 0);
}
效果图
为啥能多分配空间呢?
从fill
进行分析,fill
方法是RecyclerView
布局必走的方法对RecyclerView
进行填充,未来会在另一篇文章中进行解释,这里只需要知道fill是必走的即可
LinearLayoutManager#fill -> LinearLayoutManager#layoutChunk -> RecyclerView#measureChildWithMargins -> RecyclerView#getItemDecorInsetsForChild -> ItemDecoration.getItemOffsets
回到问题上,为啥就能预留空间呢? 从measureChildWithMargins开始分析
测量过程
RecyclerView#measureChildWithMargins
public void measureChildWithMargins(@NonNull View child, int widthUsed, int heightUsed) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
//返回Rect,根据返回的Rect测量,往下看getItemDecorInsetsForChild的解析
final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
//insets是集合了全部Decoration的预留信息,加起来也就是多测量了一些空间,要比之前的item大
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);
}
}
RecyclerView#getItemDecorInsetsForChild
Rect getItemDecorInsetsForChild(View child) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (!lp.mInsetsDirty) {
return lp.mDecorInsets;
}
if (mState.isPreLayout() && (lp.isItemChanged() || lp.isViewInvalid())) {
// 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();
//不一定只有一个Decoration,这里进行循环遍历
for (int i = 0; i < decorCount; i++) {
mTempRect.set(0, 0, 0, 0);
//getItemOffsets也就是对mTempRect进行操作,上面我们outRect.set(0, 15, 0, 0)也就是对mTempRect及逆行赋值
mItemDecorations.get(i).getItemOffsets(mTempRect, child, this, mState);
//把值添加进insets
insets.left += mTempRect.left;
insets.top += mTempRect.top;
insets.right += mTempRect.right;
insets.bottom += mTempRect.bottom;
}
lp.mInsetsDirty = false;
//返回insets
return insets;
}
布局过程
LinearLayoutManager#layoutChunk 执行完RecyclerView#measureChildWithMargins 会执行layoutDecoratedWithMargins
RecyclerView#layoutDecoratedWithMargins
//child是layoutChunk传过来的,代表正在布局哪个item
public void layoutDecoratedWithMargins(@NonNull View child, int left, int top, int right,
int bottom) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
//拿到view的ItemDecoration大小信息
final Rect insets = lp.mDecorInsets;
//算上Decoration的信息,一块计算上下足左右,在布局的时候也就把相应的那一块给预留了出来
child.layout(left + insets.left + lp.leftMargin, top + insets.top + lp.topMargin,
right - insets.right - lp.rightMargin,
bottom - insets.bottom - lp.bottomMargin);
}
onDraw
下层画布绘制
@Override
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.onDraw(c, parent, state);
if (parent.getAdapter() instanceof CustomAdapter) {
CustomAdapter adapter = (CustomAdapter) parent.getAdapter();
int count = parent.getChildCount();
int left = parent.getPaddingLeft();
int right = parent.getWidth() - parent.getPaddingRight();
for (int i = 0; i < count; i++) {
View view = parent.getChildAt(i);
int position = parent.getChildLayoutPosition(view);
c.drawRect(left, view.getTop() - 100, right, view.getTop(), headPaint);
}
}
}
效果图
onDrawOver
覆盖画布绘制
@Override
public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.onDrawOver(c, parent, state);
if (parent.getAdapter() instanceof CustomAdapter) {
CustomAdapter adapter = (CustomAdapter) parent.getAdapter();
int count = parent.getChildCount();
int left = parent.getPaddingLeft();
int right = parent.getWidth() - parent.getPaddingRight();
for (int i = 0; i < count; i++) {
View view = parent.getChildAt(i);
int position = parent.getChildLayoutPosition(view);
c.drawRect(left, view.getTop() - 80, right, view.getTop(), headPaint);
}
}
}
效果图
onDraw和onDrawOver对比
onDraw
没有覆盖原先的item
,onDrawOver
把item
覆盖掉了,为什么会有这个现象呢
view
在绘制的时候会先走draw
,draw
会走onDraw
RecyclerView#draw
@Override
public void draw(Canvas c) {
super.draw(c);
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
//调用onDrawOver进行覆盖绘制
mItemDecorations.get(i).onDrawOver(c, this, mState);
}
...
}
super.draw( c ) == View#draw
public void draw(Canvas canvas) {
...
onDraw(canvas);
//第二步先走ViewGroup的dispatchDraw,再调用drawChild,再到child.draw(canvas, this, drawingTime)绘制子view再回头看RecyclerView#draw
dispatchDraw(canvas);
...
}
RecyclerView#onDraw
@Override
public void onDraw(Canvas c) {
super.onDraw(c);
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
//第一步先走ItemDecoration的onDraw,再回头看View#draw
mItemDecorations.get(i).onDraw(c, this, mState);
}
}
综上分析,上方的效果是因为绘制方法的调用顺序不同
第一步 ItemDecoration
的onDraw
第二步 ItemView
的onDraw
第三步 ItemDecoration
的onDrawOver
吸顶实现
经上原理的分析,定义一个继承自RecyclerView.ItemDecoration
的类,重写上面三个方法即可实现。
数据Bean
只有两个属性名字和组名
现在分步实现文章开头的效果
给绘制区域预留空间
预留空间要重写getItemOffsets
方法,想要的效果是头部预留更大的空间,非头部需要预留一个小空间绘制分割线.
判断是否是头部
在预留之前需要判断是否是头部,这个方法封装在RecyclerView
的Adapter
比较合适,因为getItemOffsets
的参数返回了RecyclerView
和当前的ItemView
。
RecyclerView
提供了根据View
获取下标的方法,拿到下标即可在Adapter
拿到Bean
数据,根据Bean
即可判断是否是头部
CustomAdapter#isGourpHeader
//判断当前位置是否是顶部
public boolean isGourpHeader(int position) {
//如果是0直接就是顶
if (position == 0) {
return true;
} else {
//得到当前组名
String currentGroupName = getGroupName(position);
//拿到前一个组名
String preGroupName = getGroupName(position - 1);
//和前一个相同则为false
if (preGroupName.equals(currentGroupName)) {
return false;
} else {
return true;
}
}
}
public String getGroupName(int position) {
return dataList.get(position).getGroundName();
}
预留空间
CustomDecoration#getItemOffsets
@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 CustomAdapter) {
//获取adapter
CustomAdapter adapter = (CustomAdapter) parent.getAdapter();
//根据itemview获取下标
int position = parent.getChildLayoutPosition(view);
//调用之前封装的isGourpHeader方法
boolean isGroupHeader = adapter.isGourpHeader(position);
if (isGroupHeader) {
//为头设置100空间
outRect.set(0, groupHeaderHeight, 0, 0);
} else {
//不为头设置2空间
outRect.set(0, 2, 0, 0);
}
}
}
效果图
实现不吸顶的效果
根据之前onDraw
和onDrawOver
的解析,不吸顶需要实现onDraw
@Override
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.onDraw(c, parent, state);
if (parent.getAdapter() instanceof CustomAdapter) {
//获取adapter
CustomAdapter adapter = (CustomAdapter) parent.getAdapter();
//拿到全部的子View
int count = parent.getChildCount();
//获取左右位置
int left = parent.getPaddingLeft();
int right = parent.getWidth() - parent.getPaddingRight();
//对每个Decoration进行绘制
for (int i = 0; i < count; i++) {
//获取itemView的下标
View view = parent.getChildAt(i);
int position = parent.getChildLayoutPosition(view);
if (adapter.isGourpHeader(position)) {
//绘制背景
c.drawRect(left, view.getTop() - groupHeaderHeight, right, view.getTop(), headPaint);
String groupName = adapter.getGroupName(position);
//绘制文字
c.drawText(groupName, left + 20, view.getTop() -
groupHeaderHeight / 2, textPaint);
} else {
//绘制分割线
c.drawRect(left, view.getTop() - 2, right, view.getTop(), headPaint);
}
}
}
}
效果与上面预留空间一样只是颜色不同
实现吸顶
吸顶是在最上层画布绘制,因此重写onDrawOver
思路:LayoutManager
给我们提供了获取当前屏幕第一个ItemView
的下标的方法,可以获取下标意味着可以获取对应的itemView
。我们要思考的是啥时候会切换顶部置顶(根据以上的数据进行判断),当第二个为顶部且第一个itemView
的bottom
比ItemDecoration
的bottom
要小的时候,此时就需要将当前顶部顶上去切换下一组,这是满足切换顶部的唯一条件,看下面实现
@Override
public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.onDrawOver(c, parent, state);
if (parent.getAdapter() instanceof CustomAdapter) {
//获取Adapter
CustomAdapter adapter = (CustomAdapter) 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) {
//谁更靠近上方用谁的bottom
int bottom = Math.min(groupHeaderHeight, itemView.getBottom());
//绘制背景
c.drawRect(left, top, right, top + bottom, headPaint);
String groupName = adapter.getGroupName(position);
//绘制文字
c.drawText(groupName, left + 20, top + bottom
- groupHeaderHeight / 2, textPaint);
} else {
//置顶不变
c.drawRect(left, top, right, top + groupHeaderHeight, headPaint);
String groupName = adapter.getGroupName(position);
c.drawText(groupName, left + 20, top + groupHeaderHeight / 2, textPaint);
}
}
}
效果图
paddingTop的BUG
给RecyclerView
设置paddingTop
会导致下面情况发生:
]
先弄清楚此bug
是哪个方法绘制的,发现并没有覆盖,则一定是onDraw
绘制的,所以要修改onDraw
方法
修改绘制条件:
//当前view的顶部如果没有超过最上先就可以绘制
if(view.getTop() - groupHeaderHeight - parent.getPaddingTop() >= 0) {
//绘制操作
}
加上以后的效果图:
]
此时发现顶不上去了,是因为求较小值的代码失效了
//groupHeaderHeight永远比itemView.getBottom()小,导致失效
//int bottom = Math.min(groupHeaderHeight, itemView.getBottom());
//上方代码修改为
int bottom = Math.min(groupHeaderHeight, itemView.getBottom() - parent.getPaddingTop());
效果图
发现文字被顶上去了,根据之前文字绘制文章中的知识
在onDrawOver
绘制文字前对画布进行裁剪
c.clipRect(left, top, right, top + bottom);
最终效果
demo点赞加评论找我要哦
✨ 原 创 不 易 , 还 希 望 各 位 大 佬 支 持 一 下 \textcolor{blue}{原创不易,还希望各位大佬支持一下} 原创不易,还希望各位大佬支持一下
👍 点 赞 , 你 的 认 可 是 我 创 作 的 动 力 ! \textcolor{green}{点赞,你的认可是我创作的动力!} 点赞,你的认可是我创作的动力!
⭐️ 收 藏 , 你 的 青 睐 是 我 努 力 的 方 向 ! \textcolor{green}{收藏,你的青睐是我努力的方向!} 收藏,你的青睐是我努力的方向!
✏️ 评 论 , 你 的 意 见 是 我 进 步 的 财 富 ! \textcolor{green}{评论,你的意见是我进步的财富!} 评论,你的意见是我进步的财富!