recyclerview实现二级列表

二级列表是项目中经常会用到的组件,本着学习新组件的思想,决定不用ExpandableListView,而是用RecyclerView实现。

为了不重复造轮子,在github上搜索一番,看到一个还不错的项目thoughtbot/expandable-recycler-view,这个项目封装好了二级列表的整个实现,使用起来也很简单,下载项目看一下sample就多清楚了。但是这个项目有个问题,不能更新数据,recyclerview数据是初始化的时候给定的,没有后续变更数据的操作,所以修改了一下这个项目。

1. 实现动态变更数据

  1. ExpandableGroup类增加teamId,每个分组的唯一标识

    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
    public class ExpandableGroup<T extends Parcelable> implements Parcelable {
    private int teamId;
    // ...

    public ExpandableGroup(int teamId, String title, List<T> items) {
    this.teamId = teamId;
    this.title = title;
    this.items = items;
    }

    protected ExpandableGroup(Parcel in) {
    teamId = in.readInt();
    // ..
    }

    public int getTeamId() {
    return teamId;
    }

    public void setTeamId(int teamId) {
    this.teamId = teamId;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
    dest.writeInt(teamId);
    // ...
    }
    }
  2. 修改ExpandableList类的变量public boolean[] expandedGroupIndexes;该变量被用来记住当前分组是否展开,默认为false,用boolean数组不好的一点,在数据增加或者减少的时候,没办法把旧展开状态还原到新的数据中,修改为使用map,根据每个分组的id,来存储分组的展开状态,并在数据变更的时候,还原状态。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public class ExpandableList {

    public List<? extends ExpandableGroup> groups;
    // 修改为使用map,对项目中其他使用该变量的地方相应的修改为根据teamId获取或更新展开状态
    public SparseBooleanArray expandedGroupIndexes;

    public ExpandableList(List<? extends ExpandableGroup> groups) {
    this.groups = groups;

    // 默认都为false
    expandedGroupIndexes = new SparseBooleanArray();
    for (int i = 0; i < groups.size(); i++) {
    expandedGroupIndexes.put(groups.get(i).getTeamId(), false);
    }
    }
    }

    在ExpandableRecyclerViewAdapter.java中,增加更新数据的方法,进行数据的更新。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public void setGroups(List<? extends ExpandableGroup> groups) {
    // 保留一份老的展开状态的数据
    SparseBooleanArray oldIndex = this.expandableList.expandedGroupIndexes.clone();
    this.expandableList.groups = groups; // 设置新的数据
    // 设置新的展开状态数据
    this.expandableList.expandedGroupIndexes = new SparseBooleanArray();
    for (int i = 0; i < groups.size(); i++) {
    this.expandableList.expandedGroupIndexes.put(groups.get(i).getTeamId(), false);
    }
    SparseBooleanArray newIndex = this.expandableList.expandedGroupIndexes;
    for (int i = 0; i < newIndex.size(); i++) {
    for (int j = 0; j < oldIndex.size(); j++) {
    // 如果含有老的数据的话,还原到新数据集
    if (newIndex.keyAt(i) == oldIndex.keyAt(j)) {
    newIndex.put(oldIndex.keyAt(j), oldIndex.get(oldIndex.keyAt(j)));
    }
    }
    }
    }

    这样便可实现动态修改二级列表的数据,弥补了原项目不能动态修改数据的一个不足。

2. 实现类似ExpandableListview的列表悬浮在顶部效果+可分页加载RecyclerView

这个功能使用比较简单的方法,在RecyclerView顶部增加一个view,在滑动的时候,该view一直在顶部展示,并且展示当前分组下的Group的名称和状态等。

  1. 顶部悬浮view实现
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

/**
* 自定义recyclerview继承relativeLayout
*/
public class ExpandableRecyclerView extends RelativeLayout {
// 悬浮在顶部的view
private RelativeLayout pinnedView;
ImageView pinnedIcon; // 顶部悬浮view的icon,可用来表示展开状态
TextView pinnedText; // 顶部悬浮view的内容
TextView pinnedNum; // 顶部悬浮view的数据,展示当前分组成员个数
ExpandableRecyclerViewAdapter adapter; // 项目中的二级列表adapter
private RecyclerView recyclerView;

// 两个状态图标
private Drawable mListIndicatorExpanded;
private Drawable mListIndicatorNormal;

public ExpandableRecyclerView(Context context) {
super(context);
initView(context);
}

public ExpandableRecyclerView(Context context, AttributeSet attrs) {
super(context, attrs);
initView(context);
}

public ExpandableRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView(context);
}

