RecyclerView可以說是ListView和GridView的更新綜合版,在Android5.0推出,其特點如下:
1、支援不同方向、不同排版模式,實作多種資料展示形式(包含ListView、GridView、瀑布流);
2、裝載了ViewHolder的回收機制,無需我們考慮View的複用;
3、取消了OnItemClick點選事件,需自己實作;
4、可控制Item增删動畫;
5、可以設定Item的間隔樣式;
它的優點很多,但也給我們帶來了一定的不便,ItemDecoration用于實作Item之間的分割樣式。谷歌官方提供了DividerItemDecoration友善大家對橫向縱向清單進行分割線的添加,但是網格布局GridLayoutManager模式下的分割線布局依然需要自己實作,而且它的實作還是有一定複雜度,需要好好計算的。
以下實作有邊框和無邊框的網格布局GridItemDecoration:
1 getItemOffsets
設定每個Item的上下左右偏移,因為每行每列item個數與分割線條數都是不一樣的,是以在每兩個item之間都有分割線,但繪制的其實終止位置都需要計算。
1.1無邊框
左右方向上,每個item内分割線占的寬度 = ( item個數 - 1 ) * 分割線寬度 / item個數
每兩個item之間的分割線寬度是固定的,所有每個分割線寬度都被相鄰兩個item瓜分,其左右偏移計算如下:
left = 行中位置 * ( 分割線寬度 - 每個item内分割線占用的寬度 ) ;
right = 每個item内分割線占用的寬度 - left;
而上下分割線相對可以簡單點,上下相鄰的Item各占一半即可,第一行和最後一行不需要偏移,設定成0。
1.2 有邊框
對于有邊框的網狀分割線,左右方向上,每個item内分割線占的寬度 = ( item個數 + 1 ) * 分割線寬度 / item個數
第一列與最後一列都需要一條完整的分割線,中間部分分割線寬度則被相鄰的item瓜分,左右偏移計算如下:
left = ( 行中位置 + 1 ) * 分割線寬度 - 行中位置 * 每個item内分割線占用的寬度;
right = 每個item内分割線占用的寬度 - left;
上下偏移也是各占分割線的一半,第一行和最後一行需要有完整的分割線高度。
1.3 代碼化
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
// super.getItemOffsets(outRect, view, parent, state);
if (mVerticalDivider == null && mHorizontalDivider == null) {
outRect.set(0, 0, 0, 0);
return;
}
//清單中item個數
int itemCount = parent.getAdapter().getItemCount();
//清單中位置
int position = parent.getChildAdapterPosition(view);
//列數
int spanCount = getSpanCount(parent);
//左右偏移
//行中位置
int indexHorizontal = position % spanCount;
//縱向分割線寬度
int dividerWidth = mVerticalDivider.getIntrinsicWidth();
//有邊界 itemWidth = ( 列數 + 1 ) * 分割線寬度 / 列數
//無邊界 itemWidth = ( 列數 - 1 ) * 分割線寬度 / 列數
//每個item内分割線占用的寬度,
// 無邊框:每個item内分割線占的寬度 = ( item個數 - 1 ) * 分割線寬度 / item個數
// 有邊框:每個item内分割線占的寬度 = ( item個數 + 1 ) * 分割線寬度 / item個數
int itemDividerWidth = (spanCount + (hasBorder ? 1 : -1)) * dividerWidth / spanCount;
int left;
int right;
if (hasBorder) {
//有邊框
// left = ( 行中位置 + 1 ) * 分割線寬度 - 行中位置 * 每個item内分割線占用的寬度
left = (indexHorizontal + 1) * dividerWidth - indexHorizontal * itemDividerWidth;
//right = 每個item内分割線占用的寬度 - left
right = itemDividerWidth - left;
} else {
//無邊框
//left = 行中位置 * ( 分割線寬度 - 每個item内分割線占用的寬度 )
left = indexHorizontal * (dividerWidth - itemDividerWidth);
//right = 每個item内分割線占用的寬度 - left
right = itemDividerWidth - left;
}
//上下偏移
//橫向分割線高度
int dividerHeight = mHorizontalDivider.getIntrinsicHeight();
int top;
int bottom;
if (hasBorder) {
//有邊框,最上面偏移分割線高度,最下面偏移分割線高度,其他都上下各偏移分割線一半的高度
top = isFirstRow(position, spanCount) ? dividerHeight : (dividerHeight / 2);
bottom = isLastRow(position, itemCount, spanCount) ? dividerHeight : (dividerHeight / 2);
} else {
//無邊框,最上面高度偏移0,最下面高度偏移0,其他上下各偏移分割線一半高度
top = isFirstRow(position, spanCount) ? 0 : (dividerHeight / 2);
bottom = isLastRow(position, itemCount, spanCount) ? 0 : (dividerHeight / 2);
}
outRect.set(left, top, right, bottom);
}
2 onDraw
繪制的時候,一般情況下繪制一個item的下和左分割線(之是以繪制左分割線是因為在item的layout_width=“wrap_content”時擷取到每個item的右邊位置就是item的右邊位置而不是平分後的右邊位置),隻有在第一行需要再次繪制上分割線,最後一列需要繪制最右側分割線。
2.1 繪制水準分割線
先擷取每個item的上下左右位置,左邊位置就是分割線的左邊,右邊就是左邊位置加上左邊位置+每個item的均分寬度:
left = mBounds.left;
right = left + itemWidth;
每個item下分割線的位置就是目前item下邊位置向上移動分割線的一半高度(之前設定偏移量的時候就設定了一半的高度):
top = mBounds.bottom - dividerHeight / 2;
bottom = top + dividerHeight;
注:第一行和最後一行需要根據有無分割線做特殊處理,是否繪制。
/**
* 繪制水準分割線
*/
private void drawHorizontal(Canvas canvas, RecyclerView parent) {
canvas.save();
//總item數
int itemCount = parent.getAdapter().getItemCount();
//列數
int spanCount = getSpanCount(parent);
//每個item寬度
int itemWidth = (parent.getWidth() - parent.getPaddingLeft() - parent.getPaddingRight()) / spanCount;
//分割線高度
int dividerHeight = mVerticalDivider.getIntrinsicHeight();
int left;
int right;
int bottom;
int top;
int position;
int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
View childView = parent.getChildAt(i);
position = parent.getChildAdapterPosition(childView);
parent.getDecoratedBoundsWithMargins(childView, mBounds);
//分割線左右坐标
left = mBounds.left;
right = left + itemWidth;
//每個item下邊分割線上邊沿(getItemOffsets時每個item多空了一個分割線高度的一半)
top = mBounds.bottom - dividerHeight / 2;
if (isLastRow(position, itemCount, spanCount)) {
//最後一行,有邊界需要完整分割線高度,沒邊界減掉
top += hasBorder ? -dividerHeight / 2 : dividerHeight / 2;
}
if (position >= itemCount - spanCount && !hasBorder) {
//最後幾個,且沒有邊框
bottom = top;
} else {
bottom = top + dividerHeight;
}
mVerticalDivider.setBounds(left, top, right, bottom);
mVerticalDivider.draw(canvas);
if (isFirstRow(position, spanCount) && hasBorder) {
//第一行且有邊界,需要最上面一條
top = mBounds.top;
bottom = top + dividerHeight;
mVerticalDivider.setBounds(left, top, right, bottom);
mVerticalDivider.draw(canvas);
}
}
canvas.restore();
}
2.2 繪制垂直分割線
垂直分割線稍微麻煩點,畢竟設定偏移量的時候也是通過計算得出的。
上下位置不用說就是每個item的上下位置:
top = mBounds.top;
bottom = mBounds.bottom;
這裡有些需要特殊處理,比如:
1、無邊框時,item不是整行數的時候,倒數第二行最後幾個下分割線不需要繪制;
2、有邊框時還需要繪制最後一列邊框,最後一個item又分為是否是滿格的最後一個;
左右位置可以和設定偏移時候的item對應計算,是每個item的左邊位置再向左移動:
if (hasBorder) {
//有邊界
left = mBounds.left - (itemDividerWidth - dividerWidth) * indexHorizontal;
} else {
left = mBounds.left - (dividerWidth - indexHorizontal * (dividerWidth - itemDividerWidth));
}
右邊位置就是左邊位置+分割線寬度:
right = left + dividerWidth;
/**
* 繪制垂直分割線
*/
private void drawVertical(Canvas canvas, RecyclerView parent) {
canvas.save();
//總item數
int itemCount = parent.getAdapter().getItemCount();
//列數
int spanCount = getSpanCount(parent);
//每個item寬度
int itemWidth = (parent.getWidth() - parent.getPaddingLeft() - parent.getPaddingRight()) / spanCount;
//分割線寬度
int dividerWidth = mHorizontalDivider.getIntrinsicWidth();
//每個item中分割線占的寬度
int itemDividerWidth = (spanCount + (hasBorder ? 1 : -1)) * dividerWidth / spanCount;
int top;
int bottom;
int right;
int left;
int position;
int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
parent.getLayoutManager().getDecoratedBoundsWithMargins(child, mBounds);
position = parent.getChildAdapterPosition(child);
//分割線上下坐标
top = mBounds.top;
bottom = mBounds.bottom;
if (!hasBorder) {
//沒有邊界時
if (position + spanCount == itemCount) {
//最後一個item上面那個
bottom += mVerticalDivider.getIntrinsicHeight() / 2;
} else if ((itemCount % spanCount != 0) && (itemCount % spanCount < position % spanCount) && (position > itemCount - spanCount)) {
//item不是整行數時,倒數第二行最後幾個的下邊界線需要減去
bottom -= mVerticalDivider.getIntrinsicHeight() / 2;
}
}
//行中位置
int indexHorizontal = position % spanCount;
if (hasBorder) {
//有邊界
left = mBounds.left - (itemDividerWidth - dividerWidth) * indexHorizontal;
} else {
left = mBounds.left - (dividerWidth - indexHorizontal * (dividerWidth - itemDividerWidth));
}
right = left + dividerWidth;
//畫左邊縱向分割線
mVerticalDivider.setBounds(left, top, right, bottom);
mVerticalDivider.draw(canvas);
if (hasBorder && isLastColumn(position, spanCount, itemCount)) {
//最後一列
if ((indexHorizontal + 1) % spanCount == 0) {
//每行滿格最後一個
left = parent.getWidth() - parent.getPaddingRight() - dividerWidth;
} else {
//不滿格的最後一個
left = left + itemWidth - (itemDividerWidth - dividerWidth);
}
right = left + dividerWidth;
mVerticalDivider.setBounds(left, top, right, bottom);
mVerticalDivider.draw(canvas);
}
}
canvas.restore();
}
完整代碼