ItemDecoration
是 RecyclerView
中的一個抽象靜態內部類。java
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.canvas
這是官網對 ItemDecoration 的描述,簡單來講就是能夠爲 RecyclerView
的每個 ItemView 進行一些特殊的繪製或者特殊的佈局。從而咱們能夠爲 RecyclerView
添加一些實用好玩的效果,好比分割線,邊框,飾品,粘性頭部等。bash
此文會分析ItemDecoration
的使用及原理,而後進行一些Demo的實現,包括分割線,網格佈局的邊框,以及粘性頭部。app
ItemDecoration
中的實際方法只有6個,其中有3個是重載方法,都被標註爲 @deprecated
,即棄用了,這些方法以下ide
修飾符 | 返回值類型 | 方法名 | 標註 |
---|---|---|---|
void | public | onDraw(Canvas c, RecyclerView parent, State state) | |
void | public | onDraw(Canvas c, RecyclerView parent) | @deprecated |
void | pulbic | onDrawOver(Canvas c, RecyclerView parent, State state) | |
void | public | onDrawOver(Canvas c, RecyclerView parent) | @deprecated |
void | public | getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) | |
void | public | getItemOffsets(Rect outRect, View view, RecyclerView parent) | @deprecated |
除了 getItemOffsets
方法,其餘方法的默認實現都爲空,而 getItemOffsets
的默認實現方法也很簡單:佈局
@Deprecated
public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
outRect.set(0, 0, 0, 0);
}
複製代碼
兩個getItemOffsets
方法最終都是調用了上面實現,就一行代碼,若是咱們自定義過 ItemDecoration
的話,就會知道,咱們能夠爲 outRect
設置四邊的大小來爲 itemView
設置一個偏移量. 這個偏移量有點相似於 View
的margin,看下面的圖1:ui
圖片很清晰的表示了 ItemView 的結構(該圖不是特別精確,後面會說到),這是隻有一個 Child 的狀況,咱們從外往裏看:this
getItemOffsets
方法中給 outRect
對象設置的值整體就是說,getItemOffsets
中設置的值就至關於 margin 的一個存在。"圖說無憑",接下來就結合源碼講解一下這個圖的"依據"。首先看一下 getItemOffsets
在哪裏被調用了:spa
Rect getItemDecorInsetsForChild(View child) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
...
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;
}
複製代碼
在 RecyclerView
源碼中,這是 getItemOffsets
惟一被調用的地方,代碼也很簡單,就是將 RecyclerView
中全部的(即經過addDecoration()
方法添加的) ItemDecoration
遍歷一遍,而後將咱們設在 getItemOffsets
中設置的四個方向的值分別累加並存儲在insets
這個Rect
當中。那麼這個 insets
又在哪裏被調用了呢,順着方法繼續跟蹤下去:rest
public void measureChildWithMargins(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()
+ lp.leftMargin + lp.rightMargin + widthUsed, lp.width,
canScrollHorizontally());
final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(),
getPaddingTop() + getPaddingBottom()
+ lp.topMargin + lp.bottomMargin + heightUsed, lp.height,
canScrollVertically());
if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
child.measure(widthSpec, heightSpec);
}
}
複製代碼
咱們看到,在 measureChildWithMargins
方法中,將剛剛獲得的 insets
的值與 Recyclerview 的 Padding 以及當前 ItemView 的 Margin 相加,而後做爲 getChildMeasureSpec
的第三個參數傳進去:
public static int getChildMeasureSpec(int parentSize, int parentMode, int padding,
int childDimension, boolean canScroll) {
int size = Math.max(0, parentSize - padding);
int resultSize = 0;
int resultMode = 0;
//...省略部分代碼
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
複製代碼
getChildMeasureSpec
方法的第三個參數標註爲 padding
,在方法體這個 padding
的做用就是計算出 size
這個值,這個 size
是就是後面測量中 Child(ItemView) 能達到的最大值。
也就是說咱們設置的 ItemView 的 Margin 以及ItemDecoration.getItemOffsets
中設置的值到頭來也是跟 Parent 的 Padding 一塊兒來計算 ItemView 的可用空間,也就印證了上面的圖片,在上面說了該圖不精確就是由於
他們是一體的,並無劃分紅一段一段這樣,圖中的outRect
也應該改成insets
,可是圖中的形式能夠更方便咱們理解。
public void onDraw(Canvas c, RecyclerView parent, State state) {
onDraw(c, parent);
}
/**
* @deprecated Override {@link #onDraw(Canvas, RecyclerView, RecyclerView.State)}
*/
@Deprecated
public void onDraw(Canvas c, RecyclerView parent) {
}
複製代碼
onDraw
方法有兩個重載,一個被標註爲 @deprecated
,即棄用了,咱們知道,若是重寫了 onDraw
,就能夠在咱們上面的 getItemOffsets
中設置的範圍內繪製,知其然還要知其因此然,咱們看下源碼裏面是怎樣實現的 #RecyclerView.java
@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);
}
}
複製代碼
在 ReyclerView
的onDraw
方法中,將會把全部 Decoration
的onDraw
方法調用一遍,並且會把Recyclerview#onDraw(Canvas)
方法中的Canvas傳遞給Decoration#onDraw
,也就是說咱們在Decoration中拿到了整個 RecyclerView 的 Canvas,那麼咱們基本就能夠隨意繪製了,可是咱們使用中會發現,咱們繪製的區域若是在 ItemView 的範圍內就會被蓋住,這是爲何呢?
因爲View的繪製是先執行 draw(Canvas)
再到onDraw(Canvas)
的,咱們複習一波自定義View的知識,看下View的繪製流程: #View.java
public void draw(Canvas canvas) {
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 2. If necessary, save the canvas' layers to prepare for fading * 3. Draw view's content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
*/
// Step 1, draw the background, if needed
if (!dirtyOpaque) {
drawBackground(canvas);
}
// skip step 2 & 5 if possible (common case)
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas); //註釋1
// Step 4, draw the children
dispatchDraw(canvas); //註釋2
...
// we're done... return; } } 複製代碼
咱們直接看註釋1與註釋2那段,能夠看到,View的繪製是先繪製自身(onDraw調用),而後再繪製child,因此咱們在 Decoration#onDraw
中繪製的界面會被 ItemView 遮擋也是理所固然了。
因此咱們在繪製中就要計算好繪製的範圍,使繪製範圍在上面彩圖中藍色區域內,即getItemOffsets
設置的範圍內,避免沒有顯示或者過度繪製的狀況。
public void onDrawOver(Canvas c, RecyclerView parent, State state) {
onDrawOver(c, parent);
}
/**
* @deprecated
* Override {@link #onDrawOver(Canvas, RecyclerView, RecyclerView.State)}
*/
@Deprecated
public void onDrawOver(Canvas c, RecyclerView parent) {
}
複製代碼
onDrawOver
跟onDraw
很是相似,也是兩個重載,一個被棄用了,看名稱咱們就基本能知道這個方法的用途,它是用於補充 onDraw
的一個方法,因爲onDraw
會被 ItemView 覆蓋,因此咱們想要繪製一些漂浮在RecyclerView頂層的裝飾就沒法實現,因此就有了這個方法,他是在 ItemView 繪製完畢後纔會被調用的,看下源碼的實現: #RecyclerView.java
@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);
}
...
}
複製代碼
super.draw(c)
就是咱們在上面分析的View#draw(Canvas)
方法,會調用一系列的繪製流程,包括onDraw
(ItemDecoration的onDraw)以及dispatchDraw
(ItemView的繪製),走完這些流程後纔會調用Decoration#onDrawOver
方法.
到此,咱們就能夠得出 onDraw
>dispatchDraw
(ItemView的繪製)>onDrawOver
的執行流程。
getItemOffsets
用於提供一些空間(相似Margin)給 onDraw
繪製onDraw
方法繪製的內容若是在 ItemView 的區域則可能被覆蓋(沒效果)onDraw
>dispatchDraw
(ItemView的繪製)>onDrawOver
從左到右執行實戰將會從易到難進行幾個小的Demo練習。 因爲這篇文章內容已經比較充實了,就把實戰部分放到下篇講解。
感謝你的閱讀,因爲水平有限,若有錯誤懇請提醒。