项目中用到树形菜单,我采用第三方控件 flutter_treeview 实现,为了画出符合 UI 设计图的树形结构,花了一天时间翻看源码,总算大概熟悉了它的使用,这篇文章做个总结。
其实树形菜单的实现主要有两个难点,将数据处理成树状菜单结构、对控件设置样式。
处理数据
树形菜单控件的节点类 Node 如下
class Node<T> {
///节点唯一标识key
final String key;
///节点标题,上海工行
final String label;
///图标
final IconData? icon;
///图标颜色
final Color? iconColor;
///节点被选择时颜色
final Color? selectedIconColor;
///是否展开
final bool expanded;
final T? data;
///子节点
final List<Node> children;
///是否为父节点
final bool parent;
}
假设接口返回数据如下,相信很容易理解,这种父子结构数据其实很常见,像一些省市区、权限管理数据设计,都是这样的。
const List<Map<String,dynamic>> STATIONS = [
{
"stationId": 2,
"stationName": "上海工行",
"parentId": 0
},
{
"stationId": 3,
"stationName": "黄埔支行",
"parentId": 2
},
{
"stationId": 123456,
"stationName": "黄埔大街支行",
"parentId": 3
},
{
"stationId": 123455,
"stationName": "黄埔新城支行",
"parentId": 3
},
{
"stationId": 4,
"stationName": "徐汇支行",
"parentId": 2
},
{
"stationId": 5,
"stationName": "长宁支行",
"parentId": 2
},
{
"stationId": 11,
"stationName": "宝山支行",
"parentId": 2
},
{
"stationId": 12,
"stationName": "嘉定支行",
"parentId": 2
},
{
"stationId": 33,
"stationName": "山西工行",
"parentId": 0
},
{
"stationId": 22,
"stationName": "太原支行",
"parentId": 33
}
];
现在的问题是如何处理上面的数据,处理成节点类 List。我这分享两种方案,你们根据自己实际情况,做适当修改。
static List<Node> createNodeList(List<Map<String, dynamic>> data, int parentId) {
List<Node> nodeList = [];
for (final item in data) {
if (item['parentId'] == parentId) {
List<Node> children = createNodeList(data, item['stationId']);
nodeList.add(
Node(
key: item['stationId'].toString(),
label: item['stationName'],
children: children,
parent: children.isNotEmpty,
icon: children.isEmpty ? Icons.star : Icons.account_balance_sharp,
),
);
}
}
return nodeList;
}
static List<Node> loadNodes(){
return createNodeList(STATIONS, 0);
}
第二种方案
///获取根节点,可能是一个,可能是多个,但统一用集合接收
static List<Map<String, dynamic>> rootNode() {
List<Map<String, dynamic>> rootList = data.where((e) => !data.any((ee) => ee["stationId"] == e["parentId"])).toList();
return rootList;
}
///传入父节点
static Node buildNode(Map<String,dynamic> element){
List<Node> children = data
.where((e) => e["parentId"] == element["stationId"])
.map((e) => buildNode(e))
.toList();
///先构造父节点
Node node = Node(
key: element["stationId"],
label: element["stationName"],
icon: children.isEmpty ? Icons.star : Icons.account_balance_sharp,
children: children);
return node;
}
List<Node> nodes = [];
List<Map<String, dynamic>> rootList = rootNode();
rootList.forEach((element) {
Node node = buildNode(element);
nodes.add(node);
});
处理完后,数据会以树形结构构造。
设置 TreeView
到上面一步,我们已经可以将树状图显示在界面,只不过样式都是默认的,可能不好看。
class MyTreeViewPage extends StatefulWidget {
@override
State<MyTreeViewPage> createState() => _MyTreeViewPageState();
}
class _MyTreeViewPageState extends State<MyTreeViewPage> {
String _selectedNode = "";
List<Node<dynamic>> nodes = [];
TreeViewController? _treeViewController;
@override
void initState() {
super.initState();
nodes = TreeViewUtil.loadNodes();
log(nodes.toString());
_treeViewController = TreeViewController(
children: nodes,
selectedKey: _selectedNode,
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("树形菜单"),
),
body: Container(
child: TreeView(
controller: _treeViewController!,
),
),
);
}
}
设置样式关键是要熟悉 TreeViewTheme
各个属性含义。先构造出来设置到 TreeView
。
TreeView(
controller: _treeViewController!,
theme: _initTreeViewTheme(),
),
TreeViewTheme _initTreeViewTheme(){
TreeViewTheme treeViewTheme = TreeViewTheme(
);
return treeViewTheme;
}
接下来就是看 TreeViewTheme
各个属性。
class TreeViewTheme {
final ColorScheme colorScheme;
final double levelPadding;
///和listview中dense一样,当true时,整体同比缩小
final bool dense;
///每个Node之间垂直间隔
final double? verticalSpacing;
///每个Node之间水平间隔距离,设置成100,自己看看就懂了
final double? horizontalSpacing;
///图标icon的左右间距
final double iconPadding;
///设置图标样式(大小和颜色)
final IconThemeData iconTheme;
///展开项设置,这个属性很重要
final ExpanderThemeData expanderTheme;
///文本样式
final TextStyle labelStyle;
///父文本样式
final TextStyle parentLabelStyle;
final TextOverflow? labelOverflow;
final TextOverflow? parentLabelOverflow;
final Duration expandSpeed;
}
上面有三个属性是比较重要的,先看图标样式 IconThemeData
iconTheme: IconThemeData(
size: 36,
color: Colors.blue
),
然后是展开项 ExpanderThemeData
,包括
expanderTheme: const ExpanderThemeData(
type: ExpanderType.arrow,
modifier: ExpanderModifier.circleFilled,
position: ExpanderPosition.start,
size: 20,
color: Colors.red
),
那几个枚举里属性都可以尝试看看,当然,你也可以不要这个展开图标,ExpanderType.none
设置后其他属性都会失效。
type: ExpanderType.none,
最后是节点的点击事件
onNodeTap: (key){
setState(() {
_selectedNode = key;
_treeViewController =
_treeViewController!.copyWith(selectedKey: key);
...
});
},
OK,以上就是我要分享的,希望能帮到你。
本文由老郭种树原创,转载请注明:https://guozh.net/flutter-tree-view-widget/