RecyclerView 的 scrollbar 和 ItemDecoration 的绘制和遮挡问题
前言
RecyclerView 是自带 scrollbar 的, 可自定义设置它的展示与方向还有属性「scrollbarStyle」。
RecyclerView 的 ItemDecoration 很方便,可以为每个 item 之间添加分割线, 那么分割线的绘制是怎么绘制的呢?与 item view 的绘制顺序是什么样的呢?
以下内容分为三部分:
-
scrollbar的属性scrollbarStyle -
ItemDecoration自定义分割线的注意事项和绘制顺序 - 两者之间可能产生的问题
1. scrollbar 的属性 scrollbarStyle
在 RecyclerView 里面 scrollbar 的属性 是支持直接在 xml 中设置属性的 scrollbarStyle
如下代码:
<com.android.base.widget.ZRecyclerView
android:id="@+id/recycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbarStyle="insideOverlay"
android:scrollbars="vertical"/>
注:
ZRecyclerView简单继承自RecyclerView
1.1 android:scrollbarStyle 取值
android:scrollbarStyle 有四种:
insideOverlay默认值
表示在padding区域内并且覆盖在view上
这里的view都是指RecyclerViewinsideInset
表示在padding区域内并且插入在view后面outsideOverlay
表示在padding区域外并且覆盖在view上outsideInset
表示在padding区域外并且插入在view后面
假设设置的 RecyclerView 属性为上面代码所示,且不为它设置 padding
当 android:scrollbarStyle="insideInset|outsideInset" 时,
利用 Layout inspector的到的布局显示结果图:

会发现,RecyclerView 会额外造成 RecyclerView 多了一个 paddingRight = 11
> 注: 11 为像素值,本质是 `scrollbar` 的宽度,`4 dp`
当 android:scrollbarStyle="insideOverlay|outsideOverlay" 时,
利用 Layout inspector的到的布局显示结果图:

会发现 RecyclerView 并没有多余的 padding。
1.2 源码分析
首先 android:scrollbarStyle 对应的 java 方法是 View.setScrollBarStyle(), 在该方法中,对 mViewFlags 进行了赋值。
在 View 的源码中,setPadding(xxx) 的实现中,最后一行会调用 internalSetPadding(left, top, right, bottom)
在 internalSetPadding(xxx)方法中, 会根据 mViewFlags 对 进行判断,会对 mPaddingRight 进行 + offset 添加偏移值「getVerticalScrollbarWidth()」

结论:除非有必要,且已知的情况下,请不要使用 android:scrollbarStyle="insideInset|outsideInset", 默认的属性为 insideOverlay 可以满足我们的需要。
当修改 android:scrollbarStyle 时,会对 RecyclerView 里面的子 item 的宽有影响「宽度减少」,布局上产生影响。
2. ItemDecoration 自定义分割线的注意事项和绘制顺序
自定义分割线时,需要继承 RecyclerView.ItemDecoration 并且实现三个方法:
-
onDraw(xxx)
利用canvas可以画出你想要的分割线样式canvas.drawRect(left.toFloat(), top.toFloat(), right.toFloat(), (top + mDividerHeight).toFloat(), mPaint) onDrawOver(xxx)
利用canvas可以画出你想要的分割线样式-
getItemOffsets(xxx)这里是设置
item view绘制区域的偏移值
在 onDraw(xxx) 和 onDrawOver(xxx) 里面都可以让我们去画出分割线,那么这两个方法的区别是什么呢?从名字上来看,onDrawOver(xxx) 绘制的时机应该比 onDraw(xxx) 要晚。
2.1 绘制顺序
那么具体的实现呢?源码:
在 RecyclerView 的 draw(xxx) 方法里的代码片段:

在 draw() 里面首先调用了 super.draw(xxx) 「完成绘制 RecyclerView 和它里面的子 view」
具体逻辑如下,不再详细的分析源码:

