2019年7月1日17:56:30 更新
最新介紹:請移步這里
SuperDecoration工具類:求star
介紹
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的定義,貼出來不是為了裝逼,而是google的定義非常的精確,基本上介紹了ItemDecoration的用途。
根據(jù)自己的理解,簡單的翻譯下:
ItemDecoration 允許應(yīng)用給具體的View添加具體的圖畫或者layout的偏移,對于繪制View之間的分割線,視覺分組邊界等等是非常有用的。
所有的ItemDecorations按照被添加的順序在itemview之前(如果通過重寫`onDraw()`)或者itemview之后(如果通過重寫 `onDrawOver(Canvas, RecyclerView, RecyclerView.State)`)繪制。
先看看ItemDecoration中的方法
除去被標(biāo)記為過時的外,只剩如下三個方法:
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
可以通過outRect.set(l,t,r,b)
設(shè)置指定itemview的paddingLeft
,paddingTop
,paddingRight
,paddingBottom
-
onDraw
可以通過一系列c.drawXXX()
方法在繪制itemView之前繪制我們需要的內(nèi)容。 -
onDrawOver
與onDraw
類似,只不過是在繪制itemView之后繪制,具體表現(xiàn)形式,就是繪制的內(nèi)容在itemview上層。
調(diào)用RecyclerView
的addItemDecoration()
方法就可以給RecyclerView
添加ItemDecoration
了,注意這里是add并不是set,這意味著是可以給一個RecyclerView
設(shè)置多個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 最終會調(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);
}
}
從源碼可以看出,事實確實如此,ItemDecoration
會被add到集合中,然后RecyclerView
會根據(jù)add的順序依次調(diào)用(getItemOffsets
->onDraw
->onDrawOver
)的方法,因此,ItemDecoration
的使用也變得更加靈活。
使用
介紹了這么多,是時候?qū)扅c(diǎn)代碼用用它了。
比如,給RecyclerView的每個Item設(shè)置間隔,這里我們要區(qū)分下RecyclerView的LayoutManager的類型,以及orientation類型。
LinearLayoutManger
一般情況下,設(shè)計稿會有下面兩種樣子的情形(先考慮HORIZONTAL
的情況,VERTICAL
處理起來原理也一樣)
- 第一排(recyclerview1) 第一個item,最后一個item沒有邊距
- 第二排(recyclerview2) 第一個item和最后一個item有邊距
在沒有ItemDecoration
之前,我們一般都是在xml布局中調(diào)整Padding
或者是Margin
,然后在代碼中根據(jù)position
來控制,這樣一來的話ViewHolder
中會多出一些看上去很臃腫的代碼。對于第二種情況我們也可以通過設(shè)置RecyclerView
的paddingLeft
以及paddingRight
并設(shè)置clipToPadding
為fasle
來實現(xiàn),但是滑動到邊緣的時候,感覺會有點(diǎn)怪怪的。
如果我們使用ItemDecoration
,將這部分的邏輯抽離出來,這樣的代碼不僅看起來,用起來更舒服,也更加符合面向?qū)ο蟮乃枷搿?/p>
首先我們定義一個類繼承RecyclerView.ItemDecoration
,通過構(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
方法判斷layoutManager
,orientation
,通過outRect.set()
設(shè)置每個Item
的padding
。orientation
為HORIZONTAL
時,第一個item需要額外設(shè)置左邊距的值,最后一個item需要設(shè)置右邊距的值,其他的item只需要設(shè)置paddingRight
,orientation
為VERTICAL
時, 只需要把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) {
// 待會再處理
} 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) {
// 第一個要設(shè)置PaddingLeft
outRect.set(mEdgeSpace, 0, mSpace, 0);
} else if (childPosition == itemCount - 1) {
// 最后一個設(shè)置PaddingRight
outRect.set(0, 0, mEdgeSpace, 0);
} else {
outRect.set(0, 0, mSpace, 0);
}
} else {
if (childPosition == 0) {
// 第一個要設(shè)置PaddingTop
outRect.set(0, mEdgeSpace, 0, mSpace);
} else if (childPosition == itemCount - 1) {
// 最后一個要設(shè)置PaddingBottom
outRect.set(0, 0, 0, mEdgeSpace);
} else {
outRect.set(0, 0, 0, mSpace);
}
}
}
GridLayoutManager
很多情況下,我們需要實現(xiàn)GridView
樣式的RecyclerView
,也分有邊距和沒邊距的情況,如下圖:
為了保證每個itemView
在水平方向(orientation
為vertical
時)或者垂直方向(orientation
為horizon
時)均分,那么必須讓每個itemview
的paddingleft+paddingRight
(orientation
為vertical
時)或者paddingTop+paddingBottom
(orientation
為horizon
時)相等,如下圖,每個紅色框框的尺寸是相等的,但每個itemview
的paddingLeft
和paddingRight
不同。
當(dāng)orientation
為vertical
時,我們需要在getItemOffsets
方法中計算每個Item的PaddingLeft
,以及PaddingRight
,保證每個Item的paddingLeft+paddingRight
相等,這樣才能達(dá)到均分的目的。由于距離智商巔峰期(高三)已經(jīng)很久了,對數(shù)字也不敏感,我們不妨用最簡單粗暴的方法來找到其中的規(guī)律——套數(shù)字。
無邊距
假如 mSpace(間距)等于14,spanCount等于4,mEdgeSpace(邊距)等于0,那么
totalSpace = mSpace * (itemCount-1) + EdgeSpace * 2 = 42 // space總和
eachSpace = totalSpace / itemCount = 10.5 // 每個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ù)列
Right用eachSpace -Left算出
計算
根據(jù)上面得出的規(guī)律,paddingLeft
都是等差數(shù)列,而且我們已知以及
,根據(jù)等差數(shù)列的公式
,很容易計算出公差d:
當(dāng)邊距為0時,d = ,當(dāng)邊距不為0時,d =
;
所以;
;
列數(shù)
上面的分析并沒有考慮orientation
為horizontal
的情況,其實只需要把top
,bottom
與left
,right
對調(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時
setGridOffset(((GridLayoutManager) manager).getOrientation(), ((GridLayoutManager) manager).getSpanCount(), outRect, childPosition, itemCount);
} else if (manager instanceof LinearLayoutManager) {
// manager為LinearLayoutManager時
setLinearOffset(((LinearLayoutManager) manager).getOrientation(), outRect, childPosition, itemCount);
}
}
}
/**
* 設(shè)置GridLayoutManager 類型的 offest
*
* @param orientation 方向
* @param spanCount 個數(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; // 分配給每個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;
// 無邊距的話 只有最后一行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);
}
getItemOffsets
的用法基本介紹完了,下一章節(jié)再探討探討onDraw
以及onDrawOver
的用法。