【问题标题】:Expandable ListView / dynamic TreeView in FlutterFlutter 中的可扩展 ListView / 动态 TreeView
【发布时间】:2020-04-23 16:43:21
【问题描述】:

我需要一个嵌套的 TreeView 来遍历列表并基于父子关系构建节点。当点击一个节点时,它会展开并显示带有缩进的子节点。还更改 selectedt 节点的文本样式颜色。如果列表很长并且有很多节点,并且子节点本身有很多子节点,则 2d 可滚动。如何?请帮忙。

【问题讨论】:

    标签: flutter recursion dart dynamic tree


    【解决方案1】:

    您可以在下面复制粘贴运行完整代码
    你可以使用包https://pub.dev/packages/flutter_treeview
    它有一些您可以自定义的功能,您可以在下面查看工作演示
    父子关系,完整代码见initState()

    @override
      void initState() {
        _nodes = [
          Node(
            label: 'documents',
            key: 'docs',
            expanded: docsOpen,
            icon: NodeIcon(
              codePoint:
                  docsOpen ? Icons.folder_open.codePoint : Icons.folder.codePoint,
              color: "blue",
            ),
            children: [
              Node(
                  label: 'personal',
                  key: 'd3',
    

    代码sn-p

    TreeView(
                        controller: _treeViewController,
                        allowParentSelect: _allowParentSelect,
                        supportParentDoubleTap: _supportParentDoubleTap,
                        onExpansionChanged: (key, expanded) =>
                            _expandNode(key, expanded),
                        onNodeTap: (key) {
                          debugPrint('Selected: $key');
                          setState(() {
                            _selectedNode = key;
                            _treeViewController =
                                _treeViewController.copyWith(selectedKey: key);
                          });
                        },
                        theme: _treeViewTheme,
                      )
    

    工作演示

    完整代码

    import 'package:flutter/cupertino.dart';
    import 'package:flutter/material.dart';
    import 'package:flutter_treeview/tree_view.dart';
    import 'dart:convert';
    
    void main() => runApp(MyApp());
    
    class MyApp extends StatelessWidget {
      // This widget is the root of your application.
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'TreeView Example',
          home: MyHomePage(title: 'TreeView Example'),
        );
      }
    }
    
    class MyHomePage extends StatefulWidget {
      MyHomePage({Key key, this.title}) : super(key: key);
      final String title;
    
      @override
      _MyHomePageState createState() => _MyHomePageState();
    }
    
    class _MyHomePageState extends State<MyHomePage> {
      String _selectedNode;
      List<Node> _nodes;
      TreeViewController _treeViewController;
      bool docsOpen = true;
      final Map<ExpanderPosition, Widget> expansionPositionOptions = const {
        ExpanderPosition.start: Text('Start'),
        ExpanderPosition.end: Text('End'),
      };
      final Map<ExpanderType, Widget> expansionTypeOptions = const {
        ExpanderType.caret: Icon(
          Icons.arrow_drop_down,
          size: 28,
        ),
        ExpanderType.arrow: Icon(Icons.arrow_downward),
        ExpanderType.chevron: Icon(Icons.expand_more),
        ExpanderType.plusMinus: Icon(Icons.add),
      };
      final Map<ExpanderModifier, Widget> expansionModifierOptions = const {
        ExpanderModifier.none: ModContainer(ExpanderModifier.none),
        ExpanderModifier.circleFilled: ModContainer(ExpanderModifier.circleFilled),
        ExpanderModifier.circleOutlined:
            ModContainer(ExpanderModifier.circleOutlined),
        ExpanderModifier.squareFilled: ModContainer(ExpanderModifier.squareFilled),
        ExpanderModifier.squareOutlined:
            ModContainer(ExpanderModifier.squareOutlined),
      };
      ExpanderPosition _expanderPosition = ExpanderPosition.start;
      ExpanderType _expanderType = ExpanderType.caret;
      ExpanderModifier _expanderModifier = ExpanderModifier.none;
      bool _allowParentSelect = false;
      bool _supportParentDoubleTap = false;
    
      @override
      void initState() {
        _nodes = [
          Node(
            label: 'documents',
            key: 'docs',
            expanded: docsOpen,
            icon: NodeIcon(
              codePoint:
                  docsOpen ? Icons.folder_open.codePoint : Icons.folder.codePoint,
              color: "blue",
            ),
            children: [
              Node(
                  label: 'personal',
                  key: 'd3',
                  icon: NodeIcon.fromIconData(Icons.input),
                  children: [
                    Node(
                        label: 'Resume.docx',
                        key: 'pd1',
                        icon: NodeIcon.fromIconData(Icons.insert_drive_file)),
                    Node(
                        label: 'Cover Letter.docx',
                        key: 'pd2',
                        icon: NodeIcon.fromIconData(Icons.insert_drive_file)),
                  ]),
              Node(
                label: 'Inspection.docx',
                key: 'd1',
    //          icon: NodeIcon.fromIconData(Icons.insert_drive_file),
              ),
              Node(
                  label: 'Invoice.docx',
                  key: 'd2',
                  icon: NodeIcon.fromIconData(Icons.insert_drive_file)),
            ],
          ),
          Node(
              label: 'MeetingReport.xls',
              key: 'mrxls',
              icon: NodeIcon.fromIconData(Icons.insert_drive_file)),
          Node(
              label: 'MeetingReport.pdf',
              key: 'mrpdf',
              icon: NodeIcon.fromIconData(Icons.insert_drive_file)),
          Node(
              label: 'Demo.zip',
              key: 'demo',
              icon: NodeIcon.fromIconData(Icons.archive)),
        ];
        _treeViewController = TreeViewController(
          children: _nodes,
          selectedKey: _selectedNode,
        );
        super.initState();
      }
    
      ListTile _makeExpanderPosition() {
        return ListTile(
          title: Text('Expander Position'),
          dense: true,
          trailing: CupertinoSlidingSegmentedControl(
            children: expansionPositionOptions,
            groupValue: _expanderPosition,
            onValueChanged: (ExpanderPosition newValue) {
              setState(() {
                _expanderPosition = newValue;
              });
            },
          ),
        );
      }
    
      SwitchListTile _makeAllowParentSelect() {
        return SwitchListTile.adaptive(
          title: Text('Allow Parent Select'),
          dense: true,
          value: _allowParentSelect,
          onChanged: (v) {
            setState(() {
              _allowParentSelect = v;
            });
          },
        );
      }
    
      SwitchListTile _makeSupportParentDoubleTap() {
        return SwitchListTile.adaptive(
          title: Text('Support Parent Double Tap'),
          dense: true,
          value: _supportParentDoubleTap,
          onChanged: (v) {
            setState(() {
              _supportParentDoubleTap = v;
            });
          },
        );
      }
    
      ListTile _makeExpanderType() {
        return ListTile(
          title: Text('Expander Style'),
          dense: true,
          trailing: CupertinoSlidingSegmentedControl(
            children: expansionTypeOptions,
            groupValue: _expanderType,
            onValueChanged: (ExpanderType newValue) {
              setState(() {
                _expanderType = newValue;
              });
            },
          ),
        );
      }
    
      ListTile _makeExpanderModifier() {
        return ListTile(
          title: Text('Expander Modifier'),
          dense: true,
          trailing: CupertinoSlidingSegmentedControl(
            children: expansionModifierOptions,
            groupValue: _expanderModifier,
            onValueChanged: (ExpanderModifier newValue) {
              setState(() {
                _expanderModifier = newValue;
              });
            },
          ),
        );
      }
    
      @override
      Widget build(BuildContext context) {
        TreeViewTheme _treeViewTheme = TreeViewTheme(
          expanderTheme: ExpanderThemeData(
            type: _expanderType,
            modifier: _expanderModifier,
            position: _expanderPosition,
            color: Colors.grey.shade800,
            size: 20,
          ),
          labelStyle: TextStyle(
            fontSize: 16,
            letterSpacing: 0.3,
          ),
          parentLabelStyle: TextStyle(
            fontSize: 16,
            letterSpacing: 0.1,
            fontWeight: FontWeight.w800,
            color: Colors.blue.shade700,
          ),
          iconTheme: IconThemeData(
            size: 18,
            color: Colors.grey.shade800,
          ),
          colorScheme: Theme.of(context).brightness == Brightness.light
              ? ColorScheme.light(
                  primary: Colors.blue.shade50,
                  onPrimary: Colors.grey.shade900,
                  background: Colors.transparent,
                  onBackground: Colors.black,
                )
              : ColorScheme.dark(
                  primary: Colors.black26,
                  onPrimary: Colors.white,
                  background: Colors.transparent,
                  onBackground: Colors.white70,
                ),
        );
        return Scaffold(
          appBar: AppBar(
            title: Text(widget.title),
            elevation: 0,
          ),
          body: GestureDetector(
            onTap: () {
              FocusScope.of(context).requestFocus(FocusNode());
            },
            child: Container(
              color: Colors.grey.shade200,
              padding: EdgeInsets.all(20),
              height: double.infinity,
              child: Column(
                children: <Widget>[
                  Container(
                    height: 300,
                    child: Column(
                      children: <Widget>[
                        _makeExpanderPosition(),
                        _makeExpanderType(),
                        _makeExpanderModifier(),
                        _makeAllowParentSelect(),
                        _makeSupportParentDoubleTap(),
                      ],
                    ),
                  ),
                  Expanded(
                    child: Container(
                      decoration: BoxDecoration(
                        color: Colors.white,
                        borderRadius: BorderRadius.circular(10),
                      ),
                      padding: EdgeInsets.all(10),
                      child: TreeView(
                        controller: _treeViewController,
                        allowParentSelect: _allowParentSelect,
                        supportParentDoubleTap: _supportParentDoubleTap,
                        onExpansionChanged: (key, expanded) =>
                            _expandNode(key, expanded),
                        onNodeTap: (key) {
                          debugPrint('Selected: $key');
                          setState(() {
                            _selectedNode = key;
                            _treeViewController =
                                _treeViewController.copyWith(selectedKey: key);
                          });
                        },
                        theme: _treeViewTheme,
                      ),
                    ),
                  ),
                  GestureDetector(
                    onTap: () {
                      debugPrint('Close Keyboard');
                      FocusScope.of(context).unfocus();
                    },
                    child: Container(
                      padding: EdgeInsets.only(top: 20),
                      alignment: Alignment.center,
                      child: Text(_treeViewController.getNode(_selectedNode) == null
                          ? ''
                          : _treeViewController.getNode(_selectedNode).label),
                    ),
                  )
                ],
              ),
            ),
          ),
          bottomNavigationBar: SafeArea(
            top: false,
            child: ButtonBar(
              alignment: MainAxisAlignment.spaceEvenly,
              children: <Widget>[
                CupertinoButton(
                  child: Text('Node'),
                  onPressed: () {
                    setState(() {
                      _treeViewController = _treeViewController.copyWith(
                        children: _nodes,
                      );
                    });
                  },
                ),
                CupertinoButton(
                  child: Text('JSON'),
                  onPressed: () {
                    setState(() {
                      _treeViewController =
                          _treeViewController.loadJSON(json: US_STATES_JSON);
                    });
                  },
                ),
                CupertinoButton(
                  child: Text('Toggle'),
                  onPressed: _treeViewController.selectedNode != null &&
                          _treeViewController.selectedNode.isParent
                      ? () {
                          setState(() {
                            _treeViewController = _treeViewController
                                .withToggleNode(_treeViewController.selectedKey);
                          });
                        }
                      : null,
                ),
                CupertinoButton(
                  child: Text('Edit'),
                  onPressed: () {
                    TextEditingController editingController = TextEditingController(
                        text: _treeViewController.selectedNode.label);
                    showCupertinoDialog(
                        context: context,
                        builder: (context) {
                          return CupertinoAlertDialog(
                            title: Text('Edit Label'),
                            content: Container(
                              height: 80,
                              alignment: Alignment.center,
                              padding: EdgeInsets.all(10),
                              child: CupertinoTextField(
                                controller: editingController,
                                autofocus: true,
                              ),
                            ),
                            actions: <Widget>[
                              CupertinoDialogAction(
                                child: Text('Cancel'),
                                isDestructiveAction: true,
                                onPressed: () => Navigator.of(context).pop(),
                              ),
                              CupertinoDialogAction(
                                child: Text('Update'),
                                isDefaultAction: true,
                                onPressed: () {
                                  if (editingController.text.isNotEmpty) {
                                    setState(() {
                                      Node _node = _treeViewController.selectedNode;
                                      _treeViewController =
                                          _treeViewController.withUpdateNode(
                                              _treeViewController.selectedKey,
                                              _node.copyWith(
                                                  label: editingController.text));
                                    });
                                    debugPrint(editingController.text);
                                  }
                                  Navigator.of(context).pop();
                                },
                              ),
                            ],
                          );
                        });
                  },
                ),
              ],
            ),
          ),
        );
      }
    
      _expandNode(String key, bool expanded) {
        String msg = '${expanded ? "Expanded" : "Collapsed"}: $key';
        debugPrint(msg);
        Node node = _treeViewController.getNode(key);
        if (node != null) {
          List<Node> updated;
          if (key == 'docs') {
            updated = _treeViewController.updateNode(
              key,
              node.copyWith(
                  expanded: expanded,
                  icon: NodeIcon(
                    codePoint: expanded
                        ? Icons.folder_open.codePoint
                        : Icons.folder.codePoint,
                    color: expanded ? "blue600" : "grey700",
                  )),
            );
          } else {
            updated = _treeViewController.updateNode(
                key, node.copyWith(expanded: expanded));
          }
          setState(() {
            if (key == 'docs') docsOpen = expanded;
            _treeViewController = _treeViewController.copyWith(children: updated);
          });
        }
      }
    }
    
    class ModContainer extends StatelessWidget {
      final ExpanderModifier modifier;
    
      const ModContainer(this.modifier, {Key key}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        double _borderWidth = 0;
        BoxShape _shapeBorder = BoxShape.rectangle;
        Color _backColor = Colors.transparent;
        Color _backAltColor = Colors.grey.shade700;
        switch (modifier) {
          case ExpanderModifier.none:
            break;
          case ExpanderModifier.circleFilled:
            _shapeBorder = BoxShape.circle;
            _backColor = _backAltColor;
            break;
          case ExpanderModifier.circleOutlined:
            _borderWidth = 1;
            _shapeBorder = BoxShape.circle;
            break;
          case ExpanderModifier.squareFilled:
            _backColor = _backAltColor;
            break;
          case ExpanderModifier.squareOutlined:
            _borderWidth = 1;
            break;
        }
        return Container(
          decoration: BoxDecoration(
            shape: _shapeBorder,
            border: _borderWidth == 0
                ? null
                : Border.all(
                    width: _borderWidth,
                    color: _backAltColor,
                  ),
            color: _backColor,
          ),
          width: 15,
          height: 15,
        );
      }
    }
    
    const List<Map<String, dynamic>> US_STATES = [
      {
        "label": "A",
        "children": [
          {"label": "Alabama", "key": "AL"},
          {"label": "Alaska", "key": "AK"},
          {"label": "American Samoa", "key": "AS"},
          {"label": "Arizona", "key": "AZ"},
          {"label": "Arkansas", "key": "AR"}
        ]
      },
      {
        "label": "C",
        "children": [
          {"label": "California", "key": "CA"},
          {"label": "Colorado", "key": "CO"},
          {"label": "Connecticut", "key": "CT"},
        ]
      },
      {
        "label": "D",
        "children": [
          {"label": "Delaware", "key": "DE"},
          {"label": "District Of Columbia", "key": "DC"},
        ]
      },
      {
        "label": "F",
        "children": [
          {"label": "Federated States Of Micronesia", "key": "FM"},
          {"label": "Florida", "key": "FL"},
        ]
      },
      {
        "label": "G",
        "children": [
          {"label": "Georgia", "key": "GA"},
          {"label": "Guam", "key": "GU"},
        ]
      },
      {
        "label": "H",
        "children": [
          {"label": "Hawaii", "key": "HI"},
        ]
      },
      {
        "label": "I",
        "children": [
          {"label": "Idaho", "key": "ID"},
          {"label": "Illinois", "key": "IL"},
          {"label": "Indiana", "key": "IN"},
          {"label": "Iowa", "key": "IA"},
        ]
      },
      {
        "label": "K",
        "children": [
          {"label": "Kansas", "key": "KS"},
          {"label": "Kentucky", "key": "KY"},
        ]
      },
      {
        "label": "L",
        "children": [
          {"label": "Louisiana", "key": "LA"},
        ]
      },
      {
        "label": "M",
        "children": [
          {"label": "Maine", "key": "ME"},
          {"label": "Marshall Islands", "key": "MH"},
          {"label": "Maryland", "key": "MD"},
          {"label": "Massachusetts", "key": "MA"},
          {"label": "Michigan", "key": "MI"},
          {"label": "Minnesota", "key": "MN"},
          {"label": "Mississippi", "key": "MS"},
          {"label": "Missouri", "key": "MO"},
          {"label": "Montana", "key": "MT"},
        ]
      },
      {
        "label": "N",
        "children": [
          {"label": "Nebraska", "key": "NE"},
          {"label": "Nevada", "key": "NV"},
          {"label": "New Hampshire", "key": "NH"},
          {"label": "New Jersey", "key": "NJ"},
          {"label": "New Mexico", "key": "NM"},
          {"label": "New York", "key": "NY"},
          {"label": "North Carolina", "key": "NC"},
          {"label": "North Dakota", "key": "ND"},
          {"label": "Northern Mariana Islands", "key": "MP"},
        ]
      },
      {
        "label": "O",
        "children": [
          {"label": "Ohio", "key": "OH"},
          {"label": "Oklahoma", "key": "OK"},
          {"label": "Oregon", "key": "OR"},
        ]
      },
      {
        "label": "P",
        "children": [
          {"label": "Palau", "key": "PW"},
          {"label": "Pennsylvania", "key": "PA"},
          {"label": "Puerto Rico", "key": "PR"},
        ]
      },
      {
        "label": "R",
        "children": [
          {"label": "Rhode Island", "key": "RI"},
        ]
      },
      {
        "label": "S",
        "children": [
          {"label": "South Carolina", "key": "SC"},
          {"label": "South Dakota", "key": "SD"},
        ]
      },
      {
        "label": "T",
        "children": [
          {"label": "Tennessee", "key": "TN"},
          {"label": "Texas", "key": "TX"},
        ]
      },
      {
        "label": "U",
        "children": [
          {"label": "Utah", "key": "UT"},
        ]
      },
      {
        "label": "V",
        "children": [
          {"label": "Vermont", "key": "VT"},
          {"label": "Virgin Islands", "key": "VI"},
          {"label": "Virginia", "key": "VA"},
        ]
      },
      {
        "label": "W",
        "children": [
          {"label": "Washington", "key": "WA"},
          {"label": "West Virginia", "key": "WV"},
          {"label": "Wisconsin", "key": "WI"},
          {"label": "Wyoming", "key": "WY"}
        ]
      },
    ];
    
    String US_STATES_JSON = jsonEncode(US_STATES);
    

    【讨论】:

    • 感谢您的回答!在此示例中,_nodes 是硬编码的。是否可以基于过滤一般项目列表来构建它们?
    • 如果您已经有一个 List ,您可以像示例一样重新创建并填写节点列表,或者您可以在示例末尾看到 US_STATES 使用 List>
    猜你喜欢
    • 1970-01-01
    • 2018-01-08
    • 2018-11-04
    • 1970-01-01
    • 2019-05-21
    • 2014-08-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多