1.背景
RecyclerView 是谷歌 V7 包下新增的控件,用來替代 ListView 和 GridView 使用的一個控件。在使用的過程中,往往需要使用到 divider 的效果 ( item 之間的分割線 )。而 RecyclerView 并不像 ListView 一樣自帶有 divider 的屬性。而是需要用到 RecyclerView.ItemDecoration 這樣一個類,但是 ItemDecoration 是一個抽象類,而且 android 內部并沒有給它做一些效果的實現。那么就需要我們自己去繼承并實現其中的方法,本文講述的就是在 GridLayoutManager 和 LinearLayoutManager 下如何去實現 ItemDecoration。至于 RecyclerView.ItemDecoration 的具體分析,大家可以去看看這篇文章 深入理解 RecyclerView 系列之一:ItemDecoration 這里不作過多的闡述。
2.實現基本的 Item 的 divider
2.1 創建 SpacesItemDecoration
創建一個類 SpacesItemDecoration 繼承于 RecyclerView.ItemDecoration ,實現其中的 onDraw 和 getItemOffsets 方法,在這里我們的設計是左右距離相等,上下距離相等。
public class SpacesItemDecoration extends RecyclerView.ItemDecoration {
private int leftRight;
private int topBottom;
public SpacesItemDecoration(int leftRight, int topBottom) {
this.leftRight = leftRight;
this.topBottom = topBottom;
}
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDraw(c, parent, state);
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
}
}
在這里我們主要實現的方法是 onDraw 和 getItemOffsets, getItemOffsets 主要是確定 divider 的范圍,而 onDraw 是對 divider 的具體實現。
2.2 LinearLayoutManager 下 divider 的實現
首先在 getItemOffsets 方法中需要判斷當前的 RecyclerView 所采用的哪種 LayoutManager。這里要注意的是 GridLayoutManager 是繼承 LinearLayoutManager 的,所以需要先判斷是否為 GridLayoutManager。
private SpacesItemDecorationEntrust getEntrust(RecyclerView.LayoutManager manager) {
SpacesItemDecorationEntrust entrust = null;
//要注意這邊的GridLayoutManager是繼承LinearLayoutManager,所以要先判斷GridLayoutManager
if (manager instanceof GridLayoutManager) {
entrust = new GridEntrust(leftRight, topBottom, mColor);
} else {//其他的都當做Linear來進行計算
entrust = new LinearEntrust(leftRight, topBottom, mColor);
}
return entrust;
}
然后我們來看具體的實現,首先判斷是 VERTICAL 還是 HORIZONTAL 。對于 VERTICAL,每一個 item 必需的是 top,left 和 right,但是最后一個 item 還需要 bottom。而對于 HORIZONTAL ,每一個 item 必需的是 top,left 和 bottom,但是最后一個 item 還需要 right。
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
LinearLayoutManager layoutManager = (LinearLayoutManager) parent.getLayoutManager();
//豎直方向的
if (layoutManager.getOrientation() == LinearLayoutManager.VERTICAL) {
//最后一項需要 bottom
if (parent.getChildAdapterPosition(view) == layoutManager.getItemCount() - 1) {
outRect.bottom = topBottom;
}
outRect.top = topBottom;
outRect.left = leftRight;
outRect.right = leftRight;
} else {
//最后一項需要right
if (parent.getChildAdapterPosition(view) == layoutManager.getItemCount() - 1) {
outRect.right = leftRight;
}
outRect.top = topBottom;
outRect.left = leftRight;
outRect.bottom = topBottom;
}
}
就這樣,divider 效果就實現了(當然是沒有任何的顏色的)。調用方式只需要。
int leftRight = dip2px(7);
int topBottom = dip2px(7);
rv_content.addItemDecoration(new SpacesItemDecoration(leftRight, topBottom));
2.3 GridLayoutManager 下 divider 的實現
對于 GridLayoutManager 下的實現,相比 LinearLayoutManager 要復雜一些。首先當然是判斷 VERTICAL 還是 HORIZONTAL。一般來說有三種情況的,如圖所示:
由于第二種布局樣式考慮的情況比較多,目前沒有找到比較好的方法去進行判斷,所以在這里只對另外兩種布局樣式進行考慮
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
GridLayoutManager layoutManager = (GridLayoutManager) parent.getLayoutManager();
final GridLayoutManager.LayoutParams lp = (GridLayoutManager.LayoutParams) view.getLayoutParams();
final int childPosition = parent.getChildAdapterPosition(view);
final int spanCount = layoutManager.getSpanCount();
if (layoutManager.getOrientation() == GridLayoutManager.VERTICAL) {
//判斷是否在第一排
if (layoutManager.getSpanSizeLookup().getSpanGroupIndex(childPosition, spanCount) == 0) {//第一排的需要上面
outRect.top = topBottom;
}
outRect.bottom = topBottom;
//這里忽略和合并項的問題,只考慮占滿和單一的問題
if (lp.getSpanSize() == spanCount) {//占滿
outRect.left = leftRight;
outRect.right = leftRight;
} else {
outRect.left = (int) (((float) (spanCount - lp.getSpanIndex())) / spanCount * leftRight);
outRect.right = (int) (((float) leftRight * (spanCount + 1) / spanCount) - outRect.left);
}
} else {
if (layoutManager.getSpanSizeLookup().getSpanGroupIndex(childPosition, spanCount) == 0) {//第一排的需要left
outRect.left = leftRight;
}
outRect.right = leftRight;
//這里忽略和合并項的問題,只考慮占滿和單一的問題
if (lp.getSpanSize() == spanCount) {//占滿
outRect.top = topBottom;
outRect.bottom = topBottom;
} else {
outRect.top = (int) (((float) (spanCount - lp.getSpanIndex())) / spanCount * topBottom);
outRect.bottom = (int) (((float) topBottom * (spanCount + 1) / spanCount) - outRect.top);
}
}
}
在這里,對于 VERTICAL 下,每個 item 需要的是 bottom,然后第一排需要 top,同時在這里使用了 GridLayoutManager 的一個方法 layoutManager.getSpanSizeLookup().getSpanGroupIndex(childPosition, spanCount)
該方法可以用于判斷 item 在布局中所處于的行數。因為這里的 outRect 的值會一起統計到每個 item 的寬高之中。為了保證每個 item 的大小一致,所以這里的每個 item 的 left 和 right 的和必須保持一致,具體的計算方法如下:
這樣,GridLayoutManager 的效果就實現了,調用方法跟LinearLayoutManager 下是一樣的。效果如下
3.實現 Item 的帶顏色分割線的效果
3.1 LinearManager 下的實現
上述基本實現了 item 分割的效果,但是它沒有辦法設置顏色。要實現顏色,首先我們得傳入一個顏色色值。
//color的傳入方式是resouce.getcolor
protected Drawable mDivider;
public SpacesItemDecorationEntrust(int leftRight, int topBottom, int mColor) {
this.leftRight = leftRight;
this.topBottom = topBottom;
if (mColor != 0) {
mDivider = new ColorDrawable(mColor);
}
}
有了顏色,那么我們就需要去重寫 onDraw 方法了,我們需要去確定繪制的區域。先貼上代碼
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
LinearLayoutManager layoutManager = (LinearLayoutManager) parent.getLayoutManager();
//沒有子view或者沒有沒有顏色直接return
if (mDivider == null || layoutManager.getChildCount() == 0) {
return;
}
int left;
int right;
int top;
int bottom;
final int childCount = parent.getChildCount();
if (layoutManager.getOrientation() == GridLayoutManager.VERTICAL) {
for (int i = 0; i < childCount - 1; i++) {
final View child = parent.getChildAt(i);
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
//將有顏色的分割線處于中間位置
float center = (layoutManager.getTopDecorationHeight(child) - topBottom) / 2;
//計算下邊的
left = layoutManager.getLeftDecorationWidth(child);
right = parent.getWidth() - layoutManager.getLeftDecorationWidth(child);
top = (int) (child.getBottom() + params.bottomMargin + center);
bottom = top + topBottom;
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(c);
}
} else {
for (int i = 0; i < childCount - 1; i++) {
final View child = parent.getChildAt(i);
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
//將有顏色的分割線處于中間位置
float center = (layoutManager.getLeftDecorationWidth(child) - leftRight) / 2;
//計算右邊的
left = (int) (child.getRight() + params.rightMargin + center);
right = left + leftRight;
top = layoutManager.getTopDecorationHeight(child);
bottom = parent.getHeight() - layoutManager.getTopDecorationHeight(child);
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(c);
}
}
}
RecyclerView 的機制是去繪制要顯示在屏幕中的 View,而沒有顯示出來的是不會去繪制。所以這邊需要使用的是 layoutManager.getChildCount() 而不是 layoutManager.getItemCount()。對于 LinearManager 下來說,需要繪制分割線的區域是兩個 item 之間,這里分為 VERTICAL 和 HORIZONTAL。我們拿 VERTICAL 來進行分析,首先獲取 item 以及它的 LayoutParams,在這里計算 float center = (layoutManager.getTopDecorationHeight(child) - topBottom) / 2;
因為一個 RecyclerView 可以添加多個 ItemDecoration,而且方法的調用順序是先實現所有 ItemDecoration 的 getItemOffsets 方法,然后再去實現 onDraw 方法。目前沒有找到辦法去解決每個 ItemDecoration 的具體區域。所以退而求其次的將分割線繪制在所有 ItemDecoration 的中間區域(基本能滿足一般的需求,當然可以自己修改位置滿足自己的需求)。然后我們要去確定繪制的區域,left 就是所有 ItemDecoration 的寬度,right 就是 parent 的寬度減去所有 ItemDecoration 的寬度。top 是 child 的底部位置然后還要加上center(center的目的是繪制在中間區域),bottom 就是 top 加上需要繪制的高度。同理在 HORIZONTAL 模式下可以類似的實現。使用一個 ItemDecoration 的效果
int leftRight = dip2px(2);
int topBottom = dip2px(2);
rv_content.addItemDecoration(new SpacesItemDecoration(leftRight, topBottom,getResources().getColor(R.color.colorPrimary)));
當然你也可以使用多個 ItemDecoration
int leftRight = dip2px(10);
int topBottom = dip2px(10);
rv_content.addItemDecoration(new SpacesItemDecoration(leftRight, topBottom));
rv_content.addItemDecoration(new SpacesItemDecoration(dip2px(2), dip2px(2), getResources().getColor(R.color.colorPrimary)));
3.2 GridManager 下的實現
GridManager 下的實現的步驟類似與 LinearManager,不同的是確定繪制分割線的區域。它的分割線的區域是相鄰的 item 之間都需要有分割線。廢話不多說,先上代碼。
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
final GridLayoutManager layoutManager = (GridLayoutManager) parent.getLayoutManager();
final GridLayoutManager.SpanSizeLookup lookup = layoutManager.getSpanSizeLookup();
if (mDivider == null || layoutManager.getChildCount() == 0) {
return;
}
//判斷總的數量是否可以整除
int spanCount = layoutManager.getSpanCount();
int left, right, top, bottom;
final int childCount = parent.getChildCount();
if (layoutManager.getOrientation() == GridLayoutManager.VERTICAL) {
for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
//將帶有顏色的分割線處于中間位置
final float centerLeft = ((float) (layoutManager.getLeftDecorationWidth(child) + layoutManager.getRightDecorationWidth(child))
* spanCount / (spanCount + 1) + 1 - leftRight) / 2;
final float centerTop = (layoutManager.getBottomDecorationHeight(child) + 1 - topBottom) / 2;
//得到它在總數里面的位置
final int position = parent.getChildAdapterPosition(child);
//獲取它所占有的比重
final int spanSize = lookup.getSpanSize(position);
//獲取每排的位置
final int spanIndex = lookup.getSpanIndex(position, layoutManager.getSpanCount());
//判斷是否為第一排
boolean isFirst = layoutManager.getSpanSizeLookup().getSpanGroupIndex(position, spanCount) == 0;
//畫上邊的,第一排不需要上邊的,只需要在最左邊的那項的時候畫一次就好
if (!isFirst && spanIndex == 0) {
left = layoutManager.getLeftDecorationWidth(child);
right = parent.getWidth() - layoutManager.getLeftDecorationWidth(child);
top = (int) (child.getTop() - centerTop) - topBottom;
bottom = top + topBottom;
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(c);
}
//最右邊的一排不需要右邊的
boolean isRight = spanIndex + spanSize == spanCount;
if (!isRight) {
//計算右邊的
left = (int) (child.getRight() + centerLeft);
right = left + leftRight;
top = child.getTop();
if (!isFirst) {
top -= centerTop;
}
bottom = (int) (child.getBottom() + centerTop);
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(c);
}
}
} else {
for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
//將帶有顏色的分割線處于中間位置
final float centerLeft = (layoutManager.getRightDecorationWidth(child) + 1 - leftRight) / 2;
final float centerTop = ((float) (layoutManager.getTopDecorationHeight(child) + layoutManager.getBottomDecorationHeight(child))
* spanCount / (spanCount + 1) - topBottom) / 2;
//得到它在總數里面的位置
final int position = parent.getChildAdapterPosition(child);
//獲取它所占有的比重
final int spanSize = lookup.getSpanSize(position);
//獲取每排的位置
final int spanIndex = lookup.getSpanIndex(position, layoutManager.getSpanCount());
//判斷是否為第一列
boolean isFirst = layoutManager.getSpanSizeLookup().getSpanGroupIndex(position, spanCount) == 0;
//畫左邊的,第一排不需要左邊的,只需要在最上邊的那項的時候畫一次就好
if (!isFirst && spanIndex == 0) {
left = (int) (child.getLeft() - centerLeft) - leftRight;
right = left + leftRight;
top = layoutManager.getRightDecorationWidth(child);
bottom = parent.getHeight() - layoutManager.getTopDecorationHeight(child);
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(c);
}
//最下的一排不需要下邊的
boolean isRight = spanIndex + spanSize == spanCount;
if (!isRight) {
//計算右邊的
left = child.getLeft();
if (!isFirst) {
left -= centerLeft;
}
right = (int) (child.getRight() + centerTop);
top = (int) (child.getBottom() + centerLeft);
bottom = top + leftRight;
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(c);
}
}
}
}
我們就 VERTICAL 的情況下來進行分析,首先橫向的分割線,只需要在最左側的 item 繪制出來的時候進行分割線的繪制就行了。當然最后一排是不需要的。
if (!isFirst && spanIndex == 0) {
left = layoutManager.getLeftDecorationWidth(child);
right = parent.getWidth() - layoutManager.getLeftDecorationWidth(child);
top = (int) (child.getTop() - centerTop) - topBottom;
bottom = top + topBottom;
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(c);
}
水平的分割線的計算方式類似與 LinearLayoutManager 下的計算方式。這里不過多闡述。而豎直方向的會有一些區別。由于 GridLayoutManager 下,item 的數量不一定能夠剛好整除每排的數量。所以這邊的繪制區域是根據每個 item 來進行確定的。能被整除的或者當數量不足的時候最后一項不需要豎直的分割線。同時要注意補齊 centerTop(分割線繪制在中間區域的位置)。
//最右邊的一排不需要右邊的
boolean isRight = spanIndex + spanSize == spanCount;
if (!isRight) {
//計算右邊的
left = (int) (child.getRight() + centerLeft);
right = left + leftRight;
top = child.getTop();
if (!isFirst) {
top -= centerTop;
}
bottom = (int) (child.getBottom() + centerTop);
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(c);
}
HORIZONTAL 下的情況可以進行類似的分析,代碼的調用方式跟LinearLayoutManager 下是一樣的。
4 最后
至此,RecyclerView 的 divide r效果已經基本實現了。當然,你可以在這基礎上進行修改,尤其是 GridLayoutManager 情況下比較復雜,可以根據實際的布局進行對應的修改,滿足自己的一些需求。歡迎大家一起相互交流。代碼已經上傳 https://github.com/hzl123456/SpacesItemDecoration
(ps:在實際的使用過程中,當對 RecyclerView 的 item 進行增加和刪除的操作是,會使 ItemDecoration 的分割區域計算錯誤。原因是在添加和刪除操作的時候,只會計算更新的部分區域的 OutRect,導致出現問題,這個時候我們只需要在添加和刪除操作之后調用 RecyclerView 的 invalidateItemDecorations() 方法就可以解決問題了)