Flutter树形布局插件ploeg_tree_layout的使用

Flutter树形布局插件ploeg_tree_layout的使用

该布局是Ploeg对扩展的Reingold-Tilford算法的改编,如论文所述: van der Ploeg, A. (2014), Drawing non-layered tidy trees in linear time, Softw. Pract. Exper., 44, pages 1467– 1484, doi: 10.1002/spe.2213

原始实现可以在以下GitHub仓库找到: https://github.com/cwi-swat/non-layered-tidy-trees

此代码处于公共领域,您可以自由使用。引用该论文将不胜感激!

特性

非分层整洁树布局(单棵树) 截图

非分层整洁树布局(森林) 截图

此实现将算法与树数据结构隔离。树通过以下方式获取:

List<V> Function() roots;
List<V> Function(V) next;

使用方法

Map<int, List<int>> simpleTree = {
  0: [1, 2],
  1: [3],
  2: [4, 5],
  3: [],
  4: [],
  5: []
};

Map<int, Offset> nodePositions = {};
Map<int, Size> nodeSizes = {};

var algo = PloegTreeLayout(
    roots: () => [0],
    next: (v) => simpleTree[v]!,
    sizeGetter: (v) {
      Size size = 
          Size(Random().nextInt(90) + 30, Random().nextInt(60) + 20);
      nodeSizes[v] = size;
      return size;
    },
    onPositionChange: (v, offset) => {nodePositions[v] = offset});
algo.layout();

完整示例

以下是一个完整的示例代码,展示了如何在Flutter中使用ploeg_tree_layout插件来绘制树形布局。

import 'dart:math';

import 'package:example/random_tree.dart';
import 'package:flutter/material.dart';
import 'package:ploeg_tree_layout/ploeg_tree_layout.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  [@override](/user/override)
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const GraphScene(),
    );
  }
}

class GraphScene extends StatefulWidget {
  const GraphScene({super.key});

  [@override](/user/override)
  State<GraphScene> createState() => _GraphSceneState();
}

class _GraphSceneState extends State<GraphScene> {
  Map<int, List<int>> randomTree = RandomTree.tree(5, 2, 0);
  Map<int, List<int>> randomTree2 = RandomTree.tree(5, 3, 11);

  Map<int, Offset> nodePositions = {};
  Map<int, Size> nodeSizes = {};

  [@override](/user/override)
  void initState() {
    super.initState();
    randomTree.addAll(randomTree2);

    var algo = PloegTreeLayout(
        roots: () => [0, 11],
        next: (v) => randomTree[v]!,
        sizeGetter: (v) {
          Size size = 
              Size(Random().nextInt(90) + 30, Random().nextInt(60) + 20);
          nodeSizes[v] = size;
          return size;
        },
        onPositionChange: (v, offset) {
          nodePositions[v] = offset;
        });
    algo.layout();
  }

  final TransformationController _transformationController = TransformationController();

  [@override](/user/override)
  Widget build(BuildContext context) {
    return Center(
      child: InteractiveViewer(
        boundaryMargin: const EdgeInsets.all(20.0),
        minScale: 0.0001,
        maxScale: 10.5,
        constrained: false,
        transformationController: _transformationController,
        child: SizedBox(
          height: 1000,
          width: 1000,
          child: ColoredBox(
            color: Colors.grey,
            child: Stack(
              children: [
                CustomPaint(
                  size: const Size(double.infinity, double.infinity),
                  painter: RelationPainter(
                      map: randomTree,
                      nodePositions: nodePositions,
                      nodeSizes: nodeSizes),
                ),
                ..._buildNodes()
              ],
            ),
          ),
        ),
      ),
    );
  }

  List<Widget> _buildNodes() {
    final res = [];
    nodePositions.forEach((node, offset) {
      res.add(NodeWidget(
        offset: offset,
        size: nodeSizes[node]!,
        node: node,
      ));
    });
    return res;
  }
}

class NodeWidget extends StatelessWidget {
  const NodeWidget(
      {super.key,
      required this.offset,
      required this.size,
      required this.node});
  final Offset offset;
  final Size size;
  final int node;

