flutter - 如何使用具有全选和取消全选功能的三态复选框实现 TreeView

标签 flutter recursion treeview flutter-dependencies

我想实现一个树形层次 ListView 。我尝试了在 pub.dev 上找到的一些引用:
包链接: https://pub.dev/packages/parent_child_checkbox

https://pub.dev/packages/list_treeview

我已经测试过它们,但它们不符合我的要求。我需要一个带有复选框和选择的 n 级子树,如下图所示。有谁对如何实现这一目标有任何想法,或者有人可以提供指导吗?谢谢。

enter image description here

enter image description here

enter image description here

最佳答案

首先,创建一个名为TreeNode的类:

class TreeNode {
  final String title;
  final bool isSelected;
  final CheckBoxState checkBoxState;
  final List<TreeNode> children;

  TreeNode({
    required this.title,
    this.isSelected = false,
    this.children = const <TreeNode>[],
  }) : checkBoxState = isSelected
            ? CheckBoxState.selected
            : (children.any((element) =>
                    element.checkBoxState != CheckBoxState.unselected)
                ? CheckBoxState.partial
                : CheckBoxState.unselected);

  TreeNode copyWith({
    String? title,
    bool? isSelected,
    List<TreeNode>? children,
  }) {
    return TreeNode(
      title: title ?? this.title,
      isSelected: isSelected ?? this.isSelected,
      children: children ?? this.children,
    );
  }
}

您的数据可能是这样的:

final nodes = [
  TreeNode(
    title: "title.1",
    children: [
      TreeNode(
        title: "title.1.1",
      ),
      TreeNode(
        title: "title.1.2",
        children: [
          TreeNode(
            title: "title.1.2.1",
          ),
          TreeNode(
            title: "title.1.2.2",
          ),
        ],
      ),
      TreeNode(
        title: "title.1.3",
      ),
    ],
  ),
  TreeNode(
    title: "title.2",
  ),
  TreeNode(
    title: "title.3",
    children: [
      TreeNode(
        title: "title.3.1",
      ),
      TreeNode(
        title: "title.3.2",
      ),
    ],
  ),
  TreeNode(
    title: "title.4",
  ),
];

为复选框状态创建一个枚举:

enum CheckBoxState {
  selected,
  unselected,
  partial,
}

创建一个具有三种状态并显示标题的 TitleCheckBox 小部件:

class TitleCheckBox extends StatelessWidget {
  const TitleCheckBox({
    Key? key,
    required this.title,
    required this.checkBoxState,
    required this.onChanged,
    required this.level,
  }) : super(key: key);

  final String title;
  final CheckBoxState checkBoxState;
  final VoidCallback onChanged;
  final int level;

  @override
  Widget build(BuildContext context) {
    final themeData = Theme.of(context);
    const size = 24.0;
    const borderRadius = BorderRadius.all(Radius.circular(3.0));
    return Row(
      children: [
        SizedBox(
          width: level * 16.0,
        ),
        IconButton(
          onPressed: onChanged,
          // borderRadius: borderRadius,
          icon: Container(
            height: size,
            width: size,
            alignment: Alignment.center,
            decoration: BoxDecoration(
              border: Border.all(
                color: checkBoxState == CheckBoxState.unselected
                    ? themeData.unselectedWidgetColor
                    : themeData.primaryColor,
                width: 2.0,
              ),
              borderRadius: borderRadius,
              color: checkBoxState == CheckBoxState.unselected
                  ? Colors.transparent
                  : themeData.primaryColor,
            ),
            child: AnimatedSwitcher(
              duration: const Duration(
                milliseconds: 260,
              ),
              child: checkBoxState == CheckBoxState.unselected
                  ? const SizedBox(
                      height: size,
                      width: size,
                    )
                  : FittedBox(
                      key: ValueKey(checkBoxState.name),
                      fit: BoxFit.scaleDown,
                      child: Center(
                        child: checkBoxState == CheckBoxState.partial
                            ? Container(
                                height: 1.8,
                                width: 12.0,
                                decoration: const BoxDecoration(
                                  color: Colors.white,
                                  borderRadius: borderRadius,
                                ),
                              )
                            : const Icon(
                                Icons.check,
                                color: Colors.white,
                              ),
                      ),
                    ),
            ),
          ),
        ),
        const SizedBox(
          width: 8.0,
        ),
        Text(title),
      ],
    );
  }
}

