介紹
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代碼
public abstract static class ItemDecoration {
/**
* Draw any appropriate decorations into the Canvas supplied to the RecyclerView.
* Any content drawn by this method will be drawn before the item views are drawn,
* and will thus appear underneath the views.
*
* @param c Canvas to draw into
* @param parent RecyclerView this ItemDecoration is drawing into
* @param state The current state of RecyclerView
*/
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) {
}
/**
* Draw any appropriate decorations into the Canvas supplied to the RecyclerView.
* Any content drawn by this method will be drawn after the item views are drawn
* and will thus appear over the views.
*
* @param c Canvas to draw into
* @param parent RecyclerView this ItemDecoration is drawing into
* @param state The current state of RecyclerView.
*/
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) {
}
/**
* @deprecated
* Use {@link #getItemOffsets(Rect, View, RecyclerView, State)}
*/
@Deprecated
public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
outRect.set(0, 0, 0, 0);
}
/**
* Retrieve any offsets for the given item. Each field of <code>outRect</code> specifies
* the number of pixels that the item view should be inset by, similar to padding or margin.
* The default implementation sets the bounds of outRect to 0 and returns.
*
* <p>
* If this ItemDecoration does not affect the positioning of item views, it should set
* all four fields of <code>outRect</code> (left, top, right, bottom) to zero
* before returning.
*
* <p>
* If you need to access Adapter for additional data, you can call
* {@link RecyclerView#getChildAdapterPosition(View)} to get the adapter position of the
* View.
*
* @param outRect Rect to receive the output.
* @param view The child view to decorate
* @param parent RecyclerView this ItemDecoration is decorating
* @param state The current state of RecyclerView.
*/
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) {
getItemOffsets(outRect, ((LayoutParams) view.getLayoutParams()).getViewLayoutPosition(),
parent);
}
}
除去被標(biāo)記為過(guò)時(shí)的外,只剩如下三個(gè)方法:
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state)
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state)
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state)
- getItemOffests可以通過(guò)outRect.set(l,t,r,b)設(shè)置指定itemview的paddingLeft,paddingTop, paddingRight, paddingBottom
- onDraw可以通過(guò)一系列c.drawXXX()方法在繪制itemView之前繪制我們需要的內(nèi)容。
- onDrawOver與onDraw類似,只不過(guò)是在繪制itemView之后繪制,具體表現(xiàn)形式,就是繪制的內(nèi)容在itemview上層。
調(diào)用RecyclerView的addItemDecoration()方法就可以給RecyclerView添加ItemDecoration了,注意這里是add并不是set,這意味著是可以給一個(gè)RecyclerView設(shè)置多個(gè)ItemDecoration的。
// 添加ItemDecoration
public void addItemDecoration(ItemDecoration decor) {
addItemDecoration(decor, -1);
}
// 添加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();
}
// onLayout 最終會(huì)調(diào)用到此方法
Rect getItemDecorInsetsForChild(View child) {
....
final int decorCount = mItemDecorations.size();
for (int i = 0; i < decorCount; i++) {
...
mItemDecorations.get(i).getItemOffsets(mTempRect, child, this, mState);
...
}
...
}
@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);
}
}
@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);
}
}
從源碼可以看出,事實(shí)確實(shí)如此,ItemDecoration會(huì)被add到集合中,然后RecyclerView會(huì)根據(jù)add的順序依次調(diào)用(getItemOffsets->onDraw->onDrawOver)的方法,因此,ItemDecoration的使用也變得更加靈活。
使用
介紹了這么多,是時(shí)候?qū)扅c(diǎn)代碼用用它了。
比如,給RecyclerView的每個(gè)Item設(shè)置間隔,這里我們要區(qū)分下RecyclerView的LayoutManager的類型,以及orientation類型。
LinearLayoutManger
一般情況下,設(shè)計(jì)稿會(huì)有下面兩種樣子的情形(先考慮HORIZONTAL的情況,VERTICAL處理起來(lái)原理也一樣)
第一排(recyclerview1) 第一個(gè)item,最后一個(gè)item沒有邊距
第二排(recyclerview2) 第一個(gè)item和最后一個(gè)item有邊距