private void initView(Context context) {

LayoutInflater.from(context).inflate(R.layout.expandable_recyclerview_layout, this, true);
pinnedView = findViewById(R.id.expanded_recyclerview_pinned_view);
pinnedIcon = findViewById(R.id.expanded_recyclerview_indicator);
pinnedText = findViewById(R.id.expanded_recyclerview_name);
pinnedNum = findViewById(R.id.expanded_recyclerview_number);
recyclerView = findViewById(R.id.expanded_recyclerview);
MyLinearLayoutManager lm = new MyLinearLayoutManager(context, LinearLayoutManager.VERTICAL, false);
recyclerView.setLayoutManager(lm);

mListIndicatorExpanded = context.getResources().getDrawable(R.drawable.list_indicator_expanded);
mListIndicatorNormal = context.getResources().getDrawable(R.drawable.list_indicator_normal);

initListener();
}

private void initListener() {
// 通过recyclerview的滑动判断顶部悬浮view展示内容
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
// 滑动的时候悬浮框置顶
LinearLayoutManager lm = (LinearLayoutManager) recyclerView.getLayoutManager();
// 获取第一个可见item位置
int firstVisibleItemPosition = lm.findFirstVisibleItemPosition();
pinnedView.setVisibility(View.VISIBLE);
// 对应位置在二级列表里的坐标 该坐标包含父坐标和子坐标
ExpandableListPosition expandableListPosition =
adapter.expandableList.getUnflattenedPosition(firstVisibleItemPosition);

if (expandableListPosition.groupPos < adapter.getGroups().size()) {
// 根据二级列表坐标取出group的数据,该数据包含child的数据
ExpandableGroup expandableGroup =
(ExpandableGroup) adapter.getGroups().get(expandableListPosition.groupPos);
// 根据展开状态设置顶部悬浮view的icon状态
pinnedIcon.setImageDrawable(
adapter.isGroupExpanded(expandableGroup) ? mListIndicatorExpanded : mListIndicatorNormal);
// 设置顶部悬浮view的内容
pinnedText.setText(expandableGroup.getTitle());
// // 设置顶部悬浮view的child的数量
pinnedNum.setText(
expandableGroup.getItems() != null ? "(" + expandableGroup.getItems().size() + ")" : "");
pinnedView.setTag(expandableGroup);
}
}
});

pinnedView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// 点击悬浮框展开或伸缩列表
ExpandableGroup expandableGroup = (ExpandableGroup) pinnedView.getTag();
if (expandableGroup != null) {
adapter.toggleGroup(expandableGroup);
recyclerView.scrollToPosition(0);
}
if (pinnedIcon.getDrawable() == mListIndicatorNormal) {
pinnedIcon.setImageDrawable(mListIndicatorExpanded);
} else if (pinnedIcon.getDrawable() == mListIndicatorExpanded) {
pinnedIcon.setImageDrawable(mListIndicatorNormal);
}
}
});

}

public RecyclerView getRecyclerView() {
return recyclerView;
}

private RecyclerView.AdapterDataObserver observer = new RecyclerView.AdapterDataObserver() {
@Override
public void onChanged() {
super.onChanged();
// 更新顶部悬浮view
updatePinnedView();
}
};

public void setAdapter(final ExpandableRecyclerViewAdapter adapter) {
this.adapter = adapter;
// 数据更新的时候,如果不滑动,这个时候顶部悬浮view的内容不准,所以在数据变更的时候要更新顶部悬浮view
// adapter注册数据变更监听
this.adapter.registerAdapterDataObserver(observer);
recyclerView.setAdapter(adapter);
}

