flutter 如何实现具有三态CheckBox的TreeView,该CheckBox具有全选和未选中的全部功能

vs3odd8k  于 2023-06-30  发布在  Flutter
关注(0)|答案(2)|浏览(284)

我想实现一个树形层次结构的列表视图.我试过一些我在www.example.com上找到的参考资料pub.dev:
链接到包:https://pub.dev/packages/parent_child_checkbox
https://pub.dev/packages/list_treeview
我已经测试过了,但它们不符合我的要求。我需要一个带有复选框和选择的n级子树,如下图所示。有没有人有任何想法,如何实现这一点,或任何人可以提供指导?谢谢你。

mpgws1up

mpgws1up1#

首先,创建一个名为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",
  ),
];

为checkBox状态创建枚举:

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,
      ),

这就是结果

t9eec4r0

t9eec4r02#

也许你可以创建你自己的renderObject,我尽我所能使缩进部件看起来像你提供的图像。请记住,这不是小工具,因此这可能会花费一些执行问题。

另外,如果你对flutter中的renderObject感兴趣,我建议你看一下这个视频。https://www.youtube.com/watch?v=HqXNGawzSbY&t=7458s
main.dart

import 'package:flutter/material.dart';
import 'indent_widget.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  Widget _buildColumn() {
    return Row(
      children: [
        Checkbox(value: false, onChanged: (value) {}),
        const Expanded(child: Text('text'))
      ],
    );
  }

  @override
  Widget build(BuildContext context) {
    double tabSpace = 30;
    return Scaffold(
      body: SafeArea(
        child: SingleChildScrollView(
          child: IndentWidget(children: [
            _buildColumn(),
            IndentTab(
                tabSpace: tabSpace,
                child: IndentWidget(
                  children: [
                    IndentTab(
                        tabSpace: tabSpace,
                        child: IndentWidget(
                          children: [
                            _buildColumn(),
                            _buildColumn(),
                            IndentTab(
                                tabSpace: tabSpace,
                                child: IndentWidget(
                                  children: [
                                    _buildColumn(),
                                    _buildColumn(),
                                    IndentTab(
                                        tabSpace: tabSpace,
                                        child: IndentWidget(
                                          children: [
                                            _buildColumn(),
                                            _buildColumn(),
                                            _buildColumn(),
                                          ],
                                        )),
                                    _buildColumn(),
                                    IndentTab(
                                        tabSpace: tabSpace,
                                        child: IndentWidget(
                                          children: [
                                            _buildColumn(),
                                            _buildColumn(),
                                            _buildColumn(),
                                          ],
                                        )),
                                  ],
                                )),
                            _buildColumn(),
                          ],
                        )),
                    _buildColumn(),
                    _buildColumn(),
                    _buildColumn(),
                  ],
                )),
            _buildColumn(),
          ]),
        ),
      ),
    );
  }
}

indent_widget.dart

import 'dart:math';

import 'package:flutter/cupertino.dart';
import 'package:flutter/rendering.dart';

class IndentWidget extends MultiChildRenderObjectWidget {
  IndentWidget({super.key, super.children});

  @override
  RenderObject createRenderObject(BuildContext context) {
    /// 1. entry point.
    return RenderIndent();
  }
}

/// provide information to RenderIndent;
class RenderIndentParentData extends ContainerBoxParentData<RenderBox> {
  double? tabSpace;
}

class IndentTab extends ParentDataWidget<RenderIndentParentData> {
  final double tabSpace;

  const IndentTab({super.key, required this.tabSpace, required super.child});

  @override
  void applyParentData(RenderObject renderObject) {
    final RenderIndentParentData parentData =
        renderObject.parentData! as RenderIndentParentData;

    if (parentData.tabSpace != tabSpace) {
      parentData.tabSpace = tabSpace;
      final targetObject = renderObject.parent;
      if (targetObject is RenderObject) {
        targetObject.markNeedsLayout();
      }
    }
  }

  @override
  Type get debugTypicalAncestorWidgetClass => RenderIndentParentData;
}

class RenderIndent extends RenderBox
    with
        ContainerRenderObjectMixin<RenderBox, RenderIndentParentData>,
        RenderBoxContainerDefaultsMixin<RenderBox, RenderIndentParentData> {
  @override
  void setupParentData(RenderBox child) {
    if (child.parentData is! RenderIndentParentData) {
      child.parentData = RenderIndentParentData();
    }
  }

  Size _performLayout(BoxConstraints constraints, bool dry) {
    RenderBox? child = firstChild;
    double width = 0, height = 0;

    while (child != null) {
      final RenderIndentParentData childParentData =
          child.parentData as RenderIndentParentData;
      final double leftShift = childParentData.tabSpace ?? 0;

      if (!dry) {
        childParentData.offset = Offset(leftShift, height);
        child.layout(BoxConstraints(maxWidth: constraints.maxWidth),
            parentUsesSize: true);
      }
      height += child.size.height;
      width = max(width, leftShift + child.size.width);
      child = childParentData.nextSibling;
    }

    if (width > constraints.maxWidth) {
      width = constraints.maxWidth;
    }

    return Size(width, height);
  }

  @override
  void performLayout() {
    size = _performLayout(constraints, false);
  }

  @override
  Size computeDryLayout(BoxConstraints constraints) {
    return _performLayout(constraints, true);
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    defaultPaint(context, offset);
  }

  @override
  bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
    return defaultHitTestChildren(result, position: position);
  }
}

相关问题