RecyclerView分割线附上顶吸

参考:RecyclerView 自定义ItemDecoration从入门到实现吸顶效果
Solartisan/WaveSideBar

简单分割线

实现分割线要继承RecyclerView.ItemDecoration,重写三个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MyItemDecoration extends RecyclerView.ItemDecoration {
public MyItemDecoration(Context context) {
}
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDraw(c, parent, state);int count=parent.getChildCount();
}
@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);
}
}

  • onDraw方法和自定义View中的onDraw方法一样,是用来在屏幕上画东西的
  • onDrawOver 英文Over的意思在…的上面 ,可以理解成是图层关系,item的内容和分割线是第一层(要在第一层画东西要调用onDraw),而onDrawOver是第二层,位于onDraw的上面
  • getItemOffsets 看名字可以知道是设置item的偏移值,其实效果和padding一样
  • 注意:三个方法中都有一个RecyclerView类型的parent,这个不是整个RecyclerView,而是当前屏幕显示的那几个RecyclerView,所以当滚动屏幕的时候会重新调用此类来绘制分割线。同理调用parent.getChildCount()获取到的时当前屏幕上显示的item个数

一条简单的红色分割线

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class MyItemDecoration extends RecyclerView.ItemDecoration {
private int width;
private int height;
private int item_height;
private int item_padding;
private Paint paint;
private Context context;
public MyItemDecoration(Context context) {
this.context = context;
width=context.getResources().getDisplayMetrics().widthPixels;
height=context.getResources().getDisplayMetrics().heightPixels;
paint=new Paint(Paint.ANTI_ALIAS_FLAG|Paint.DITHER_FLAG);
paint.setColor(Color.RED);
item_height=DensityUtil.dip2px(context, 1);
item_padding=DensityUtil.dip2px(context, 10);
}
@Override
public void onDraw(Canvas c, RecyclerView parent, 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 top=view.getBottom();
int bottom=top+item_height;
c.drawRect(0,top,width,bottom,paint);
}
}
@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);
}
}
  • 以上是实现item底部的分割线
  • 主要是调用onDraw方法画分割线,c.drawRect是画矩形,参数分别是左上右下和画笔对象
  • 设置分割线左右边距就改成c.drawRect(0+padding,top,width-padding,bottom,paint);
  • 因为onDraw和item内容处于一个图层,所以把分割线画的比较粗的话会和item内容重合,此时第三个方法就派上用场了,设置一个偏移值

    1
    2
    3
    4
    5
    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
    super.getItemOffsets(outRect, view, parent, state);
    outRect.bottom=item_height;
    }
  • 该偏移值的意思是让列表中每一个item的底部向下空出一个item_height的宽度用来绘制分割线

实现顶吸

顶部绘制一个固定的区域

顶部固定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class MyItemDecoration extends RecyclerView.ItemDecoration {
private int width;
private int item_height;
private Paint paint;
private Paint paint2;
public MyItemDecoration(Context context) {
width=context.getResources().getDisplayMetrics().widthPixels;
paint=new Paint(Paint.ANTI_ALIAS_FLAG|Paint.DITHER_FLAG);
paint.setColor(Color.GRAY);
item_height=DensityUtil.dip2px(context, 20);
paint2=new Paint(Paint.ANTI_ALIAS_FLAG|Paint.DITHER_FLAG);
paint2.setColor(Color.parseColor("#52ff0000"));
}
@Override
public void onDraw(Canvas c, RecyclerView parent, 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 bottom=view.getTop();
int top=bottom-item_height;
c.drawRect(0,top,width,bottom,paint);
}
}
@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDrawOver(c, parent, state);
c.drawRect(0,0,width,item_height,paint2);
}

