graphview
graphview copied to clipboard
Add option to center `InteractiveViewer` at a node
We are really enjoying your package and it works great!
It would be nice to have to option to center the InteractiveViewer
to a specific node.
TransformationController
already provides the option to move the viewer around but we still need to know the position of a node to be able to generate the correct Matrix4
.
Do you have an idea how this can be done?
@nabil6391 Do you think you have time to take a look? :)
Apologies for not getting the time yet
This might be related to #33. You mentioned that you already started with an implementation of focsuedNode
. Can you share some more details about that?
Yes I did actually, but as I mentioned there, I didnt finish it properly. But I have some ideas to do it.
I implemented it in my app using the InteractiveView
and the InteractiveViewController
which worked well. I am not sure how you want to center the node in the algorithm if the InteractiveView
needs to adjust the position 🤔
That's why I was asking for more details. I might be able to implement your solution if it works better than mine.
In my case I wasnt thinking of using InteractiveView, rather I was thinking of use a focusedNode inside the graphview that I can change.
I think implementing InteractiveViewController might be a good idea, would love if you can share your implementation
I took the size and position of the Node
and used that to adjust the Matrix of the controller. In my case, this only happens initially so there was no animation involved. This can easily be done using a Tween
.
I will share the most important parts of my implementation:
- Get notified when the algorithm has run and all nodes have size. For this I extended the Algorithm.
class _CallbackSugiyamaAlgorithm extends SugiyamaAlgorithm {
/*constructor and member vars*/
@override
Size run(Graph? graph, double shiftX, double shiftY) {
final size = super.run(graph, shiftX, shiftY);
if (!_wasCalculated) {
onFirstCalculated();
_wasCalculated = true;
}
return size;
}
}
- Get the focused
Node
and move the matrix.
// created in initState for example
_algorithm = _CallbackSugiyamaAlgorithm(
configuration: _configuration,
onFirstCalculated: () => _jumpToNode(nodeId), // nodeId was my focused node
);
Future<void> _jumpToNode(String nodeId) async {
final startNode = _graph.nodes.firstWhere(
(node) => node.key!.value == nodeId,
);
// Positions are custom for our page. You might need something different.
final position = Offset(
-(startNode.x - startNode.size.width / 2),
-(startNode.y - startNode.size.height - kToolbarHeight),
);
_controller.value = _controller.value.clone()
..translate(position.dx, position.dy);
}
Let me know what you think :)
I took the size and position of the
Node
and used that to adjust the Matrix of the controller. In my case, this only happens initially so there was no animation involved. This can easily be done using aTween
.I will share the most important parts of my implementation:
1. Get notified when the algorithm has run and all nodes have size. For this I extended the Algorithm.
class _CallbackSugiyamaAlgorithm extends SugiyamaAlgorithm { /*constructor and member vars*/ @override Size run(Graph? graph, double shiftX, double shiftY) { final size = super.run(graph, shiftX, shiftY); if (!_wasCalculated) { onFirstCalculated(); _wasCalculated = true; } return size; } }
2. Get the focused `Node` and move the matrix.
// created in initState for example _algorithm = _CallbackSugiyamaAlgorithm( configuration: _configuration, onFirstCalculated: () => _jumpToNode(nodeId), // nodeId was my focused node ); Future<void> _jumpToNode(String nodeId) async { final startNode = _graph.nodes.firstWhere( (node) => node.key!.value == nodeId, ); // Positions are custom for our page. You might need something different. final position = Offset( -(startNode.x - startNode.size.width / 2), -(startNode.y - startNode.size.height - kToolbarHeight), ); _controller.value = _controller.value.clone() ..translate(position.dx, position.dy); }
Let me know what you think :)
can you create a snippet or example of a working solution with tween? i'm trying to create a "center on node" button that happens after render
@rfgmendoza
You can find an example in
examples/api/lib/widgets/interactive_viewer/interactive_viewer.transformation_controller.0.dart
in the flutter sdk.
Here is the source:
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
static const String _title = 'Flutter Code Sample';
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: _title,
home: MyStatefulWidget(),
);
}
}
class MyStatefulWidget extends StatefulWidget {
const MyStatefulWidget({Key? key}) : super(key: key);
@override
State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}
/// AnimationControllers can be created with `vsync: this` because of TickerProviderStateMixin.
class _MyStatefulWidgetState extends State<MyStatefulWidget>
with TickerProviderStateMixin {
final TransformationController _transformationController =
TransformationController();
Animation<Matrix4>? _animationReset;
late final AnimationController _controllerReset;
void _onAnimateReset() {
_transformationController.value = _animationReset!.value;
if (!_controllerReset.isAnimating) {
_animationReset!.removeListener(_onAnimateReset);
_animationReset = null;
_controllerReset.reset();
}
}
void _animateResetInitialize() {
_controllerReset.reset();
_animationReset = Matrix4Tween(
begin: _transformationController.value,
end: Matrix4.identity(),
).animate(_controllerReset);
_animationReset!.addListener(_onAnimateReset);
_controllerReset.forward();
}
// Stop a running reset to home transform animation.
void _animateResetStop() {
_controllerReset.stop();
_animationReset?.removeListener(_onAnimateReset);
_animationReset = null;
_controllerReset.reset();
}
void _onInteractionStart(ScaleStartDetails details) {
// If the user tries to cause a transformation while the reset animation is
// running, cancel the reset animation.
if (_controllerReset.status == AnimationStatus.forward) {
_animateResetStop();
}
}
@override
void initState() {
super.initState();
_controllerReset = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 400),
);
}
@override
void dispose() {
_controllerReset.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Theme.of(context).colorScheme.primary,
appBar: AppBar(
automaticallyImplyLeading: false,
title: const Text('Controller demo'),
),
body: Center(
child: InteractiveViewer(
boundaryMargin: const EdgeInsets.all(double.infinity),
transformationController: _transformationController,
minScale: 0.1,
maxScale: 1.0,
onInteractionStart: _onInteractionStart,
child: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: <Color>[Colors.orange, Colors.red],
stops: <double>[0.0, 1.0],
),
),
),
),
),
persistentFooterButtons: <Widget>[
IconButton(
onPressed: _animateResetInitialize,
tooltip: 'Reset',
color: Theme.of(context).colorScheme.surface,
icon: const Icon(Icons.replay),
),
],
);
}
}
@rfgmendoza how about the issues bro?
How about the status of the issue ? Did someone able to fix it ?