nabil6391 / graphview

Flutter GraphView is used to display data in graph structures. It can display Tree layout, Directed and Layered graph. Useful for Family Tree, Hierarchy View.
MIT License
420 stars 114 forks source link

Add option to center `InteractiveViewer` at a node #47

Open JulianBissekkou opened 3 years ago

JulianBissekkou commented 3 years ago

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 commented 2 years ago

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

nabil6391 commented 2 years ago

Apologies for not getting the time yet

JulianBissekkou commented 2 years ago

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?

nabil6391 commented 2 years ago

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

JulianBissekkou commented 2 years ago

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.

nabil6391 commented 2 years ago

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

JulianBissekkou commented 2 years ago

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 :)

rfgmendoza commented 2 years ago

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

JulianBissekkou commented 2 years ago

@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),
        ),
      ],
    );
  }
}
cokuscz commented 2 years ago

@rfgmendoza how about the issues bro?

shanaka-sync commented 10 months ago

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