一、概述
通過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è)簡單的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.lxweimin.com/p/fd82d18994ce
- 個(gè)人主頁:http://lizejun.cn
- 個(gè)人知識(shí)總結(jié)目錄:http://lizejun.cn/categories/