前言
在很早很早以前(long long ago),ListView鼎盛的時代有一個屬性叫做divider粱挡。但是在RecycleView上面就是找不到他殊橙,那怎么辦呢辐宾?妆偏?泻肯?直到后來有一天發(fā)現(xiàn)他變身了,變成了ItemDecoration嫌套。實在是扯不下去了敞葛,直接開始吧誉察!
這篇博客醞釀了好長時間,希望不會讓各位看官失望惹谐。
任務(wù)
了解ItemDecoration的原理持偏,自己可以添加分割線,每個 ItemView 上疊加一個角標(biāo)氨肌,自定義 RecyclerView 中的頭部或者是粘性頭部鸿秆。
分析和實戰(zhàn)
1.具體的使用
RecyclerView的簡單使用可以參考前一篇RecyclerView系列一:簡單使用
我們在頁面中放兩個RecyclerView,上面一個下面一個怎囚,用來對比卿叽。如下圖所示:
現(xiàn)在開始處理:寫TextItemDecoration類讓他繼承RecyclerView.ItemDecoration,主要的代碼如下所示:
class TextItemDecoration extends RecyclerView.ItemDecoration {
//設(shè)置ItemView的內(nèi)嵌偏移長度(inset)
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
}
// 在子視圖上設(shè)置繪制范圍恳守,并繪制內(nèi)容
// 繪制圖層在ItemView以下考婴,所以如果繪制區(qū)域與ItemView區(qū)域相重疊,會被遮擋
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDraw(c, parent, state);
}
//同樣是繪制內(nèi)容井誉,但與onDraw()的區(qū)別是:繪制在圖層的最上層
@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDrawOver(c, parent, state);
}
}
2.分析方法getItemOffsets()
分析這里的時候蕉扮,我們先來盜個圖,如下:
我們所有的分析這個圖就可以概括了】攀ィ現(xiàn)在我們開始分析這個方法喳钟,在Android Studio中看super.getItemOffsets(outRect, view, parent, state);
這個方法,最終我們在RecyclerView
看到outRect.set(0, 0, 0, 0);
這一行代碼在岂。
那么我們就拿outRect
開刀奔则。TextItemDecoration
中代碼如下:
//設(shè)置ItemView的內(nèi)嵌偏移長度(inset)
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
//只是添加下面這一行代碼
outRect.set(50, 50, 50, 50);
}
把這個·ItemDecoration·放在下面的RecyclerView
上面,代碼如下:
recyclerView2.addItemDecoration(new TextItemDecoration());
運行效果蔽午,如下圖所示:
再來一張單獨的圖片易茬,如下所示:
- 如上圖所示,RecyclerView 中的 ItemView 外面會包裹著一個矩形(outRect)。
- 內(nèi)嵌偏移長度:該矩形(outRect)與 ItemView的間隔.
- 默認(rèn)的情況下抽莱,top范抓、left、right食铐、bottom都是0匕垫,所以矩形和ItemView就重疊了。
2.1源碼分析(直接上源碼):
下面的代碼都是在RecyclerView中虐呻,可以在RecyclerView里面找到源碼:
//測量所有的子view的寬和高象泵,得到這個子view的Rect。然后就能得到這一塊真正的寬和高斟叼。
//注意還有padding的值偶惠。
public void measureChild(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() + widthUsed, lp.width,
canScrollHorizontally());
final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(),
getPaddingTop() + getPaddingBottom() + heightUsed, lp.height,
canScrollVertically());
if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
child.measure(widthSpec, heightSpec);
}
}
//得到每個子view相應(yīng)的Rect,
//mItemDecorations.get(i).getItemOffsets(mTempRect, child, this, mState);
//上面的代碼就是設(shè)置相應(yīng)的四個值朗涩,就和我們的TextItemDecoration類里面的代碼對應(yīng)起來了忽孽。
Rect getItemDecorInsetsForChild(View child) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (!lp.mInsetsDirty) {
return lp.mDecorInsets;
}
if (mState.isPreLayout() && (lp.isItemChanged() || lp.isViewInvalid())) {
// changed/invalid items should not be updated until they are rebound.
return lp.mDecorInsets;
}
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);
//下面就是詳細的賦值
insets.left += mTempRect.left;
insets.top += mTempRect.top;
insets.right += mTempRect.right;
insets.bottom += mTempRect.bottom;
}
lp.mInsetsDirty = false;
return insets;
}
3.onDraw()
我們先來看看我們自定義的TextItemDecoration類里面的onDraw()代碼,如下所示:
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDraw(c, parent, state);
}
很明顯上面?zhèn)鬟f了一個Canvas 參數(shù)對象馋缅,所以它擁有了繪制的能力扒腕。
注意: 1.getItemOffsets 是針對每一個 ItemView绢淀,而 onDraw 方法卻是針對 RecyclerView 本身萤悴,所以在 onDraw 方法中需要遍歷屏幕上可見的 ItemView,分別獲取它們的位置信息皆的,然后分別的繪制對應(yīng)的分割線覆履。
2.Itemdecoration的onDraw()繪制會先于ItemView的onDraw()繪制,
第二點會出現(xiàn)如下的情況:
出現(xiàn)上面的問題解決方案是getItemOffsets()與onDraw()一塊使用费薄。說的我一愣一愣的硝全,最主要的問題onDrow()的時候,是怎樣得到相應(yīng)的點楞抡。
英雄莫怕伟众,請看上面的代碼onDraw(Canvas c, RecyclerView parent, RecyclerView.State state)
:
看第二個參數(shù):RecyclerView parent 這就是我們的突破點。(這里有一個疑問點召廷?凳厢??我們第4節(jié)處理竞慢。)
int childCount = parent.getChildCount();
View child = parent.getChildAt(i);
上面的代碼就能解決我們的問題先紫。
上代碼實戰(zhàn):要實現(xiàn)的效果如下:
代碼實現(xiàn)如下:
class TextItemDecoration extends RecyclerView.ItemDecoration {
private Paint mPaint;
public TextItemDecoration() {
this.mPaint = new Paint();
mPaint.setColor(Color.YELLOW);
// 畫筆顏色設(shè)置為黃色
}
//設(shè)置ItemView的內(nèi)嵌偏移長度(inset)
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
outRect.set(50, 50, 50, 50);
}
// 在子視圖上設(shè)置繪制范圍,并繪制內(nèi)容
// 繪制圖層在ItemView以下筹煮,所以如果繪制區(qū)域與ItemView區(qū)域相重疊遮精,會被遮擋
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDraw(c, parent, state);
// 獲取RecyclerView的Child view的個數(shù)
int childCount = parent.getChildCount();
// 遍歷每個Item,分別獲取它們的位置信息败潦,然后再繪制對應(yīng)的分割線
for (int i = 0; i < childCount; i++) {
// 獲取每個Item的位置
final View child = parent.getChildAt(i);
// 設(shè)置矩形(分割線)的寬度為10px
final int mDivider = 10;
// 矩形左上頂點 = (ItemView的左邊界,ItemView的下邊界)
final int left = child.getLeft();
final int top = child.getBottom();
// 矩形右下頂點 = (ItemView的右邊界,矩形的下邊界)
final int right = child.getRight();
final int bottom = top + mDivider;
// 通過Canvas繪制矩形(分割線)
c.drawRect(left, top, right, bottom, mPaint);
}
}
//同樣是繪制內(nèi)容本冲,但與onDraw()的區(qū)別是:繪制在圖層的最上層
@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDrawOver(c, parent, state);
}
}
4.onDrawOver()
我們先來看看我們自定義的TextItemDecoration類里面的onDrawOver()代碼准脂,如下所示:
@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDrawOver(c, parent, state);
}
很明顯onDrawOver里面的參數(shù)和onDraw里面的參數(shù)一模一樣,那還要onDrawOver有什么用呢檬洞?意狠??疮胖。請看下圖:
最主要的就是紫色區(qū)域环戈,onDrawOver的使用方法和onDraw類似。現(xiàn)在我們在每一個條目的右上角加一個圖標(biāo)澎灸。效果如下:
代碼實現(xiàn)如下:
class TextItemDecoration extends RecyclerView.ItemDecoration {
private Paint mPaint;
private Bitmap bitmap;
public TextItemDecoration() {
this.mPaint = new Paint();
mPaint.setColor(Color.YELLOW);
// 畫筆顏色設(shè)置為黃色
bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.email);
}
//設(shè)置ItemView的內(nèi)嵌偏移長度(inset)
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
outRect.set(50, 50, 50, 50);
}
// 在子視圖上設(shè)置繪制范圍院塞,并繪制內(nèi)容
// 繪制圖層在ItemView以下,所以如果繪制區(qū)域與ItemView區(qū)域相重疊性昭,會被遮擋
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDraw(c, parent, state);
// 獲取RecyclerView的Child view的個數(shù)
int childCount = parent.getChildCount();
// 遍歷每個Item拦止,分別獲取它們的位置信息,然后再繪制對應(yīng)的分割線
for (int i = 0; i < childCount; i++) {
// 獲取每個Item的位置
final View child = parent.getChildAt(i);
// 設(shè)置矩形(分割線)的寬度為10px
final int mDivider = 10;
// 矩形左上頂點 = (ItemView的左邊界,ItemView的下邊界)
final int left = child.getLeft();
final int top = child.getBottom();
// 矩形右下頂點 = (ItemView的右邊界,矩形的下邊界)
final int right = child.getRight();
final int bottom = top + mDivider;
// 通過Canvas繪制矩形(分割線)
c.drawRect(left, top, right, bottom, mPaint);
}
}
//同樣是繪制內(nèi)容糜颠,但與onDraw()的區(qū)別是:繪制在圖層的最上層
@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDrawOver(c, parent, state);
int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
View child = parent.getChildAt(i);
final int left = child.getRight() - bitmap.getWidth();
final int top = child.getTop();
c.drawBitmap(bitmap, left, top, mPaint);
}
}
}
5.自定義 RecyclerView 中的頭部或者是粘性頭部
具體的思路如下圖所示:
我們的操作在OutRect里面處理汹族。下面我們用假數(shù)據(jù)處理,頁面中只保留一個RecyclerView其兴。我們只分析TextItemDecoration
里面的代碼顶瞒。代碼如下:
class TextItemDecoration extends RecyclerView.ItemDecoration {
private Paint mPaint;
public TextItemDecoration() {
this.mPaint = new Paint();
// 畫筆顏色設(shè)置為黃色
mPaint.setColor(Color.YELLOW);
}
//設(shè)置ItemView的內(nèi)嵌偏移長度(inset)
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
int position = parent.getChildAdapterPosition(view);
if (position % 5 == 0) {
outRect.set(0, 50, 0, 0);
}
}
// 在子視圖上設(shè)置繪制范圍,并繪制內(nèi)容
// 繪制圖層在ItemView以下元旬,所以如果繪制區(qū)域與ItemView區(qū)域相重疊榴徐,會被遮擋
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDraw(c, parent, state);
// 獲取RecyclerView的Child view的個數(shù)
int childCount = parent.getChildCount();
// 遍歷每個Item,分別獲取它們的位置信息匀归,然后再繪制對應(yīng)的分割線
for (int i = 0; i < childCount; i++) {
if (i % 5 == 0) {
View child = parent.getChildAt(i);
int left = 0;
int top = child.getTop() - 50;
int right = child.getRight();
int bottom = child.getTop();
c.drawRect(left, top, right, bottom, mPaint);
}
}
}
//同樣是繪制內(nèi)容坑资,但與onDraw()的區(qū)別是:繪制在圖層的最上層
@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDrawOver(c, parent, state);
}
}
效果圖如下:
黃條才是我要的真愛,這怎么亂套了穆端?袱贮?問題就出在如下的代碼里面
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDraw(c, parent, state);
// 獲取RecyclerView的Child view的個數(shù)
int childCount = parent.getChildCount();
// 遍歷每個Item,分別獲取它們的位置信息体啰,然后再繪制對應(yīng)的分割線
for (int i = 0; i < childCount; i++) {
if (i % 5 == 0) {
View child = parent.getChildAt(i);
int left = 0;
int top = child.getTop() - 50;
int right = child.getRight();
int bottom = child.getTop();
c.drawRect(left, top, right, bottom, mPaint);
}
}
}
上面就是有問題的代碼:
int childCount = parent.getChildCount();
得到的是這一屏幕有多少個孩子攒巍。不是說得到的總的孩子的數(shù)目。
我們修改一下onDraw里面的代碼狡赐,如下:
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDraw(c, parent, state);
// 獲取RecyclerView的Child view的個數(shù)
int childCount = parent.getChildCount();
// 遍歷每個Item窑业,分別獲取它們的位置信息,然后再繪制對應(yīng)的分割線
for (int i = 0; i < childCount; i++) {
View child = parent.getChildAt(i);
int index = parent.getChildAdapterPosition(child);
if (index % 5 == 0) {
int left = 0;
int top = child.getTop() - 50;
int right = child.getRight();
int bottom = child.getTop();
c.drawRect(left, top, right, bottom, mPaint);
}
}
}
效果圖如下:
這距離我們想要的效果越來躍進了枕屉。能不能在黃條在最上面的時候停留呢常柄??還有就是推上去?西潘?不出所料所有的操作都是在這里面了卷玉。
下面我們直接上代碼,思考留給我親愛的讀者:
@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDrawOver(c, parent, state);
// 獲取RecyclerView的Child view的個數(shù)
int childCount = parent.getChildCount();
// 遍歷每個Item喷市,分別獲取它們的位置信息相种,然后再繪制對應(yīng)的分割線
for (int i = 0; i < childCount; i++) {
View child = parent.getChildAt(i);
int index = parent.getChildAdapterPosition(child);
if (index % 5 == 0) {
int item = (index) / 5;
if (i < 5) {
if (i == 1 && child.getTop() < 100) {
int left = 0;
int top = child.getTop() - 100;
int right = child.getRight();
int bottom = child.getTop() - 50;
c.drawRect(left, top, right, bottom, mPaint);
c.drawText("這是條目" + item + "視圖i是+" + i + "頂部的高低" + child.getTop() + "index++" + index, left, top + 50, textPaint);
} else {
int left = 0;
int top = 0;
int right = child.getRight();
int bottom = 50;
c.drawRect(left, top, right, bottom, mPaint);
if (i == 0) {
c.drawText("這是條目" + (item + 1) + "視圖i是+" + i + "頂部的高低" + child.getTop() + "index++" + index, left, top + 50, textPaint);
} else {
c.drawText("這是條目" + (item) + "視圖i是+" + i + "頂部的高低" + child.getTop() + "index++" + index, left, top + 50, textPaint);
}
}
}
if (i != 0) {
int left = 0;
int top = child.getTop() - 50;
int right = child.getRight();
int bottom = child.getTop();
c.drawRect(left, top, right, bottom, mPaint);
c.drawText("這是條目" + item + "視圖i是+" + i + "頂部的高低" + child.getTop() + "index++" + index, left, top + 50, textPaint);
}
}
}
}
我們的效果圖如下:
我把要打印的都給小伙伴們打印出來了。具體的優(yōu)化看自己的需求優(yōu)化就可以了品姓。
總結(jié)
本篇文章介紹了RecyclerView.ItemDecoration的使用寝并,還有它的原理。其實還是挺簡單的腹备。我相信簡單的自定義小伙伴應(yīng)該都會了衬潦。