一 概述
ItemDecoration
是 RecyclerView
中的一個抽象靜態內部類。
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.
這是官網對 ItemDecoration 的描述,簡單來說就是可以為 RecyclerView
的每一個 ItemView 進行一些特殊的繪製或者特殊的佈局。從而我們可以為 RecyclerView
新增一些實用好玩的效果,比如分割線,邊框,飾品,粘性頭部等。
此文會分析ItemDecoration
的使用及原理,然後進行一些Demo的實現,包括分割線,網格佈局的邊框,以及粘性頭部。
二 方法
1. 方法概述
ItemDecoration
中的實際方法只有6個,其中有3個是過載方法,都被標註為 @deprecated
,即棄用了,這些方法如下
修飾符 | 返回值型別 | 方法名 | 標註 |
---|---|---|---|
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 |
2. getItemOffsets
除了 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:
圖片很清晰的表示了 ItemView 的結構(該圖不是特別精確,後面會說到),這是隻有一個 Child 的情況,我們從外往裡看:
- 最外的邊界即 RecyclerView 的邊界
- 紅色部分是 RecyclerView 的 Padding,這個我們應該能理解
- 橙色部分是我們為 ItemView 設定的 Margin,這個相信寫過佈局都能理解
- 藍色部分就是我們在
getItemOffsets
方法中給outRect
物件設定的值 - 最後的的黃色部分就是我們的 ItemView 了
總體就是說,getItemOffsets
中設定的值就相當於 margin 的一個存在。”圖說無憑”,接下來就結合原始碼講解一下這個圖的”依據”。首先看一下 getItemOffsets
在哪裡被呼叫了:
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
又在哪裡被呼叫了呢,順著方法繼續跟蹤下去:
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 的可用空間,也就印證了上面的圖片,在上面說了該圖不精確就是因為
- parent-padding
- layout_margin
- insets(all outRect)
他們是一體的,並沒有劃分成一段一段這樣,圖中的outRect
也應該改為insets
,但是圖中的形式可以更方便我們理解。
3. onDraw
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
設定的範圍內,避免沒有顯示或者過分繪製的情況。
4.onDrawOver
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
的執行流程。
5. 總結
getItemOffsets
用於提供一些空間(類似Margin)給onDraw
繪製onDraw
方法繪製的內容如果在 ItemView 的區域則可能被覆蓋(沒效果)onDraw
>dispatchDraw
(ItemView的繪製)>onDrawOver
從左到右執行
三 實戰
實戰將會從易到難進行幾個小的Demo練習。
由於這篇文章內容已經比較充實了,就把實戰部分放到下篇講解。
感謝你的閱讀,由於水平有限,如有錯誤懇請提醒。