在沒有ItemDecoration之前,我們一般都是在xml布局中調(diào)整Padding或者是Margin,然后在代碼中根據(jù)position來(lái)控制,這樣一來(lái)的話ViewHolder中會(huì)多出一些看上去很臃腫的代碼。對(duì)于第二種情況我們也可以通過(guò)設(shè)置RecyclerView的paddingLeft以及paddingRight并設(shè)置clipToPadding為fasle來(lái)實(shí)現(xiàn),但是滑動(dòng)到邊緣的時(shí)候,感覺會(huì)有點(diǎn)怪怪的。
如果我們使用ItemDecoration,將這部分的邏輯抽離出來(lái),這樣的代碼不僅看起來(lái),用起來(lái)更舒服,也更加符合面向?qū)ο蟮乃枷搿?/p>
首先我們定義一個(gè)類繼承RecyclerView.ItemDecoration,通過(guò)構(gòu)造方法傳入item間的間距mSpace以及邊距mEdgeSpace。
/**
* @param mSpace item間的間距 默認(rèn)沒有邊距
*/
public OffestDecoration(int mSpace, Context ctx) {
this(mSpace, 0, ctx);
}
/**
* @param mSpace item間的間距
* @param mEdgeSpace 邊距(padding)
*/
public OffestDecoration(int mSpace, int mEdgeSpace, Context ctx) {
this.mSpace = (int) (TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, mSpace, ctx.getResources().getDisplayMetrics()) + 0.5f);
this.mEdgeSpace = (int) (TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, mEdgeSpace, ctx.getResources().getDisplayMetrics()) + 0.5f);
}
重寫getItemOffsets方法判斷l(xiāng)ayoutManager,orientation,通過(guò)outRect.set()設(shè)置每個(gè)Item的padding。orientation為HORIZONTAL時(shí),第一個(gè)item需要額外設(shè)置左邊距的值,最后一個(gè)item需要設(shè)置右邊距的值,其他的item只需要設(shè)置paddingRight,orientation為VERTICAL時(shí), 只需要把left,right換成top,bottom就ok了。
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
Log.i(TAG, "getItemOffsets");
RecyclerView.LayoutManager manager = parent.getLayoutManager();
int childPosition = parent.getChildAdapterPosition(view);
int itemCount = parent.getAdapter().getItemCount();
if (manager != null) {
if (manager instanceof GridLayoutManager) {
// 待會(huì)再處理
} else if (manager instanceof LinearLayoutManager) {
setLinearOffset(((LinearLayoutManager) manager).getOrientation(), outRect, childPosition, itemCount);
}
}
}
private void setLinearOffset(int orientation, Rect outRect, int childPosition, int itemCount) {
if (orientation == LinearLayoutManager.HORIZONTAL) {
if (childPosition == 0) {
// 第一個(gè)要設(shè)置PaddingLeft
outRect.set(mEdgeSpace, 0, mSpace, 0);
} else if (childPosition == itemCount - 1) {
// 最后一個(gè)設(shè)置PaddingRight
outRect.set(0, 0, mEdgeSpace, 0);
} else {
outRect.set(0, 0, mSpace, 0);
}
} else {
if (childPosition == 0) {
// 第一個(gè)要設(shè)置PaddingTop
outRect.set(0, mEdgeSpace, 0, mSpace);
} else if (childPosition == itemCount - 1) {
// 最后一個(gè)要設(shè)置PaddingBottom
outRect.set(0, 0, 0, mEdgeSpace);
} else {
outRect.set(0, 0, 0, mSpace);
}
}
}
GridLayoutManager
很多情況下,我們需要實(shí)現(xiàn)GridView樣式的RecyclerView,也分有邊距和沒邊距的情況,如下圖:

為了保證每個(gè)itemView在水平方向(orientation為vertical時(shí))或者垂直方向(orientation為horizon時(shí))均分,那么必須讓每個(gè)itemview的paddingleft+paddingRight(orientation為vertical時(shí))或者paddingTop+paddingBottom(orientation為horizon時(shí))相等,如下圖,每個(gè)紅色框框的尺寸是相等的,但每個(gè)itemview的paddingLeft和paddingRight不同。

