实现基于RecyclerView的树形组件封装

最近因项目需求需要一个可以进行展开收缩的多级树形控件,首先考虑的是ExpandableListView,但是这个控件只支持一级展开不符合需求,
之后在百度谷歌了一下发现有用List实现的多级树形控件,预览效果是不错的,但是经测试大量数据展开时会造成严重的卡顿现象,也不符合需求。
最终决定以RecyclerView封装一个可服用的多级树形组件,以满足大量数据的展开折叠,以及展开折叠的动画效果。

最终效果图:

员工效果图

这个树形组件是在RecyclerView的基础上对Adapter进行了封装,在使用时可以使用原生的RecyclerView设定为树形结构的Adapter即可实现树形的效果。

首先我们定义一个枚举来标识树形节点的类型(叶子/分支)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 节点类型
* Created by zhangfan on 16-6-24.
*/
public enum NodeType {
/**
* 分组节点,可打开
*/
BRANCH,
/**
* 叶子节点
*/
LEAF
}

接下来我们考虑树形节点的数据存储模型,有两种方案,一种是定义一个节点抽象类,另一种是定义一个节点接口。
这两种方案对于开发者来说,抽象类可以存储组件开放过程中需要的一些临时数据,更加方便,而接口则需要组件使用其他折中的方法来存储节点的临时数据
而对于调用者来说,继承一个接口会更加的灵活,而且不需要额外的数据转换开销,而继承抽象类则会约束使用者只能将父类指定为组件提供的类,在通过其他途径拿到节点数据之后还要进行额外的转换(如数据库/网络等)。

这里为了确保组件在使用过程中能更加的简洁清爽,更小的入侵性,我们采用接口来定义树形节点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 树形节点
* Created by zhangfan on 16-6-24.
*/
public interface TreeNode {
/**
*获取子节点
*/
ArrayList<? extends TreeNode> getChildren();
/**
*获取节点名称
*/
String getTitle();
/**
*获取节点类型
*/
NodeType getNodeType();
}

这样我们的数据模型已经定义完毕,然后我需要定义一个拥有树形特性的Adapter来让RecyclerView具有树形控件的特性。

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
169
170
171
172
173
174
175
176
177
178
/**
* 树形adapter
* Created by zhangfan on 16-6-24.
*/
public abstract class RecyclerTreeAdapter<N extends TreeNode, T extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<T> {
ArrayList<N> dataList = new ArrayList<>();
HashMap<N, Boolean> nodeChildrenStatusMap = new HashMap<>();
HashMap<N, Integer> nodeLevelMap = new HashMap<>();
HashSet<N> checkedNodes = new HashSet<>();
/**
* @return 每级缩进,单位px
*/
protected abstract int indentPerLevel();
/**
* @param dataList 已传入的数据参数在控件展开和折叠后会产生变化,不可在其他地方引用或使用
*/
public RecyclerTreeAdapter(List<N> dataList) {
this.dataList.addAll(dataList);
}
public ArrayList<N> getCheckedNodes() {
return new ArrayList<>(checkedNodes);
}
@Override
public final T onCreateViewHolder(ViewGroup parent, int viewType) {
NodeType nodeType;
if (viewType == 0) {
nodeType = NodeType.LEAF;
} else {
nodeType = NodeType.BRANCH;
}
return onCreateViewHolder(parent, nodeType);
}
protected abstract T onCreateViewHolder(ViewGroup parent, NodeType nodeType);
@Override
public final void onBindViewHolder(final T holder, int position) {
N treeNode = dataList.get(position);
Integer integerLevel = nodeLevelMap.get(treeNode);
int level = integerLevel == null ? 0 : integerLevel;
onBindViewHolder(holder, treeNode);
final View itemView = holder.itemView;
itemView.setPadding(level * indentPerLevel(), itemView.getPaddingTop(),
itemView.getPaddingRight(), itemView.getPaddingBottom());
itemView.setTag(treeNode);
itemView.setClickable(true);
itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
N treeNode = (N) itemView.getTag();
switch (treeNode.getNodeType()) {
case BRANCH:
int location = dataList.indexOf(treeNode);
ArrayList<? extends TreeNode> children = treeNode.getChildren();
if (children.size() > 0) {
Boolean aBoolean = nodeChildrenStatusMap.get(treeNode);
boolean isShowChildren = aBoolean != null && aBoolean;
if (isShowChildren) {
List<N> allShowedChildren = getAllShowedChildren(treeNode);
dataList.removeAll(allShowedChildren);
notifyItemRangeRemoved(location + 1, allShowedChildren.size());
updateChildrenStatusMap(treeNode, false);
} else {
Integer integer = nodeLevelMap.get(treeNode);
int thisLevel = integer == null ? 0 : integer;
for (TreeNode n :
children) {
nodeLevelMap.put((N) n, thisLevel + 1);
}
dataList.addAll(location + 1, (Collection<? extends N>) children);
notifyItemRangeInserted(location + 1, children.size());
nodeChildrenStatusMap.put(treeNode, true);
}
}
break;
case LEAF:
boolean checked = checkedNodes.contains(treeNode);
setNodeChecked(treeNode, !checked);
onLeafTouch(treeNode);
break;
}
}
});
}
private void updateChildrenStatusMap(N treeNode, boolean isShowChildren) {
nodeChildrenStatusMap.put(treeNode, isShowChildren);
ArrayList<? extends TreeNode> children = treeNode.getChildren();
if (children == null) return;
for (TreeNode n :
children) {
Boolean aBoolean = nodeChildrenStatusMap.get(n);
if ((aBoolean != null && aBoolean) == isShowChildren) continue;
updateChildrenStatusMap((N) n, isShowChildren);
}
}
private List<N> getAllShowedChildren(N treeNode) {
List<N> result = new ArrayList<>();
Boolean aBoolean = nodeChildrenStatusMap.get(treeNode);
boolean showed = aBoolean != null && aBoolean;
if (showed) {
ArrayList<? extends TreeNode> children = treeNode.getChildren();
if (children == null) return result;
result.addAll((Collection<? extends N>) children);
for (TreeNode n : children) {
result.addAll(getAllShowedChildren((N) n));
}
}
return result;
}
protected void onLeafTouch(N treeNode) {
}
public void setNodeChecked(N treeNode, boolean checked) {
if (checked) {
checkedNodes.add(treeNode);
} else {
checkedNodes.remove(treeNode);
}
ArrayList<? extends TreeNode> children = treeNode.getChildren();
if (children == null) return;
for (TreeNode n : children) {
setNodeChecked((N) n, checked);
}
}
/**
* 获取节点层级,只能获取已经加载的节点
*
* @param treeNode 节点
* @return 节点层级,从0开始
*/
public int getNodeLevel(N treeNode) {
Integer integer = nodeLevelMap.get(treeNode);
return integer == null ? 0 : integer;
}
/**
* 数据和UI绑定
*
* @param holder holder内itemView的tag方法已被使用,不建议重新设置该值
* @param treeNode 节点数据
*/
public abstract void onBindViewHolder(T holder, N treeNode);
/**
* 节点类型,1分支,0叶子
*
* @param position
* @return
*/
@Override
public int getItemViewType(int position) {
return NodeType.BRANCH.equals(dataList.get(position).getNodeType()) ? 1 : 0;
}
@Override
public int getItemCount() {
return dataList.size();
}
}

