Home / Blog / long-text-post-about-flutter-next-gen-tool-ojde2o

Long text post about flutter next gen tool

Uncategorized
Posted By Fadlullahi Last modified: 30 Sep, 2020 ・ 3.2 min read

I recently made a Flutter package called widget_arrows which caught some attention on the FlutterDev subreddit.
I thought I'd take a minute to explain how it works, since the technique can be applied to other things, such as a tutorial hole overlay.

Schwusch / widget_arrows

Draw arrows between widgets in Flutter

widget_arrows

Draw arrows between widgets

 

 

View on GitHub

 

Using it is fairly straight forward, wrap the Widgets you want to draw arrows between with an ArrowElement and wrap both with an ArrowContainer higher up in the Widget hierarchy .

In this case I wrap the whole MaterialApp in an ArrowContainer in order to display the arrows while dragging an element:

 

The core Widget is the ArrowContainer, which is the one drawing the arrows on a canvas layered on top of the child, using a Stack.
Here's an example:

ArrowContainer(
  child: Column(
    children: [
      ArrowElement(
        id: 'top',
        targetId: 'bottom',
        child: Text('Text1'),
      ),
      ArrowElement(
        show: showArrows,
        id: 'bottom',
        child: Text('Text2'),
      ),
    ],
  ),
)

During initState(), the _ArrowElementState looks up the widget hierarchy and finds the closest _ArrowContainerState and registers itself by calling addArrow(this) on the container. The _ArrowContainerState will then trigger a repaint on the CustomPainter that draws on top of ArrowContainers child, which in this case is a Column.

How does the _ArrowElementState find the _ArrowContainerState? By calling this method on the BuildContext:

context.findAncestorStateOfType<_ArrowContainerState>();

Since ArrowContainer is a StatefulWidget, it has its associated _ArrowContainerState. It's a relatively expensive lookup, but the good news is that it is only needed once per ArrowElement.

The _ArrowContainerState has ChangeNotifier mixed in, in its class declaration. That means that it can be passed in as an argument to the CustomPainter constructor, and it will repaint every time _ArrowContainerState calls notifyListeners().

_ArrowContainerState.notifyListeners() is called every time an arrow element is added or removed, which means it is always kept in sync with all ArrowElements currently mounted in its child Widget subtree.

The _ArrowContainerState build method look like this:

@override
Widget build(BuildContext context) => Stack(
      children: [
        widget.child,
        IgnorePointer(
          child: CustomPaint(
            foregroundPainter: _ArrowPainter(
              _elements,
              Directionality.of(context),
              this,
            ),
            child: Container(),
          ),
        ),
      ],
    );

The CustomPaint is wrapped with an IgnorePointer, to let through touch events to widget.child, which is the users real UI. The _ArrowPainter is a custom class extending CustomPainter to draw the Arrows. It takes the _ArrowElementStates as a first argument, the TextDirection as the second argument (we will cover why later), and a Listenable as the third argument. The _ArrowContainerState is a Listenable, due to having ChangeNotifier mixed in.

What happens in _ArrowPainter.paint() is the real magic. It loops through all _ArrowElementStates, and calls this method:

final renderBox = arrowElementState
                    .context
                    .findRenderObject() as RenderBox;

The RenderBox is the key to finding the position of the start and end of the arrows we're about to draw.
By calling this method:

final Offset upperLeftCorner = renderBox.localToGlobal(Offset.zero);

We get an Offset of where it is relative to the top-left corner of the screen. The RenderBox also has RenderBox.size which gives us its dimensions, which is used to describe the rectangle in which the child is drawn.

This is where TextDirection comes in. The ArrowElement has a sourceAnchor and a targetAnchor argument which takes an AlignmentGeometry. This allows us to specify where in the child Widget we like the arrow to start or end from, and is Alignment.centerLeft by default. You can pass an AlignmentDirectional to make it adapt to whether the text is right-to-left or left-to-right. To adapt to the text direction, this method is called on the AlignmentGeometry object:

final Offset startPosition = sourceAnchor
  .resolve(textDirection)
  .withinRect(/*child widget rectangle*/);

From there, all that's left is to draw a Path or whatever you like on the canvas at that position.

Happy Fluttering!


AfricatedPreneurs


As a community of Entrepreneurs who acknowledge "Problem Solving" as the cell of Entrepreneurship - a cornerstone & driving force for development in Africa; It is our responsibility to identify, empathize, define and analyse problems in local communities and establish startups to effectively implement the best of proferred solutions with dual pursuits of profit and social good. WE also organize Masterclasses, Seminars and Conferences to Inspire and encourage Creativity, Innovation and Provide Opportunities for young and budding entrepreneurs to illuminate their journey.


Copyright © 2020 - 2021 AfricatedPreneurs. All rights reserved