2.2 总结一下绘制顺序为:
- 先绘制
RecyclerView自身; - 再调用
ItemDecoration.onDraw(); - 再调用了
RecyclerView里面的子view; - 调用了
ItemDecoration.onDrawOver().
所以,如果我们自定义 ItemDecoration 是在 onDraw() 里面画的分割线,那么会早与 item view 的绘制;
所以,如果我们自定义 ItemDecoration 是在 onDrawOver() 里面画的分割线,那么会晚与 item view 的绘制;
2.3 覆盖问题
既然绘制有先后,那么就会存在被覆盖的问题。
当对 getItemOffsets(xxx) 方法不做任何操作时,
当在
ItemDecoration.onDraw()方法里画分割线时,画出来的效果,会被item view覆盖, 即有可能看不出分割线「与没添加分割线一样」当在
ItemDecoration.onDrawOver()方法里画分割线时,画出来的效果,会遮挡item view部分区域
假设,是在卡片下方画分割线,那么画出来的效果是:分割线遮挡住item view的底部位置。
上述两个问题,并不是我们实际想要的效果,我们想要的分割线效果是不影响 item view 的展示。
所以, 特别重要的是,我们需要重写 getItemOffsets(xxx) 这个方法,添加我们想要的分割线的 offset
2.4 getItemOffsets(xxx) 的重写
官方源码,示例如下:
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
RecyclerView.State state) {
if (mDivider == null) {
outRect.set(0, 0, 0, 0);
return;
}
if (mOrientation == VERTICAL) {
outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
} else {
outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
}
}
当需要在竖直方向上依次画分割线时,添加的偏移值是 mDivider.getIntrinsicHeight() 就是我们想要的分割线的高度。
因为我们需要在 getItemOffsets(xxx) 方法中,添加我们想要的分割线的宽度给 outRect 的 offset.
3. 两者之间可能产生的问题
RecyclerView.scrollbar 和 RecyclerView.ItemDecoration 之间会产生什么问题呢?
-
在列表滑动的过程中,分割线会覆盖在
scrollbar的上面如果分割线的样式「颜色」和
scrollbar的差别很大,那么会产生的视觉效果是:当滑动到两个卡片的交界处「分割线的地方」,「分割线」分割开了scrollbar, 十分的丑。 RecyclerView.ItemDecoration分割线并未完全画满屏幕的宽度「即使是match_parent」
3.1 在列表滑动的过程中,分割线会覆盖在 scrollbar 的上面
如图:

可猜测问题出在:ItemDecoration 绘制的时机晚与 scrollbar 绘制的时机,导致分割线覆盖在了 scrollbar 上面。
那么 scrollbar 的绘制时机是在哪里呢?源码中,View 的 onDraw() 里部分代码如下:
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
drawAutofilledHighlight(canvas);
// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
// Step 7, draw the default focus highlight
drawDefaultFocusHighlight(canvas);
...
}
在 step 6 中,调用了 onDrawForeground(xxx), 而在这个方法中,调用了
// 绘制 `scrollbar` 的位置
onDrawScrollIndicators(canvas);
onDrawScrollBars(canvas);
这是绘制 scrollbar 的位置。
那么,我们就知道了:scrollbar 的绘制晚与 item view 的 onDraw, 早与 ItemDecoration 的 onDrawOver().
根据上面我们的分析,
出现该问题的原因是:自定义的 ItemDecoration 分割线绘制是在 onDrawOver()这个里面绘制的。
正确的解决办法: 把绘制分割线时机放在 ItemDecoration.onDraw()这个时机,就可以解决该问题。
错误的解决办法: 设置 RecyclerView 的 android:scrollbarStyle="insideInset|outsideInset"。这样会导致 3.2 的问题 ,
3.2 RecyclerView.ItemDecoration 分割线并未完全画满屏幕的宽度「即使是 match_parent」
从上面,我们也知道了,当设置 RecyclerView 的 android:scrollbarStyle="insideInset|outsideInset"时,就会额外为 RecyclerView 添加一个 paddingRight, 导致分割线未绘制全屏。
解决办法: 不要使用 android:scrollbarStyle="insideInset|outsideInset"
总结
以上内容,其实都是对 RecyclerView 里面的一些属性的研究,有些内容很细节,
往往不是那么引人注意,但真的可能会造成很困扰的问题,Android 里面的一些源码设计里面,还是蛮有逻辑在的。
上述的问题,本质上还是 view 的绘制引起的,所以界面遇到遮档问题时,不妨想一想绘制顺序。
水平有限,文中有些内容可能存在错误,如有,大胆指出,哪个程序员还没翻过车 ~_~
参考链接
- 有关
ItemDecoration的绘制顺序 -
RecyclerView.java源码 - RecyclerView之ItemDecoration