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
更多关于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'),
),
),
),
);
}
}
注意
-
WebView管理:上述示例中,WebView并未实际添加到布局中。在实际应用中,你需要将WebView添加到Activity的布局中,并管理其生命周期(如暂停、恢复等)。
-
通信:如果需要从GoJS向Flutter发送消息,可以通过JavaScript与Native代码的通信机制(如Android的
addJavascriptInterface
或iOS的WKScriptMessageHandler
)来实现。 -
跨平台:上述示例仅展示了Android平台的实现。对于iOS平台,你需要使用
WKWebView
和相应的Swift/Objective-C代码来实现类似的功能。
这个示例提供了一个基本的框架,展示了如何在Flutter中通过Platform Channel与原生代码交互,从而使用GoJS进行图形绘制。根据实际需求,你可能需要进一步完善和扩展这个框架。