上一篇我介紹了水準線性布局和RecyclerView動态添加删除資料行時的動畫效果。這一篇将講下RecyclerView的分隔圖自定義。
對于RecyclerView,它的分隔圖沒有ListView來的那麼簡單,隻要設定android:divider屬性就行。它把分隔圖的繪制給抽離出來了。這樣做犧牲了設定的簡單性,但是也帶來了靈活性。
比方說,ListView的分隔圖,我不能簡單做到隔一行顯示或者其他邏輯方式處理。不過在RecyclerView,就可以很輕易的實作。因為分隔圖的繪制範圍是受你控制,你愛咋畫就咋畫。
ok,說了這麼多,先來看看添加分隔圖的效果

上面包含LinearLayout和GridLayout的橫向和縱向的分隔圖,至于瀑布流的添加,其實和Grid的分隔圖差不多。為了看起來清晰一點,我把分隔的高度調大了。
那麼,要實作自定義分隔圖,首先,我們得熟悉一下它的接口。
public static abstract class ItemDecoration {
/**
* Draw any appropriate decorations into the Canvas supplied to the RecyclerView.
* Any content drawn by this method will be drawn before the item views are drawn,
* and will thus appear underneath the views.
*
* @param c Canvas to draw into
* @param parent RecyclerView this ItemDecoration is drawing into
* @param state The current state of RecyclerView
*/
public void onDraw(Canvas c, RecyclerView parent, State state) {
onDraw(c, parent);
}
/**
* @deprecated
* Override {@link #onDraw(Canvas, RecyclerView, RecyclerView.State)}
*/
@Deprecated
public void onDraw(Canvas c, RecyclerView parent) {
}
/**
* Draw any appropriate decorations into the Canvas supplied to the RecyclerView.
* Any content drawn by this method will be drawn after the item views are drawn
* and will thus appear over the views.
*
* @param c Canvas to draw into
* @param parent RecyclerView this ItemDecoration is drawing into
* @param state The current state of RecyclerView.
*/
public void onDrawOver(Canvas c, RecyclerView parent, State state) {
onDrawOver(c, parent);
}
/**
* @deprecated
* Override {@link #onDrawOver(Canvas, RecyclerView, RecyclerView.State)}
*/
@Deprecated
public void onDrawOver(Canvas c, RecyclerView parent) {
}
/**
* @deprecated
* Use {@link #getItemOffsets(Rect, View, RecyclerView, State)}
*/
@Deprecated
public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
outRect.set(, , , );
}
/**
* Retrieve any offsets for the given item. Each field of <code>outRect</code> specifies
* the number of pixels that the item view should be inset by, similar to padding or margin.
* The default implementation sets the bounds of outRect to 0 and returns.
*
* <p>
* If this ItemDecoration does not affect the positioning of item views, it should set
* all four fields of <code>outRect</code> (left, top, right, bottom) to zero
* before returning.
*
* <p>
* If you need to access Adapter for additional data, you can call
* {@link RecyclerView#getChildAdapterPosition(View)} to get the adapter position of the
* View.
*
* @param outRect Rect to receive the output.
* @param view The child view to decorate
* @param parent RecyclerView this ItemDecoration is decorating
* @param state The current state of RecyclerView.
*/
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) {
getItemOffsets(outRect, ((LayoutParams) view.getLayoutParams()).getViewLayoutPosition(),
parent);
}
}
上面的抽象類就是我們需要複寫的對象,可以看到它包含了如下接口:
public void onDraw(Canvas c, RecyclerView parent, State state)
public void onDrawOver(Canvas c, RecyclerView parent, State state)
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state)
過時的就不列出來了。
這裡可以看到有兩個draw,這兩者的差別正如它們的名稱,一個在前畫,一個在後畫。
onDraw在每個item畫之前先開始畫,可以被item的内容覆寫。
onDrawOver在每個item畫完之後再開始畫,可以覆寫item的内容。
一般來說,我們可以選擇任意一個複寫就行。如果有特殊需求,比方說要透明覆寫在item上面,這樣的話,就可以用onDrawOver。
getItemOffsets是設定每個item的偏移量,這個偏移量部分一般就是用來繪制分隔圖。
好了,了解了這些接口,我就直接貼出LinearLayoutItemDecoration代碼:
public class LinearLayoutItemDecoration extends RecyclerView.ItemDecoration{
final Context mContext;
final int mOrientation;
final Drawable mDividerDrawable;
int mDividerHeight;
private static final int[] ATTRS = new int[]{
android.R.attr.listDivider,
android.R.attr.dividerHeight
};
public LinearLayoutItemDecoration(Context context, int orientation) {
mContext = context;
mOrientation = orientation;
TypedArray ta = context.obtainStyledAttributes(ATTRS);
mDividerDrawable = ta.getDrawable();
mDividerHeight = ta.getDimensionPixelSize(, );
ta.recycle();
}
public LinearLayoutItemDecoration(Context context, int orientation, int dividerHeight) {
mOrientation = orientation;
mContext = context;
mDividerHeight = dividerHeight;
TypedArray ta = context.obtainStyledAttributes(ATTRS);
mDividerDrawable = ta.getDrawable();
ta.recycle();
}
public LinearLayoutItemDecoration(Context context, int orientation, Drawable dividerDrawable, int dividerHeight) {
mContext = context;
mOrientation = orientation;
mDividerDrawable = dividerDrawable;
mDividerHeight = dividerHeight;
}
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
if (mOrientation == LinearLayoutManager.HORIZONTAL) {
drawHorizontal(c, parent);
} else {
drawVertical(c, parent);
}
}
private void drawHorizontal(Canvas c, RecyclerView parent) {
final int top = parent.getPaddingTop();
final int bottom = parent.getHeight()-parent.getPaddingBottom();
final int count = parent.getChildCount();
for (int i = ; i < count; i++) {
final View child = parent.getChildAt(i);
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
final int left = child.getRight()+params.rightMargin;
final int right = left+mDividerHeight;
mDividerDrawable.setBounds(left, top, right, bottom);
mDividerDrawable.draw(c);
}
}
private void drawVertical(Canvas c, RecyclerView parent) {
final int left = parent.getPaddingLeft();
final int right = parent.getWidth()-parent.getPaddingRight();
final int count = parent.getChildCount();
for (int i = ; i < count; i++) {
final View child = parent.getChildAt(i);
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
final int top = child.getBottom()+params.bottomMargin;
final int bottom = top + mDividerHeight;
mDividerDrawable.setBounds(left, top, right, bottom);
mDividerDrawable.draw(c);
}
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
if (mOrientation == LinearLayoutManager.VERTICAL) {
outRect.bottom = mDividerHeight;
} else {
outRect.right = mDividerHeight;
}
}
}
我定義了一個id數組:
private static final int[] ATTRS = new int[]{
android.R.attr.listDivider,
android.R.attr.dividerHeight
};
這樣定義,是從主題裡去擷取資源屬性。比方說,我可以定義如下主題:
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
</style>
<style name="ItemDecorationTheme" parent="AppTheme">
<!--<item name="android:listDivider">@drawable/img_line_x</item>-->
<item name="android:listDivider">@drawable/line_divide</item>
<item name="android:dividerHeight">px</item>
</style>
然後,就可以通過id數組和obtainStyledAttributes(ATTRS),獲得主題裡定義的屬性。
其餘代碼就不講了,需要注意的是parent.getChildCount()傳回的是一屏的view數量,不是所有item的數量。
接着,看下Grid布局的分隔圖,為了看起來清晰,我把垂直布局和橫向布局分開寫了。先來看看垂直布局:
public class GridLayoutItemDecoration extends RecyclerView.ItemDecoration{
private static final String TAG = "GridLayoutItemDecoration";
final Context mContext;
final Drawable mDividerDrawable;
int mDividerHeight;
private static final int[] ATTRS = new int[] {
android.R.attr.listDivider,
android.R.attr.dividerHeight
};
public GridLayoutItemDecoration(Context context) {
mContext = context;
// 從主題去擷取屬性鍵值
TypedArray ta = context.obtainStyledAttributes(ATTRS);
mDividerDrawable = ta.getDrawable();
mDividerHeight = ta.getDimensionPixelSize(, );
ta.recycle();
}
public GridLayoutItemDecoration(Context context, int height) {
mContext = context;
// 從主題去擷取屬性鍵值
TypedArray ta = context.obtainStyledAttributes(ATTRS);
mDividerDrawable = ta.getDrawable();
mDividerHeight = height;
ta.recycle();
}
public GridLayoutItemDecoration(Context context, Drawable drawable) {
mContext = context;
mDividerDrawable = drawable;
TypedArray ta = context.obtainStyledAttributes(ATTRS);
mDividerHeight = ta.getDimensionPixelSize(, );
ta.recycle();
}
public GridLayoutItemDecoration(Context context, Drawable drawable, int height) {
mContext = context;
mDividerDrawable = drawable;
mDividerHeight = height;
}
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
final GridLayoutManager manager = (GridLayoutManager) parent.getLayoutManager();
final int spanCount = manager.getSpanCount();
drawHorizontal(c, parent, state, spanCount);
drawVertical(c, parent, state, spanCount);
}
private void drawHorizontal(Canvas c, RecyclerView parent, RecyclerView.State state, final int spanCount) {
final int count = parent.getChildCount();
// 确定有幾行
final int rowCount = count/spanCount + (count%spanCount==?:);
final int left = parent.getPaddingLeft();
final int right = parent.getWidth() - parent.getPaddingRight();
for (int i = ; i < rowCount; i++) {
final View child = parent.getChildAt(i*spanCount);
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
final int top = child.getBottom() + params.bottomMargin;
final int bottom = top + mDividerHeight;
mDividerDrawable.setBounds(left, top, right, bottom);
mDividerDrawable.draw(c);
}
}
private void drawVertical(Canvas c, RecyclerView parent, RecyclerView.State state, final int spanCount) {
final int top = parent.getPaddingTop();
final int bottom = parent.getHeight() - parent.getPaddingBottom();
for (int i = ; i < spanCount; i++) {
final View child = parent.getChildAt(i);
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
final int left = child.getRight() + params.rightMargin;
final int right = left + mDividerHeight;
mDividerDrawable.setBounds(left, top, right, bottom);
mDividerDrawable.draw(c);
}
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
final GridLayoutManager manager = (GridLayoutManager) parent.getLayoutManager();
final int spanCount = manager.getSpanCount();
final int position = parent.getChildAdapterPosition(view);
final int count1 = parent.getChildCount();
final int count = parent.getAdapter().getItemCount();
Log.d(TAG, "getItemOffsets count = " + count1 + ", position = " + position);
if ((position == count -) && (position % spanCount) == (spanCount - )) {
// 最後一個,如果也是最右邊,那麼就不需要偏移
} else if (position >= (count - (count % spanCount))) {
// 最下面一行,隻要右邊偏移就行
outRect.right = mDividerHeight;
} else if ((position % spanCount) == (spanCount - )) {
// 最右邊一列,隻要下面偏移就行
outRect.bottom = mDividerHeight;
} else {
// 其他的話,右邊和下面都要偏移
outRect.set(, , mDividerHeight, mDividerHeight);
}
}
}
橫豎分隔圖的繪制看代碼,很清晰,我直接是繪制在每個item的下面和右邊。這裡主要說下偏移值的設定。
根據grid的布局,可以分成四個情況:
1. 最後一個item,如果也是最右邊那列裡的,是不需要偏移值的
2. 最下面一行,不需要下面的偏移值
3. 最右邊一列,不需要右邊的偏移值。(因為最右邊不需要畫分隔圖)
4. 其餘情況,下面和右邊都偏移就行。
需要注意的是,别用parent.getChildCount()去做為總數。應該用parent.getAdapter().getItemCount()。
橫向grid的代碼和縱向的類似,我貼下:
public class HorizontalGridLayoutItemDecoration extends RecyclerView.ItemDecoration{
final Context mContext;
final Drawable mDividerDrawable;
int mDividerHeight;
private static final int[] ATTRS = new int[] {
android.R.attr.listDivider,
android.R.attr.dividerHeight
};
public HorizontalGridLayoutItemDecoration(Context context) {
mContext = context;
// 從主題去擷取屬性鍵值
TypedArray ta = context.obtainStyledAttributes(ATTRS);
mDividerDrawable = ta.getDrawable();
mDividerHeight = ta.getDimensionPixelSize(, );
ta.recycle();
}
public HorizontalGridLayoutItemDecoration(Context context, int height) {
mContext = context;
// 從主題去擷取屬性鍵值
TypedArray ta = context.obtainStyledAttributes(ATTRS);
mDividerDrawable = ta.getDrawable();
mDividerHeight = height;
ta.recycle();
}
public HorizontalGridLayoutItemDecoration(Context context, Drawable drawable) {
mContext = context;
mDividerDrawable = drawable;
TypedArray ta = context.obtainStyledAttributes(ATTRS);
mDividerHeight = ta.getDimensionPixelSize(, );
ta.recycle();
}
public HorizontalGridLayoutItemDecoration(Context context, Drawable drawable, int height) {
mContext = context;
mDividerDrawable = drawable;
mDividerHeight = height;
}
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
final GridLayoutManager manager = (GridLayoutManager) parent.getLayoutManager();
final int spanCount = manager.getSpanCount();
drawHorizontal(c, parent, state, spanCount);
drawVertical(c, parent, state, spanCount);
}
private void drawVertical(Canvas c, RecyclerView parent, RecyclerView.State state, int spanCount) {
final int count = parent.getChildCount();
// 确定有幾列
final int columnCount = count/spanCount + (count%spanCount==?:);
final int top = parent.getPaddingTop();
final int bottom = parent.getHeight() - parent.getPaddingBottom();
for (int i = ; i < columnCount; i++) {
final View child = parent.getChildAt(i*spanCount);
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
final int left = child.getRight() + params.rightMargin;
final int right = left + mDividerHeight;
mDividerDrawable.setBounds(left, top, right, bottom);
mDividerDrawable.draw(c);
}
}
private void drawHorizontal(Canvas c, RecyclerView parent, RecyclerView.State state, int spanCount) {
final int left = parent.getPaddingLeft();
final int right = parent.getWidth() - parent.getPaddingRight();
for (int i = ; i < spanCount; i++) {
final View child = parent.getChildAt(i);
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
final int top = child.getBottom() + params.bottomMargin;
final int bottom = top + mDividerHeight;
mDividerDrawable.setBounds(left, top, right, bottom);
mDividerDrawable.draw(c);
}
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
final GridLayoutManager manager = (GridLayoutManager) parent.getLayoutManager();
final int spanCount = manager.getSpanCount();
final int position = parent.getChildAdapterPosition(view);
final int count = parent.getAdapter().getItemCount();
if ((position == count -) && (position % spanCount) == (spanCount - )) {
// 最後一個,如果也是最下面一行,那麼就不需要偏移
} else if (position >= (count - (count % spanCount))) {
// 最右邊一列,隻要下面偏移就行
outRect.bottom = mDividerHeight;
} else if ((position % spanCount) == (spanCount - )) {
// 最下面一行,隻要右邊偏移就行
outRect.right = mDividerHeight;
} else {
// 其他的話,右邊和下面都要偏移
outRect.set(, , mDividerHeight, mDividerHeight);
}
}
}
代碼不講了,隻需要注意一點,橫向grid的item的索引是從上到下,從左到右,知道這點,就應該沒問題了。
ok,基本的自定義分隔圖講完了,接下去,擴充一下思路,來展現一下自定義的好處。我就舉個分隔圖不斷遞寬的例子。
代碼就不用貼了吧,其實就是在上述LinearLayoutItemDecoration的基礎上,增加一個遞增值就行。
接下去說說,怎麼使用這些ItemDecoration。包含添加和移除:
RecyclerView.addItemDecoration(ItemDecoration);
RecyclerView.removeItemDecoration(ItemDecoration);
你可以給一個RecyclerView添加多個分隔圖。
好了,關于RecyclerView的分隔圖自定義講解完了。