Flutter图形绘制与可视化插件gojs的使用

Flutter图形绘制与可视化插件gojs的使用

简介

gojs 是一个用于构建交互式图表和图形的 Dart 接口。GoJS 是一个用于构建交互式图表和图形的 JavaScript 和 TypeScript 库。GoJS 可以帮助你构建各种类型的图表和图形,从简单的流程图和组织结构图到高度特定的工业图表、SCADA 和 BPMN 图表、医学图表如家谱等。

GoJS 提供了许多高级功能,例如拖放、复制粘贴、就地文本编辑、工具提示、上下文菜单、自动布局、模板、数据绑定和模型、事务状态和撤销管理、调色板、概览、事件处理程序、命令、可扩展工具用于自定义操作以及可定制的动画。

你可以在这里查看 GoJS 的工作示例:GoJS 示例

简单示例

以下是一个简单的 GoJS 示例:

import 'package:gojs/gojs.dart';
import 'dart:html' as html;

// 一些简单的 HTML 代码,包含一个类似 <div id="diagram"></div> 的 div

void main() {
  var diagram = html.document.getElementById('diagram');
  
  var myDiagram = GoJSDiagram(diagram)
    ..model = GoJSModel.fromJson('''
        [
          { "key": "Alpha" },
          { "key": "Beta" },
          { "key": "Gamma" }
        ]
    ''');
}

完整示例

以下是一个完整的流程图应用程序的示例代码:

import 'package:gojs/gojs.dart';
import 'package:js/js.dart';
import 'dart:html' as html;

