一 概述
ItemDecoration
是 RecyclerView
中的一個抽象靜態(tài)內(nèi)部類基括。
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.
這是官網(wǎng)對 ItemDecoration 的描述财岔,簡單來說就是可以為 RecyclerView
的每一個 ItemView 進(jìn)行一些特殊的繪制或者特殊的布局风皿。從而我們可以為 RecyclerView
添加一些實用好玩的效果,比如分割線匠璧,邊框揪阶,飾品,粘性頭部等患朱。
此文會分析ItemDecoration
的使用及原理,然后進(jìn)行一些Demo的實現(xiàn)炊苫,包括分割線裁厅,網(wǎng)格布局的邊框侨艾,以及粘性頭部执虹。
二 方法
1. 方法概述
ItemDecoration
中的實際方法只有6個,其中有3個是重載方法唠梨,都被標(biāo)注為 @deprecated
袋励,即棄用了,這些方法如下
修飾符 | 返回值類型 | 方法名 | 標(biāo)注 |
---|---|---|---|
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
方法,其他方法的默認(rèn)實現(xiàn)都為空,而 getItemOffsets
的默認(rèn)實現(xiàn)方法也很簡單:
@Deprecated
public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
outRect.set(0, 0, 0, 0);
}
兩個getItemOffsets
方法最終都是調(diào)用了上面實現(xiàn),就一行代碼,如果我們自定義過 ItemDecoration
的話,就會知道,我們可以為 outRect
設(shè)置四邊的大小來為 itemView
設(shè)置一個偏移量.
這個偏移量有點類似于 View
的margin,看下面的圖1:
圖片很清晰的表示了 ItemView 的結(jié)構(gòu)(該圖不是特別精確茬故,后面會說到),這是只有一個 Child 的情況盖灸,我們從外往里看:
- 最外的邊界即 RecyclerView 的邊界
- 紅色部分是 RecyclerView 的 Padding,這個我們應(yīng)該能理解
- 橙色部分是我們?yōu)?ItemView 設(shè)置的 Margin磺芭,這個相信寫過布局都能理解
- 藍(lán)色部分就是我們在
getItemOffsets
方法中給outRect
對象設(shè)置的值 - 最后的的黃色部分就是我們的 ItemView 了
總體就是說赁炎,getItemOffsets
中設(shè)置的值就相當(dāng)于 margin 的一個存在。"圖說無憑",接下來就結(jié)合源碼講解一下這個圖的"依據(jù)"钾腺。首先看一下 getItemOffsets
在哪里被調(diào)用了:
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); //被調(diào)用
insets.left += mTempRect.left;
insets.top += mTempRect.top;
insets.right += mTempRect.right;
insets.bottom += mTempRect.bottom;
}
lp.mInsetsDirty = false;
return insets;
}
在 RecyclerView
源碼中徙垫,這是 getItemOffsets
唯一被調(diào)用的地方,代碼也很簡單放棒,就是將 RecyclerView
中所有的(即通過addDecoration()
方法添加的) ItemDecoration
遍歷一遍姻报,然后將我們設(shè)在 getItemOffsets
中設(shè)置的四個方向的值分別累加并存儲在insets
這個Rect
當(dāng)中。那么這個 insets
又在哪里被調(diào)用了呢间螟,順著方法繼續(xù)跟蹤下去:
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 以及當(dāng)前 ItemView 的 Margin 相加寒亥,然后作為 getChildMeasureSpec
的第三個參數(shù)傳進(jìn)去:
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
方法的第三個參數(shù)標(biāo)注為 padding
邮府,在方法體這個 padding
的作用就是計算出 size
這個值,這個 size
是就是后面測量中 Child(ItemView) 能達(dá)到的最大值溉奕。
也就是說我們設(shè)置的 ItemView 的 Margin 以及ItemDecoration.getItemOffsets
中設(shè)置的值到頭來也是跟 Parent 的 Padding 一起來計算 ItemView 的可用空間褂傀,也就印證了上面的圖片,在上面說了該圖不精確就是因為
- parent-padding
- layout_margin
- insets(all outRect)
他們是一體的加勤,并沒有劃分成一段一段這樣仙辟,圖中的outRect
也應(yīng)該改為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
方法有兩個重載鳄梅,一個被標(biāo)注為 @deprecated
,即棄用了叠国,我們知道,如果重寫了 onDraw
戴尸,就可以在我們上面的 getItemOffsets
中設(shè)置的范圍內(nèi)繪制粟焊,知其然還要知其所以然,我們看下源碼里面是怎樣實現(xiàn)的
#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
方法調(diào)用一遍项棠,而且會把Recyclerview#onDraw(Canvas)
方法中的Canvas傳遞給Decoration#onDraw
,也就是說我們在Decoration中拿到了整個 RecyclerView 的 Canvas,那么我們基本就可以隨意繪制了挎峦,但是我們使用中會發(fā)現(xiàn)香追,我們繪制的區(qū)域如果在 ItemView 的范圍內(nèi)就會被蓋住,這是為什么呢坦胶?
由于View的繪制是先執(zhí)行 draw(Canvas)
再到onDraw(Canvas)
的透典,我們復(fù)習(xí)一波自定義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調(diào)用)税弃,然后再繪制child,所以我們在 Decoration#onDraw
中繪制的界面會被 ItemView 遮擋也是理所當(dāng)然了讹语。
所以我們在繪制中就要計算好繪制的范圍钙皮,使繪制范圍在上面彩圖中藍(lán)色區(qū)域內(nèi),即getItemOffsets
設(shè)置的范圍內(nèi)顽决,避免沒有顯示或者過分繪制的情況短条。
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頂層的裝飾就無法實現(xiàn),所以就有了這個方法蚓耽,他是在 ItemView 繪制完畢后才會被調(diào)用的渠牲,看下源碼的實現(xiàn):
#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)
方法,會調(diào)用一系列的繪制流程步悠,包括onDraw
(ItemDecoration的onDraw)以及dispatchDraw
(ItemView的繪制)签杈,走完這些流程后才會調(diào)用Decoration#onDrawOver
方法.
到此,我們就可以得出 onDraw
>dispatchDraw
(ItemView的繪制)>onDrawOver
的執(zhí)行流程鼎兽。
5. 總結(jié)
-
getItemOffsets
用于提供一些空間(類似Margin)給onDraw
繪制 -
onDraw
方法繪制的內(nèi)容如果在 ItemView 的區(qū)域則可能被覆蓋(沒效果) -
onDraw
>dispatchDraw
(ItemView的繪制)>onDrawOver
從左到右執(zhí)行
三 實戰(zhàn)
實戰(zhàn)將會從易到難進(jìn)行幾個小的Demo練習(xí)答姥。
由于這篇文章內(nèi)容已經(jīng)比較充實了,就把實戰(zhàn)部分放到下篇講解谚咬。
感謝你的閱讀鹦付,由于水平有限,如有錯誤懇請?zhí)嵝选?/p>