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之后會調用該方法。

此時,也許你會疑問,他真的是這樣執行的么?為了一探究竟,我們來看下源碼吧。

 @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方法;

     @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的布局參數里面了么?預知真相,看源碼。

   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在哪被調用的。

      public void measureChild(View child, int widthUsed, int heightUsed) {
          final LayoutParams lp = (LayoutParams) child.getLayoutParams();
    
          final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
    
  • 在measureChild方法中被調用,也就是recyclerview在測量childView的時候
    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添加進去就可以實現分割線效果了,如果我們添加多次會如何呢?

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方法有什么用呢?我們看下源碼

    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() {
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有什么用呢 ?我們繼續看源碼:

    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;
    }
    
  • 看到這段代碼感覺應該是它了,可以看到,

    • 判斷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偏離值。達到我們想要的效果。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容