实现带有粘性头部的列表

本文最后更新于:1 年前

Reference


实现效果


实现过程

获取数据

例如使用 Retrofit 访问接口获取数据:http://api.meituan.com/mmdb/movie/v2/list/rt/order/coming.json?ci=1&limit=12&token=&__vhost=api.maoyan.com&utm_campaign=AmovieBmovieCD-1&movieBundleVersion=6801&utm_source=xiaomi&utm_medium=android&utm_term=6.8.0&utm_content=868030022327462&net=255&dModel=MI 5&uuid=0894DE03C76F6045D55977B6D4E32B7F3C6AAB02F9CEA042987B380EC5687C43&lat=40.100673&lng=116.378619&__skck=6a375bce8c66a0dc293860dfa83833ef&__skts=1463704714271&__skua=7e01cf8dd30a179800a7a93979b430b2&__skno=1a0b4a9b-44ec-42fc-b110-ead68bcc2824&__skcy=sXcDKbGi20CGXQPPZvhCU3%2FkzdE%3D

同时可以将获取到的 JSON 格式的数据封装成 Bean 对象。

使用数据

将数据放入 RecyclerView 中

1、使用到的只是 Bean 对象中的 comingBeanList,所以先获取 comingBeanList:

1
List<DataResponseBean.DataBean.ComingBean> comingBeanList = bean.getData().getComing();

2、因为将日前作为分组的依据,同时将日期作为 title,所以先获取所有 comingBeanList 中元素的日期:

1
2
3
4
5
6
private void setPullAction(List<DataResponseBean.DataBean.ComingBean> comingslist) {
mTitleList = new ArrayList<>();
for (int i = 0; i < comingslist.size(); i++) {
mTitleList.add(comingslist.get(i).getComingTitle());
}
}

3、给 RecyclerView 设置 ItemDecoration,用于显示每组第一个元素的 title:

1
2
3
4
5
6
7
8
9
10
11
12
13
mRecyclerView.addItemDecoration(new SectionDecoration(this, new SectionDecoration.DecorationCallback() {
// 返回标记 id (即每一项对应的标志性的字符串)
@Override
public String getGroupId(int position) {
return mTitleList.get(position);
}

// 获取组的 title
@Override
public String getGroupTitle(int position) {
return mTitleList.get(position);
}
}));

4、给 RecyclerView 添加 adapter,显示列表数据:

1
mRecyclerView.setAdapter(new MyRecyclerAdapter(this, comingBeanList));

将数据分组,每组第一个元素显示 title,其余元素不显示 title

1、重写 getItemOffsets 方法:

1
2
3
4
5
6
7
8
9
10
11
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
int pos = parent.getChildAdapterPosition(view);
// 只有是同一组的第一个 item 才显示悬浮栏
if (isFirstInGroup(pos)) {
outRect.top = barTopGap;
} else {
outRect.top = 0;
}
}

如果判断出当前位置的元素是该组的第一个元素,则将 outRect.top 置为 barTopGap,即在元素的上方预留出高度为 barTopGap 的区域;反之,则不预留区域。

2、判断当前位置的元素是否为该组的第一个元素的方法:

1
2
3
4
5
6
7
8
9
10
private boolean isFirstInGroup(int pos) {
if (pos == 0) {
return true;
} else {
String groupId = mCallback.getGroupId(pos);// 当前这个 item 的 id
String prevGroupId = mCallback.getGroupId(pos - 1);// 前一个 item 的 id
Log.d(TAG, "pos:" + pos + ", " + "groupId:" + groupId + ", " + "prevGroupId:" + prevGroupId);
return !groupId.equals(prevGroupId);
}
}

如果当前位置是 0,则必然是第一个元素;反之,则比较当前位置的元素及其前一个元素的 id 是否相同,此处,比较的是它们的 title。

始终保持 RecyclerView 顶部有最上面显示的元素对应的组的 title

1、重写 onDrawOver 方法:

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
@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDrawOver(c, parent, state);
int childCount = parent.getChildCount();// 一屏显示的 item 数量

for (int i = 0; i < childCount; i++) {
View view = parent.getChildAt(i);
int position = parent.getChildAdapterPosition(view);

String title = mCallback.getGroupTitle(position).toUpperCase();
if (TextUtils.isEmpty(title)) {
continue;
}

int left = parent.getPaddingLeft();// 悬浮栏左侧绘制的边界
int right = parent.getWidth() - parent.getPaddingRight();// 悬浮栏右侧绘制的边界

// 第一个 item 特殊处理
if (i == 0) {
// 组内最后一个 item
int viewBottom = view.getBottom();
if (isLastInGroup(position) && viewBottom < barTopGap) {
c.drawRect(left, viewBottom - barTopGap, right, viewBottom, barPaint);
c.drawText(title, left, viewBottom - alignBottom, textPaint);
} else
// 非组内最后一个 item
{
c.drawRect(left, 0, right, barTopGap, barPaint);
c.drawText(title, left, barTopGap - alignBottom, textPaint);
}
} else
// 除了第一个 item
{
// 组内第一个 item
if (isFirstInGroup(position)) {
int viewTop = view.getTop();
c.drawRect(left, viewTop - barTopGap, right, viewTop, barPaint);
c.drawText(title, left, viewTop - alignBottom, textPaint);
}
}
}
}

遍历 RecyclerView 上能显示出的所有元素:

(1)第一个元素

判断该元素是不是该组的最后一个元素,并且其剩余高度已经小于悬浮栏的高度,对应的状态是:

悬浮栏绘制的高度不变,依然是 barTopGap,但是绘制的上下界变了,上界从 RecyclerView 外的区域开始绘制。

否则,保持在 RecyclerView 的顶部有一高度为 barTopGap 的悬浮栏,对应的状态是:

(2)非第一个元素

判断该元素是不适该组的第一个元素,对应的状态是:

显示标题,标题对应的悬浮栏位置在该元素的上方,高度为 barTopGap。

否则,不需要处理,对应的状态是:

2、判断当前位置的元素是否为该组的最后一个元素的方法:

1
2
3
4
5
6
private boolean isLastInGroup(int pos) {
String groupId = mCallback.getGroupId(pos);// 当前这个 item 的 id
String nextGroupId = mCallback.getGroupId(pos + 1);// 后一个 item 的 id
Log.d(TAG, "pos:" + pos + ", " + "groupId:" + groupId + ", " + "nextGroupId:" + nextGroupId);
return !groupId.equals(nextGroupId);
}

源码地址

StickyNavigationBar



实现带有粘性头部的列表
https://weichao.io/5f71bc7e43d3/
作者
魏超
发布于
2018年1月26日
更新于
2022年12月4日
许可协议