RecycleView分割线、分组、粘性布局之ItemDecoration

我们在使用 ListView 的时候要设置分割线只要在xml文件中,使用android:divider就可以了,但是在RecyclerView 却没有直接的设置方法。ListView中要实现分组效果、粘性布局,绝大多数都会使用PinnedSectionListView、StickyListHeaders等开源框架来实现这个功能,那么在RecycleView中该怎么使用呢?接下来就通过RecycleView的ItemDecoration来实现这些功能。

ItemDecoration类主要包含三个方法:

  •     public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state)
  •     public void onDraw(Canvas c, RecyclerView parent, State state)
  •     public void onDrawOver(Canvas c, RecyclerView parent, State state)

    方法一:可以实现类似padding的效果。
    方法二:可以实现类似绘制背景的效果,内容在底部。

    方法三:可以绘制在内容的上面,覆盖内容。

    需要注意的是onDraw在绘制ItemView之前绘制,onDrawOver会在绘制ItemView之后绘制,使用onDraw可以实现分割线效果,使用onDrawOver可以实现蒙层效果,二者可以混合使用,也可以实现一样的效果,实际使用中应该选择合适的。

一、RecycleView的下划线

1、获取下划线的Drawable对象

    这里使用Android自带的android.R.attr.listDivider下划线样式,在构造方法里面得到Drawable:

  1. final TypedArray a = context.obtainStyledAttributes(android.R.attr.listDivider);
  2. mDivider = a.getDrawable(0);
  3. a.recycle();
2、设置padding

    getItemOffsets方法中设置Margin:

  1. outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
  2. 或者:
  3. outRect.bottom = mDivider.getIntrinsicHeight();
3、绘制
  1. //获取Recycleview的left、right
  2. final int left = parent.getPaddingLeft();
  3. final int right = parent.getWidth() - parent.getPaddingRight();
  4. //获取Recycleviewitem个数
  5. final int childCount = parent.getChildCount();
  6. //遍历每一个item,给ItemDecoration绘制线条
  7. for (int i = 0; i < childCount; i++) {
  8. final View child = parent.getChildAt(i);
  9. final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
  10. .getLayoutParams();
  11. final int top = child.getBottom() + params.bottomMargin;
  12. final int bottom = top + mDivider.getIntrinsicHeight();
  13. mDivider.setBounds(left, top, right, bottom);
  14. mDivider.draw(c);

在Activity加入:

recycleview.addItemDecoration(new DividerDecoration(this));

运行效果:

 

完整代码:

  1. public class DividerDecoration extends RecyclerView.ItemDecoration {
  2. private static final int[] ATTRS = new int[]{
  3. android.R.attr.listDivider
  4. };
  5.  
  6. private Drawable mDivider;
  7.  
  8. public DividerDecoration(Context context) {
  9. final TypedArray a = context.obtainStyledAttributes(ATTRS);
  10. mDivider = a.getDrawable(0);
  11. a.recycle();
  12. }
  13.  
  14.  
  15. @Override
  16. public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
  17. outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
  18. }
  19.  
  20. @Override
  21. public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
  22. final int left = parent.getPaddingLeft();
  23. final int right = parent.getWidth() - parent.getPaddingRight();
  24. final int childCount = parent.getChildCount();
  25. for (int i = 0; i < childCount; i++) {
  26. final View child = parent.getChildAt(i);
  27. final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
  28. .getLayoutParams();
  29. final int top = child.getBottom() + params.bottomMargin;
  30. final int bottom = top + mDivider.getIntrinsicHeight();
  31. mDivider.setBounds(left, top, right, bottom);
  32. mDivider.draw(c);
  33. }
  34. }
  35. }

二、RecyclerView分组

    通过RecycleView的下划线的设置方法,下一步扩展RecyclerView,使其显示为分组的效果,原理也类似,绘制分组首先需要绘制一个分组的View,和绘制一个文字的View,也就是说View需要两个,那么就需要定义两个画笔进行绘制。同时分组也需要一个分组的依据,这里根据以日期为例进行分组,同一日期的为一组。

1、定义分组的依据接口
  1. public interface DecorationCallback {
  2. String getData(int position);
  3. }