/**
* 更新pinnedview的内容
*/
private void updatePinnedView() {
// 查找第一个父布局
LinearLayoutManager lm = (LinearLayoutManager) recyclerView.getLayoutManager();
int firstVisibleItemPosition = lm.findFirstVisibleItemPosition();
// 对应位置在列表里的坐标
ExpandableListPosition expandableListPosition =
adapter.expandableList.getUnflattenedPosition(firstVisibleItemPosition);

if (expandableListPosition.groupPos < adapter.getGroups().size()) {
ExpandableGroup expandableGroup =
(ExpandableGroup) adapter.getGroups().get(expandableListPosition.groupPos);
pinnedIcon.setImageDrawable(
adapter.isGroupExpanded(expandableGroup) ? mListIndicatorExpanded : mListIndicatorNormal);
pinnedText.setText(expandableGroup.getTitle());
pinnedNum.setText(expandableGroup.getItems() != null ? "(" + expandableGroup.getItems().size() + ")" : "");
pinnedView.setTag(expandableGroup);
}
}
// java.lang.IndexOutOfBoundsException: Inconsistency detected.Invalid item position
// 这个crash据说是recyclerview的bug,google建议重写下面的方法改成false,我试试
class MyLinearLayoutManager extends LinearLayoutManager {

@SuppressWarnings("SameParameterValue")
MyLinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
super(context, orientation, reverseLayout);
}

@Override
public boolean supportsPredictiveItemAnimations() {
return false;
}
}
}

使用方就像正常的使用RecyclerView一样使用该自定义RecyclerView,最后顶部的悬浮view效果如下:

3. ExpandableRecyclerView实现分页加载

对于可能数据量很大的数据来说,分页加载是必不可少的,一次大量的加载请求数据会造成不必要的浪费。

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
public class ExpandableRecyclerView extends RelativeLayout {
SparseIntArray pageMap = new SparseIntArray(); // 记录每个分组当前分页的页数
boolean isNeedPageUpdateData = false; // 是否要分页加载数据
// 分页加载滑动到底部回调,由使用方决定下一页的数据加载
PageUpdateDataCallback pageUpdateDataCallback;
int pageSize = 50;

// ....

private void initListener() {
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
// ...
// 向上滚动 清空数据
if (dy < 0) {
for (int i = 0; i < pageMap.size(); i++) {
pageMap.put(pageMap.keyAt(i), 1);
}
}

// 向下滚动 分页加载
if (isNeedPageUpdateData && dy > 0) {
// 获取最后一个可见item在列表中的位置
int lastVisiableItemPos = lm.findLastVisibleItemPosition();
// 对应位置在列表里的坐标
ExpandableListPosition lastExpandableListPos =
adapter.expandableList.getUnflattenedPosition(lastVisiableItemPos);
ExpandableGroup expandableGroup =
(ExpandableGroup) adapter.getGroups().get(lastExpandableListPos.groupPos);
int childPos = lastExpandableListPos.childPos;
int page = pageMap.get(expandableGroup.getTeamId());
// 如果最后一个可见item的childPos等于当前分组的分页加载总数量,表示滑动到当前分页的最后一条数据,可以开始加载下一页数据
if (childPos == page * pageSize) {
if (pageUpdateDataCallback != null) {
page++;
pageMap.put(expandableGroup.getTeamId(), page);
// 回调给使用方
pageUpdateDataCallback.getPageData(expandableGroup, page);
}
}
}
}
});

}
/**
* 分页更新数据的size
*/
public void setPageSize(int pageSize) {
this.pageSize = pageSize;
}

/**
* 是否需要分页更新数据
*/
public void setNeedPageUpdateData(boolean needPageUpdateData) {
isNeedPageUpdateData = needPageUpdateData;
}

/**
* 分页更新数据的callback,使用方自己决定怎么更新数据
*/
public void setPageUpdateDataCallback(PageUpdateDataCallback pageUpdateDataCallback) {
this.pageUpdateDataCallback = pageUpdateDataCallback;
}
}

// 使用方设置使用分页加载 分页加载的数量和分页监听
recyclerview.setPageSize(PAGE_MAX_SIZE);
recyclerview.setNeedPageUpdateData(true);
recyclerview.setPageUpdateDataCallback(new ExpandableRecyclerView.PageUpdateDataCallback() {
@Override
public void getPageData(ExpandableGroup group, int page) {
// 加载下一页数据
}
});

改造完后,该项目基本实现了二级列表+动态更新+分页加载功能,算是一个比较完善的二级列表RecyclerView了,后续有时间继续研究一下看能否把RecyclerView的局部刷新功能也加上。最后,再次感谢thoughtbot/expandable-recycler-view项目,也是这个项目封装的比较好,才能实现这些改造。

0%