持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第7天,点击查看活动详情
目录
- Android 源码浅析:RecyclerView 源码浅析(1)—— 回收、复用、预加载机制
- Android 源码浅析:RecyclerView 源码浅析(2)—— 测量、布局、绘制、预布局
- Android 源码浅析:RecyclerView 源码浅析(3)—— LayoutManager
- Android 源码浅析:RecyclerView 源码浅析(4)—— ItemDecoration
- Android 源码浅析:RecyclerView 源码浅析(5)—— ItemAnimator
- Android 源码浅析:RecyclerView 源码浅析(6)—— Adapter
前言
上一篇博客内容对 LayoutManager 进行了一定的了解,末尾 Demo 中自定义 LayoutManager 实现的分页导航,仅仅实现了布局,一般来说在底部会有一个分页指示器,这篇内容就来利用 ItemDecoration 来实现这个需求,ItemDecoration 在我看来算是 RecyclerView 中比较浅显易懂的部分了。
源码分析
由于 ItemDecoration 的源码很简单,直接进入源码:
public abstract static class ItemDecoration {
public void onDraw(Canvas c, RecyclerView parent,State state) {
onDraw(c, parent);
}
public void onDrawOver(Canvas c, RecyclerView parent, State state) {
onDrawOver(c, parent);
}
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) {
getItemOffsets(outRect, ((LayoutParams)view.getLayoutParams()).getViewLayoutPosition(), parent);
}
// 方法标记删除 使用上面的 onDraw
@Deprecated
public void onDraw(Canvas c, RecyclerView parent) {
}
// 方法标记删除 使用上面的 onDrawOver
@Deprecated
public void onDrawOver(Canvas c, RecyclerView parent) {
}
// 方法标记删除 使用上面的 getItemOffsets
@Deprecated
public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
outRect.set(0, 0, 0, 0);
}
}
六个方法,三个标记删除,那么针对三个方法挨着查看其调用时机即可。
onDraw、onDrawOver
这两个方法,一眼看上去就是绘制对吧?说到绘制,在第二篇博客中分析 RecyclerView 绘制流程时调用过这两个方法,回顾下 RecyclerView 的绘制,首先是 draw 方法:
RecyclerView.java
public void draw(Canvas c) {
super.draw(c);
// draw 方法上来就循环 mItemDecorations 逐个调用了 onDrawOver 方法
// mItemDecorations 就不贴代码了 就是个 ArrayList 存储 ItemDecoration 对象
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
mItemDecorations.get(i).onDrawOver(c, this, mState);
}
// ...
}
View 的 draw 方法中会调用 onDraw,那么接着看一下 onDraw 方法:
RecyclerView.java
public void onDraw(Canvas c) {
super.onDraw(c);
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
// 同样是遍历 这次调用的是 ItemDecoration 的 onDraw 方法
mItemDecorations.get(i).onDraw(c, this, mState);
}
}
调用顺序注意一下, RecyclerView 的 draw 方法先调用了 super.draw(),在父类 View 的 draw 方法中先调用了 onDraw,然后再执行RecyclerView 重写后剩余 draw 方法中的代码,所以 ItemDecoration onDraw 方法是先执行,onDrawOver 则是后执行。
getItemOffsets
public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
outRect.set(0, 0, 0, 0);
}
先来个示意图来理解一下 getItemOffsets 中 outRect 的含义:
getItemOffsets 设置偏移前后的区别:
就是给 itemView 设置了一个内部偏移,说的再简单点就是 itemView 内容的绘制区域缩小了。
接着查看下其在 RecyclerView 的调用时机,在 RecyclerView 中搜索后发现仅有一处调用:
RecyclerView.java
Rect getItemDecorInsetsForChild(View child) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
// ...
// 从 LayoutParams 中取出
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 进行累加
insets.left += mTempRect.left;
insets.top += mTempRect.top;
insets.right += mTempRect.right;
insets.bottom += mTempRect.bottom;
}
lp.mInsetsDirty = false;
return insets;
}
可以看出每个 ItemDecoration 设置的偏移量都进行累加存储在了 LayoutParams 的 mDecorInsets 中,继续查看下 getItemDecorInsetsForChild 的调用时机:
RecyclerView.java
// 测量 View
public void measureChild(@NonNull View child, int widthUsed, int heightUsed) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
// 获取 mDecorInsets 累加完成后的结果
final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
widthUsed += insets.left + insets.right;
heightUsed += insets.top + insets.bottom;
final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(),
getPaddingLeft() + getPaddingRight() + widthUsed, lp.width,
canScrollHorizontally());
final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(),
getPaddingTop() + getPaddingBottom() + heightUsed, lp.height,
canScrollVertically());
if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
child.measure(widthSpec, heightSpec);
}
}
// 和上面 measureChild 是一样的
public void measureChildWithMargins(@NonNull View child, int widthUsed, int heightUsed) {
//...
// 区别在于 widthSpec heightSpec 计算时增加了 Margin
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());
//...
}
从上面的代码不难看出,在测量子 View(也就是 itemView)时,整体占用的宽高是包含设置的偏移量的。
示意图
最后再来个示意图将三个方法结合起来理解下:
- onDraw 绘制的内容在最底层,itemView 绘制在其内容之上,而 onDrawOver 绘制的内容在最上层;
- 图中 onDrawOver 虽然绘制在了 itemView 之中,但其实可以超出 itemView,我怕会有歧义特别说明一下;
- 如果想让 onDraw 绘制的内容不被 itemView 遮挡就重写 getItemOffsets 方法设置合适的偏移;
自定义 ItemDecoration
ItemDecoration 源码比较简单,也比较好理解,趁热打铁将上一篇内容中未完成的分页进度条部分通过自定义 ItemDecoration 来实现一下加深理解。
效果图
思路
进度条首先要保证不被 item 遮挡,那么绘制的逻辑要写在 onDrawOver 保证其绘制在最上层。其次也要保证进度条不遮挡 item,需要重写 getItemOffsets 对 item 设置底部偏移。
绘制逻辑比较简单用 drawLine 画进度条背景、进度条即可。进度等于 当滑动距离 / 最大滑动距离 ,获取这两个值可以通过 RecyclerView 的 重写 computeHorizontalScrollOffset 和 computeHorizontalScrollRange 两个方法,可以看下源码:
RecyclerView.java
public class RecyclerView {
// ...
public int computeHorizontalScrollOffset() {
if (mLayout == null) {
return 0;
}
return mLayout.canScrollHorizontally() ? mLayout.computeHorizontalScrollOffset(mState) : 0;
}
@Override
public int computeHorizontalScrollRange() {
if (mLayout == null) {
return 0;
}
return mLayout.canScrollHorizontally() ? mLayout.computeHorizontalScrollRange(mState) : 0;
}
// ...
}
可以看出 RecyclerView 又交给了 LayoutManager 去获取,而 LayoutManager 中两个方法默认返回 0,需要我们自己重写实现计算。
实现
上一篇博客中自定义的 LayoutManager 并没有实现 computeHorizontalScrollOffset 和 computeHorizontalScrollRange 两个方法,这两个方法默认返回 0 需要手动实现:
class NavigationGridLayoutManager : RecyclerView.LayoutManager() {
// ...
override fun computeHorizontalScrollOffset(state: RecyclerView.State): Int {
return mOffsetHorizontal
}
override fun computeHorizontalScrollRange(state: RecyclerView.State): Int {
return mMaxOffsetHorizontal
}
}
接着自定义 ItemDecoration:
class NavigationItemDecoration : RecyclerView.ItemDecoration() {
private val mPaint = Paint().apply {
strokeCap = Paint.Cap.ROUND
}
override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
super.onDrawOver(c, parent, state)
// 滑动偏移量
val scrollOffset = parent.computeHorizontalScrollOffset().toFloat()
// 最大滑动距离
val scrollRange = parent.computeHorizontalScrollRange().toFloat()
// 进度条高度
val progressHeight = 4f.dp
// 进度条整体宽度
val progressBgWidth = 100f.dp
// 进度条宽度
val progressWidth = 10f.dp
// 先画进度条背景
mPaint.color = Color.LTGRAY
mPaint.strokeWidth = progressHeight
val progressBgStartX = parent.width / 2f - progressBgWidth / 2
val progressBgEndX = parent.width / 2f + progressBgWidth / 2
val progressBgStartY = parent.height - progressHeight
val progressBgEndY = parent.height - progressHeight
c.drawLine(progressBgStartX, progressBgStartY, progressBgEndX, progressBgEndY, mPaint)
// 画进度条
mPaint.color = Color.GREEN
val progressStartX = min(
progressBgEndX - progressWidth,
progressBgStartX + progressBgWidth * (scrollOffset / scrollRange)
)
val progressEndX = progressStartX + progressWidth
val progressStartY = progressBgStartY
val progressEndY = progressBgEndY
c.drawLine(progressStartX, progressStartY, progressEndX, progressEndY, mPaint)
}
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
// 设置底部偏移 防止被遮挡
outRect.set(0, 0, 0, 10.dp)
}
}
最后
ItemDecoration 是比较简单的一部分,只需了解 onDraw、onDrawOver、getItemOffsets 三个方法的作用,就能通过自定义 ItemDecoration 实现分割线、滚动条等等效果。
如果我的博客分享对你有点帮助,不妨点个赞支持下!