Flutter节点视图插件vs_node_view的使用
Flutter节点视图插件vs_node_view的使用
简介
vs_node_view
是一个用于创建和使用的可视化脚本工具包。通过此插件,您可以定义自己的VS节点及其交互方式。这些节点可以被用户用来在您的应用程序中创建自定义行为。
示例演示:
如果您希望在Web上使用此插件,请确保在index.html
文件顶部添加以下代码:
<html oncontextmenu="event.preventDefault();"></html>
特性
使用此插件,您可以在Flutter应用中:
- 创建带有类型化输入输出的节点
- 在可缩放画布上与节点进行视觉交互
- 对节点进行评估(也可以不使用UI组件)
- 序列化和反序列化节点
使用方法
接口
接口由节点使用以创建连接。接口具有类型,并且只会与特定类型的接口交互。
接口分为两类:
- 输入
- 输出
有6种基本接口,每种接口都有输入和输出两种变体:
dynamic
(作为输入:接受任何输出)bool
(作为输入:接受bool
和dynamic
输出)int
(作为输入:接受int
、num
和dynamic
输出)double
(作为输入:接受double
、num
和dynamic
输出)num
(作为输入:接受int
、double
、num
和dynamic
输出)string
(作为输入:接受string
和dynamic
输出)
所有接口都有一个“类型”,类型将用于反序列化,如果它已经进入生产阶段,则不应更改,因为反序列化将会失败。 使用“标题”来添加与序列化无关的本地化。 使用“提示”来在鼠标悬停时添加提示。 使用“interfaceIconBuilder”来为每个接口实例自定义接口小部件,而不是为该类型的全部接口。
如果您想使用自己的类进行可视化脚本,可以这样定义接口:
/// 定义一个将在UI中用于其输入和输出的接口颜色
/// 您也可以在类中定义它们,如果您希望输入和输出有不同的颜色
const Color _interfaceColor = Colors.pink;
/// 这是您的输入接口
/// 它需要扩展VSInputData并为其超级提供类型和初始连接
class MyFirstInputData extends VSInputData {
MyFirstInputData({
required super.type,
super.title,
super.toolTip,
super.initialConnection,
super.interfaceIconBuilder,
});
/// 列表中定义此输入将接受的类型
/// 定义此输入将与哪些输出交互
/// 默认情况下,动态输出将被任何输入接受
[@override](/user/override)
List<Type> get acceptedTypes => [
MyFirstOutputData,
];
/// 定义界面在UI中图标和节点之间线条的颜色
[@override](/user/override)
Color get interfaceColor => _interfaceColor;
/// 可以通过覆盖此函数来控制特定接口类型的界面小部件
/// 这适用于所有此类类型的接口,使用interfaceIconBuilder可以为每个接口实例自定义小部件
[@override](/user/override)
Widget getInterfaceIcon({required BuildContext context, required GlobalKey anchor,}) {
return MyWidget();
}
}
/// 这是您的输出接口
/// 它需要扩展VSOutputData并带有一个类型
/// 类型定义附加的输出函数将返回什么
/// 您需要将类型和输出函数传递给超级
class MyFirstOutputData extends VSOutputData<MyCoolClass> {
MyFirstOutputData({
required super.type,
super.title,
super.toolTip,
super.outputFunction,
super.interfaceIconBuilder,
});
[@override](/user/override)
Color get interfaceColor => _interfaceColor;
/// 这可以为两种类型的接口执行
[@override](/user/override)
Widget getInterfaceIcon({required BuildContext context, required GlobalKey anchor,}) {
return MyWidget();
}
}
现在我们了解了接口,接下来让我们看看如何定义节点。
定义节点
所有节点都通过一个函数定义。 当创建新节点或反序列化它们时,该函数会被调用以确保所有类都是新实例。
所有节点都有一个“类型”,不能定义具有相同类型的多个节点。
“类型”将用于反序列化,并且如果它已经进入生产阶段,则不应更改,因为反序列化将会失败。
“类型”将作为回退显示给用户,如果未给出标题。
节点有输入,不能在同一节点中定义具有相同“名称”的多个输入。
节点有输出,不能在同一节点中定义具有相同“名称”的多个输出。
输入数据将通过Map<String, dynamic>
传递给所有输出。键是输入的名称,值是输入接收到的内容。
正常节点
正常节点是使用VSNodeData的所有节点。这些节点期望输入并返回输出。
定义如下:
VSNodeData parseIntNode(Offset offset, VSOutputData? ref) {
return VSNodeData(
type: "Parse int",
widgetOffset: offset,
inputData: [
VSStringInputData(
name: "Input",
initialConnection: ref,
)
],
outputData: [
VSIntOutputData(
name: "Output",
outputFunction: (data) {
return int.parse(data["Input"]);
},
),
],
);
}
小部件节点
小部件节点是使用VSWidgetNode的所有节点。它们需要一个子小部件以及用于序列化/反序列化的setValue/getValue函数。
小部件节点允许您使用任何小部件创建用户输入。它们不能有任何输入,并返回一个输出。
定义如下:
VSWidgetNode textInputNode(
Offset offset,
VSOutputData? ref,
) {
final controller = TextEditingController();
final inputWidget = TextField(
controller: controller,
decoration: const InputDecoration(
isDense: true,
contentPadding: EdgeInsets.symmetric(horizontal: 0, vertical: 10),
),
);
return VSWidgetNode(
type: "Input",
widgetOffset: offset,
outputData: VSStringOutputData(
type: "Output",
outputFunction: (data) => controller.text,
),
child: Expanded(child: inputWidget),
setValue: (value) => controller.text = value,
getValue: () => controller.text,
);
}
列表节点
列表节点是使用VSListNode的所有节点。它们需要一个在运行时创建新接口的inputBuilder。生成器会获取新构建接口的索引以及该接口将拥有的连接。务必在生成器中将连接传递给接口的initialConnection,否则它将无法正常工作。
定义如下:
VSListNode sumNode(
Offset offset,
VSOutputData? ref,
) {
return VSListNode(
type: "Sum",
toolTip: "Adds all supplied Numbers together",
widgetOffset: offset,
inputBuilder: (index, connection) => VSNumInputData(
type: "Input $index",
initialConnection: connection,
),
outputData: [
VSNumOutputData(
type: "output",
toolTip: "The sum of all supplied values",
outputFunction: (data) {
return data.values.reduce((value, element) => value + element);
},
)
],
);
}
输出节点
输出节点是使用VSOutputNode的所有节点。它们只接受一个输入(动态)并且可用于评估节点树。
要访问它们,您可以使用VSNodeManager的getOutputNodes函数。这将返回当前节点树中的所有输出。 VSOutputNode有一个evaluate函数,它将返回包含节点名称和结果的MapEntry<String,dynamic>。
定义如下:
VSOutputNode outputNode(Offset offset, VSOutputData? ref) {
return VSOutputNode(
type: "Output",
widgetOffset: offset,
ref: ref,
);
}
使用VSNodeDataProvider
准备生成器
现在我们已经定义了我们的节点,我们需要将它们移动到一个集合中并传递给VSNodeDataProvider。该集合将传递给VSNodeSerializationManager,以确保所有规则(在此处定义)都被遵守,并将创建两个映射:
- Map<String, VSNodeDataBuilder> _nodeBuilders (用于反序列化)
- Map<String, dynamic> contextNodeBuilders
contextNodeBuilders被传递给上下文菜单并定义UI。
我们的节点生成器集合可能只是一个函数列表,如下所示:
final List<dynamic> nodeBuilders = [
textInputNode,
parseIntNode,
outputNode,
];
但是由于UI将基于集合创建,我们还可以使用VSSubgroups对节点进行分组。VSSubgroups定义了一个名称和一个新的节点集合。
final List<dynamic> nodeBuilders = [
VSSubgroup(
name: "Number",
subgroup: [
parseIntNode,
parseDoubleNode,
sumNode,
],
),
VSSubgroup(
name: "Logik",
subgroup: [
biggerNode,
ifNode,
],
),
textInputNode,
outputNode,
];
这样,您的UI可能会看起来像这样:
直接使用VSNodeManager
VSNodeManager是所有节点操作的核心。它跟踪数据并具有一些API,可以在低级别调用这些API来修改所述数据。
节点管理器使用您想要使用的节点生成器初始化,并且可选地可以使用任何序列化的节点。
如果传入了serializedNodes参数,它将尝试反序列化字符串并重新创建所有节点。 重要的是要注意,反序列化的节点将使用提供的节点生成器来重新创建节点。 这意味着如果一个节点不在节点生成器中,它将无法反序列化并且会丢失。
final manager = VSNodeManager(
nodeBuilders: nodeBuilders,
serializedNodes: serializedNodes,
);
serializedNodes只是一个字符串,可以通过调用以下方法获得:
VSNodeManager.serializeNodes()
VSNodeManager还公开了getOutputNodes,这将给您当前节点树中的所有输出。 您可以评估它们以获取指定节点的最终结果。
VSNodeManager.getOutputNodes.map((e) => e.evaluate());
创建VSNodeDataProvider
VSNodeDataProvider接受VSNodeManager的一个实例作为参数。 您还可以选择传递VSHistoryManager的实例以跟踪节点历史。 然后您可以调用以下方法来遍历历史:
VSNodeDataProvider.historyManager!.undo()
VSNodeDataProvider.historyManager!.redo()
一个VSNodeDataProvider可以这样初始化:
final provider = VSNodeDataProvider(
nodeManager: VSNodeManager(nodeBuilders: nodeBuilders),
historyManager: VSHistoryManger(),
);
VSNodeDataProvider通过VSNodeView小部件使用InheritedNodeDataProvider InheritedWidget注入到小部件树中。 您可以在小部件树下的任何地方使用VSNodeDataProvider.of(context)来访问它。 如果您需要监听更改,VSNodeDataProvider扩展了ChangeNotifier,因此您可以直接使用ListenableBuilder。
这是一个完整的示例:
final List<dynamic> nodeBuilders = [
textInputNode,
VSSubgroup(
name: "Number",
subgroup: [
parseIntNode,
parseDoubleNode,
sumNode,
],
),
outputNode,
];
VSNodeDataProvider(
nodeManager: VSNodeManager(nodeBuilders: nodeBuilders),
);
使用VSNodeView
VSNodeView是主要的UI小部件。 它可以覆盖以下内容:
- 节点UI
- 节点标题UI
- 上下文菜单UI
- 选择区域UI 如果您想以不同的样式来设置它们。
它期望一个VSNodeDataProvider并将其注入到小部件树中。
VSNodeView(
nodeDataProvider: nodeDataProvider,
),
使用InteractiveVSNodeView
InteractiveVSNodeView将VSNodeView包装在一个InteractiveViewer中。 这允许用户在画布上缩放和平移。
您可以传递宽度和高度参数来定义画布大小。 如果没有给出宽度或高度,将使用屏幕宽度或高度。
VSNodeDataProvider有一个名为applyViewPortTransfrom的函数,它将应用所有的视口变换(缩放、平移)到给定的Offset。 这是必要的,因为偏移通常以屏幕坐标给出,因此一旦视口被改变,它们就不再起作用。
您可以传递自己的VSNodeView小部件来更改其特定设置。
InteractiveVSNodeView(
width: 5000,
height: 5000,
nodeDataProvider: nodeDataProvider,
baseNodeView: VSNodeView(
nodeDataProvider: nodeDataProvider,
),
),
示例代码
以下是一个完整的示例代码,展示了如何使用vs_node_view
插件:
import 'package:flutter/material.dart';
import 'package:vs_node_view/vs_node_view.dart';
void main() async {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
[@override](/user/override)
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
scaffoldBackgroundColor: const Color.fromARGB(255, 46, 46, 46),
),
home: const Scaffold(body: VSNodeExample()),
);
}
}
class VSNodeExample extends StatefulWidget {
const VSNodeExample({super.key});
[@override](/user/override)
State<VSNodeExample> createState() => _VSNodeExampleState();
}
class _VSNodeExampleState extends State<VSNodeExample> {
Iterable<String>? results;
late VSNodeDataProvider nodeDataProvider;
[@override](/user/override)
void initState() {
super.initState();
final nodeBuilders = [
VSSubgroup(
name: "Numbers",
subgroup: [
(Offset offset, VSOutputData? ref) => VSNodeData(
type: "Parse int",
widgetOffset: offset,
inputData: [
VSStringInputData(
type: "Input",
initialConnection: ref,
)
],
outputData: [
VSIntOutputData(
type: "Output",
outputFunction: (data) => int.parse(data["Input"]),
),
],
),
(Offset offset, VSOutputData? ref) => VSNodeData(
type: "Sum",
widgetOffset: offset,
inputData: [
VSNumInputData(
type: "Input 1",
initialConnection: ref,
),
VSNumInputData(
type: "Input 2",
initialConnection: ref,
)
],
outputData: [
VSNumOutputData(
type: "output",
outputFunction: (data) {
return (data["Input 1"] ?? 0) + (data["Input 2"] ?? 0);
},
),
],
),
],
),
VSSubgroup(
name: "Logik",
subgroup: [
(Offset offset, VSOutputData? ref) => VSNodeData(
type: "Bigger then",
widgetOffset: offset,
inputData: [
VSNumInputData(
type: "First",
initialConnection: ref,
),
VSNumInputData(
type: "Second",
initialConnection: ref,
),
],
outputData: [
VSBoolOutputData(
type: "Output",
outputFunction: (data) => data["First"] > data["Second"],
),
],
),
(Offset offset, VSOutputData? ref) => VSNodeData(
type: "If",
widgetOffset: offset,
inputData: [
VSBoolInputData(
type: "Input",
initialConnection: ref,
),
VSDynamicInputData(
type: "True",
initialConnection: ref,
),
VSDynamicInputData(
type: "False",
initialConnection: ref,
),
],
outputData: [
VSDynamicOutputData(
type: "Output",
outputFunction: (data) =>
data["Input"] ? data["True"] : data["False"],
),
],
),
],
),
(Offset offset, VSOutputData? ref) {
final controller = TextEditingController();
final input = TextField(
controller: controller,
decoration: const InputDecoration(
isDense: true,
contentPadding: EdgeInsets.symmetric(horizontal: 0, vertical: 10),
),
);
return VSWidgetNode(
type: "Input",
widgetOffset: offset,
outputData: VSStringOutputData(
type: "Output",
outputFunction: (data) => controller.text,
),
child: Expanded(child: input),
setValue: (value) => controller.text = value,
getValue: () => controller.text,
);
},
(Offset offset, VSOutputData? ref) => VSOutputNode(
type: "Output",
widgetOffset: offset,
ref: ref,
),
];
nodeDataProvider = VSNodeDataProvider(
nodeManager: VSNodeManager(nodeBuilders: nodeBuilders),
);
}
[@override](/user/override)
Widget build(BuildContext context) {
return Stack(
children: [
InteractiveVSNodeView(
width: 5000,
height: 5000,
nodeDataProvider: nodeDataProvider,
),
const Positioned(
bottom: 0,
right: 0,
child: Legend(),
),
Positioned(
top: 0,
right: 0,
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
ElevatedButton(
onPressed: () => setState(() {
final entries =
nodeDataProvider.nodeManager.getOutputNodes.map(
(e) => e.evaluate(
onError: (_, __) => Future.delayed(Duration.zero, () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
backgroundColor: Colors.deepOrange,
content: Text('An error occurred'),
),
);
}),
),
);
results = entries.map((e) => "${e.key}: ${e.value}");
}),
child: const Text("Evaluate"),
),
if (results != null)
...results!.map(
(e) => Card(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(e),
),
),
),
],
),
),
],
);
}
}
Map<String, Color> inputTypes = {
"String": VSStringInputData(type: "legend").interfaceColor,
"Int": VSIntInputData(type: "legend").interfaceColor,
"Double": VSDoubleInputData(type: "legend").interfaceColor,
"Num": VSNumInputData(type: "legend").interfaceColor,
"Bool": VSBoolInputData(type: "legend").interfaceColor,
"Dynamic": VSDynamicInputData(type: "legend").interfaceColor,
};
class Legend extends StatelessWidget {
const Legend({super.key});
[@override](/user/override)
Widget build(BuildContext context) {
final List<Widget> widgets = [];
final entries = inputTypes.entries;
for (final entry in entries) {
widgets.add(
Row(
children: [
Text(entry.key),
Icon(
Icons.circle,
color: entry.value,
),
if (entry != entries.last) const Divider(),
],
),
);
}
return Card(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: widgets,
),
));
}
}
更多关于Flutter节点视图插件vs_node_view的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html
更多关于Flutter节点视图插件vs_node_view的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html
当然,以下是如何在Flutter项目中集成和使用vs_node_view
插件的一个基本示例。vs_node_view
插件通常用于在Flutter应用中显示和操作节点视图,尽管具体的插件功能和API可能会随着版本的更新而变化,以下代码提供了一个基本的框架来演示如何集成和使用该插件。
首先,确保你已经在pubspec.yaml
文件中添加了vs_node_view
依赖项:
dependencies:
flutter:
sdk: flutter
vs_node_view: ^最新版本号 # 替换为实际最新版本号
然后,运行flutter pub get
来安装依赖项。
接下来,在Flutter应用中创建一个简单的节点视图。以下是一个示例代码,展示了如何初始化和使用vs_node_view
插件:
import 'package:flutter/material.dart';
import 'package:vs_node_view/vs_node_view.dart'; // 导入插件
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('VS Node View Example'),
),
body: Center(
child: NodeViewExample(),
),
),
);
}
}
class NodeViewExample extends StatefulWidget {
@override
_NodeViewExampleState createState() => _NodeViewExampleState();
}
class _NodeViewExampleState extends State<NodeViewExample> {
// 假设我们有一些节点数据
final List<Node> nodes = [
Node(id: '1', label: 'Node 1', position: Offset(100, 100)),
Node(id: '2', label: 'Node 2', position: Offset(300, 100)),
Node(id: '3', label: 'Node 3', position: Offset(200, 300)),
];
// 连接节点的边
final List<Edge> edges = [
Edge(from: '1', to: '2'),
Edge(from: '1', to: '3'),
];
@override
Widget build(BuildContext context) {
return VSNodeView(
nodes: nodes,
edges: edges,
nodeBuilder: (context, node) {
return Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.black),
borderRadius: BorderRadius.circular(5),
),
padding: EdgeInsets.all(8.0),
child: Text(node.label),
);
},
edgeBuilder: (context, edge) {
return CustomPaint(
painter: LinePainter(edge.fromNodePosition, edge.toNodePosition),
size: Size.zero, // 因为CustomPaint本身不定义大小
);
},
onNodeTap: (node) {
// 当节点被点击时执行的回调
print('Node ${node.label} tapped!');
},
);
}
}
// 节点数据类
class Node {
final String id;
final String label;
final Offset position;
Node({required this.id, required this.label, required this.position});
Offset get nodePosition => position;
}
// 边数据类
class Edge {
final String from;
final String to;
Edge({required this.from, required this.to});
Node get fromNode {
// 在实际应用中,这里应该通过ID查找对应的Node对象
// 这里为了简化,我们假设nodes列表已经按照ID顺序排列并可直接访问
// 注意:这只是一个示例,实际中应该实现更复杂的查找逻辑
return nodes.firstWhere((node) => node.id == from);
}
Node get toNode {
// 同上
return nodes.firstWhere((node) => node.id == to);
}
Offset get fromNodePosition => fromNode.nodePosition;
Offset get toNodePosition => toNode.nodePosition;
}
// 自定义画笔类,用于绘制边
class LinePainter extends CustomPainter {
final Offset from;
final Offset to;
final Paint paint = Paint()
..color = Colors.black
..strokeWidth = 2.0;
LinePainter(this.from, this.to);
@override
void paint(Canvas canvas, Size size) {
canvas.drawLine(from, to, paint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return false; // 如果不需要重绘,返回false
}
}
注意:
- 这里的
Node
和Edge
类是根据假设创建的,你可能需要根据vs_node_view
插件的实际API进行调整。 LinePainter
是一个简单的自定义画笔类,用于绘制从一个节点到另一个节点的线。- 示例中的
nodes
和edges
列表是硬编码的,你可能需要根据你的应用逻辑动态生成它们。 vs_node_view
插件的具体API和用法可能会随着版本的更新而变化,请参考插件的官方文档和示例代码进行进一步的开发和调整。
这个示例提供了一个基本的框架,展示了如何在Flutter应用中使用vs_node_view
插件来显示和操作节点视图。根据你的具体需求,你可能需要添加更多的功能和样式。