@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
outRect.top=item_height;
}
}

  • 先把分隔条写到每一个item的上面,修改了onDraw和getItemOffsets
  • 实现每一个间隔把顶部固定区域顶上去

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @Override
    public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
    super.onDrawOver(c, parent, state);
    //获取屏幕上的第一个item
    View child0 = parent.getChildAt(0);
    //如果第一个item的Bottom<=分割线的高度
    if (child0.getBottom() <= item_height) {
    //随着RecyclerView滑动 分割线的top=固定为0不动,bottom则赋值为child0的bottom值.
    c.drawRect(0, 0, width,child0.getBottom() , paint2);
    } else {
    //固定不动
    c.drawRect(0, 0, width, item_height, paint2);

    }
    }
  • 实现方法是获取屏幕第一个recyclerview的item,看他底部的距离,如果小于一个分割线的高度,那就把分割线高度弄成第一个item到底部的距离

一个简单完整的吸顶,用Canvas绘制

参考:RecyclerView 自定义ItemDecoration从入门到实现吸顶效果
效果:
一个完整顶吸

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
public class MyItemDecoration extends RecyclerView.ItemDecoration {
private Paint.FontMetrics fontMetrics;
private int wight;
private int itemDecorationHeight;
private Paint paint;
private ObtainTextCallback callback;
private float itemDecorationPadding;
private TextPaint textPaint;
private Rect text_rect=new Rect();
public MyItemDecoration(Context context, ObtainTextCallback callback) {
//获取item宽度
wight=context.getResources().getDisplayMetrics().widthPixels;

//绘制间隔背景为红色
paint=new Paint(Paint.ANTI_ALIAS_FLAG|Paint.DITHER_FLAG);
paint.setColor(Color.RED);

//间隔高度
itemDecorationHeight=DensityUtil.dip2px(context, 30);
//间隔左右padding
itemDecorationPadding=DensityUtil.dip2px(context, 10);
this.callback = callback;

//绘制间隔上的文字
textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
textPaint.setColor(Color.WHITE);
textPaint.setTextAlign(Paint.Align.LEFT);
textPaint.setTextSize(DensityUtil.dip2px(context, 25));

//绘制文字相关的参数
fontMetrics = new Paint.FontMetrics();
textPaint.getFontMetrics(fontMetrics);
}
@Override
public void onDraw(Canvas c, RecyclerView parent, 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 top=view.getTop()-itemDecorationHeight;
int bottom=top+itemDecorationHeight;

//获取该item在总列表里是第几个
int position = parent.getChildAdapterPosition(view);
//通过callback获取到这个item的第一个字
String content = callback.getText(position);

textPaint.getTextBounds(content,0, content.length(),text_rect);

//如果需要画分割线
if(isFirstInGroup(position)) {
c.drawRect(0,top,wight,bottom,paint);
c.drawText(content, itemDecorationPadding, bottom-fontMetrics.descent, textPaint);
}
}
}
@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDrawOver(c, parent, state);

//获取到屏幕上第一个item
View child0=parent.getChildAt(0);
//获取到屏幕上第一个item是总列表中第几个
int position = parent.getChildAdapterPosition(child0);
//获取到这个item的第一个字
String content = callback.getText(position);
//如果已经重合了并且第一个item前需要画分割线,那么就按照第一个item的底部高度来画分割线
if(child0.getBottom()<=itemDecorationHeight&&isFirstInGroup(position+1)){
c.drawRect(0, 0, wight, child0.getBottom(), paint);
c.drawText(content, itemDecorationPadding, child0.getBottom()-fontMetrics.descent, textPaint);
}
//否则就按照一个完整的分割线来画,这里通过textPaint来写不同的字
else {
c.drawRect(0, 0, wight, itemDecorationHeight, paint);
c.drawText(content, itemDecorationPadding, itemDecorationHeight-fontMetrics.descent, textPaint);
}
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
int position= parent.getChildAdapterPosition(view);
//如果不是在同一组就腾出分割线需要的高度
if(isFirstInGroup(position)){
outRect.top=itemDecorationHeight;
}
}

//回调接口,通过该回调获取item的内容的第一个文字
public interface ObtainTextCallback {
String getText(int position);
}

