RecyclerView 的 scrollbar 和 ItemDecoration 的绘制和遮挡问题

RecyclerViewscrollbarItemDecoration 的绘制和遮挡问题

前言

RecyclerView 是自带 scrollbar 的, 可自定义设置它的展示与方向还有属性「scrollbarStyle」。

RecyclerViewItemDecoration 很方便,可以为每个 item 之间添加分割线, 那么分割线的绘制是怎么绘制的呢?与 item view绘制顺序是什么样的呢?

以下内容分为三部分:

  1. scrollbar 的属性 scrollbarStyle
  2. ItemDecoration 自定义分割线的注意事项和绘制顺序
  3. 两者之间可能产生的问题

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 有四种:

  1. insideOverlay 默认值
    表示在 padding 区域内并且覆盖在 view
    这里的 view 都是指 RecyclerView

  2. insideInset
    表示在 padding 区域内并且插入在 view 后面

  3. outsideOverlay
    表示在 padding 区域外并且覆盖在 view

  4. outsideInset
    表示在 padding 区域外并且插入在 view 后面

假设设置的 RecyclerView 属性为上面代码所示,且不为它设置 padding

android:scrollbarStyle="insideInset|outsideInset" 时,

利用 Layout inspector的到的布局显示结果图:

layout-inspect

会发现,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 并且实现三个方法:

  1. onDraw(xxx)
    利用 canvas 可以画出你想要的分割线样式

    canvas.drawRect(left.toFloat(), top.toFloat(), right.toFloat(), (top + mDividerHeight).toFloat(), mPaint)
    
  2. onDrawOver(xxx)
    利用 canvas 可以画出你想要的分割线样式

  3. getItemOffsets(xxx)

    这里是设置 item view 绘制区域的偏移值

onDraw(xxx)onDrawOver(xxx) 里面都可以让我们去画出分割线,那么这两个方法的区别是什么呢?从名字上来看,onDrawOver(xxx) 绘制的时机应该比 onDraw(xxx) 要晚。

2.1 绘制顺序

那么具体的实现呢?源码:
RecyclerViewdraw(xxx) 方法里的代码片段:

代码片段

draw() 里面首先调用了 super.draw(xxx) 「完成绘制 RecyclerView 和它里面的子 view

具体逻辑如下,不再详细的分析源码:

屏幕快照 2019-08-08 下午8.05.38.png

2.2 总结一下绘制顺序为:

  1. 先绘制 RecyclerView 自身;
  2. 再调用 ItemDecoration.onDraw()
  3. 再调用了 RecyclerView 里面的子 view
  4. 调用了 ItemDecoration.onDrawOver().

所以,如果我们自定义 ItemDecoration 是在 onDraw() 里面画的分割线,那么会早与 item view 的绘制;

所以,如果我们自定义 ItemDecoration 是在 onDrawOver() 里面画的分割线,那么会晚与 item view 的绘制;

2.3 覆盖问题

既然绘制有先后,那么就会存在被覆盖的问题。

当对 getItemOffsets(xxx) 方法不做任何操作时,

  1. 当在 ItemDecoration.onDraw() 方法里画分割线时,画出来的效果,会被 item view 覆盖, 即有可能看不出分割线「与没添加分割线一样」

  2. 当在 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) 方法中,添加我们想要的分割线的宽度给 outRectoffset.

3. 两者之间可能产生的问题

RecyclerView.scrollbarRecyclerView.ItemDecoration 之间会产生什么问题呢?

  1. 在列表滑动的过程中,分割线会覆盖在 scrollbar 的上面

    如果分割线的样式「颜色」和 scrollbar 的差别很大,那么会产生的视觉效果是:当滑动到两个卡片的交界处「分割线的地方」,「分割线」分割开了 scrollbar, 十分的丑。

  2. RecyclerView.ItemDecoration 分割线并未完全画满屏幕的宽度「即使是 match_parent

3.1 在列表滑动的过程中,分割线会覆盖在 scrollbar 的上面

如图:

分割线错误效果

可猜测问题出在:ItemDecoration 绘制的时机晚与 scrollbar 绘制的时机,导致分割线覆盖在了 scrollbar 上面。

那么 scrollbar 的绘制时机是在哪里呢?源码中,ViewonDraw() 里部分代码如下:

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 viewonDraw, 早与 ItemDecorationonDrawOver().

根据上面我们的分析,

出现该问题的原因是:自定义的 ItemDecoration 分割线绘制是在 onDrawOver()这个里面绘制的。

正确的解决办法: 把绘制分割线时机放在 ItemDecoration.onDraw()这个时机,就可以解决该问题。

错误的解决办法: 设置 RecyclerViewandroid:scrollbarStyle="insideInset|outsideInset"。这样会导致 3.2 的问题 ,

3.2 RecyclerView.ItemDecoration 分割线并未完全画满屏幕的宽度「即使是 match_parent

从上面,我们也知道了,当设置 RecyclerViewandroid:scrollbarStyle="insideInset|outsideInset"时,就会额外为 RecyclerView 添加一个 paddingRight, 导致分割线未绘制全屏。

解决办法: 不要使用 android:scrollbarStyle="insideInset|outsideInset"

总结

以上内容,其实都是对 RecyclerView 里面的一些属性的研究,有些内容很细节,
往往不是那么引人注意,但真的可能会造成很困扰的问题,Android 里面的一些源码设计里面,还是蛮有逻辑在的。

上述的问题,本质上还是 view 的绘制引起的,所以界面遇到遮档问题时,不妨想一想绘制顺序。

水平有限,文中有些内容可能存在错误,如有,大胆指出,哪个程序员还没翻过车 ~_~

参考链接

  1. 有关 ItemDecoration 的绘制顺序
  2. RecyclerView.java 源码
  3. RecyclerView之ItemDecoration
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容