當(dāng)orientation為vertical時(shí),我們需要在getItemOffsets方法中計(jì)算每個(gè)Item的PaddingLeft,以及PaddingRight,保證每個(gè)Item的paddingLeft+paddingRight相等,這樣才能達(dá)到均分的目的。由于距離智商巔峰期(高三)已經(jīng)很久了,對(duì)數(shù)字也不敏感,我們不妨用最簡(jiǎn)單粗暴的方法來(lái)找到其中的規(guī)律——套數(shù)字。
無(wú)邊距
假如 mSpace(間距)等于14,spanCount等于4,mEdgeSpace(邊距)等于0,那么
totalSpace = mSpace * (itemCount-1) + EdgeSpace * 2 = 42 // space總和
eachSpace = totalSpace / itemCount = 10.5 // 每個(gè)item的leftPadding+rightPadding的和
列出每一列的paddingLeft以及paddingRight:
| colunm | L | R |
|---|---|---|
| 0 | EdgeSpace(0) | eachSpace-L0(10.5) |
| 1 | mSpace-R0(3.5) | eachSpace-L1 (7) |
| 2 | mSpace-R1(7) | eachSpace-R2(3.5) |
| 3 | mSpace-R2(10.5) | EdgeSpace(0) |
可以看出
Left是從 0 到 eachSpace 等差數(shù)列
Right用eachSpace -Left算出
有邊距
假如 mSpace(間距)等于14,spanCount等于4,mEdgeSpace(邊距)等于12,那么
totalSpace = mSpace * (itemCount-1) + EdgeSpace * 2 = 66 // space總和
eachSpace = totalSpace / itemCount= 16.5 // item的leftPadding+rightPadding的和
列出每一列的paddingLeft以及paddingRight:
| colunm | L | R |
|---|---|---|
| 0 | EdgeSpace(12) | eachSpace-L0(4.5) |
| 1 | mSpace-R0(9.5) | eachSpace-L1 (7) |
| 2 | mSpace-R1(7) | eachSpace-R2(9.5) |
| 3 | mSpace-R2(4.5) | EdgeSpace(12) |
可以看出
Left是從 EdgeSpace 到 (eachSpace - EdgeSpace) 等差數(shù)列(關(guān)于什么是等差數(shù)列需要自行補(bǔ)習(xí)...)
Right用eachSpace -Left算出
根據(jù)上面得出的規(guī)律,paddingLeft都是等差數(shù)列,而且我們已知以及
,根據(jù)等差數(shù)列的公式
,很容易計(jì)算出公差
:
當(dāng)邊距為0時(shí),
當(dāng)邊距不為0時(shí),
所以
列數(shù)
上面的分析并沒有考慮orientation為horizontal的情況,其實(shí)只需要把top,bottom與left,right對(duì)調(diào)下就行了,最后貼下代碼:
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
RecyclerView.LayoutManager manager = parent.getLayoutManager();
int childPosition = parent.getChildAdapterPosition(view);
int itemCount = parent.getAdapter().getItemCount();
if (manager != null) {
if (manager instanceof GridLayoutManager) {
// manager為GridLayoutManager時(shí)
setGridOffset(((GridLayoutManager) manager).getOrientation(), ((GridLayoutManager) manager).getSpanCount(), outRect, childPosition, itemCount);
} else if (manager instanceof LinearLayoutManager) {
// manager為L(zhǎng)inearLayoutManager時(shí)
setLinearOffset(((LinearLayoutManager) manager).getOrientation(), outRect, childPosition, itemCount);
}
}
}
/**
* 設(shè)置GridLayoutManager 類型的 offest
*
* @param orientation 方向
* @param spanCount 個(gè)數(shù)
* @param outRect padding
* @param childPosition 在 list 中的 postion
* @param itemCount list size
*/
private void setGridOffset(int orientation, int spanCount, Rect outRect, int childPosition, int itemCount) {
float totalSpace = mSpace * (spanCount - 1) + mEdgeSpace * 2; // 總共的padding值
float eachSpace = totalSpace / spanCount; // 分配給每個(gè)item的padding值
int column = childPosition % spanCount; // 列數(shù)
int row = childPosition / spanCount;// 行數(shù)
float left;
float right;
float top;
float bottom;
if (orientation == GridLayoutManager.VERTICAL) {
top = 0; // 默認(rèn) top為0
bottom = mSpace; // 默認(rèn)bottom為間距值
if (mEdgeSpace == 0) {
left = column * eachSpace / (spanCount - 1);
right = eachSpace - left;
// 無(wú)邊距的話 只有最后一行bottom為0
if (itemCount / spanCount == row) {
bottom = 0;
}
} else {
if (childPosition < spanCount) {
// 有邊距的話 第一行top為邊距值
top = mEdgeSpace;
} else if (itemCount / spanCount == row) {
// 有邊距的話 最后一行bottom為邊距值
bottom = mEdgeSpace;
}
left = column * (eachSpace - mEdgeSpace - mEdgeSpace) / (spanCount - 1) + mEdgeSpace;
right = eachSpace - left;
}
} else {
// orientation == GridLayoutManager.HORIZONTAL 跟上面的大同小異, 將top,bottom替換為left,right即可
left = 0;
right = mSpace;
if (mEdgeSpace == 0) {
top = column * eachSpace / (spanCount - 1);
bottom = eachSpace - top;
if (itemCount / spanCount == row) {
right = 0;
}
} else {
if (childPosition < spanCount) {
left = mEdgeSpace;
} else if (itemCount / spanCount == row) {
right = mEdgeSpace;
}
top = column * (eachSpace - mEdgeSpace - mEdgeSpace) / (spanCount - 1) + mEdgeSpace;
bottom = eachSpace - top;
}
}
outRect.set((int) left, (int) top, (int) right, (int) bottom);
}