Drawing arrows in Flutter, or anything else, based on widget positions
Tags: flutter, dart
Published: 2020-08-07 07:19:32

I recently made a Flutter package called `widget_arrows` which caught some attention on the [FlutterDev subreddit](https://www.reddit.com/r/FlutterDev/comments/i2dv6o/i_made_a_package_for_drawing_arrows_between_any/).
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](https://www.flutterclutter.dev/flutter/tutorials/how-to-cut-a-hole-in-an-overlay/2020/510/).
{% github Schwusch/widget_arrows %}

Using it is fairly straight forward, wrap the Widgets you want to draw arrows between with an [`ArrowElement`](https://pub.dev/documentation/widget_arrows/latest/widget_arrows/ArrowElement-class.html) and wrap both with an [`ArrowContainer`](https://pub.dev/documentation/widget_arrows/latest/widget_arrows/ArrowContainer-class.html) 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:

{% codepen https://codepen.io/schwusch/pen/yLemBZQ %}

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: 

```dart
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 `ArrowContainer`s child, which in this case is a `Column`.
![](https://dev-to-uploads.s3.amazonaws.com/i/kakpxhetb19tfjxqm1we.png)

How does the `_ArrowElementState` find the `_ArrowContainerState`? By calling this method on the `BuildContext`:
```dart
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 `ArrowElement`s currently mounted in its child `Widget` subtree.

The `_ArrowContainerState` build method look like this:
```dart
@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 `_ArrowElementState`s 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 `_ArrowElementState`s, and calls this method:
```dart
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:
```dart
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`](https://api.flutter.dev/flutter/painting/AlignmentGeometry-class.html). 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`](https://api.flutter.dev/flutter/painting/AlignmentDirectional-class.html) 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:
```dart
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!