//判断当前item和下一个item的第一个文字是否相同,如果相同说明是同一组,不需要画分割线
private boolean isFirstInGroup(int pos) {
//如果是adapter的第一个position直接return,因为第一个item必须有分割线
if (pos == 0) {
return true;
} else {
//否者判断前一个item的字符串 与 当前item字符串 是否相同
String prevGroupId = callback.getText(pos - 1);
String groupId = callback.getText(pos);
//如果前一个item和这个item的第一个字相同那就不用画分割线
if (prevGroupId.equals(groupId)) {
return false;
} else {
return true;
}
}
}
}

Activity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
recyclerView = (RecyclerView)findViewById(R.id.recycler_view);
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
recyclerView.setLayoutManager(layoutManager);
final List<String> list = new ArrayList<String>();
initList(list);
Adapter adapter = new Adapter(list);
recyclerView.setAdapter(adapter);
//通过每一个item内容的第一个字进行分类
recyclerView.addItemDecoration(new MyItemDecoration(this, new MyItemDecoration.ObtainTextCallback() {
@Override
public String getText(int position) {
return list.get(position).substring(0,1);
}
}));

附上一个更完整的吸顶,用布局绘制

参考:Solartisan/WaveSideBarhttps://github.com/Solartisan/TurboRecyclerViewHelper)

  1. 首先是bean
    Fruit.java
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26

    public class Fruit {
    private String name;
    private int type;

    public Fruit(String name, int type) {
    this.name = name;
    this.type = type;
    }

    public String getName() {
    return name;
    }

    public void setName(String name) {
    this.name = name;
    }

    public int getType() {
    return type;
    }

    public void setType(int type) {
    this.type = type;
    }
    }

