目录

Recyclerview学习系类之ItemDecoration(一)

Google官方解释

  • An ItemDecoration allows the application to add a special drawing and layout offset to specific item views from the adapter’s data set. This can be useful for drawing dividers between items, highlights, visual grouping boundaries and more.

  • All ItemDecorations are drawn in the order they were added, before the item views (in onDraw() and after the items (in onDrawOver(Canvas, RecyclerView, RecyclerView.State).

个人理解:

大致意思是:

  • ItemDecoration允许应用程序从适配器的数据集中为制定的view添加制定的图形和布局偏移量。该特性一般被用于在两个item之间绘制分割线,高亮度以及视觉分组等等。

  • 所有的ItemDecorations都按照它们被添加的顺序在item被绘制之前(在onDraw方法中)和在Items被绘制之后(在onDrawOver(Canvas,RecyclerView,RecyclerView.State))进行绘制。

    可以看到,ItemDecoration是相当强大和灵活的。

method学习:

[getItemOffsets](https://developer.android.com/reference/android/support/v7/widget/RecyclerView.ItemDecoration.html#getItemOffsets(android.graphics.Rect, int, android.support.v7.widget.RecyclerView))(Rect outRect, int itemPosition, RecyclerView parent) This method was deprecated in API level 22.0.0. Use [getItemOffsets(Rect, View, RecyclerView, State)](https://developer.android.com/reference/android/support/v7/widget/RecyclerView.ItemDecoration.html#getItemOffsets(android.graphics.Rect, android.view.View, android.support.v7.widget.RecyclerView, android.support.v7.widget.RecyclerView.State))

  • 该方法在API 22.0.0之后已被废弃,我们可以看代替的方法

[getItemOffsets](https://developer.android.com/reference/android/support/v7/widget/RecyclerView.ItemDecoration.html#getItemOffsets(android.graphics.Rect, android.view.View, android.support.v7.widget.RecyclerView, android.support.v7.widget.RecyclerView.State))(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) Retrieve any offsets for the given item.

  • 我们可以通过该方法中的outRect来设置item的padding值。比如你要在item底部添加一条分割线,此时为了不影响item原来的布局参数,我们一般会返回一个地步padding为某个pd的outRect,在recyclerview绘制item的时候会讲该布局数据加入,我们原来的item就会多出一个底部padding,是不是解耦的很完美呢?

[onDraw](https://developer.android.com/reference/android/support/v7/widget/RecyclerView.ItemDecoration.html#onDraw(android.graphics.Canvas, android.support.v7.widget.RecyclerView))(Canvas c, RecyclerView parent) _This method was deprecated in API level 22.0.0. Override [onDraw(Canvas, RecyclerView, RecyclerView.State)](https://developer.android.com/reference/android/support/v7/widget/RecyclerView.ItemDecoration.html#onDraw(android.graphics.Canvas, android.support.v7.widget.RecyclerView, android.support.v7.widget.RecyclerView.State)) _

  • 该方法也已经过期了,看下面的

[onDraw](https://developer.android.com/reference/android/support/v7/widget/RecyclerView.ItemDecoration.html#onDraw(android.graphics.Canvas, android.support.v7.widget.RecyclerView, android.support.v7.widget.RecyclerView.State))(Canvas c, RecyclerView parent, RecyclerView.State state) Draw any appropriate decorations into the Canvas supplied to the RecyclerView.

  • 该方法会在绘制item之前调用,也就是说他的层级是在item之下的,通过该方法,我们可以爱绘制item之前绘制我们需要的内容。

[onDrawOver](https://developer.android.com/reference/android/support/v7/widget/RecyclerView.ItemDecoration.html#onDrawOver(android.graphics.Canvas, android.support.v7.widget.RecyclerView, android.support.v7.widget.RecyclerView.State))(Canvas c, RecyclerView parent, RecyclerView.State state) Draw any appropriate decorations into the Canvas supplied to the RecyclerView.

  • 该方法已过期,看下面的

[onDrawOver](https://developer.android.com/reference/android/support/v7/widget/RecyclerView.ItemDecoration.html#onDrawOver(android.graphics.Canvas, android.support.v7.widget.RecyclerView))(Canvas c, RecyclerView parent) _This method was deprecated in API level 22.0.0. Override [onDrawOver(Canvas, RecyclerView, RecyclerView.State)](https://developer.android.com/reference/android/support/v7/widget/RecyclerView.ItemDecoration.html#onDrawOver(android.graphics.Canvas, android.support.v7.widget.RecyclerView, android.support.v7.widget.RecyclerView.State)) _

  • 该方法于onDrawOver类似,在绘制item之后会调用该方法。

此时,也许你会疑问,他真的是这样执行的么?为了一探究竟,我们来看下源码吧。

1
2
3
4
5
6
7
 @Override
public void draw(Canvas c) {
    super.draw(c);
    final int count = mItemDecorations.size();
    for (int i = 0; i < count; i++) {
        mItemDecorations.get(i).onDrawOver(c, this, mState);
    }...}
  • 从recyclerview的源码中我们可以看到,在draw方法中后会遍历recyclerview里面的itemDecoration然后调用itemdecoration的onDrawOver方法;而recyclerview调用了super.draw(c)之后会先,父类会先调用recyclerview的onDraw方法;

    1
    2
    3
    4
    5
    6
    7
    
     @Override
    public void onDraw(Canvas c) {
      super.onDraw(c);
      final int count = mItemDecorations.size();
      for (int i = 0; i < count; i++) {
          mItemDecorations.get(i).onDraw(c, this, mState);
      } }
    
  • 在recyclerview的onDraw里又会调用itemDecoration的onDraw方法,当recyclerview的onDraw方法执行完之后,recyclerview的draw方法中super.draw(c);后面的代码才会继续执行,而recyclerview是在绘制了自己之后才会去绘制item。

  • 结论:itemDecoration的onDraw方法在item绘制之前调用,itemDecoration的onDrawOver方法在绘制item之后调用。

接下来我们在看下getItemOffsets这个方法。他真的把我们的outRect加到item的布局参数里面了么?预知真相,看源码。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
   Rect getItemDecorInsetsForChild(View child) {
    final LayoutParams lp = (LayoutParams) child.getLayoutParams();
    if (!lp.mInsetsDirty) {
        return lp.mDecorInsets;
    }

    if (mState.isPreLayout() && (lp.isItemChanged() || lp.isViewInvalid())) {
        // changed/invalid items should not be updated until they are rebound.
        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这个方法在recyclerview的getItemDecorInsetsForChild 中被调用,该方法会把所有的itemDecortion中的rect累加后返回;我们再看下getItemDecorInsetsForChild在哪被调用的。

    1
    2
    3
    4
    
      public void measureChild(View child, int widthUsed, int heightUsed) {
          final LayoutParams lp = (LayoutParams) child.getLayoutParams();
    
          final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
    
  • 在measureChild方法中被调用,也就是recyclerview在测量childView的时候

    1
    2
    3
    4
    
      public void measureChildWithMargins(View child, int widthUsed, int heightUsed) {
          final LayoutParams lp = (LayoutParams) child.getLayoutParams();
    
          final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
    
  • 使用margins测量childView时会用到

  • 结论,在getItemOffsets方法中outRect会影响到recyclerview中childView的布局。

使用ItemDecoration实现分割线的都调用过addItemDecoration方法。发现,只要调用一次addItemDecoration将自定义的分割线ItemDecoration添加进去就可以实现分割线效果了,如果我们添加多次会如何呢?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public void addItemDecoration(ItemDecoration decor, int index) {
    if (mLayout != null) {
        mLayout.assertNotInLayoutOrScroll("Cannot add item decoration during a scroll  or"
                + " layout");
    }
    if (mItemDecorations.isEmpty()) {
        setWillNotDraw(false);
    }
    if (index < 0) {
        mItemDecorations.add(decor);
    } else {
        mItemDecorations.add(index, decor);
    }
    markItemDecorInsetsDirty();
    requestLayout();
}
  • 从RecyclerView.addItemDecoration方法源码可以看到,内部使用了一个ArrayList类型的mItemDecorations存储我们添加的所有ItemDecoration。markItemDecorInsetsDirty方法有什么用呢?我们看下源码

    1
    2
    3
    4
    5
    6
    7
    8
    
    void markItemDecorInsetsDirty() {
      final int childCount = mChildHelper.getUnfilteredChildCount();
      for (int i = 0; i < childCount; i++) {
          final View child = mChildHelper.getUnfilteredChildAt(i);
          ((LayoutParams) child.getLayoutParams()).mInsetsDirty = true;
      }
      mRecycler.markItemDecorInsetsDirty();
    }
    
  • 里面有一个mInsetsDirty被重置为true,最终调用mRecycler.markItemDecorInsetsDirty();我们继续看mRecycler.markItemDecorInsetsDirty();方法源码:

    void markItemDecorInsetsDirty() {

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
          final int cachedCount = mCachedViews.size();
          for (int i = 0; i < cachedCount; i++) {
              final ViewHolder holder = mCachedViews.get(i);
              LayoutParams layoutParams = (LayoutParams) holder.itemView.getLayoutParams();
              if (layoutParams != null) {
                  layoutParams.mInsetsDirty = true;
              }
          }
      }
    
  • 里面也是将layoutParams的mInsetsDirty重置为true,这个mInsetsDirty有什么用呢 ?我们继续看源码:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    
    Rect getItemDecorInsetsForChild(View child) {
      final LayoutParams lp = (LayoutParams) child.getLayoutParams();
      if (!lp.mInsetsDirty) {
          return lp.mDeorInsets;
      }
    
      if (mState.isPreLayout() && (lp.isItemChanged() || lp.isViewInvalid())) {
          // changed/invalid items should not be updated until they are rebound.
          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;
    }
    
  • 看到这段代码感觉应该是它了,可以看到,

    • 判断childView的layoutParams的mInsetsDirty是不是false 是false直接返回mDecorInsets。
    • 判断itemDecoration是否已改变或者已不可用,mState.isPreLayout是recyclerview用来处理动画的。
    • 如果前面的都不是,就会从新调用itemDecoration的getItemOffsets方法,重新计算layout偏离值之后返回。
  • 出于性能的考虑,如果之前为ChildView生成过DecorInsets,那么会缓存在ChildView的LayoutParam中(mDecorInsets), 同时为了保证mDecorInsets的时效性,还同步维护了一个mInsetsDirty标记在LayoutParam中

  • 在获取ChidlView的DecorInsets时,如果其mInsetsDirty为false,那么代表缓存没有过期,直接返回缓存的mDecorInsets。

  • 如果mInsetsDirty为true,表示缓存已过期,需要根据ItemDecoration集合重新生成

    • 添加或者删除ItemDecoration的时候,会将所有ChildView包括Recycler中的mInsetsDirty设置为true来使DecorInsets缓存失效

总结:其实getItemDecorInsetsForChild方法我们之前在本章前面有分析到。他就是在测量childView的时候会调用,所以如果我们的itemDecortion中途需要更新,我们需要调用markItemDecorInsetsDirty方法,然后调用requestLayout请求重新绘制,这样在重新绘制childView的时候,就会重新计算ItemDecortion中返回的layout偏离值。达到我们想要的效果。