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.
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 ArrowContainer
s 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 ArrowElement
s 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 _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:
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!
Top comments (1)
This is freaking awesome for visual learners