参考:RecyclerView 自定义ItemDecoration从入门到实现吸顶效果
Solartisan/WaveSideBar
简单分割线
实现分割线要继承RecyclerView.ItemDecoration,重写三个方法1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public class MyItemDecoration extends RecyclerView.ItemDecoration {
public MyItemDecoration(Context context) {
}
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDraw(c, parent, state);int count=parent.getChildCount();
}
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDrawOver(c, parent, state);
}
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 | public class MyItemDecoration extends RecyclerView.ItemDecoration { |
- 以上是实现item底部的分割线
- 主要是调用onDraw方法画分割线,c.drawRect是画矩形,参数分别是左上右下和画笔对象
- 设置分割线左右边距就改成c.drawRect(0+padding,top,width-padding,bottom,paint);
因为onDraw和item内容处于一个图层,所以把分割线画的比较粗的话会和item内容重合,此时第三个方法就派上用场了,设置一个偏移值
1
2
3
4
5
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
35public 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"));
}
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);
}
}
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDrawOver(c, parent, state);
c.drawRect(0,0,width,item_height,paint2);
}
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
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
110public 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);
}
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);
}
}
}
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);
}
}
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;
}
}
}
}
Activity1
2
3
4
5
6
7
8
9
10
11
12
13
14recyclerView = (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() {
public String getText(int position) {
return list.get(position).substring(0,1);
}
}));
附上一个更完整的吸顶,用布局绘制
参考:Solartisan/WaveSideBarhttps://github.com/Solartisan/TurboRecyclerViewHelper)
- 首先是bean
Fruit.java1
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区分
- 适配器
FruitAdapter.java1
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
public int getItemViewType(int position) {
return mFruitList.get(position).getType();
}
//不同的type创建不同的布局
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;
}
//填充不同的数据
public void onBindViewHolder(@NonNull BaseHolder viewHolder, int i) {
Fruit fruit = mFruitList.get(i);
viewHolder.setData(fruit);
}
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;
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;
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填充数据
分割线
PinnedHeaderDecoration.java1
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
168public 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() {
public void onChanged() {
mIsAdapterDataChanged = true;
}
};
//初始化
public PinnedHeaderDecoration() {
this.mHeaderPosition = -1;
}
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);
}
}
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);
}
}使用
MainActivity.java1
2
3
4
5
6
7
8final PinnedHeaderDecoration decoration = new PinnedHeaderDecoration();
decoration.registerTypePinnedHeader(1, new PinnedHeaderDecoration.PinnedHeaderCreator() {
public boolean create(RecyclerView parent, int adapterPosition) {
return true;
}
});
mRecyclerView.addItemDecoration(decoration);
- registerTypePinnedHeader传的参数1是当adapter那个getviewtype中返回的是1的时候调用create方法,如果返回的是true,那就让他顶吸