【原创不易,转载请注明出处:https://blog.youkuaiyun.com/email_jade/article/details/86528143】
在日常开发中,我们经常会遇到展示机构树的应用场景,比如说展示某公司的组织架构,或者是展示某一个目录的结构,在flutter中,暂时还没有发现类似的开源库,那么只能自己动手撸一个了,先展示下效果图:
数据结构如下,每个机构分为两类,一类是子机构,另一类是机构成员,都有name属性,这也是日常应用中最常见的形式:
class Organ{
List<Organ> subOrgans;
List<Member> members;
String name;
Organ(
this.subOrgans,
this.members,
this.name
);
}
class Member{
String name;
Member(
this.name
);
}
测试数据如下,平时我们遇到的大多数情况是顶级机构只有一个节点, 为了测试顶级下面可以挂多个机构的情况,将数据改造了下:
List<Organ> _buildData(){
return [Organ([
Organ([
Organ([
Organ(
[
Organ(
null,
[
Member("五级机构成员1"),
Member("五级机构成员2"),
Member("五级机构成员3"),
Member("五级机构成员4"),
],
"五级机构"
)
], [
Member("四级机构成员1"),
Member("四级机构成员2"),
Member("四级机构成员3"),
Member("四级机构成员4"),
],"四级机构"),
Organ(
[
Organ(
null,
[
Member("六级机构成员1"),
Member("六级机构成员2"),
Member("六级机构成员3"),
Member("六级机构成员4"),
],
"六级机构"
)
], [
Member("七级机构成员1"),
Member("七级机构成员2"),
Member("七级机构成员3"),
Member("七级机构成员4"),
],"七级机构")
], [
Member("三级机构成员1"),
Member("三级机构成员2"),
Member("三级机构成员3"),
Member("三级机构成员4"),
], "三级机构")
], [
Member("二级机构成员1"),
Member("二级机构成员2"),
Member("二级机构成员3"),
], "二级机构")
], [
Member("一级机构成员1"),
Member("一级机构成员2"),
Member("一级机构成员3"),
Member("一级机构成员4"),
Member("一级机构成员5"),
], "一级机构"),
Organ(null, [
Member("八级机构成员1"),
Member("八级机构成员2"),
Member("八级机构成员3"),
Member("八级机构成员4"),
Member("八级机构成员5"),
], "八级机构")];
}
}
先说下思路,机构树列表,了解设计模式的同学应该很清楚,这个场景跟设计模式的Composite模式特别相似,参考下Composite模式的思想,保持容器与内容的一致性,因此,我们可以将机构与成员看成同一种数据,区别可能是,机构下面可能有子机构和成员,但是成员下面不可能有其他数据,是叶节点。
为了绘制的方便,我们先对原始数据进行处理,将所有的数据封装成Node节点,Node节点包含是否展开的标记expand,深度depth,类型type,唯一节点号nodeId,父节点号fatherId,还有原始数据,如下:
class Node<T>{
static int typeOrgan = 10000;
static int typeMember = 10001;
bool expand;
int depth;
int type;
int nodeId;
int fatherId;
T object;
Node(
this.expand,
this.depth,
this.type,
this.nodeId,
this.fatherId,
this.object,
);
}
数据的处理如下:
///保存所有数据的List
List<Node> list = new List();
///第一个节点的index
int nodeId = 1;
///如果解析的数据是一个list列表,采用这个方法
void _parseOrgans(List<Organ> organs){
for(Organ organ in organs){
_parseOrgan(organ);
}
}
///递归解析原始数据,将organ递归,记录其深度,nodeID和fatherID,将根节点的fatherID置为-1,
///保存原始数据为泛型T
void _parseOrgan(Organ organ, {int depth = 0, int fatherId = -1}) {
int currentId = nodeId;
list.add(Node(false, depth, Node.typeOrgan, nodeId++, fatherId, organ));
List<Node<Member>> members = new List();
if (organ.members != null) {
for (Member member in organ.members) {
members.add(Node(
false, depth + 1, Node.typeMember, nodeId++, currentId, member));
}
}
list.addAll(members);
if (organ.subOrgans != null) {
for (Organ organ in organ.subOrgans) {
_parseOrgan(organ, depth: depth + 1, fatherId: currentId);
}
}
}
对于树的展示,我们采用的是ListView,为了区分不同的层级,可以根据depth为每个Item增加缩进,对于树状的列表来说,最重要的是树的展开与收起,先来说树的展开,我们点击了一个机构,肯定是想展示该机构下的所有子机构和成员,那么,我们可以遍历Node列表,发现了fatherId==该机构的nodeId,那么代表这些是需要展示的数据,将其保存,插入到改机构的后面,至于为啥要插入,是因为如果直接加到展示数据的末尾,那么子成员也展示到了整个ListView的末尾,极度没有体验。然后再来说说树的收起,收起的时候,要收起所有直接挂在该机构下(子树)以及所有简介挂在该机构下(儿子的子树。。)的数据,依旧采用递归,先对已有的树进行递归,将所有与该树相关的子孙树删掉即可,本处采用的是先标记再将非标记的树替换为现有的数据的方法。
树的展示如下:
///扩展机构树:id代表被点击的机构id
/// 做法是遍历整个list列表,将直接挂在该机构下面的节点增加到一个临时列表中,
///然后将临时列表插入到被点击的机构下面
void _expand(int id) {
//保存到临时列表
List<Node> tmp = new List();
for (Node node in list) {
if (node.fatherId == id) {
tmp.add(node);
}
}
//找到插入点
int index = -1;
int length = expand.length;
for(int i=0; i<length; i++){
if(id == expand[i].nodeId){
index = i+1;
break;
}
}
//插入
expand.insertAll(index, tmp);
}
///收起机构树:id代表被点击的机构id
/// 做法是遍历整个expand列表,将直接和间接挂在该机构下面的节点标记,
///将这些被标记节点删除即可,此处用到的是将没有被标记的节点加入到新的列表中
void _collect(int id){
//清楚之前的标记
mark.clear();
//标记
_mark(id);
//重新对expand赋值
List<Node> tmp = new List();
for(Node node in expand){
if(mark.indexOf(node.nodeId) < 0){
tmp.add(node);
}else{
node.expand = false;
}
}
expand.clear();
expand.addAll(tmp);
}
///标记,在收起机构树的时候用到
void _mark(int id) {
for (Node node in expand) {
if (id == node.fatherId) {
if (node.type == Node.typeOrgan) {
_mark(node.nodeId);
}
mark.add(node.nodeId);
}
}
}
///增加根
void _addRoot() {
for (Node node in list) {
if (node.fatherId == -1) {
expand.add(node);
}
}
}
///构建元素
List<Widget> _buildNode(List<Node> nodes) {
List<Widget> widgets = List();
if (nodes != null && nodes.length > 0) {
for (Node node in nodes) {
widgets.add(GestureDetector(
child: ImageText(
node.type == Node.typeOrgan
? node.expand ? "images/expand.png" : "images/collect.png"
: "images/member.png",
node.type == Node.typeOrgan ? (node.object as Organ).name : (node.object as Member).name,
padding: node.depth * 20.0,
),
onTap: (){
if(node.type == Node.typeOrgan){
if(node.expand){ //之前是扩展状态,收起列表
node.expand = false;
_collect(node.nodeId);
}else{ //之前是收起状态,扩展列表
node.expand = true;
_expand(node.nodeId);
}
setState(() {
});
}
},
));
}
}
return widgets;
}
做完了收起和展开,那么一个树差不多就完成了,至于树的展示顺序,因为对于不同的应用场景来说,展示的顺序也有所不同,因此,本代码暂未涉及。当然,博客里面的一些代码可能不全,完整的代码见:
GitHub地址:https://github.com/jadennn/flutter_tree
(20190214更新,增加搜索功能)
相关代码见:
https://github.com/jadennn/flutter_tree/commit/f7fa077f0aee92cb7bfb0db7cc996f7603f29c6c
flutter很好,路还很长,让我们一起奋斗前行!