这个bean包含了item和分割线的内容,用type区分

  1. 适配器
    FruitAdapter.java
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80

    public class FruitAdapter extends RecyclerView.Adapter<FruitAdapter.BaseHolder> {
    private List<Fruit> mFruitList;
    private static final String TAG = "FruitAdapter";

    //重写区分不同的元素,不同的type
    @Override
    public int getItemViewType(int position) {
    return mFruitList.get(position).getType();
    }

    //不同的type创建不同的布局
    @Override
    public BaseHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int type) {
    View view;
    final BaseHolder holder;
    if (type== 1) {
    view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.fruit_item, viewGroup, false);
    holder = new ItemHolder(view);
    } else {
    view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.head_item, viewGroup, false);
    holder = new HeadHolder(view);
    }
    return holder;
    }

    //填充不同的数据
    @Override
    public void onBindViewHolder(@NonNull BaseHolder viewHolder, int i) {
    Fruit fruit = mFruitList.get(i);
    viewHolder.setData(fruit);
    }

    @Override
    public int getItemCount() {
    return mFruitList.size();
    }

    //不同布局的Viewholder,先创建一个baseHolder,然后item和顶吸的分别继承
    public abstract class BaseHolder extends RecyclerView.ViewHolder {
    public abstract void setData(Fruit fruit);

    public BaseHolder(@NonNull View itemView) {
    super(itemView);
    }
    }


    public class ItemHolder extends BaseHolder {
    private TextView textView;

    @Override
    public void setData(Fruit fruit) {
    textView.setText(fruit.getName());
    }

    public ItemHolder(@NonNull View itemView) {
    super(itemView);
    textView = (TextView) itemView.findViewById(R.id.fruit_name);
    }
    }

    public class HeadHolder extends BaseHolder {
    private TextView textView;

    @Override
    public void setData(Fruit fruit) {
    textView.setText(fruit.getName().substring(0,1));
    }

    public HeadHolder(@NonNull View itemView) {
    super(itemView);
    textView = (TextView) itemView.findViewById(R.id.head_name);
    }
    }

    public FruitAdapter(List<Fruit> fruitList) {
    mFruitList = fruitList;
    }
    }
  • 这个adapter中提供了两个viewholder,一个是普通item,另一个是分割线
  • 重写getItemViewType方法定义区分两种holder的方法,上面的区分方法是通过bean的type区分
  • onCreateViewHolder通过上面的方法判定不同的viewtype构造不同的holder
  • onBindViewHolder填充数据
  1. 分割线
    PinnedHeaderDecoration.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    public class PinnedHeaderDecoration extends RecyclerView.ItemDecoration {
    private int mHeaderPosition;
    private int mPinnedHeaderTop;
    private static final String TAG = "PinnedHeaderDecoration";
    private boolean mIsAdapterDataChanged;
    private Rect mClipBounds;
    //是在顶部固定的那个框框
    private View mPinnedHeaderView;
    //父布局的整个adapter
    private RecyclerView.Adapter mAdapter;
    //一个安卓特有的稀疏数组,取代hashmap,key-value保存值,只需要指定value的类型即可
    private final SparseArray<PinnedHeaderCreator> mTypePinnedHeaderFactories = new SparseArray<>();
    //recyclerview适配器的监听
    private final RecyclerView.AdapterDataObserver mAdapterDataObserver = new RecyclerView.AdapterDataObserver() {
    @Override
    public void onChanged() {
    mIsAdapterDataChanged = true;
    }
    };
    //初始化
    public PinnedHeaderDecoration() {
    this.mHeaderPosition = -1;
    }
    @Override
    public void onDraw(Canvas c, final RecyclerView parent, RecyclerView.State state) {
    createPinnedHeader(parent);

    //mPinnedHeaderView是顶部固定的框框
    if (mPinnedHeaderView != null) {
    int headerEndAt = mPinnedHeaderView.getTop() + mPinnedHeaderView.getHeight();
    //获取列表可见部分的第一个view
    View v = parent.findChildViewUnder(c.getWidth() / 2, headerEndAt + 1);

    //判断这个v是不是分隔,如果是分割那么顶部的框框的顶要向上移动,否则就在0处,即最顶端
    if (isPinnedView(parent, v)) {
    mPinnedHeaderTop = v.getTop() - mPinnedHeaderView.getHeight();
    } else {
    mPinnedHeaderTop = 0;
    }

    //开始画分隔线,分隔线的头部就是顶部框框的底部
    mClipBounds = c.getClipBounds();
    mClipBounds.top = mPinnedHeaderTop + mPinnedHeaderView.getHeight();
    c.clipRect(mClipBounds);
    }
    }
    @Override
    public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
    if (mPinnedHeaderView != null) {
    //先保存c的状态
    c.save();

    mClipBounds.top = 0;
    c.clipRect(mClipBounds, Region.Op.UNION);
    //平移到框框顶部
    c.translate(0, mPinnedHeaderTop);
    //绘制出顶部的框框
    mPinnedHeaderView.draw(c);

    //把c还原到save的状态
    c.restore();
    }
    }
    private void createPinnedHeader(RecyclerView parent) {
    updatePinnedHeader(parent);

    RecyclerView.LayoutManager layoutManager = parent.getLayoutManager();
    if (layoutManager == null || layoutManager.getChildCount() <= 0) {
    return;
    }

    //获取第一个元素在整个recyclerview中是第几个
    int firstVisiblePosition = ((RecyclerView.LayoutParams) layoutManager.getChildAt(0).getLayoutParams()).getViewAdapterPosition();
    int headerPosition = findPinnedHeaderPosition(parent, firstVisiblePosition);

    if (headerPosition >= 0 && mHeaderPosition != headerPosition) {
    mHeaderPosition = headerPosition;
    int viewType = mAdapter.getItemViewType(headerPosition);

    RecyclerView.ViewHolder pinnedViewHolder = mAdapter.createViewHolder(parent, viewType);
    mAdapter.bindViewHolder(pinnedViewHolder, headerPosition);
    mPinnedHeaderView = pinnedViewHolder.itemView;

    // read layout parameters
    ViewGroup.LayoutParams layoutParams = mPinnedHeaderView.getLayoutParams();
    if (layoutParams == null) {
    layoutParams = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
    mPinnedHeaderView.setLayoutParams(layoutParams);
    }

    int heightMode = View.MeasureSpec.getMode(layoutParams.height);
    int heightSize = View.MeasureSpec.getSize(layoutParams.height);

    if (heightMode == View.MeasureSpec.UNSPECIFIED) {
    heightMode = View.MeasureSpec.EXACTLY;
    }

    int maxHeight = parent.getHeight() - parent.getPaddingTop() - parent.getPaddingBottom();
    if (heightSize > maxHeight) {
    heightSize = maxHeight;
    }

    // measure & layout
    int ws = View.MeasureSpec.makeMeasureSpec(parent.getWidth() - parent.getPaddingLeft() - parent.getPaddingRight(), View.MeasureSpec.EXACTLY);
    int hs = View.MeasureSpec.makeMeasureSpec(heightSize, heightMode);
    mPinnedHeaderView.measure(ws, hs);
    mPinnedHeaderView.layout(0, 0, mPinnedHeaderView.getMeasuredWidth(), mPinnedHeaderView.getMeasuredHeight());
    }
    }
    private int findPinnedHeaderPosition(RecyclerView parent, int fromPosition) {
    if (fromPosition > mAdapter.getItemCount() || fromPosition < 0) {
    return -1;
    }

    for (int position = fromPosition; position >= 0; position--) {
    final int viewType = mAdapter.getItemViewType(position);
    if (isPinnedViewType(parent, position, viewType)) {
    return position;
    }
    }

    return -1;
    }
    private boolean isPinnedViewType(RecyclerView parent, int adapterPosition, int viewType) {
    PinnedHeaderCreator pinnedHeaderCreator = mTypePinnedHeaderFactories.get(viewType);

    return pinnedHeaderCreator != null && pinnedHeaderCreator.create(parent, adapterPosition);
    }

    private boolean isPinnedView(RecyclerView parent, View v) {
    int position = parent.getChildAdapterPosition(v);
    //如果获取的是非法值
    if (position == RecyclerView.NO_POSITION) {
    return false;
    }

    return isPinnedViewType(parent, position, mAdapter.getItemViewType(position));
    }
    private void updatePinnedHeader(RecyclerView parent) {
    RecyclerView.Adapter adapter = parent.getAdapter();
    //获取的是recyclerview所有的adapter,如果adapter发生改变或者监听到内容发生改变执行
    //重新绘制
    if (mAdapter != adapter || mIsAdapterDataChanged) {
    //重置
    resetPinnedHeader();
    if (mAdapter != null) {
    mAdapter.unregisterAdapterDataObserver(mAdapterDataObserver);
    }

    mAdapter = adapter;
    if (mAdapter != null) {
    //设置监听
    mAdapter.registerAdapterDataObserver(mAdapterDataObserver);
    }
    }
    }
    //重置header
    private void resetPinnedHeader() {
    mHeaderPosition = -1;
    mPinnedHeaderView = null;
    }
    public void registerTypePinnedHeader(int itemType, PinnedHeaderCreator pinnedHeaderCreator) {
    mTypePinnedHeaderFactories.put(itemType, pinnedHeaderCreator);
    }
    public interface PinnedHeaderCreator {
    boolean create(RecyclerView parent, int adapterPosition);
    }
    }
  2. 使用
    MainActivity.java

    1
    2
    3
    4
    5
    6
    7
    8
    final PinnedHeaderDecoration decoration = new PinnedHeaderDecoration();
    decoration.registerTypePinnedHeader(1, new PinnedHeaderDecoration.PinnedHeaderCreator() {
    @Override
    public boolean create(RecyclerView parent, int adapterPosition) {
    return true;
    }
    });
    mRecyclerView.addItemDecoration(decoration);
  • registerTypePinnedHeader传的参数1是当adapter那个getviewtype中返回的是1的时候调用create方法,如果返回的是true,那就让他顶吸