一、概述
通過ItemDecoration,可以給RecyclerView或者RecyclerView中的每個(gè)Item添加額外的裝飾效果,最常用的就是用來為Item之間添加分割線,今天,我們就來一起學(xué)習(xí)有關(guān)的知識(shí):
API-
DividerItemDecoration解析 - 自定義
ItemDecoration
二、API介紹
當(dāng)我們實(shí)現(xiàn)自己的ItemDecoration時(shí),需要繼承于ItemDecoration,并根據(jù)需要實(shí)現(xiàn)以下三個(gè)方法:
2.1 public void onDraw(Canvas c, RecyclerView parent, State state)
-
canvas:RecyclerView的canvas -
parent:RecyclerView實(shí)例 -
State:RecyclerView當(dāng)前的狀態(tài),值包括START/LAYOUT/ANIMATION。
所有在這個(gè)方法中的繪制操作,將會(huì)在itemViews被繪制之前執(zhí)行,因此,它會(huì)顯示在itemView之下。
2.2 public void onDrawOver(Canvas c, RecyclerView parent, State state)
和2.1方法類似,區(qū)別在于它繪制在itemViews之上。
2.3 public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state)
通過outRect,可以設(shè)置item之間的間隔,間隔區(qū)域的大小就是outRect所指定的范圍,view就是對(duì)應(yīng)位置的itemView,其它的參數(shù)解釋和上面相同。
三、DividerItemDecoration解析
3.1 使用方法
上面我們解釋了需要重寫的方法以及其中參數(shù)的含義,下面,我們通過官方自帶的DividerItemDecoration,來進(jìn)一步加深對(duì)這些方法的認(rèn)識(shí)。
DividerItemDecoration是為LinearLayoutManager提供的分割線,在創(chuàng)建它的時(shí)候,需要指定ORIENTATION,這個(gè)方向應(yīng)當(dāng)和LinearLayoutManager的方向相同。
private void init() {
RecyclerView recyclerView = (RecyclerView) findViewById(R.id.rv_content);
mTitles = new ArrayList<>();
for (int i = 0; i < 20; i++) {
mTitles.add(String.valueOf(i));
}
BaseAdapter baseAdapter = new BaseAdapter(mTitles);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
recyclerView.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL));
recyclerView.setAdapter(baseAdapter);
}
最終展示的效果為:

3.2 源碼解析
3.2.1 繪制
DividerItemDecoration重寫了基類當(dāng)中的onDraw方法,也就是說這個(gè)分割線是在itemView之前繪制的:
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
if (parent.getLayoutManager() == null) {
return;
}
if (mOrientation == VERTICAL) {
drawVertical(c, parent);
} else {
drawHorizontal(c, parent);
}
}
我們先看縱向排列的RecyclerView分割線:
@SuppressLint("NewApi")
private void drawVertical(Canvas canvas, RecyclerView parent) {
//首先保存畫布
canvas.save();
final int left;
final int right;
//確定左右邊界的范圍,如果RecyclerView不允許子View繪制在Padding內(nèi),那么這個(gè)范圍為去掉Padding后的范圍
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);
//獲得itemView的范圍,這個(gè)范圍包括了margin和offset,它們被保存在mBounds當(dāng)中
parent.getDecoratedBoundsWithMargins(child, mBounds);
//需要考慮translationY和translationY
final int bottom = mBounds.bottom + Math.round(ViewCompat.getTranslationY(child));
//由于是垂直排列的,因此上邊界等于下邊界減去分割線的高度.
final int top = bottom - mDivider.getIntrinsicHeight();
//設(shè)置divider和范圍
mDivider.setBounds(left, top, right, bottom);
//繪制.
mDivider.draw(canvas);
}
//回復(fù)畫布.
canvas.restore();
}
整個(gè)過程分為三步:
- 確定子
View在RecyclerView中的繪制范圍 - 確定每個(gè)子
View的范圍 - 確定
mDivider的繪制范圍
下圖就是最終計(jì)算的結(jié)果:

橫向排列的
RecyclerView列表和上面的原理是相同的,區(qū)別就在于計(jì)算mDivider.setBounds的計(jì)算:
//....
parent.getLayoutManager().getDecoratedBoundsWithMargins(child, mBounds);
final int right = mBounds.right + Math.round(ViewCompat.getTranslationX(child));
final int left = right - mDivider.getIntrinsicWidth();
mDivider.setBounds(left, top, right, bottom);
//..
3.2.2 邊界處理
從上面的分析可以知道,如果將divider直接繪制在itemView的范圍內(nèi),那么由于我們是先繪制divider,再繪制itemView的內(nèi)容的,那么它就會(huì)被覆蓋,因此,通過重寫getItemOffsets,通過其中的outRect來指定留出的空隙:
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
RecyclerView.State state) {
if (mOrientation == VERTICAL) {
//如果是縱向排列,那么要在itemView的下方留出一個(gè)下邊界
outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
} else {
//如果是橫向排列,那么要在itemView的右方留出一個(gè)右邊界
outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
}
}
四、自定義ItemDecoration
下面,我們參考上面的寫法,寫一個(gè)簡(jiǎn)單的GridLayoutManager的分割線:
public class GridDividerItemDecoration extends RecyclerView.ItemDecoration {
private static final int[] ATTRS = new int[] { android.R.attr.listDivider };
private Drawable mDivider;
private final Rect mBounds = new Rect();
public GridDividerItemDecoration(Context context) {
final TypedArray a = context.obtainStyledAttributes(ATTRS);
mDivider = a.getDrawable(0);
a.recycle();
}
public void setDrawable(@NonNull Drawable drawable) {
mDivider = drawable;
}
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
drawDivider(c, parent);
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
outRect.set(0, 0, mDivider.getIntrinsicWidth(), mDivider.getIntrinsicHeight());
}
private void drawDivider(Canvas canvas, RecyclerView parent) {
canvas.save();
final int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
final View view = parent.getChildAt(i);
parent.getDecoratedBoundsWithMargins(view, mBounds);
mDivider.setBounds(mBounds.right - mDivider.getIntrinsicWidth(), mBounds.top, mBounds.right, mBounds.bottom);
mDivider.draw(canvas);
mDivider.setBounds(mBounds.left, mBounds.bottom - mDivider.getIntrinsicHeight(), mBounds.right , mBounds.bottom);
mDivider.draw(canvas);
}
canvas.restore();
}
}
最終的效果為:

這里我們?yōu)榱搜菔痉奖?,沒有考慮最后一列或者最后一行沒有分割線的情況,這篇文章寫的比較好:Android RecyclerView 使用完全解析 體驗(yàn)藝術(shù)般的控件。
五、總結(jié)
ItemDecoration的使用并不難,大多數(shù)情況下就只需要重寫onDraw和onDrawOver中的一個(gè);如果需要在Item之間添加間隔,那么要重寫getItemOffsets并理解outRect的含義,假如不需要添加間隔,那么不需要重寫該方法。
更多文章,歡迎訪問我的 Android 知識(shí)梳理系列:
- Android 知識(shí)梳理目錄:http://www.itdecent.cn/p/fd82d18994ce
- 個(gè)人主頁:http://lizejun.cn
- 個(gè)人知識(shí)總結(jié)目錄:http://lizejun.cn/categories/