在RecyclerTreeAdapter中根据节点类型来绑定节点的点击事件,分支节点的点击事件指定为展开/折叠子节点,叶子节点的点击事件则通过onLeafTouch方法回调给具体的Adapter实现类。
而树形结构的缩进则是通过计算出节点的所属层级,给相应节点的View设定leftPadding。
这里可能会注意到,在RecyclerTreeAdapter中定义了一些Map这样的结构数据,这是因为我们采用了接口来定义节点,所以节点的展开/收缩状态以及节点的层级数据都不能够存储在节点中,所以需要这些数据结构来存储这些堆数据。

至此,我们的树形控件已经全部封装完毕,下面看一下这个控件该如何使用
首先让我们的节点类实现TreeNode

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
/**
*
* Created by zhangfan on 16-6-24.
*/
public class TestNode implements TreeNode {
ArrayList<TestNode> children;
String name;
NodeType type = NodeType.BRANCH;
public TestNode(ArrayList<TestNode> children, String name) {
this.children = children;
this.name = name;
}
@Override
public ArrayList<? extends TreeNode> getChildren() {
return children;
}
@Override
public String getTitle() {
return name;
}
@Override
public NodeType getNodeType() {
return type;
}
}

然后定义一个Adapter继承自刚才的RecyclerTreeAdapter

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
/**
* 树形Adapter的实现
* Created by zhangfan on 16-6-24.
*/
public class MyTreeAdapter extends RecyclerTreeAdapter<TestNode, MyTreeAdapter.MyHolder> {
private final Context context;
/**
* @param dataList 已传入的数据参数在控件展开和折叠后会产生变化,不可在其他地方引用或使用
*/
public MyTreeAdapter(List<TestNode> dataList, Context context) {
super(dataList);
this.context = context;
}
@Override
protected int indentPerLevel() {
return 50;
}
@Override
protected MyHolder onCreateViewHolder(ViewGroup parent, NodeType nodeType) {
View inflate = null;
switch (nodeType) {
case LEAF:
inflate = LayoutInflater.from(parent.getContext()).inflate(R.layout.treeitem2, parent, false);
break;
case BRANCH:
inflate = LayoutInflater.from(parent.getContext()).inflate(R.layout.treeitem, parent, false);
break;
}
return new MyHolder(inflate);
}
@Override
public void onBindViewHolder(MyHolder holder, TestNode treeNode) {
holder.tv.setText("" + treeNode.getTitle() + ",第" + getNodeLevel(treeNode) + "层");
}
public class MyHolder extends RecyclerView.ViewHolder {
TextView tv;
public MyHolder(View itemView) {
super(itemView);
tv = (TextView) itemView.findViewById(R.id.title);
}
}
@Override
protected void onLeafTouch(TestNode treeNode) {
super.onLeafTouch(treeNode);
Toast.makeText(context, "点到叶子了:" + treeNode.getTitle() + ",第" + getNodeLevel(treeNode) + "层", Toast.LENGTH_SHORT).show();
}
}

在这个adapter中我们可用根据节点类型的不同来生成同步类型的节点View
然后在Activity中造一些模拟数据给RecyclerView指定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
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recyclerView);
ArrayList<TestNode> list = new ArrayList<>();
ArrayList<TestNode> nodes1 = new ArrayList<>();
TestNode node1 = new TestNode(nodes1, "1层");
list.add(node1);
ArrayList<TestNode> nodes2 = new ArrayList<>();
TestNode node2 = new TestNode(nodes2, "2层");
nodes1.add(node2);
ArrayList<TestNode> nodes3 = new ArrayList<>();
TestNode node3 = new TestNode(nodes3, "3层");
nodes2.add(node3);
ArrayList<TestNode> nodes4 = new ArrayList<>();
TestNode node4 = new TestNode(nodes4, "4层");
node4.type = NodeType.LEAF;
nodes3.add(node4);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
recyclerView.setAdapter(new MyTreeAdapter(list, this));
}
}

大功告成,效果图:
treeDemo

代码地址:https://github.com/NightFarmer/RecyclerTree

文章目录
,