void main() {
  var diagram = html.document.getElementById('diagram');
  var palette = html.document.getElementById('palette');

  try {
    var god = GoJSDiagram(diagram)
      ..linkDrawn = allowInterop(showLinkLabel)
      ..linkRelinked = allowInterop(showLinkLabel);

    god.nodeTemplateMap.add(
        '', // 默认类别
        nodeStyle()
          ..type = GoJSPanel.table
          ..addAll([
            GoJSPanel()
              ..type = GoJSPanel.auto
              ..addAll([
                GoJSShape()
                  ..figure = 'Rectangle'
                  ..fill = '#282c34'
                  ..stroke = '#00A9C9'
                  ..strokeWidth = 3.5
                  ..bind(GoJSBinding('figure', 'figure')),
                textStyle()
                  ..margin = 8
                  ..maxSize = GoJSSize(160, nan)
                  ..wrap = GoJSTextBlock.wrapFit
                  ..editable = true
                  ..bind(GoJSBinding('text').makeTwoWay())
              ]),
            makePort('T', GoJSSpot.top, GoJSSpot.topSide, false, true),
            makePort('L', GoJSSpot.left, GoJSSpot.leftSide, true, true),
            makePort('R', GoJSSpot.right, GoJSSpot.rightSide, true, true),
            makePort('B', GoJSSpot.bottom, GoJSSpot.bottomSide, true, false)
          ]));

    god.nodeTemplateMap.add(
        'Conditional',
        nodeStyle()
          ..type = GoJSPanel.table
          ..addAll([
            GoJSPanel()
              ..type = GoJSPanel.auto
              ..addAll([
                GoJSShape()
                  ..figure = 'Diamond'
                  ..fill = '#282c34'
                  ..stroke = '#00A9C9'
                  ..strokeWidth = 3.5
                  ..bind(GoJSBinding('figure', 'figure')),
                textStyle()
                  ..margin = 8
                  ..maxSize = GoJSSize(160, nan)
                  ..wrap = GoJSTextBlock.wrapFit
                  ..editable = true
                  ..bind(GoJSBinding('text').makeTwoWay())
              ]),
            makePort('T', GoJSSpot.top, GoJSSpot.top, false, true),
            makePort('L', GoJSSpot.left, GoJSSpot.left, true, true),
            makePort('R', GoJSSpot.right, GoJSSpot.right, true, true),
            makePort('B', GoJSSpot.bottom, GoJSSpot.bottom, true, false)
          ]));

    god.nodeTemplateMap.add(
        'Start',
        nodeStyle()
          ..type = GoJSPanel.table
          ..addAll([
            GoJSPanel()
              ..type = GoJSPanel.auto
              ..addAll([
                GoJSShape()
                  ..figure = 'Circle'
                  ..fill = '#282c34'
                  ..stroke = '#09d3ac'
                  ..strokeWidth = 3.5
                  ..desiredSize = GoJSSize(70, 70),
                textStyle('Start')..bind(GoJSBinding('text'))
              ]),
            makePort('L', GoJSSpot.left, GoJSSpot.left, true, false),
            makePort('R', GoJSSpot.right, GoJSSpot.right, true, false),
            makePort('B', GoJSSpot.bottom, GoJSSpot.bottom, true, false)
          ]));

    god.nodeTemplateMap.add(
        'End',
        nodeStyle()
          ..type = GoJSPanel.table
          ..addAll([
            (GoJSPanel()
              ..type = GoJSPanel.auto
              ..addAll([
                GoJSShape()
                  ..figure = 'Circle'
                  ..fill = '#282c34'
                  ..stroke = '#DC3C00'
                  ..strokeWidth = 3.5
                  ..desiredSize = GoJSSize(70, 70),
                textStyle('End')..bind(GoJSBinding('text'))
              ])),
            makePort('T', GoJSSpot.top, GoJSSpot.top, false, true),
            makePort('L', GoJSSpot.left, GoJSSpot.left, false, true),
            makePort('R', GoJSSpot.right, GoJSSpot.right, false, true)
          ]));

    GoJSShape.defineFigureGenerator('File', allowInterop((shape, w, h) {
      var geo = GoJSGeometry();
      var fig = GoJSPathFigure(0, 0, true); // 起点
      geo.add(fig);
      fig.addAll([
        GoJSPathSegment(GoJSPathSegment.line, .75 * w, 0),
        GoJSPathSegment(GoJSPathSegment.line, w, .25 * h),
        GoJSPathSegment(GoJSPathSegment.line, w, h),
        GoJSPathSegment(GoJSPathSegment.line, 0, h).close()
      ]);
      var fig2 = GoJSPathFigure(.75 * w, 0, false);
      geo.add(fig2);
      // 折叠部分
      fig2.addAll([
        GoJSPathSegment(GoJSPathSegment.line, .75 * w, .25 * h),
        GoJSPathSegment(GoJSPathSegment.line, w, .25 * h)
      ]);
      geo
        ..spot1 = GoJSSpot(0, .25)
        ..spot2 = GoJSSpot.bottomRight;
      return geo;
    }));

    god.nodeTemplateMap.add(
        'Comment',
        nodeStyle()
          ..type = GoJSPanel.auto
          ..addAll([
            GoJSShape()
              ..figure = 'File'
              ..fill = '#282c34'
              ..stroke = '#DEE0A3'
              ..strokeWidth = 3,
            textStyle()
              ..margin = 8
              ..maxSize = GoJSSize(200, nan)
              ..wrap = GoJSTextBlock.wrapFit
              ..textAlign = 'center'
              ..editable = true
              ..bind(GoJSBinding('text').makeTwoWay())
          ]));

    god.linkTemplate = GoJSLink()
      ..routing = GoJSLink.avoidsNodes
      ..curve = GoJSLink.jumpOver
      ..corner = 5
      ..toShortLength = 4
      ..relinkableFrom = true
      ..relinkableTo = true
      ..reshapable = true
      ..resegmentable = true
      // 鼠标悬停时,链接会微妙地高亮:
      ..mouseEnter = allowInterop(([e, link, k]) {
        (link.findObject('HIGHLIGHT') as GoJSShape).stroke =
            'rgba(30,144,255,0.2)';
      })
      ..mouseLeave = allowInterop(([e, link, k]) {
        (link.findObject('HIGHLIGHT') as GoJSShape).stroke = 'transparent';
      })
      ..selectionAdorned = false
      ..bind(GoJSBinding('points').makeTwoWay())
      ..addAll([
        GoJSShape() // 高亮形状,默认透明
          ..isPanelMain = true
          ..strokeWidth = 8
          ..stroke = 'transparent'
          ..name = 'HIGHLIGHT',
        GoJSShape() // 链接路径形状
          ..isPanelMain = true
          ..strokeWidth = 2
          ..stroke = 'gray'
          ..bind(GoJSBinding('stroke', 'isSelected', allowInterop((sel, k) {
            return sel ? 'dodgerblue' : 'gray';
          })).ofObject()),
        GoJSShape() // 箭头
          ..toArrow = 'standard'
          ..strokeWidth = 0
          ..fill = 'gray',
        GoJSPanel()
          ..type = GoJSPanel.auto
          ..visible = false
          ..name = 'LABEL'
          ..segmentIndex = 2
          ..segmentFraction = 0.5
          ..bind(GoJSBinding('visible', 'visible').makeTwoWay())
          ..addAll([
            GoJSShape()
              ..figure = 'RoundedRectangle' // 标签形状
              ..fill = '#F8F8F8'
              ..strokeWidth = 0,
            GoJSTextBlock()
              ..text = 'Yes'
              ..textAlign = 'center'
              ..font = '10pt helvetica, arial, sans-serif'
              ..stroke = '#333333'
              ..editable = true
              ..bind(GoJSBinding('text').makeTwoWay())
          ])
      ]);

    god.toolManager.linkingTool.temporaryLink.routing = GoJSLink.orthogonal;
    god.toolManager.relinkingTool.temporaryLink.routing = GoJSLink.orthogonal;

    // 加载数据
    var load = '''
      { "class": "go.GraphLinksModel",
        "linkFromPortIdProperty": "fromPort",
        "linkToPortIdProperty": "toPort",
        "nodeDataArray": [
      {"category":"Comment", "loc":"360 -10", "text":"Kookie Brittle", "key":-13},
      {"key":-1, "category":"Start", "loc":"175 0", "text":"Start"},
      {"key":0, "loc":"-5 75", "text":"Preheat oven to 375 F"},
      {"key":1, "loc":"175 100", "text":"In a bowl, blend: 1 cup margarine, 1.5 teaspoon vanilla, 1 teaspoon salt"},
      {"key":2, "loc":"175 200", "text":"Gradually beat in 1 cup sugar and 2 cups sifted flour"},
      {"key":3, "loc":"175 290", "text":"Mix in 6 oz (1 cup) Nestle's Semi-Sweet Chocolate Morsels"},
      {"key":4, "loc":"175 380", "text":"Press evenly into ungreased 15x10x1 pan"},
      {"key":5, "loc":"355 85", "text":"Finely chop 1/2 cup of your choice of nuts"},
      {"key":6, "loc":"175 450", "text":"Sprinkle nuts on top"},
      {"key":7, "loc":"175 515", "text":"Bake for 25 minutes and let cool"},
      {"key":8, "loc":"175 585", "text":"Cut into rectangular grid"},
      {"key":-2, "category":"End", "loc":"175 660", "text":"Enjoy!"}
      ],
        "linkDataArray": [
      {"from":1, "to":2, "fromPort":"B", "toPort":"T"},
      {"from":2, "to":3, "fromPort":"B", "toPort":"T"},
      {"from":3, "to":4, "fromPort":"B", "toPort":"T"},
      {"from":4, "to":6, "fromPort":"B", "toPort":"T"},
      {"from":6, "to":7, "fromPort":"B", "toPort":"T"},
      {"from":7, "to":8, "fromPort":"B", "toPort":"T"},
      {"from":8, "to":-2, "fromPort":"B", "toPort":"T"},
      {"from":-1, "to":0, "fromPort":"B", "toPort":"T"},
      {"from":-1, "to":1, "fromPort":"B", "toPort":"T"},
      {"from":-1, "to":5, "fromPort":"B", "toPort":"T"},
      {"from":5, "to":4, "fromPort":"B", "toPort":"T"},
      {"from":0, "to":4, "fromPort":"B", "toPort":"T"}
      ]}
      ''';

    god.model = GoJSModel.fromJson(load);

    var buf = '''
              { "class": "go.GraphLinksModel",
        "nodeDataArray": [{"category": "Start", "text": "Start"},
              {"text": "Step"},
              {"category": "Conditional", "text": "???"},
              {"category": "End", "text": "End"},
              {"category": "Comment", "text": "Comment"}]
              }
          ''';
    GoJSPalette(palette)
      ..nodeTemplateMap = god.nodeTemplateMap
      //..model = GoJSLinksModel(lm)
      ..model = GoJSModel.fromJson(buf)
      ..initialAnimationStarting = allowInterop(animateFadeDown)
      ..animationManager.initialAnimationStyle = GoJSAnimationManager.none;
  } catch (e) {
    print('FlowChart example error: $e');
    html.window.console.error(e);
  } finally {
    print('Done! :D');
  }
}

