使用RecyclerView替代ListView已經(jīng)是老生常談的話題了,RecyclerView的優(yōu)秀和靈活已經(jīng)經(jīng)過了大量項目的實踐。最近在完成一個分組列表的需求時,使用到ItemDecoration,故在此對其做一番總結(jié),加深對其的理解。
ItemDecoration介紹
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允許應用結(jié)合adapter的數(shù)據(jù)集,對特定的item添加繪制一個周邊圖案??梢杂糜诮oitems之間添加分割線、高亮裝飾效果或者分組邊界等等。
從谷歌官方的介紹可以知道,ItemDecoration是用于給列表的item添加各種裝飾效果,開發(fā)中最常見的就是為item添加分割線。
ItemDecoration本身是一個抽象類,拋去廢棄的方法,我們需要關(guān)心的方法只有三個:
public static abstract class ItemDecoration {
public void onDraw(Canvas c, RecyclerView parent, State state) {
onDraw(c, parent);
}
public void onDrawOver(Canvas c, RecyclerView parent, State state) {
onDrawOver(c, parent);
}
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) {
getItemOffsets(outRect, ((LayoutParams) view.getLayoutParams()).getViewLayoutPosition(),parent);
}
}
從源碼注釋中,可以大概了解這三個方法的用途:
-
onDraw:在item繪制之前時被調(diào)用,將指定的內(nèi)容繪制到item view內(nèi)容之下; -
onDrawOver:在item被繪制之后調(diào)用,將指定的內(nèi)容繪制到item view內(nèi)容之上 -
getItemOffsets:在每次測量item尺寸時被調(diào)用,將decoration的尺寸計算到item的尺寸中

ItemDecoration三個方法的測試
谷歌官方在support.v7包中提供了ItemDecoration的一個實現(xiàn)DividerItemDecoration,這里結(jié)合這個實現(xiàn),來看看其三個需要實現(xiàn)的方法對UI的影響。
onDraw
private void drawVertical(Canvas canvas, RecyclerView parent) {
canvas.save();
final int left;
final int right;
if (parent.getClipToPadding()) {
left = parent.getPaddingLeft();
right = parent.getWidth() - parent.getPaddingRight();
canvas.clipRect(left, parent.getPaddingTop(), right,
parent.getHeight() - parent.getPaddingBottom());
} else {
left = 0;
right = parent.getWidth();
}
final int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
parent.getDecoratedBoundsWithMargins(child, mBounds);
final int bottom = mBounds.bottom + Math.round(ViewCompat.getTranslationY(child));
final int top = bottom - mDivider.getIntrinsicHeight();
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(canvas);
}
canvas.restore();
}
drawVertical方法實現(xiàn)了對Orientation == VERTICAL的RecyclerView繪制item之間的分割線。從傳入的canvas參數(shù)可以推斷,分割線的繪制是通過canvas機制繪制到屏幕上:mDivider.draw(canvas);其中,mDivider是一個Drawable對象,可以通過setDrawable傳入自定義對象,不傳入時,會自動使用系統(tǒng)內(nèi)置的分割線樣式:android.R.attr.listDivider。通過遍歷每一個可見的child view,計算mDivider對應的left、top、right、bottom值,從而繪制到正確的位置上。對于縱向的RecyclerView而言,mDivider的left和right是固定的,和parent的左右內(nèi)容邊界保持一致,也就是說,把parent的左右padding都計算進去,因而是代表了RecyclerView實際的內(nèi)容區(qū)域??v向的分割線一般位于每個item的底部,因此mDivider的top值理論上應該和child view的內(nèi)容下邊界保持貼合。實際上,計算top和bottom的代碼,谷歌官方也有所調(diào)整,在最新的實現(xiàn)中,先通過parent.getDecoratedBoundsWithMargins(child, mBounds);拿到之前在onMeasure過程中,通過調(diào)用getItemOffsets獲取到的mBounds,mBounds是包括了整個child view以及其decoration的總邊界,之后再計算mDivider的bottom、top值。
getItemOffsets
public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
RecyclerView.State state) {
if (mOrientation == VERTICAL) {
outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
} else {
outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
}
}
官方實現(xiàn)的getItemOffsets比較簡單,只是根據(jù)列表的方向,返回了分割線在相應方向的尺寸。這里可能有一個坑,即通過setDrawable設(shè)置自定義的分割線時,容易傳入一個無尺寸的drawable對象,導致分割線無法顯示出來的bug,典型的代碼是這樣:
decoration.setDrawable(new ColorDrawable(Color.RED));
DividerItemDecoration的實現(xiàn)中,是沒有復寫onDrawOver方法的,對于分割線場景而言,也確實不需要去實現(xiàn)它。接下來,通過幾個例子,展示一下getItemOffsets對于ItemDecoration在UI上的影響。
getItemOffsets & onDraw
先上動圖【注2】:


上圖中,getItemOffsets方法里,返回outRect不同,而onDraw方法繪制的分割線高度初始值設(shè)為25,并通過外部增減來觀察其UI效果。
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
outRect.set(0, 0, 0, 50);// outRect.set(50,50,50,50);
}
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
for (int i = 0; i < childCount; i++) {
final View view = parent.getChildAt(i);
top = view.getBottom();
left = view.getPaddingLeft() + mSize;
right = view.getWidth() - view.getPaddingRight() - mSize ;
bottom = top + mSize;
divider.setBounds(left, top, right, bottom);
divider.draw(c);
}
}
從上面兩個動圖對比,可以得出以下幾個結(jié)論:
- getItemOffsets返回的矩形outRect會被計算到child view的尺寸當中;
- onDraw方法繪制的圖形,可以超出outRect所規(guī)定的區(qū)域;
- onDraw方法繪制的圖形,確實是處于child view的底下,當兩者發(fā)生重疊時,只會顯示child view的內(nèi)容;
getItemOffsets & onDrawOver

將之前onDraw方法內(nèi)代碼完整拷貝到onDrawOver下,并注釋掉之前onDraw中的方法,很容易驗證出onDrawOver與onDraw的唯一不同之處。
- onDrawOver繪制的圖形,處于child view之上,當兩者發(fā)生重疊時,會顯示onDrawOver的內(nèi)容;
ItemDecoration三個方法的含義,就介紹到這里。可以感覺到,三個方法都很簡單而基礎(chǔ),可以十分優(yōu)雅的實現(xiàn)item的分割線效果,然而簡單的如DividerItemDecoration,往往是無法滿足項目開發(fā)需求的。經(jīng)常會遇到某幾個item不想要分割線(如頭部或者最后一個item),這就需要開發(fā)者自行來實現(xiàn)。
利用ItemDecoration實現(xiàn)分組列表效果
先看效果圖:

上圖展示了利用ItemDecoration實現(xiàn)分組欄的效果,對于分組效果,需要注意的點在于,如何確定分組欄位置和內(nèi)容,如何實現(xiàn)分組欄吸頂效果(如果需要)。
- 分組欄位置一般是由外部決定,常見是根據(jù)數(shù)據(jù)源list中某個特征值來決定,比較好的做法是通過接口來實現(xiàn)。
public interface IHover {
/**
* 當前position是否需要繪制分組欄
* @param position 當前位置
* @return true表示需要繪制
*/
boolean isGroup(int position);
/**
* 當前位置需要繪制的文本
* @param position 當前位置
* @return String
*/
String groupText(int position);
}
- 分組欄效果實際上是利用了onDrawOver和onDraw方法,onDraw方法負責繪制每一個需要分組的Decoration,而onDrawOver方法只繪制最頂部item的Decoration,由于onDrawOver繪制的內(nèi)容永遠會顯示在最頂層,因此,實際上是,每一個頂部item都繪制了一個Decoration,但是相同分組的Decoration內(nèi)容和位置一摸一樣,就導致看上去是一直吸頂?shù)男Ч?。部分代碼如下:
#onDraw:
if (builder.iHover.isGroup(position)) {
bottom = childView.getTop();
top = bottom - builder.decorationHeight;
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(c);
String text = builder.iHover.groupText(position);
if (!TextUtils.isEmpty(text)) {
Paint.FontMetrics fm = textPaint.getFontMetrics();
//文字豎直居中顯示
float baseLine = bottom - (builder.decorationHeight - (fm.bottom - fm.top)) / 2 - fm.bottom;
int textLeft = left;
float textWidth = textPaint.measureText(text, 0, text.length());
if (builder.textAlign == Builder.ALIGN_MIDDLE) {
textLeft = (int) (parent.getPaddingLeft() + parent.getWidth()/2 - textWidth/2);
}
c.drawText(text, textLeft + builder.textLeftPadding, baseLine, textPaint);
}
}
#getItemOffsets:
// 分組模式只在分組時才繪制
if (builder.iHover.isGroup(pos)) {
outRect.set(0, builder.decorationHeight, 0, 0);
}
#onDrawOver:
// 只有需要分組功能時,才走以下邏輯
if (builder.iHover != null) {
int position = ((LinearLayoutManager) (parent.getLayoutManager())).findFirstVisibleItemPosition();
int bottom, top;
int left = parent.getPaddingLeft();
int right = parent.getWidth() - parent.getPaddingRight();
top = parent.getPaddingTop();
bottom = top + builder.decorationHeight;
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(c);
String text = builder.iHover.groupText(position);
if (!TextUtils.isEmpty(text)) {
Paint.FontMetrics fm = textPaint.getFontMetrics();
//文字豎直居中顯示
float baseLine = bottom - (builder.decorationHeight - (fm.bottom - fm.top)) / 2 - fm.bottom;
int textLeft = left;
float textWidth = textPaint.measureText(text, 0, text.length());
if (builder.textAlign == Builder.ALIGN_MIDDLE) {
textLeft = (int) (parent.getPaddingLeft() + parent.getWidth()/2 - textWidth/2);
}
c.drawText(text, textLeft + builder.textLeftPadding, baseLine, textPaint);
}
}
簡單的封裝MKItemDecoration
- 支持簡單顏色分割線
- 支持簡單顏色分割線 + 文字:文字可以居左、居中
- 支持分割線跳過起始諾干個item,跳過最后一個item
- 支持分組懸停效果
-
支持自定義View作為Decoration
customView.gif
上圖hoverGroup.gif的使用代碼如下:
recyclerView.addItemDecoration(new MKItemDecoration.Builder()
.height(50)
.color(Color.parseColor("#525D97"))
.textSize(30)
.textColor(Color.WHITE)
.itemOffset(0)
.iHover(new IHover() {
@Override
public boolean isGroup(int position) {
return position % 4 == 0;
}
@Override
public String groupText(int position) {
return adapter.data.get(4 * (position / 4));
}
}).
.textAlign(MKItemDecoration.Builder.ALIGN_MIDDLE)
.build());
通過封裝,利用builder模式來更好的自定義需要的Decoration,其中,為了支持自定義View,需要外部傳入相關(guān)的view的資源id和需要綁定的數(shù)據(jù)List,控件內(nèi)部會通過view的measure,layout,draw的流程,將其繪制在屏幕上。
具體代碼見:https://github.com/Dragon-Boat/library
歡迎提issue 和 star~
TODO:
- itemDecoration是通過draw繪制圖形,不支持點擊事件
感謝:
- https://blog.piasy.com/2016/03/26/Insight-Android-RecyclerView-ItemDecoration/
- https://github.com/fishyer/PinnedRecyclerView
注1:圖片引用自該文章鏈接。
注2:動圖使用Vysor+GifCam錄制,前者將手機屏幕內(nèi)容投射到電腦上,后者錄制git圖片。
