美文网首页Android开发经验谈Android开发
RecyclerView 的 scrollbar 和 ItemD

RecyclerView 的 scrollbar 和 ItemD

作者: chendroid | 来源:发表于2019-08-08 20:10 被阅读43次

    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

    相关文章

      网友评论

        本文标题:RecyclerView 的 scrollbar 和 ItemD

        本文链接:https://www.haomeiwen.com/subject/tyufjctx.html