深刻理解 RecyclerView 系列之一:ItemDecoration

RecyclerView 已经推出了一年多了,平常开发中也已经完全从 ListView 迁移到了 RecyclerView,但前两天有人在一个安卓群里面问了个关于最顶上的 item view 加蒙层的问题,被人用 ItemDecoration 完美解决。此时我发现本身对 RecyclerView 的使用一直太过基本,更深刻更强大的功能彻底没有涉及,像 ItemDecoration, ItemAnimator, SmoothScroller, OnItemTouchListener, LayoutManager 之类,以及 RecyclerView 重用 view 的原理。网上也有不少对 RecyclerView 使用的讲解博客,要么讲的内容很是少,要么提到了高级功能,可是并没讲代码为何这样写,每一个方法和参数的含义是什么,像张鸿洋的博客,也讲了 ItemDecoration 的使用,可是看了仍然云里雾里,只能把他的代码拿来用,并不能根据本身的需求编写本身的 ItemDecoration。html

在这个系列中,我将对上述各个部分进行深刻研究,目标就是看了这一系列的文章以后,开发者能够清楚快捷的根据本身的需求,编写本身须要的各个高级模块。本系列第一篇就聚焦在:RecyclerView.ItemDecoration。本文涉及到的完整代码能够在 Github 获取java

TL; DR

  • getItemOffsets 中为 outRect 设置的4个方向的值,将被计算进全部 decoration 的尺寸中,而这个尺寸,被计入了 RecyclerView 每一个 item view 的 padding 中
  • 在 onDraw 为 divider 设置绘制范围,并绘制到 canvas 上,而这个绘制范围能够超出在 getItemOffsets 中设置的范围,但因为 decoration 是绘制在 child view 的底下,因此并不可见,可是会存在 overdraw
  • decoration 的 onDraw,child view 的 onDraw,decoration 的 onDrawOver,这三者是依次发生的
  • onDrawOver 是绘制在最上层的,因此它的绘制位置并不受限制

RecyclerView.ItemDecoration

这个类包含三个方法 1android

  • onDraw(Canvas c, RecyclerView parent, State state)
  • onDrawOver(Canvas c, RecyclerView parent, State state)
  • getItemOffsets(Rect outRect, View view, RecyclerView parent, State state)

getItemOffsets

官方样例的 DividerItemDecoration 里面是这样实现的:git

[代码]java代码:

1github

2canvas

3ide

4函数

5测试

if (mOrientation == VERTICAL_LIST) {ui

    outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());

} else {

    outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);

}

这个outRect设置的四个值是什么意思呢?先来看看它是在哪里调用的,它在RecyclerView中惟一被调用的地方就是 getItemDecorInsetsForChild(View child) 函数。

[代码]java代码:

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

Rect getItemDecorInsetsForChild(View child) {

    final LayoutParams lp = (LayoutParams) child.getLayoutParams();

    if (!lp.mInsetsDirty) {

        return lp.mDecorInsets;

    }

 

    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.left += mTempRect.left;

        insets.top += mTempRect.top;

        insets.right += mTempRect.right;

        insets.bottom += mTempRect.bottom;

    }

    lp.mInsetsDirty = false;

    return insets;

}

 

能够看到,getItemOffsets 函数中设置的值被加到了 insets 变量中,并被该函数返回,那么 insets 又是啥呢?

insets 是啥?

根据Inset Drawable文档,它的使用场景是:当一个view须要的背景小于它的边界时。例如按钮图标较小,可是咱们但愿按钮有较大的点击热区,一种作法是使用ImageButton,设置background="@null",把图标资源设置给src属性,这样ImageButton能够大于图标,而不会致使图标也跟着拉伸到ImageButton那么大。那么使用Inset drawable也能达到这样的目的。可是相比之下有什么优点呢?src属性也能设置selector drawable,因此点击态也不是问题。也许惟一的优点就是更“优雅”吧 :)

回到正题,getItemDecorInsetsForChild 函数中会重置 insets 的值,并从新计算,计算方式就是把全部 ItemDecoration 的 getItemOffsets 中设置的值累加起来 2,而这个 insets 其实是 RecyclerView 的 child 的 LayoutParams 中的一个属性,它会在 getTopDecorationHeight,getBottomDecorationHeight 等函数中被返回,那么这个 insets 的意义就很明显了,它记录的是全部 ItemDecoration 所须要的 3尺寸的总和。

而在 RecyclerView 的 measureChild(View child, int widthUsed, int heightUsed) 函数中,调用了 getItemDecorInsetsForChild,并把它算在了 child view 的 padding 中。

[代码]java代码:

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

public void measureChild(View child, int widthUsed, int heightUsed) {

    final LayoutParams lp = (LayoutParams) child.getLayoutParams();

 

    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);

    }

}

上面这段代码中调用 getChildMeasureSpec 函数的第三个参数就是 child view 的 padding,而这个参数就把 insets 的值算进去了。那么如今就能够确认了,getItemOffsets 中为 outRect 设置的4个方向的值,将被计算进全部 decoration 的尺寸中,而这个尺寸,被计入了 RecyclerView 每一个 item view 的 padding 中。

PoC

