Dynamically layout widgets based on the size/position of other widgets (sub-widgets)

Viewed 24

I'd like to draw a graph of nodes and edges. The graph should appear like a forest of trees. To simplify my question, let's focus on a tree, which should be drawn like this:

enter image description here

I'm not in search of the algorithm, which computes positions of nodes on the drawing plane. Recursively computing sizes and positions of visual representations of a nodes / subtrees using depth first tree traversal is trivial.

I'm search if a flutter implementation:

I suppose, a Stack and Positioned widgets would be fine for placing nodes.

  • But how do I get the rendered size / dimension of a widget and recursively the dimension of a subtree?
  • And how do I get this dimensions when the code is about to place nodes / subtrees using Positioned on Stack?

Could you please provide an example or a recipe?

2 Answers

I am the author of the Boxy package and this question is a perfect use case, since there is no way to do this with core flutter widgets (even with CustomMultiChildLayout).

Here is a working solution:

enter image description here

class TreeNode {
  const TreeNode(this.widget, [this.children = const []]);

  final Widget widget;
  final List<TreeNode> children;

  Iterable<Widget> get allWidgets =>
      [widget].followedBy(children.expand((e) => e.allWidgets));
}

class TreeView extends StatelessWidget {
  const TreeView({
    required this.root,
    required this.verticalSpacing,
    required this.horizontalSpacing,
    super.key,
  });

  final TreeNode root;
  final double verticalSpacing;
  final double horizontalSpacing;

  @override
  Widget build(BuildContext context) {
    return CustomBoxy(
      delegate: _TreeViewBoxy(
        root: root,
        verticalSpacing: verticalSpacing,
        horizontalSpacing: horizontalSpacing,
      ),
      children: [...root.allWidgets],
    );
  }
}

class _TreeViewBoxy extends BoxyDelegate {
  _TreeViewBoxy({
    required this.root,
    required this.verticalSpacing,
    required this.horizontalSpacing,
  });

  final TreeNode root;
  final double verticalSpacing;
  final double horizontalSpacing;

  @override
  Size layout() {
    var index = 0;
    Size visit(TreeNode node, Offset offset) {
      final nodeIndex = index++;
      final child = children[nodeIndex];
      final size = child.layout(const BoxConstraints());
      final Size subtreeSize;

      if (node.children.isEmpty) {
        subtreeSize = size;
      } else {
        var width = 0.0;
        var height = 0.0;
        var x = 0.0;
        final y = offset.dy + child.size.height + verticalSpacing;
        for (final child in node.children) {
          final childSize = visit(child, Offset(offset.dx + x, y));
          height = max(height, childSize.height);
          width += childSize.width;
          x += childSize.width + horizontalSpacing;
        }
        width += (node.children.length - 1) * horizontalSpacing;
        subtreeSize = Size(
          max(width, size.width),
          size.height + height + verticalSpacing,
        );
      }

      child.position(
        offset +
            Offset(
              subtreeSize.width / 2 - child.size.width / 2,
              0,
            ),
      );

      return subtreeSize;
    }

    return visit(root, Offset.zero);
  }

  @override
  void paint() {
    var index = 0;
    void paintLines(TreeNode node) {
      final nodeOffset = children[index++].rect.bottomCenter;
      for (final child in node.children) {
        final childOffset = children[index].rect.topCenter;
        canvas.drawPath(
          Path()
            ..moveTo(nodeOffset.dx, nodeOffset.dy)
            ..cubicTo(
              nodeOffset.dx,
              nodeOffset.dy + verticalSpacing,
              childOffset.dx,
              childOffset.dy - verticalSpacing,
              childOffset.dx,
              childOffset.dy,
            ),
          Paint()
            ..style = PaintingStyle.stroke
            ..strokeWidth = 3.0,
        );
        paintLines(child);
      }
    }

    paintLines(root);
  }

  @override
  bool shouldRelayout(_TreeViewBoxy oldDelegate) =>
      root != oldDelegate.root ||
      verticalSpacing != oldDelegate.verticalSpacing ||
      horizontalSpacing != oldDelegate.horizontalSpacing;
}

The full example can be found here: https://gist.github.com/PixelToast/3739dee678ee1b19e4d299c0025794b9

You want the Boxy package in the Pub. There's even an example of a hierarchical set of widgets being displayed in the Examples.

Related