Flutter 树形控件 TreeView,快速实现一个树形菜单

项目中用到树形菜单,我采用第三方控件 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/

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注