现在使用选择逻辑实现递归TreeView:

class TreeView extends StatefulWidget {
  const TreeView({
    Key? key,
    required this.nodes,
    this.level = 0,
    required this.onChanged,
  }) : super(key: key);

  final List<TreeNode> nodes;
  final int level;
  final void Function(List<TreeNode> newNodes) onChanged;

  @override
  State<TreeView> createState() => _TreeViewState();
}

class _TreeViewState extends State<TreeView> {
  late List<TreeNode> nodes;

  @override
  void initState() {
    super.initState();
    nodes = widget.nodes;
  }

  TreeNode _unselectAllSubTree(TreeNode node) {
    final treeNode = node.copyWith(
      isSelected: false,
      children: node.children.isEmpty
          ? null
          : node.children.map((e) => _unselectAllSubTree(e)).toList(),
    );
    return treeNode;
  }

  TreeNode _selectAllSubTree(TreeNode node) {
    final treeNode = node.copyWith(
      isSelected: true,
      children: node.children.isEmpty
          ? null
          : node.children.map((e) => _selectAllSubTree(e)).toList(),
    );
    return treeNode;
  }

  @override
  Widget build(BuildContext context) {
    if (widget.nodes != nodes) {
      nodes = widget.nodes;
    }

    return ListView.builder(
      itemCount: nodes.length,
      physics: widget.level != 0 ? const NeverScrollableScrollPhysics() : null,
      shrinkWrap: widget.level != 0,
      itemBuilder: (context, index) {
        return ExpansionTile(
          title: TitleCheckBox(
            onChanged: () {
              switch (nodes[index].checkBoxState) {
                case CheckBoxState.selected:
                  nodes[index] = _unselectAllSubTree(nodes[index]);
                  break;
                case CheckBoxState.unselected:
                  nodes[index] = _selectAllSubTree(nodes[index]);
                  break;
                case CheckBoxState.partial:
                  nodes[index] = _unselectAllSubTree(nodes[index]);
                  break;
              }
              if (widget.level == 0) {
                setState(() {});
              }
              widget.onChanged(nodes);
            },
            title: nodes[index].title,
            checkBoxState: nodes[index].checkBoxState,
            level: widget.level,
          ),
          trailing:
              nodes[index].children.isEmpty ? const SizedBox.shrink() : null,
          children: [
            TreeView(
              nodes: nodes[index].children,
              level: widget.level + 1,
              onChanged: (newNodes) {
                bool areAllItemsSelected = !nodes[index]
                    .children
                    .any((element) => !element.isSelected);

                nodes[index] = nodes[index].copyWith(
                  isSelected: areAllItemsSelected,
                  children: newNodes,
                );

                widget.onChanged(nodes);
                if (widget.level == 0) {
                  setState(() {});
                }
              },
            ),
          ],
        );
      },
    );
  }
}

全部完成!你可以像这样使用你的TreeView:

 TreeView(
        onChanged: (newNodes) {},
        nodes: nodes,
      ),

这就是结果:
enter image description here

关于flutter - 如何使用具有全选和取消全选功能的三态复选框实现 TreeView,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/76523285/

相关文章:

c# - 如果 SelectionChanged 事件触发得太快,WPF Treeview 会溢出吗?

dart - 导入的库 'package:geolocator/model/position.dart' 不能有 part-of 指令

flutter - 所有与编译不同的 AndroidX 版本

haskell - 为什么haskell中的递归列表这么慢?

f# - F# 中的显式类型递归

c# - 复选框的树形 View

c++ - 在 TreeView 中双击后焦点丢失

flutter - 跨多个页面持久化提供者数据不起作用

dart - 在Dart中,静态成员,最终成员和const成员在编译时有什么区别?

javascript - 基于数组值的链函数