  [@override](/user/override)
  Widget build(BuildContext context) {
    return Positioned(
      left: offset.dx,
      top: offset.dy,
      child: Container(
        width: size.width,
        height: size.height,
        decoration: BoxDecoration(
          color: Colors.white,
          border: Border.all(width: 1),
          shape: BoxShape.rectangle,
          borderRadius: BorderRadius.circular(10),
        ),
        child: Center(child: Material(child: Text('$node'))),
      ),
    );
  }
}

class RelationPainter extends CustomPainter {
  RelationPainter(
      {required this.map,
      required this.nodePositions,
      required this.nodeSizes});

  final Map<int, List<int>> map;
  final Map<int, Offset> nodePositions;
  final Map<int, Size> nodeSizes;

  [@override](/user/override)
  void paint(Canvas canvas, Size size) {
    if (nodePositions.length > 1) {
      nodePositions.forEach((index, offset) {
        for (var t in map[index]!) {
          canvas.drawLine(
              Offset(offset.dx + nodeSizes[index]!.width / 2,
                  offset.dy + nodeSizes[index]!.height / 2),
              Offset(nodePositions[t]!.dx + nodeSizes[t]!.width / 2,
                  nodePositions[t]!.dy + nodeSizes[t]!.height / 2),
              Paint()
                ..color = Colors.black
                ..strokeWidth = 1);
        }
      });
    }
  }

  [@override](/user/override)
  bool shouldRepaint(RelationPainter oldDelegate) => true;
}

更多关于Flutter树形布局插件ploeg_tree_layout的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter树形布局插件ploeg_tree_layout的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


ploeg_tree_layout 是 Flutter 中的一个树形布局插件,用于在 Flutter 应用中实现树形结构的数据展示。它可以帮助开发者轻松地构建和展示树形结构的数据,如组织结构图、文件目录、家族树等。

安装插件

首先,你需要在 pubspec.yaml 文件中添加 ploeg_tree_layout 插件的依赖:

dependencies:
  flutter:
    sdk: flutter
  ploeg_tree_layout: ^1.0.0  # 请确保使用最新版本

然后运行 flutter pub get 来安装插件。

基本使用

以下是一个简单的示例,展示如何使用 ploeg_tree_layout 插件来展示一个树形结构。

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

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Tree Layout Example'),
        ),
        body: TreeLayoutExample(),
      ),
    );
  }
}

class TreeLayoutExample extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: TreeLayout(
        root: TreeNode(
          widget: Text('Root'),
          children: [
            TreeNode(
              widget: Text('Child 1'),
              children: [
                TreeNode(widget: Text('Grandchild 1.1')),
                TreeNode(widget: Text('Grandchild 1.2')),
              ],
            ),
            TreeNode(
              widget: Text('Child 2'),
              children: [
                TreeNode(widget: Text('Grandchild 2.1')),
                TreeNode(widget: Text('Grandchild 2.2')),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

解释

  1. TreeLayout: 这是 ploeg_tree_layout 插件的主要组件,用于展示树形结构。它接受一个 TreeNode 作为根节点。

  2. TreeNode: TreeNode 是树形结构中的一个节点。它包含一个 widget 属性,用于显示该节点的内容,以及一个可选的 children 属性,用于指定该节点的子节点。

  3. Widget: 每个 TreeNodewidget 属性可以是任何 Flutter widget,例如 TextContainer 等。

自定义样式

你可以通过传递不同的参数来自定义树形布局的样式。例如,你可以设置节点的间距、连接线的样式等。

TreeLayout(
  root: TreeNode(
    widget: Text('Root'),
    children: [
      TreeNode(widget: Text('Child 1')),
      TreeNode(widget: Text('Child 2')),
    ],
  ),
  nodeSpacing: 20.0,  // 设置节点之间的间距
  connectorStyle: ConnectorStyle(
    color: Colors.blue,  // 设置连接线的颜色
    thickness: 2.0,     // 设置连接线的粗细
  ),
)
回到顶部