这一步测试主要是对 getItemOffsets 函数传入的 outRect 参数各个值的设置,以证明上述分析的结论。

getItemOffsets测试结果

能够看到,当 left, top, right, bottom 所有设置为50时,RecyclerView 的每一个 item view 各个方向的 padding 都增长了,对比各类状况,确实 getItemOffsets 中为 outRect 设置的值都将被计入 RecyclerView 每一个 item view 的 padding 中。

onDraw

先来看看官方样例的 DividerItemDecoration 实现:

[代码]java代码:

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

public void drawVertical(Canvas c, RecyclerView parent) {

    final int left = parent.getPaddingLeft();

    final int right = parent.getWidth() - parent.getPaddingRight();

    final int childCount = parent.getChildCount();

    for (int i = 0; i < childCount; i++) {

        final View child = parent.getChildAt(i);

        final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child

                .getLayoutParams();

        final int top = child.getBottom() + params.bottomMargin +

                Math.round(ViewCompat.getTranslationY(child));

        final int bottom = top + mDivider.getIntrinsicHeight();

        mDivider.setBounds(left, top, right, bottom);

        mDivider.draw(c);

    }

}

drawVertical 是为纵向的 RecyclerView 绘制 divider,遍历每一个 child view 4 ,把 divider 绘制到 canvas 上,而 mDivider.setBounds 则设置了 divider 的绘制范围。其中,left 设置为 parent.getPaddingLeft(),也就是左边是 parent 也就是 RecyclerView 的左边界加上 paddingLeft 以后的位置,而 right 则设置为了 RecyclerView 的右边界减去 paddingRight 以后的位置,那这里左右边界就是 RecyclerView 的内容区域 5了。top 设置为了 child 的 bottom 加上 marginBottom 再加上 translationY,这其实就是 child view 的下边界 6,bottom 就是 divider 绘制的下边界了,它就是简单地 top 加上 divider 的高度。

PoC

这一步测试主要是对 onDraw 函数中对 divider 的绘制边界的设置。

onDraw 测试结果

能够看到,当咱们把 left, right, top 7 设置得和官方样例同样,bottom 设置为 top + 25,注意,这里 getItemOffsets 对 outSets 的设置只有 bottom = 50,也就是 decoration 高度为50,咱们能够看到,decoration 的上半部分就绘制为黑色了,下半部分没有绘制。而若是设置top = child.getBottom() + params.bottomMargin - 25bottom = top + 50,就会发现 child view 的底部出现了 overdraw。因此这里咱们能够得出结论:在 onDraw 为 divider 设置绘制范围,并绘制到 canvas 上,而这个绘制范围能够超出在 getItemOffsets 中设置的范围,但因为 decoration 是绘制在 child view 的底下,因此并不可见,可是会存在 overdraw。

onDrawOver

有一点须要注意:decoration 的 onDraw,child view 的 onDraw,decoration 的 onDrawOver,这三者是依次发生的。而因为 onDrawOver 是绘制在最上层的,因此它的绘制位置并不受限制(固然,decoration 的 onDraw 绘制范围也不受限制,只不过不可见),因此利用 onDrawOver 能够作不少事情,例如为 RecyclerView 总体顶部绘制一个蒙层,或者为特定的 item view 绘制蒙层。这里就不单独进行测试了,请见下一节的总体效果。

All in together

实现的效果:除了最后一个 item view,底部都有一个高度为25的黑色 divider,为整个 RecyclerView 的顶部绘制了一个渐变的蒙层。效果图以下:

总体效果

小结

  • getItemOffsets 中为 outRect 设置的4个方向的值,将被计算进全部 decoration 的尺寸中,而这个尺寸,被计入了 RecyclerView 每一个 item view 的 padding 中
  • 在 onDraw 为 divider 设置绘制范围,并绘制到 canvas 上,而这个绘制范围能够超出在 getItemOffsets 中设置的范围,但因为 decoration 是绘制在 child view 的底下,因此并不可见,可是会存在 overdraw
  • decoration 的 onDraw,child view 的 onDraw,decoration 的 onDrawOver,这三者是依次发生的
  • onDrawOver 是绘制在最上层的,因此它的绘制位置并不受限制

脚注

  1. 不算被 Deprecated 的方法 

  2. 把 left, top, right, bottom 4个属性分别累加 

  3. 也就是在 getItemOffsets 函数中为 outRect 参数设置的4个属性值 

  4. child view,并非 adapter 的每个 item,只有可见的 item 才会绘制,才是 RecyclerView 的 child view 

  5. 能够类比 CSS 的盒子模型,一个 view 包括 content, padding, margin 三个部分,content 和 padding 加起来就是 view 的尺寸,而 margin 不会增长 view 的尺寸,可是会影响和其余 view 的位置间距,可是安卓的 view 没有 margin 的合并 

  6. bottom 就是 content 的下边界加上 paddingBottom,而为了避免“吃掉” child view 的底部边距,因此就加上 marginBottom,而 view 还能设置 translation 属性,用于 layout 完成以后的再次偏移,同理,为了避免“吃掉”这个偏移,因此也要加上 translationY 

  7. 这里因为并无对 child view 设置 translation,为了代码简短,就没有减去 translationY,其实是须要的 

相关文章
相关标签/搜索