Last active
September 25, 2022 01:42
-
-
Save pingbird/3739dee678ee1b19e4d299c0025794b9 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'dart:math'; | |
import 'package:boxy/boxy.dart'; | |
import 'package:flutter/material.dart'; | |
void main() { | |
runApp(const MyApp()); | |
} | |
class MyApp extends StatelessWidget { | |
const MyApp({Key? key}) : super(key: key); | |
@override | |
Widget build(BuildContext context) { | |
return const MaterialApp( | |
debugShowCheckedModeBanner: false, | |
home: MyHomePage(), | |
); | |
} | |
} | |
class MyHomePage extends StatelessWidget { | |
const MyHomePage({Key? key}) : super(key: key); | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
body: Center( | |
child: UnconstrainedBox( | |
child: Container( | |
decoration: BoxDecoration( | |
border: Border.all(width: 1, color: Colors.black12), | |
), | |
child: const Center( | |
child: TreeView( | |
root: TreeNode( | |
ExampleCard('A'), | |
[ | |
TreeNode(ExampleCard('A.A')), | |
TreeNode( | |
ExampleCard('A.B'), | |
[ | |
TreeNode(ExampleCard('A.B.A')), | |
TreeNode(ExampleCard('A.B.B')), | |
], | |
), | |
TreeNode(ExampleCard('A.C')), | |
], | |
), | |
horizontalSpacing: 8, | |
verticalSpacing: 32, | |
), | |
), | |
), | |
), | |
), | |
); | |
} | |
} | |
class ExampleCard extends StatelessWidget { | |
const ExampleCard(this.label, {Key? key}) : super(key: key); | |
final String label; | |
@override | |
Widget build(BuildContext context) { | |
return Container( | |
width: 150, | |
height: 100, | |
decoration: BoxDecoration( | |
border: Border.all(color: Colors.black, width: 3.0), | |
borderRadius: BorderRadius.circular(16.0), | |
), | |
child: Center( | |
child: Text( | |
label, | |
style: const TextStyle( | |
fontSize: 20.0, | |
fontWeight: FontWeight.bold, | |
), | |
), | |
), | |
); | |
} | |
} | |
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; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment