用過ListView的朋友都知道,ListView是帶有分割線效果的,只需要設置divider屬性。RecyclerView本身并沒有分割線,很多人為了省事也是粗暴的在item布局里面直接添加分割線,雖然沒什么問題,但我覺得不夠優雅。當然官方也是提供了分割線的解決方案,就是采用DividerItemDecoration來設置分割線。
我們來分析一下DividerItemDecoration源碼
public class DividerItemDecoration extends ItemDecoration {
public static final int HORIZONTAL = 0;
public static final int VERTICAL = 1;
private static final String TAG = "DividerItem";
private static final int[] ATTRS = new int[]{16843284};
private Drawable mDivider;
private int mOrientation;
private final Rect mBounds = new Rect();
public DividerItemDecoration(Context context, int orientation) {
TypedArray a = context.obtainStyledAttributes(ATTRS);
this.mDivider = a.getDrawable(0);
if (this.mDivider == null) {
Log.w("DividerItem", "@android:attr/listDivider was not set in the theme used for this DividerItemDecoration. Please set that attribute all call setDrawable()");
}
a.recycle();
this.setOrientation(orientation);
}
public void setOrientation(int orientation) {
if (orientation != 0 && orientation != 1) {
throw new IllegalArgumentException("Invalid orientation. It should be either HORIZONTAL or VERTICAL");
} else {
this.mOrientation = orientation;
}
}
public void setDrawable(@NonNull Drawable drawable) {
if (drawable == null) {
throw new IllegalArgumentException("Drawable cannot be null.");
} else {
this.mDivider = drawable;
}
}
public void onDraw(Canvas c, RecyclerView parent, State state) {
if (parent.getLayoutManager() != null && this.mDivider != null) {
if (this.mOrientation == 1) {
this.drawVertical(c, parent);
} else {
this.drawHorizontal(c, parent);
}
}
}
private void drawVertical(Canvas canvas, RecyclerView parent) {
canvas.save();
int left;
int right;
if (parent.getClipToPadding()) {
left = parent.getPaddingLeft();
right = parent.getWidth() - parent.getPaddingRight();
canvas.clipRect(left, parent.getPaddingTop(), right, parent.getHeight() - parent.getPaddingBottom());
} else {
left = 0;
right = parent.getWidth();
}
int childCount = parent.getChildCount();
for(int i = 0; i < childCount; ++i) {
View child = parent.getChildAt(i);
parent.getDecoratedBoundsWithMargins(child, this.mBounds);
int bottom = this.mBounds.bottom + Math.round(child.getTranslationY());
int top = bottom - this.mDivider.getIntrinsicHeight();
this.mDivider.setBounds(left, top, right, bottom);
this.mDivider.draw(canvas);
}
canvas.restore();
}
private void drawHorizontal(Canvas canvas, RecyclerView parent) {
canvas.save();
int top;
int bottom;
if (parent.getClipToPadding()) {
top = parent.getPaddingTop();
bottom = parent.getHeight() - parent.getPaddingBottom();
canvas.clipRect(parent.getPaddingLeft(), top, parent.getWidth() - parent.getPaddingRight(), bottom);
} else {
top = 0;
bottom = parent.getHeight();
}
int childCount = parent.getChildCount();
for(int i = 0; i < childCount; ++i) {
View child = parent.getChildAt(i);
parent.getLayoutManager().getDecoratedBoundsWithMargins(child, this.mBounds);
int right = this.mBounds.right + Math.round(child.getTranslationX());
int left = right - this.mDivider.getIntrinsicWidth();
this.mDivider.setBounds(left, top, right, bottom);
this.mDivider.draw(canvas);
}
canvas.restore();
}
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) {
if (this.mDivider == null) {
outRect.set(0, 0, 0, 0);
} else {
if (this.mOrientation == 1) {
outRect.set(0, 0, 0, this.mDivider.getIntrinsicHeight());
} else {
outRect.set(0, 0, this.mDivider.getIntrinsicWidth(), 0);
}
}
}
}
DividerItemDecoration 是繼承自ItemDecoration,Decoration意思為裝飾,也就是說這個東西就是條目裝飾器,我們來看最重要的兩個方法
@Override
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.onDraw(c, parent, state);
}
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
}
onDraw我們最了解了,就是繪制。getItemOffsets就是設置偏移量如果不設置這個就可能會造成你繪制的view和item重疊。
也就是說要設置分割線我們就要從這兩者下手,創建一個類DividerDecoration繼承自ItemDecoration,如下
public class DividerDecoration extends ItemDecoration {
@Override
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.onDraw(c, parent, state);
}
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
}
}
在構造方法里創建畫筆和相關設置,在onDraw方法里面進行繪制
public class DividerDecoration extends ItemDecoration {
private int width;
private int divider_height;
private int divider_padding;
private Paint paint;
public DividerDecoration(Context context, int resColor, float dividerHeight, float padding) {
width = context.getResources().getDisplayMetrics().widthPixels;
paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
paint.setColor(context.getResources().getColor(resColor));
divider_height = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dividerHeight, context.getResources().getDisplayMetrics());
divider_padding = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, padding, context.getResources().getDisplayMetrics());
}
@Override
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.onDraw(c, parent, state);
int count = parent.getChildCount();
for (int i = 0; i < count-1; i++) {
View view = parent.getChildAt(i);
int top = view.getBottom();
int bottom = top + divider_height;
//這里把left和right的值分別增加divider_padding和減去divider_padding
c.drawRect(divider_padding, top, width - divider_padding, bottom, paint);
}
}
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
outRect.bottom = divider_height;
}
}
運行結果如下圖:
代碼比較簡單,接下來我們來實現今天的重點——懸浮吸頂效果。在實現這個效果之前我們先把頭部布局繪制出來,頭部布局就是繪制背景和文字
@Override
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.onDraw(c, parent, state);
int count = parent.getChildCount();
for (int i = 0; i < count; i++) {
View view = parent.getChildAt(i);
int position = parent.getChildLayoutPosition(view);
boolean isFirst = mISticky.isFirstPosition(position);
String text = mISticky.getGroupTitle(position);
int left = parent.getPaddingLeft();
int right = parent.getWidth() - parent.getPaddingRight();
if (isFirst) {
c.drawRect(left, view.getTop() - mRectHeight, right, view.getTop(), mRectPaint);
mTextPaint.getTextBounds(text, 0, text.length(), textRect);
float baseLine = (view.getTop() - mRectHeight) + mRectHeight / 2 + textRect.height() / 2;
c.drawText(text, left + mTextPaddingLeft, baseLine, mTextPaint);
} else {
c.drawRect(left, view.getTop() - 1, right, view.getTop(), mDividerPaint);
}
}
}
mISticky是用于區分頭部與普通item的接口,這里做判斷如果是頭部就繪制背景框和文字這里需要注意文字居中。文字的基準線就是背景框的中間位置y坐標加上文字框的一半。運行效果如下圖:
但是這樣沒辦法讓標題懸頂,ItemDecoration里面還有一個方法onDrawOver,它可以繪制一個view覆蓋在item之上,假設我們在第一個位置繪制號頭布局不就可以實現一直在頂上了嗎,我們在onDrawOver再繪制一個
@Override
public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.onDrawOver(c, parent, state);
int position = ((LinearLayoutManager) (parent.getLayoutManager())).findFirstVisibleItemPosition();
String text = mISticky.getGroupTitle(position);
int top = parent.getPaddingTop();
int left = parent.getPaddingLeft();
int right = parent.getWidth() - parent.getPaddingRight();
c.drawRect(left, top, right, top + mRectHeight, mRectPaint);
mTextPaint.getTextBounds(text, 0, text.length(), textRect);
float baseLine = top + mRectHeight / 2 + textRect.height() / 2;
c.drawText(text, left + mTextPaddingLeft, baseLine, mTextPaint);
}
但是還有一個問題,頂部布局不能被下面的頂上去。所以當第一個可見的view的下面一個如果是另外一組就需要動態改變這個頭布局的繪制。具體實現就是我們先通過findFirstVisibleItemPosition拿到第一個可見的Item的position,那我們就可以根據position+1判斷下一個Item是否是另一組的頭布局,如果不是,繪制固定布局,如果是,我們根據第一個可見Item的getBottom值改變頭部布局的繪制。
@Override
public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.onDrawOver(c, parent, state);
int position = ((LinearLayoutManager) (parent.getLayoutManager())).findFirstVisibleItemPosition();
if (position <= -1 || position >= parent.getAdapter().getItemCount() - 1) {
return;
}
View view = parent.findViewHolderForAdapterPosition(position).itemView;
String text = mISticky.getGroupTitle(position);
boolean isFirst = mISticky.isFirstPosition(position + 1);
int top = parent.getPaddingTop();
int left = parent.getPaddingLeft();
int right = parent.getWidth() - parent.getPaddingRight();
if (isFirst) {
int bottom = Math.min(mRectHeight, view.getBottom());
c.drawRect(left, top + bottom - mRectHeight, right, top + bottom, mRectPaint);
mTextPaint.getTextBounds(text, 0, text.length(), textRect);
float baseLine = top + bottom - mRectHeight / 2 + textRect.height() / 2;
c.drawText(text, left + mTextPaddingLeft, baseLine, mTextPaint);
}else {
c.drawRect(left, top, right, top + mRectHeight, mRectPaint);
mTextPaint.getTextBounds(text, 0, text.length(), textRect);
float baseLine = top + mRectHeight / 2 + textRect.height() / 2;
c.drawText(text, left + mTextPaddingLeft, baseLine, mTextPaint);
}
}
重點在于頂部布局滑動時坐標的計算,至此一個懸浮吸頂的StickyItemDecoration就完成了
public class StickyItemDecoration extends RecyclerView.ItemDecoration {
private ISticky mISticky;
private int mRectHeight;//背景高度
private int mTextPaintSize; // 文字TextSize
private int mTextPaddingLeft; // 文字到左邊的距離
private Paint mTextPaint; //文字畫筆
private Paint mRectPaint; //標題背景框畫筆
private Paint mDividerPaint; //分割線畫筆
private final Rect textRect;
public StickyItemDecoration(Context context, ISticky iSticky) {
mISticky = iSticky;
mRectHeight = dp2px(context, 25);
mTextPaintSize = sp2px(context, 16);
mTextPaddingLeft = dp2px(context, 14);
mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mTextPaint.setColor(Color.BLACK);
mTextPaint.setTextSize(mTextPaintSize);
mRectPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mRectPaint.setStyle(Paint.Style.FILL);
mRectPaint.setColor(Color.parseColor("#DDDDDD"));
mDividerPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mDividerPaint.setStyle(Paint.Style.FILL);
mDividerPaint.setColor(Color.BLUE);
textRect = new Rect();
}
@Override
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.onDraw(c, parent, state);
int count = parent.getChildCount();
for (int i = 0; i < count; i++) {
View view = parent.getChildAt(i);
int position = parent.getChildLayoutPosition(view);
boolean isFirst = mISticky.isFirstPosition(position);
String text = mISticky.getGroupTitle(position);
int left = parent.getPaddingLeft();
int right = parent.getWidth() - parent.getPaddingRight();
if (isFirst) {
c.drawRect(left, view.getTop() - mRectHeight, right, view.getTop(), mRectPaint);
mTextPaint.getTextBounds(text, 0, text.length(), textRect);
float baseLine = (view.getTop() - mRectHeight) + mRectHeight / 2 + textRect.height() / 2;
c.drawText(text, left + mTextPaddingLeft, baseLine, mTextPaint);
} else {
c.drawRect(left, view.getTop() - 1, right, view.getTop(), mDividerPaint);
}
}
}
@Override
public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.onDrawOver(c, parent, state);
int position = ((LinearLayoutManager) (parent.getLayoutManager())).findFirstVisibleItemPosition();
if (position <= -1 || position >= parent.getAdapter().getItemCount() - 1) {
return;
}
View view = parent.findViewHolderForAdapterPosition(position).itemView;
String text = mISticky.getGroupTitle(position);
boolean isFirst = mISticky.isFirstPosition(position + 1);
int top = parent.getPaddingTop();
int left = parent.getPaddingLeft();
int right = parent.getWidth() - parent.getPaddingRight();
if (isFirst) {
int bottom = Math.min(mRectHeight, view.getBottom());
c.drawRect(left, top + bottom - mRectHeight, right, top + bottom, mRectPaint);
mTextPaint.getTextBounds(text, 0, text.length(), textRect);
float baseLine = top + bottom - mRectHeight / 2 + textRect.height() / 2;
c.drawText(text, left + mTextPaddingLeft, baseLine, mTextPaint);
}else {
c.drawRect(left, top, right, top + mRectHeight, mRectPaint);
mTextPaint.getTextBounds(text, 0, text.length(), textRect);
float baseLine = top + mRectHeight / 2 + textRect.height() / 2;
c.drawText(text, left + mTextPaddingLeft, baseLine, mTextPaint);
}
}
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView
parent, @NonNull RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
int pos = parent.getChildLayoutPosition(view);
if (mISticky.isFirstPosition(pos)) {
outRect.top = mRectHeight;
} else {
outRect.top = 1;
}
}
/**
* dp轉換成px
*/
private int dp2px(Context context, float dpValue) {
float scale = context.getResources().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
private int sp2px(Context context, float spValue) {
final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
return (int) (spValue * fontScale + 0.5f);
}
}
不過此方案太依賴于第一個可見的item。如果item的高度 小于標題高度會導致文字快速上移,還有就是無法在標題設置點擊事件。