2、初始化绘制文字的画笔和分组背景画笔。

    这里初始化画笔也需要在构造方法里进行:

    (1)View的画笔初始化

  1. Paint paint = new Paint();
  2. paint.setColor(ContextCompat.getColor(context, R.color.bg_header));

    (2)文字画笔的初始化

  1. Paint textPaint = new TextPaint();
  2. textPaint.setTypeface(Typeface.DEFAULT_BOLD);//普通字体
  3. textPaint.setFakeBoldText(false);//不加粗
  4. textPaint.setAntiAlias(true);//抗锯齿
  5. textPaint.setTextSize(40);//文字大小
  6. textPaint.setColor(Color.BLACK);//背景颜色
  7. textPaint.setTextAlign(Paint.Align.LEFT);//绘制起始位置
3、设置分组的padding

    分组的padding用于显示分组的head布局,绘制背景颜色、绘制文字等需求,这里需要明白那些部分需要设置padding,上面说过咱们是依据日期分组的,那个就要根据日期判断,日期相同的为一组,不同的视为不同的分组。

    (1)分组依据

    如果是第一个则视为新的分组,记录当前的日期,然后将后面的和上一个日期对比,一样则为同一个分组,不一样则为不同的分组。

  1. private boolean isHeader(int pos) {
  2. if (pos == 0) {
  3. return true;
  4. } else {
  5. String preData = callback.getData(pos - 1);
  6. String data = callback.getData(pos);
  7. return !preData.equals(data);
  8. }
  9. }

    (2)设置padding

  1. @Override
  2. public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
  3. super.getItemOffsets(outRect, view, parent, state);
  4. //获取绘制的item所在的位置
  5. int pos = parent.getChildAdapterPosition(view);
  6. String data = callback.getData(pos);
  7. if (TextUtils.isEmpty(data)) {
  8. return;
  9. }
  10. //不同组、和第一个才添加padding,其他的不处理
  11. if (pos == 0 || isHeader(pos)) {
  12. outRect.top = topHead;
  13. } else {
  14. outRect.top = 0;
  15. }
  16. }
3、绘制

    矩形的绘制比较简单,这里着重介绍下TextView的绘制。一般而言,绘制的起始位置是所画图形对应的矩形的左上角点。但在drawText中是非常例外的,y所代表的是基线的位置,只要x坐标、基线位置、文字大小确定以后,文字的位置就确定的了,所以这边需要处理基线位置,才能使文字垂直居中,基线的处理如下:

  1. Paint.FontMetrics fontMetrics = textPaint.getFontMetrics();
  2. float baseline = (rect.bottom + rect.top - fontMetrics.bottom - fontMetrics.top) / 2;

    绘制原理看代码备注:

  1. @Override
  2. public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
  3. super.onDraw(c, parent, state);
  4. //获取绘制起始点
  5. int left = parent.getPaddingLeft();
  6. //获取绘制终点
  7. int right = parent.getWidth() - parent.getPaddingRight();
  8. //获取RecyclerView中item的个数
  9. int childCount = parent.getChildCount();
  10. for (int i = 0; i < childCount; i++) {
  11. //获取item的View
  12. View view = parent.getChildAt(i);
  13. //获取所在位置
  14. int position = parent.getChildAdapterPosition(view);
  15. //获取回调日期
  16. String textLine = callback.getData(position);
  17. if (TextUtils.isEmpty(textLine)) {
  18. return;
  19. }
  20. //绘制分组日期
  21. if (position == 0 || isHeader(position)) {
  22. float top = view.getTop() - topHead;
  23. float bottom = view.getTop();
  24. //绘制矩形
  25. Rect rect = new Rect(left, (int) top, right, (int) bottom);
  26. c.drawRect(rect, paint);
  27. //绘制文字基线,文字的的绘制是从绘制的矩形底部开始的
  28. Paint.FontMetrics fontMetrics = textPaint.getFontMetrics();
  29. float baseline = (rect.bottom + rect.top - fontMetrics.bottom - fontMetrics.top) / 2;
  30. textPaint.setTextAlign(Paint.Align.CENTER);//文字居中
  31. //绘制文本
  32. c.drawText(textLine, rect.centerX(), baseline, textPaint);
  33. }
  34. }
  35. }

    这样这个分组就完成了,在Activity中进行使用,注意ItemDecoration是可以叠加的:

  1. recycleview.addItemDecoration(new DividerDecoration(this));
  2. recycleview.addItemDecoration(new SectionDecoration(this, new SectionDecoration.DecorationCallback() {
  3. @Override
  4. public String getData(int position) {
  5. return mDataList.get(position).data;
  6. }
  7. }));

效果如下:

 

    源码地址:https://github.com/yoonerloop/StickyRecycleView 点击打开链接