void animateFadeDown(e) {
  var diagram = e.diagram;
  var animation = GoJSAnimation();
  animation.isViewportUnconstrained =
      true; // 所以在开始时允许图表在屏幕外定位
  animation.easing = GoJSAnimation.easeOutExpo;
  animation.duration = 900;
  // 向下淡入,即从上方淡入
  animation.add(diagram, 'position', diagram.position.copy().offset(0, 200),
      diagram.position);
  animation.add(diagram, 'opacity', 0, 1);
  animation.start();
}

void showLinkLabel(e) {
  var label = e.subject.findObject('LABEL');

  if (label == null) {
    label.visible = (e.subject.fromNode.data.category == 'Conditional');
  }
}

GoJSNode nodeStyle() => GoJSNode()
  ..locationSpot = GoJSSpot.center
  ..bind(GoJSBinding('location', 'loc', GoJSPoint.parse)
      .makeTwoWay(GoJSPoint.stringify));

GoJSTextBlock textStyle([String text]) => GoJSTextBlock()
  ..font = 'bold 11pt Lato, Helvetica, Arial, sans-serif'
  ..stroke = '#F8F8F8'
  ..text = text ?? undefined;

GoJSShape makePort(
    String name, GoJSSpot align, GoJSSpot spot, dynamic output, dynamic input) {
  var horizontal = align.equals(GoJSSpot.top) || align.equals(GoJSSpot.bottom);
  // 端口基本上只是一个沿节点侧面拉伸的透明矩形,在鼠标经过时变为颜色
  return GoJSShape()
    ..fill = 'transparent' // 改为颜色在鼠标进入事件处理器中
    ..strokeWidth = 0 // 没有描边
    ..width =
        horizontal ? nan : 8 // 如果不是水平拉伸,则只有 8 宽
    ..height =
        !horizontal ? nan : 8 // 如果不是垂直拉伸,则只有 8 高
    ..alignment = align // 将端口对齐到主形状上
    ..stretch =
        (horizontal ? GoJSGraphObject.horizontal : GoJSGraphObject.vertical)
    ..portId = name // 声明此对象为“端口”
    ..fromSpot = spot // 声明在此端口处链接可以连接的位置
    ..fromLinkable = output // 声明用户是否可以从这里绘制链接
    ..toSpot = spot // 声明在此端口处链接可以连接的位置
    ..toLinkable = input // 声明用户是否可以在此处绘制链接
    ..cursor =
        'pointer' // 显示不同的光标以指示潜在的链接点
    ..mouseEnter = allowInterop(([e, port, k]) {
      // PORT 参数将是此形状
      if (!e.diagram.isReadOnly) port.fill = 'rgba(255,0,255,0.5)';
    })
    ..mouseLeave = allowInterop(([e, port, k]) {
      port.fill = 'transparent';
    });
}

更多关于Flutter图形绘制与可视化插件gojs的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter图形绘制与可视化插件gojs的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


在Flutter中集成并使用GoJS进行图形绘制与可视化,可以通过Platform Channels与原生代码进行交互。虽然Flutter本身并没有直接支持GoJS的插件,但你可以通过Method Channel在Flutter和原生平台(如Android和iOS)之间传递消息,从而调用GoJS的相关功能。

以下是一个简要的示例,展示如何在Flutter中通过Platform Channel调用GoJS进行图形绘制。由于GoJS是一个JavaScript库,通常用于Web开发,这里我们需要在原生平台(如Android的WebView或iOS的WKWebView)中加载GoJS,并通过Platform Channel与Flutter通信。

1. 创建Flutter项目

首先,创建一个新的Flutter项目:

flutter create gojs_flutter_example
cd gojs_flutter_example

2. 设置Android平台

android/app/src/main/java/com/example/gojs_flutter_example/MainActivity.kt(或Java文件)中,添加一个WebView和MethodChannel来处理与Flutter的通信:

package com.example.gojs_flutter_example

import android.os.Bundle
import android.webkit.WebView
import android.webkit.WebViewClient
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel

class MainActivity: FlutterActivity() {
    private val CHANNEL = "com.example.gojs_flutter_example/channel"

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
            if (call.method == "loadGoJS") {
                // WebView setup
                val webView = WebView(this)
                webView.settings.javaScriptEnabled = true
                webView.webViewClient = WebViewClient()
                webView.loadUrl("file:///android_asset/gojs_page.html")

                // Listen for messages from JavaScript (optional, for two-way communication)
                webView.evaluateJavascript("javascript:window.FlutterWebViewInterface.receiveMessageFromJS('Hello from GoJS!')") { value ->
                    // Handle message from GoJS
                }

                // For simplicity, we're not attaching the WebView to the layout here.
                // In a real app, you would add it to a layout and manage its lifecycle.

                result.success(null)
            } else {
                result.notImplemented()
            }
        }
    }
}

注意:这里的WebView设置是为了演示目的,并未实际添加到布局中。在实际应用中,你需要将WebView添加到布局中,并管理其生命周期。

3. 创建GoJS HTML页面

android/app/src/main/assets/目录下创建一个名为gojs_page.html的文件,并添加GoJS的基本代码:

<!DOCTYPE html>
<html>
<head>
    <script src="https://unpkg.com/gojs/release/go.js"></script>
    <script>
        function init() {
            var $ = go.GraphObject.make;

            var myDiagram =
                $(go.Diagram, "myDiagramDiv",
                    {
                        initialContentAlignment: go.Spot.Center,
                        "undoManager.isEnabled": true
                    });

            myDiagram.nodeTemplate =
                $(go.Node, "Auto",
                    $(go.Shape, "RoundedRectangle",
                        { strokeWidth: 0, fill: "white" },
                        new go.Binding("figure", "figure")),
                    $(go.TextBlock,
                        { margin: 8, editable: true },
                        new go.Binding("text", "key").makeTwoWay())
                );

            myDiagram.model = new go.GraphLinksModel(
                [
                    { key: "Alpha" },
                    { key: "Beta" }
                ],
                [
                    { from: "Alpha", to: "Beta" }
                ]);
        }

        window.onload = init;
    </script>
</head>
<body>
    <div id="myDiagramDiv" style="width:100%; height:600px; border:1px solid black;"></div>
</body>
</html>

4. Flutter端代码

在Flutter的lib/main.dart文件中,添加一个按钮来触发加载GoJS页面的操作:

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

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

class MyApp extends StatelessWidget {
  static const platform = MethodChannel('com.example.gojs_flutter_example/channel');

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('GoJS Flutter Example'),
        ),
        body: Center(
          child: ElevatedButton(
            onPressed: () async {
              try {
                await platform.invokeMethod('loadGoJS');
              } on PlatformException catch (e) {
                print("Failed to invoke: '${e.message}'.");
              }
            },
            child: Text('Load GoJS'),
          ),
        ),
      ),
    );
  }
}

注意

  1. WebView管理:上述示例中,WebView并未实际添加到布局中。在实际应用中,你需要将WebView添加到Activity的布局中,并管理其生命周期(如暂停、恢复等)。

  2. 通信:如果需要从GoJS向Flutter发送消息,可以通过JavaScript与Native代码的通信机制(如Android的addJavascriptInterface或iOS的WKScriptMessageHandler)来实现。

  3. 跨平台:上述示例仅展示了Android平台的实现。对于iOS平台,你需要使用WKWebView和相应的Swift/Objective-C代码来实现类似的功能。

这个示例提供了一个基本的框架,展示了如何在Flutter中通过Platform Channel与原生代码交互,从而使用GoJS进行图形绘制。根据实际需求,你可能需要进一步完善和扩展这个框架。

回到顶部