用过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的高度 小于标题高度会导致文字快速上移,还有就是无法在标题设置点击事件。