graphview icon indicating copy to clipboard operation
graphview copied to clipboard

Add option to center `InteractiveViewer` at a node

Open JulianBissekkou opened this issue 3 years ago • 11 comments

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?

JulianBissekkou avatar Aug 02 '21 04:08 JulianBissekkou

@nabil6391 Do you think you have time to take a look? :)

JulianBissekkou avatar Sep 22 '21 04:09 JulianBissekkou

Apologies for not getting the time yet

nabil6391 avatar Sep 22 '21 10:09 nabil6391

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?

JulianBissekkou avatar Sep 28 '21 05:09 JulianBissekkou

Yes I did actually, but as I mentioned there, I didnt finish it properly. But I have some ideas to do it.

nabil6391 avatar Sep 29 '21 04:09 nabil6391

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.

JulianBissekkou avatar Sep 29 '21 06:09 JulianBissekkou

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

nabil6391 avatar Sep 29 '21 06:09 nabil6391

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:

  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;
  }
}
  1. 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 :)

JulianBissekkou avatar Sep 29 '21 10:09 JulianBissekkou

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:

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 avatar Dec 31 '21 21:12 rfgmendoza

@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),
        ),
      ],
    );
  }
}

JulianBissekkou avatar Jan 03 '22 10:01 JulianBissekkou

@rfgmendoza how about the issues bro?

cokuscz avatar Apr 25 '22 10:04 cokuscz

How about the status of the issue ? Did someone able to fix it ?

shanaka-sync avatar Oct 04 '23 04:10 shanaka-sync