RecyclerView.ItemDecoration实现StrickyHeader、粘性头部、悬停头部

参考

其实在GitHub上已经有很多开源的很成熟的StickyHeader项目

先来实现效果镇楼

实现代码

Activity

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
package com.him.stickyheader;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import java.util.ArrayList;
import java.util.List;
public class MainActivity extends AppCompatActivity {

public List<String> datas;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

RecyclerView mRecyclerView = findViewById(R.id.recycler_view);
mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
mRecyclerView.addItemDecoration(new MyItemDecoration(new MyItemDecoration.GroupInfoCallback() {
// 根据全局数据的位置position查询所属分组的信息GroupInfo
@Override
public GroupInfo getGroupInfo(int position) {
// 测试数据,暂时10条数据一组
int size = 10;
GroupInfo info = new GroupInfo();
info.groupId = position / size;
info.title = info.groupId + "";
info.position = position % size;
info.groupSize = size;
return info;
}
}));

initDatas();
mRecyclerView.setAdapter(new MyAdapter(datas));
}

// 初始化测试数据
private void initDatas() {
datas = new ArrayList<>();
for (int i = 0; i < 100;i++) {
datas.add("test " + i);
}
}
}

Adapter

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
package com.him.stickyheader;
import android.support.annotation.NonNull;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import java.util.List;
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.MyViewHolder> {

public List<String> datas;

public MyAdapter(List<String> datas) {
this.datas = datas;
}

@NonNull
@Override
public MyViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
View itemView = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.item, viewGroup, false);
return new MyViewHolder(itemView);
}

@Override
public void onBindViewHolder(@NonNull MyViewHolder viewHolder, int i) {
viewHolder.text.setText(datas.get(i));
}

@Override
public int getItemCount() {
return datas == null ? 0 :datas.size();
}

public class MyViewHolder extends RecyclerView.ViewHolder {
TextView text;
public MyViewHolder(@NonNull View itemView) {
super(itemView);
text = itemView.findViewById(R.id.text);
}
}
}

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
package com.him.stickyheader;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.support.annotation.NonNull;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import java.util.function.IntBinaryOperator;
public class MyItemDecoration extends RecyclerView.ItemDecoration {

// 普通分割线的高度
private int dividerHeight = 1;
// 分组header高度
private int headerHeight = 65;
// 根据position获取分组信息的回调
private GroupInfoCallback callback;
// 画笔
private Paint paint;

public MyItemDecoration(GroupInfoCallback callback) {
this.callback = callback;
paint = new Paint();
}

@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
int itemPosition = parent.getChildAdapterPosition(view);

GroupInfo info = callback.getGroupInfo(itemPosition);
if (info.isFirstItem()) {
// 分组第一个item撑开一个header的高度
outRect.top = headerHeight;
} else {
// 其他item撑开一个普通divider的高度
outRect.top = dividerHeight;
}
}

@Override
public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.onDraw(c, parent, state);

// header左右边的坐标,注意考虑parent的左右padding
float left = parent.getPaddingLeft();
float right = parent.getWidth() - parent.getPaddingRight();
// 循环RecyclerView中当前可见的View
for (int i = 0; i < parent.getChildCount(); i++) {
View child = parent.getChildAt(i);
// 根据当前view获取在整个列表中的位置
int itemPosition = parent.getChildAdapterPosition(child);
// 根据position获取所属分组的信息
GroupInfo info = callback.getGroupInfo(itemPosition);

// 重点来了,敲黑板
// 注意这里的i表示在RecyclerView当前可见View中的位置(第几个)
// i == 0表示当前可见的第一个View
// 这里绘制分组标题悬停的效果
if (i == 0) {
// 分组标题停在最顶部,就是parent的顶部,然后再考虑顶部的padding
float top = parent.getPaddingTop();
// top加分组高度就是底部坐标
float bottom = top + headerHeight;
// 这里是第二个重点
// 如果没这段代码,就无法实现下面的分组标题把当前悬停的分组标题往上推出页面的效果
// 分组的最后一个item且该item的底部坐标小于悬停顶部的header的底部坐标的情况下,需执行以下代码,重新计算header的位置
if (info.isLastItem() && bottom > child.getBottom()) {
bottom = child.getBottom();
top = bottom - headerHeight;
}
drawHeader(left, top, right, bottom, c, info);
} else if (info.isFirstItem()) { // 如果不是可见的第一个view但是是分组的第一个item,则绘制分组标题
// item的顶部再往上一个header的高度
float top = child.getTop() - headerHeight;
// 顶部坐标加上header高度就是底部坐标
float bottom = top + headerHeight;
drawHeader(left, top, right, bottom, c, info);
}
}
}

// 根据计算出的header位置绘制header及header中的title
private void drawHeader(float left, float top, float right, float bottom, Canvas c, GroupInfo info) {
// 设置header背景色,然后绘制
paint.setColor(Color.GREEN);
c.drawRect(left, top, right, bottom, paint);

// 设置title文字大小,然后计算文字高度,用于接下来计算文字绘制的位置
paint.setTextSize(32);
paint.setColor(Color.BLUE);
Rect rect = new Rect();
// 计算要绘制的文字的宽高
paint.getTextBounds(info.title, 0, info.title.length(), rect);
int textWidth = rect.width();
int textHeight = rect.height();

// 文字距左边的距离
float textX = left + 40;
// 计算baseline,中文情况下为文字的底部
float textY = top + (headerHeight + textHeight)/2;
c.drawText(info.title, textX, textY, paint);
}

// 根据所在列表的位置计算所属分组的信息
public interface GroupInfoCallback {
GroupInfo getGroupInfo(int position);
}
}

分组信息的Model类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.him.stickyheader;
public class GroupInfo {

// 分组的id
public int groupId;
// 分组显示的title
public String title;
// 在分组中的位置
public int position;
// 分组的大小
public int groupSize;

// 是否是分组的第一个
public boolean isFirstItem() {
return position == 0;
}

// 是否是分组的最后一个
public boolean isLastItem() {
return position == (groupSize - 1);
}
}

Activity布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"
android:background="#b5b8de"> <!--设置和item不同的背景色,当做divider的颜色-->

<android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>

</android.support.constraint.ConstraintLayout>

item布局

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#ffffff">
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"/>
</android.support.constraint.ConstraintLayout>

获取源代码

扫描以下二维码关注我的微信公众号野猿新一,发送“悬停头部”获取源代码的下载方式。