三、RecyclerView粘性布局

    粘性头布局要求头常驻布局最上面,和RecycleView滑动与不滑动没有关系,并且随着RecycleView的滑动header的数据依据特定的分组变化,在这种情况下显然onDrawOver更加合适,因此需要重写onDrawOver方法,在这个方法里面处理,而不是在Draw方法里面进行处理。构造方法与getItemOffsets方法保持不变,和上面分组的一致。onDrawOver方法重写的如下:

  1. @Override
  2. public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
  3. super.onDrawOver(c, parent, state);
  4. //获取当前可见的item的数量,不包括分组项,注意区分下面的
  5. int childCount = parent.getChildCount();
  6. //获取所有的的item个数,源码中不建议使用Adapter中获取
  7. int itemCount = state.getItemCount();
  8. //考虑padding,得到绘制的x轴起始点和终点的坐标
  9. int left = parent.getLeft() + parent.getPaddingLeft();
  10. int right = parent.getRight() - parent.getPaddingRight();
  11. //获取上一个和当前的日期
  12. String preDate;
  13. String currentDate = null;
  14. //注意:这里不能使用itemCount
  15. for (int i = 0; i < childCount; i++) {
  16. View view = parent.getChildAt(i);
  17. int position = parent.getChildAdapterPosition(view);
  18. String textLine = callback.getData(position);
  19. //前一个Date作为preDate
  20. preDate = currentDate;
  21. //获取当前的currentDate
  22. currentDate = callback.getData(position);
  23. //日期相同,跳出本次循环
  24. if (TextUtils.isEmpty(currentDate) || TextUtils.equals(currentDate, preDate)) {
  25. continue;
  26. }
  27. if (TextUtils.isEmpty(textLine)) {
  28. continue;
  29. }
  30. int viewBottom = view.getBottom();
  31. float textY = Math.max(topHead, view.getTop());
  32. //下一个和当前不一样,item小于header的高度时候移动当前的header
  33. if (position + 1 < itemCount) {
  34. String nextData = callback.getData(position + 1);
  35. if (!currentDate.equals(nextData) && viewBottom < textY) {
  36. textY = viewBottom;
  37. }
  38. }
  39. //不断的触发Canvas绘制,生成动态效果
  40. Rect rect = new Rect(left, (int) textY - topHead, right, (int) textY);
  41. c.drawRect(rect, paint);
  42. //绘制文字基线,文字的的绘制是从绘制的矩形底部开始的
  43. Paint.FontMetrics fontMetrics = textPaint.getFontMetrics();
  44. float baseline = (rect.bottom + rect.top - fontMetrics.bottom - fontMetrics.top) / 2;
  45. textPaint.setTextAlign(Paint.Align.CENTER);//文字居中
  46. //绘制文本
  47. c.drawText(textLine, rect.centerX(), baseline, textPaint);
  48. }
  49. }

    原理总结:

    浮在itemView上层的header被顶上去和被拉下来的效果,只需要当每组最后一个Item的bottom小于header的height时,让header跟随这个item就行,换句话说就是此时让header的bottom等于即将消失的Item的bottom。这里的核心代码如下:

  1. int viewBottom = view.getBottom();
  2. float textY = Math.max(topHead, view.getTop());
  3. //下一个和当前不一样,item小于header的高度时候移动当前的header
  4. if (position + 1 < itemCount) {
  5. String nextData = callback.getData(position + 1);
  6. if (!currentDate.equals(nextData) && viewBottom < textY) {
  7. textY = viewBottom;
  8. }
  9. }
  10. //不断的触发Canvas绘制,生成动态效果
  11. Rect rect = new Rect(left, (int) textY - topHead, right, (int) textY);

    1、viewBottom:即每个item的bottom距离屏幕的最上边的距离。

    2、textY:在设定的header的高度与每个item的top距离屏幕的最上边的距离之间取最大的。如果header<top会导致存在间隙;如果header>top,会存在下拉延迟,所以一般情况下topHead和view.getTop()相等。

    3、viewBottom < textY:滑动的viewBottom的距离小于header的距离,才使得header的y坐标发生变化,使得header的y坐标动态的等于viewBottom,这样就能实现上推效果。

 

源码地址:https://github.com/yoonerloop/StickyRecycleView 记得start点赞哦吐舌头点击打开链接

--------------------- 本文来自 一杯清泉 的CSDN 博客 ,全文地址请点击:https://blog.csdn.net/yoonerloop/article/details/80444523?utm_source=copy

posted @ 2018-10-08 17:55  天涯海角路  阅读(689)  评论(0)    收藏  举报