最近在项目项目中要做个类似于美团城市列表选择的页面,也就是我们常说的带索引并且粘性头部的列表。
如下图是最终效果
大家能直观的看出:每个字母开头相同的城市分为了一组,并且滚动的时候那个头部都是是固定在其顶部,以此替换。
在这之前的一个项目,做过手机联系人的目录和这个需求很类似,本想把之前的东西直接套过来。但发现当时的做法有点不够优雅而且item 的效率很低,实现方式:
每个item 都设有灰色头部的索引,只不过是 当只在每组的第一个位置可见其他时候隐藏。这样就把整个列表进行了分类处理,至于那个粘性头部,是在父布局外加了一个头部View固定在顶部并做滚动的处理。
后来在网上查看到一种很不错的做法,通过给recyclerview添加ItemDecoration装饰的效果。网上关于这个知识的文章很多,也有很多现成的代码。但是作为一个合格的程序猿一定不是复制粘贴,要明白其所以然,并运用到自己实际需求当中。那我们就通过代码一起学习ItemDecoration的使用吧。
通过继承ItemDecoration实现三个很重要的方法
/**
* 对itemview矩形区域进行的装饰, Rect的上下左右,类似padding的效果
* @param outRect
* @param view
* @param parent
* @param state
*/
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
}
/**
* 就像自定义View中onDraw绘制作用 只不过是在ItemView的指定位置绘制指定的内容
* 当然要和onDrawOver有个细微的区分,它绘制的范围在itemview的下面
* 比如:所绘制的区域和itemView有重叠的则会被遮挡。而在onDrawOver中绘制的范围则相反
* @param c
* @param parent
* @param state
*/
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDraw(c, parent, state);
}
/**
* @param c
* @param parent
* @param state
*/
@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDrawOver(c, parent, state);
}
那我们就通过具体的实现看看到底有效果
recyclerview.addItemDecoration(new CustomItemDecoration(this));
然后在自定义的Decoration中简单的添加的做一下处理,注意:
outRect.bottom = dividerHeight;
outRect.right = dividerHeight;
outRect.left = dividerHeight;
给每个item的是三个方向添加个类似10dp高的padding
public class CustomItemDecoration extends RecyclerView.ItemDecoration {
private final int dividerHeight;
private final int dividerColor;
private final Paint dividerPaint;
private final Paint dividerPaintL;
private final Paint dividerPaintR;
public CustomItemDecoration(Context context) {
dividerPaint =new Paint();
dividerPaintL = new Paint();
dividerPaintR = new Paint();
dividerHeight =context.getResources().getDimensionPixelOffset(R.dimen.divider_height);
dividerColor = context.getResources().getColor(R.color.colorPrimary);
dividerPaint.setColor(dividerColor);
dividerPaintL.setColor(context.getResources().getColor(R.color.colorAccent));
dividerPaintR.setColor(context.getResources().getColor(R.color.colorAccent));
}
@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDrawOver(c, parent, state);
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
outRect.bottom = dividerHeight;
outRect.right = dividerHeight;
outRect.left = dividerHeight;
}
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDraw(c, parent, state);
int childCount = parent.getChildCount();
int left = parent.getPaddingLeft()+dividerHeight;
int right = parent.getWidth() - parent.getPaddingRight()+parent.getPaddingLeft()-dividerHeight;
int lLeft=parent.getPaddingLeft() ;
int lRight = lLeft + dividerHeight;
for (int i = 0; i < childCount; i++) {
View view = parent.getChildAt(i);
float top = view.getBottom();
float bottom = view.getBottom() + dividerHeight;
float lTop = view.getTop();
float lBottom = view.getBottom();
int left1 = view.getRight();
int right1 = left1 + dividerHeight;
int top1 = view.getTop();
int bottom1 = view.getBottom();
//底部
c.drawRect(left, top, right, bottom, dividerPaint);
//左侧
c.drawRect(lLeft, lTop, lRight, lBottom, dividerPaintL);
//右侧
c.drawRect(left1, top1, right1, bottom1, dividerPaintR);
}
}
}
那我们来看效果
可以看到在每个itemView的左右和下方多出了间隙,并且在onDraw()方法中绘制了不同的颜色,我们能很直观的了解每个方法的作用和它工作的原理。
然后我们运用到需求当中。
- 只有在同一组中才添加ItemDecoration
- 滚动的时候固定头部
看下面的代码实现:
public class SectionDecoration extends RecyclerView.ItemDecoration {
private final String TAG = SectionDecoration.class.getName();
private final Paint paint;
private final TextPaint textPaint;
private final Paint.FontMetrics fontMetrics;
private final int topGap;
private DecorationCallback decorationCallback;
public SectionDecoration(Context context, DecorationCallback callback) {
this.decorationCallback = callback;
Resources resources = context.getResources();
paint = new Paint();
paint.setColor(resources.getColor(R.color.colorf5));
textPaint = new TextPaint();
textPaint.setTypeface(Typeface.DEFAULT_BOLD);
textPaint.setAntiAlias(false);
textPaint.setTextSize(80);
textPaint.setColor(Color.BLACK);
textPaint.setTextAlign(Paint.Align.LEFT);
fontMetrics = new Paint.FontMetrics();
textPaint.getFontMetrics(fontMetrics);
topGap = resources.getDimensionPixelOffset(R.dimen.section_top);
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
int position = parent.getChildAdapterPosition(view);
Log.e(TAG, "getItemOffsets :" + position);
long groupId = decorationCallback.getGroupId(position);
if(groupId<0) return;
if (position == 0 || isFirstInGroup(position)) {//同组的第一个才添加padding
outRect.top = topGap;
} else {
outRect.top = 0;
}
}
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDraw(c, parent, state);
int left = parent.getPaddingLeft();
int right = parent.getWidth() - parent.getPaddingRight();
int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
View view = parent.getChildAt(i);
int position = parent.getChildAdapterPosition(view);
long groupId = decorationCallback.getGroupId(position);
if(groupId<0) return;
String textLine = decorationCallback.getGroupFirstLine(position).toUpperCase();
if (position == 0 || isFirstInGroup(position)) {
int top = view.getTop() - topGap;
int bottom = view.getTop();
c.drawRect(left,top,right,bottom,paint);//绘制矩形区
c.drawText(textLine, left, bottom, textPaint);
}
}
}
@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDrawOver(c, parent, state);
int itemCount = state.getItemCount();
int childCount = parent.getChildCount();
int left = parent.getPaddingLeft();
int right = parent.getWidth() - parent.getPaddingRight();
float lineHeight = textPaint.getTextSize() + fontMetrics.descent;
long preGroupId, groupId = -1;
for (int i = 0; i < childCount; i++) {
View view = parent.getChildAt(i);
int position = parent.getChildAdapterPosition(view);
preGroupId = groupId;
groupId = decorationCallback.getGroupId(position);
if (groupId < 0 || groupId == preGroupId) continue;
String textLine = decorationCallback.getGroupFirstLine(position).toUpperCase();
if(TextUtils.isEmpty(textLine)) continue;
int viewBottom = view.getBottom();
int textY = Math.max(topGap, view.getTop());
if (position + 1 < itemCount) {
long nextGroupId = decorationCallback.getGroupId(position + 1);
if (nextGroupId != groupId && viewBottom < textY) {
textY = viewBottom;
}
}
c.drawRect(left, textY - topGap, right, textY, paint);
c.drawText(textLine, left, textY, textPaint);
}
}
private boolean isFirstInGroup(int position) {
if (position == 0) {
return true;
} else {
long prevGroupId = decorationCallback.getGroupId(position - 1);
long groupId = decorationCallback.getGroupId(position);
return prevGroupId != groupId;
}
}
}
定义一个回调
public interface DecorationCallback {
long getGroupId(int position);
String getGroupFirstLine(int position);
}
这就完成了简单的效果
说实话刚开始使用itemDecorartion是比较麻烦的,但是当掌握上手以后,用起来真的很爽,避免了很多在代码中逻辑判断,比如有的时候在列表中只需要在某个位置才需要添加灰色的间距,如果不用itemDecoration,则通常的就是整加视图的层级,通过if else判断显示和隐藏。现在既然在RecyclerView中又itemDecorartion这个装饰,没什么不用让你的代码实现的更优雅。
这是写的第一篇博客,也希望自己能坚持这个习惯,记录自己的学习过程
风后面是风,天空上面是天空,而你的生活可以与众不同