深入理解 RecyclerView 系列之一:ItemDecoration


RecyclerView 已經推出了一年多了,日常開發中也已經徹底從 ListView 遷移到了 RecyclerView,但前兩天有人在一個安卓群里面問了個關於最頂上的 item view 加蒙層的問題,被人用 ItemDecoration 完美解決。此時我發現自己對 RecyclerView 的使用一直太過基本,更深入更強大的功能完全沒有涉及,像 ItemDecoration, ItemAnimator, SmoothScroller, OnItemTouchListener, LayoutManager 之類,以及 RecyclerView 重用 view 的原理。網上也有很多對 RecyclerView 使用的講解博客,要么講的內容非常少,要么提到了高級功能,但是並沒講代碼為什么這樣寫,每個方法和參數的含義是什么,像張鴻洋的博客,也講了 ItemDecoration 的使用,但是看了仍然雲里霧里,只能把他的代碼拿來用,並不能根據自己的需求編寫自己的 ItemDecoration。

在這個系列中,我將對上述各個部分進行深入研究,目標就是看了這一系列的文章之后,開發者可以清楚快捷的根據自己的需求,編寫自己需要的各個高級模塊。本系列第一篇就聚焦在:RecyclerView.ItemDecoration。本文涉及到的完整代碼可以在 Github 獲取

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

這個類包含三個方法 1

  • 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 里面是這樣實現的:

[代碼]java代碼:

?
1
2
3
4
5
if (mOrientation == VERTICAL_LIST) {
     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,實際上是需要的 

繼續深入理解:深入理解 RecyclerView 系列之二:ItemAnimator

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM