Drawing a Hexagon Grid in Flutter

This post aims to show you how to extend the CustomPaint to draw your own figures. We will also draw multiple CustomPaint Widgets on the same canvas so that we can reuse our figure and draw it multiple times. This post uses a hexagon as an example, but it can be done with any figure.

Drawing a hexagon

The goal is to draw a hexagon. We can extend the CustomPaint to draw a hexagon. The CustomPaint widget expects a painter. For the painter, we have to supply a class that extends the CustomPainter. To extend the CustomPainter, we need to implement two methods. The paint method and the shouldRepaint method. For now, the shouldRepaint can return false since our Hexagon will not change.

So we focus on the paint method. The paint method provides a Canvas and a Size. The Size contains the height and the width of the container in which we can paint. The Canvas is the element on which we will paint our hexagon.

<span class="token keyword">class</span> <span class="token class-name">HexagonPainter</span> <span class="token keyword">extends</span> <span class="token class-name">CustomPainter</span> <span class="token punctuation">{</span>

  <span class="token metadata symbol">@override</span>
  <span class="token keyword">void</span> <span class="token function">paint</span><span class="token punctuation">(</span>Canvas canvas<span class="token punctuation">,</span> Size size<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token comment">//    implement method to draw something on canvas</span>
  <span class="token punctuation">}</span>

  <span class="token metadata symbol">@override</span>
  bool <span class="token function">shouldRepaint</span><span class="token punctuation">(</span>CustomPainter oldDelegate<span class="token punctuation">)</span> <span class="token operator">=</span><span class="token operator">></span> <span class="token boolean">false</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>

The goal is to draw a path on the canvas by calling the drawPath method. This method expects two parameters, the path and the paint. The path contains the points of the figure we want to draw. The paint contains graphical information. For example, whether we want to fill the space within the path, the border size, color of the fill, and border color.

canvas<span class="token punctuation">.</span><span class="token function">drawPath</span><span class="token punctuation">(</span>path<span class="token punctuation">,</span> paint<span class="token punctuation">)</span><span class="token punctuation">;</span>

To compute the hexagon points for the path, we will get six points on the circle, as displayed in the following image.

We can compute the six points and add them to the path.
We start with 0 degrees and then compute the next point that is rotated 60 degrees further. Note that after the computation of X and Y of the point, we move the path to that point, and the next point is again computed from the center with a different rotation. Thus, if we wanted to rotate the whole hexagon a bit, we could start at a different degree.

<span class="token keyword">class</span> <span class="token class-name">HexagonPainter</span> <span class="token keyword">extends</span> <span class="token class-name">CustomPainter</span> <span class="token punctuation">{</span>
  <span class="token keyword">static</span> <span class="token keyword">const</span> int SIDES_OF_HEXAGON <span class="token operator">=</span> <span class="token number">6</span><span class="token punctuation">;</span>
  <span class="token keyword">final</span> double radius<span class="token punctuation">;</span>
  <span class="token keyword">final</span> Offset center<span class="token punctuation">;</span>
  <span class="token function">HexagonPainter</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span>center<span class="token punctuation">,</span> <span class="token keyword">this</span><span class="token punctuation">.</span>radius<span class="token punctuation">)</span><span class="token punctuation">;</span>

  <span class="token metadata symbol">@override</span>
  <span class="token keyword">void</span> <span class="token function">paint</span><span class="token punctuation">(</span>Canvas canvas<span class="token punctuation">,</span> Size size<span class="token punctuation">)</span> <span class="token punctuation">{</span>
    Paint paint <span class="token operator">=</span> <span class="token function">Paint</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token punctuation">.</span>color <span class="token operator">=</span> Colors<span class="token punctuation">.</span>blue<span class="token punctuation">;</span>
    Path path <span class="token operator">=</span> <span class="token function">createHexagonPath</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    canvas<span class="token punctuation">.</span><span class="token function">drawPath</span><span class="token punctuation">(</span>path<span class="token punctuation">,</span> paint<span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>

  Path <span class="token function">createHexagonPath</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token keyword">final</span> path <span class="token operator">=</span> <span class="token function">Path</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">var</span> angle <span class="token operator">=</span> <span class="token punctuation">(</span>math<span class="token punctuation">.</span>pi <span class="token operator">*</span> <span class="token number">2</span><span class="token punctuation">)</span> <span class="token operator">/</span> SIDES_OF_HEXAGON<span class="token punctuation">;</span>
    Offset firstPoint <span class="token operator">=</span> <span class="token function">Offset</span><span class="token punctuation">(</span>radius <span class="token operator">*</span> math<span class="token punctuation">.</span><span class="token function">cos</span><span class="token punctuation">(</span><span class="token number">0.0</span><span class="token punctuation">)</span><span class="token punctuation">,</span> radius <span class="token operator">*</span> math<span class="token punctuation">.</span><span class="token function">sin</span><span class="token punctuation">(</span><span class="token number">0.0</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    path<span class="token punctuation">.</span><span class="token function">moveTo</span><span class="token punctuation">(</span>firstPoint<span class="token punctuation">.</span>dx <span class="token operator">+</span> center<span class="token punctuation">.</span>dx<span class="token punctuation">,</span> firstPoint<span class="token punctuation">.</span>dy <span class="token operator">+</span> center<span class="token punctuation">.</span>dy<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">for</span> <span class="token punctuation">(</span>int i <span class="token operator">=</span> <span class="token number">1</span><span class="token punctuation">;</span> i <span class="token operator"><=</span> SIDES_OF_HEXAGON<span class="token punctuation">;</span> i<span class="token operator">++</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
      double x <span class="token operator">=</span> radius <span class="token operator">*</span> math<span class="token punctuation">.</span><span class="token function">cos</span><span class="token punctuation">(</span>angle <span class="token operator">*</span> i<span class="token punctuation">)</span> <span class="token operator">+</span> center<span class="token punctuation">.</span>dx<span class="token punctuation">;</span>
      double y <span class="token operator">=</span> radius <span class="token operator">*</span> math<span class="token punctuation">.</span><span class="token function">sin</span><span class="token punctuation">(</span>angle <span class="token operator">*</span> i<span class="token punctuation">)</span> <span class="token operator">+</span> center<span class="token punctuation">.</span>dy<span class="token punctuation">;</span>
      path<span class="token punctuation">.</span><span class="token function">lineTo</span><span class="token punctuation">(</span>x<span class="token punctuation">,</span> y<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
    path<span class="token punctuation">.</span><span class="token function">close</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">return</span> path<span class="token punctuation">;</span>
  <span class="token punctuation">}</span>

  <span class="token metadata symbol">@override</span>
  bool <span class="token function">shouldRepaint</span><span class="token punctuation">(</span>CustomPainter oldDelegate<span class="token punctuation">)</span> <span class="token operator">=</span><span class="token operator">></span> <span class="token boolean">false</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>

To use the CustomPaint, we set it as a Container child with a width and height of 200. We choose this as the width of the hexagon so that the Center widget above the container nicely centers it.

<span class="token keyword">void</span> <span class="token function">main</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=</span><span class="token operator">></span> <span class="token function">runApp</span><span class="token punctuation">(</span><span class="token function">HexagonGridDemo</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token keyword">class</span> <span class="token class-name">HexagonGridDemo</span> <span class="token keyword">extends</span> <span class="token class-name">StatelessWidget</span> <span class="token punctuation">{</span>
  <span class="token metadata symbol">@override</span>
  Widget <span class="token function">build</span><span class="token punctuation">(</span>BuildContext context<span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token keyword">return</span> <span class="token function">MaterialApp</span><span class="token punctuation">(</span>
      home<span class="token punctuation">:</span> <span class="token function">Scaffold</span><span class="token punctuation">(</span>
        appBar<span class="token punctuation">:</span> <span class="token function">AppBar</span><span class="token punctuation">(</span>
          title<span class="token punctuation">:</span> <span class="token function">Text</span><span class="token punctuation">(</span><span class="token string">'Hexagon Grid Demo'</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
        <span class="token punctuation">)</span><span class="token punctuation">,</span>
        body<span class="token punctuation">:</span> <span class="token function">Center</span><span class="token punctuation">(</span>
          child<span class="token punctuation">:</span> <span class="token function">Container</span><span class="token punctuation">(</span>
            width<span class="token punctuation">:</span> <span class="token number">200</span><span class="token punctuation">,</span>
            height<span class="token punctuation">:</span> <span class="token number">200</span><span class="token punctuation">,</span>
            child<span class="token punctuation">:</span> <span class="token function">CustomPaint</span><span class="token punctuation">(</span>
              painter<span class="token punctuation">:</span> <span class="token function">HexagonPainter</span><span class="token punctuation">(</span><span class="token function">Offset</span><span class="token punctuation">(</span><span class="token number">100</span><span class="token punctuation">,</span> <span class="token number">100</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token number">100</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
            <span class="token punctuation">)</span><span class="token punctuation">,</span>
          <span class="token punctuation">)</span><span class="token punctuation">,</span>
        <span class="token punctuation">)</span><span class="token punctuation">,</span>
      <span class="token punctuation">)</span><span class="token punctuation">,</span>
    <span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span>

To play around with the code, you can open this Dartpad and view or change the code in your browser. This should produce the following hexagon:

Drawing multiple hexagons

Great, so now we can draw a single hexagon, but we actually want to display multiple hexagons in the same canvas.
We could change the painter to do it all in the single CustomPainter; however, it would be nice to reuse the code of the HexagonPainter. So we will be using a stacked-layer with a list of Hexagons to display them all in the same window. First, we will use another widget to wrap a bit of the CustomPaint and CustomPainter since they are the same for each hexagon.

<span class="token keyword">class</span> <span class="token class-name">HexagonPaint</span> <span class="token keyword">extends</span> <span class="token class-name">StatelessWidget</span> <span class="token punctuation">{</span>
  <span class="token keyword">final</span> Offset center<span class="token punctuation">;</span>
  <span class="token keyword">final</span> double radius<span class="token punctuation">;</span>

  <span class="token function">HexagonPaint</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span>center<span class="token punctuation">,</span> <span class="token keyword">this</span><span class="token punctuation">.</span>radius<span class="token punctuation">)</span><span class="token punctuation">;</span>

  <span class="token metadata symbol">@override</span>
  Widget <span class="token function">build</span><span class="token punctuation">(</span>BuildContext context<span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token keyword">return</span> <span class="token function">CustomPaint</span><span class="token punctuation">(</span>
      painter<span class="token punctuation">:</span> <span class="token function">HexagonPainter</span><span class="token punctuation">(</span>center<span class="token punctuation">,</span> radius<span class="token punctuation">)</span><span class="token punctuation">,</span>
    <span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span>

In the stacked layer, we will put a list of our hexagon widgets. Each of those hexagons needs to know where it is going to be placed. For the drawing, we will use Offset hexagon coordinates. Those are x and y based hexagons and we will draw them in the following order:

Offset hexagon coordinates]

The HexagonGrid first computes the radius, such that depending on the number of horizontal and vertical hexagons, the largest possible radius is used without going outside the canvas. After we know the radius and the height. We use the height for easier computation of the Y coordinates. Finally, for each offset hexagon coordinate, we compute the center and add a HexagonPaint to the list.

<span class="token keyword">class</span> <span class="token class-name">HexagonGrid</span> <span class="token keyword">extends</span> <span class="token class-name">StatelessWidget</span> <span class="token punctuation">{</span>
  <span class="token comment">//other variables</span>
  <span class="token keyword">final</span> List<span class="token operator"><</span>HexagonPaint<span class="token operator">></span> hexagons <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">List</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token function">HexagonGrid</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span>screenWidth<span class="token punctuation">,</span> <span class="token keyword">this</span><span class="token punctuation">.</span>screenHeight<span class="token punctuation">)</span> <span class="token punctuation">{</span>
    radius <span class="token operator">=</span> <span class="token function">computeRadius</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    height <span class="token operator">=</span> <span class="token function">computeHeight</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">for</span> <span class="token punctuation">(</span>int x <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span> x <span class="token operator"><</span> nrX<span class="token punctuation">;</span> x<span class="token operator">++</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
      <span class="token keyword">for</span> <span class="token punctuation">(</span>int y <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span> y <span class="token operator"><</span> nrY<span class="token punctuation">;</span> y<span class="token operator">++</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
        hexagons<span class="token punctuation">.</span><span class="token function">add</span><span class="token punctuation">(</span><span class="token function">HexagonPaint</span><span class="token punctuation">(</span><span class="token function">computeCenter</span><span class="token punctuation">(</span>x<span class="token punctuation">,</span> y<span class="token punctuation">)</span><span class="token punctuation">,</span> radius<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
      <span class="token punctuation">}</span>
    <span class="token punctuation">}</span>
  <span class="token punctuation">}</span>

  <span class="token metadata symbol">@override</span>
  Widget <span class="token function">build</span><span class="token punctuation">(</span>BuildContext context<span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token keyword">return</span> <span class="token function">Stack</span><span class="token punctuation">(</span>children<span class="token punctuation">:</span> hexagons<span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
<span class="token comment">//all helpers methods</span>
<span class="token punctuation">}</span>

So how do we use the HexagonGrid? We start with a Container to add some padding on all sides. To make sure we utilize the screen width and screen height, a LayoutBuilder knows the height and width available. We pass the constraints.maxWidth and constraints.maxHeight to the HexagonGrid uses those values to determine the largest possible radius.

<span class="token keyword">void</span> <span class="token function">main</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=</span><span class="token operator">></span> <span class="token function">runApp</span><span class="token punctuation">(</span><span class="token function">HexagonGridDemo</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token keyword">class</span> <span class="token class-name">HexagonGridDemo</span> <span class="token keyword">extends</span> <span class="token class-name">StatelessWidget</span> <span class="token punctuation">{</span>
  <span class="token metadata symbol">@override</span>
  Widget <span class="token function">build</span><span class="token punctuation">(</span>BuildContext context<span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token keyword">return</span> <span class="token function">MaterialApp</span><span class="token punctuation">(</span>
      home<span class="token punctuation">:</span> <span class="token function">Scaffold</span><span class="token punctuation">(</span>
        appBar<span class="token punctuation">:</span> <span class="token function">AppBar</span><span class="token punctuation">(</span>
          title<span class="token punctuation">:</span> <span class="token function">Text</span><span class="token punctuation">(</span><span class="token string">'Hexagon Grid Demo'</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
        <span class="token punctuation">)</span><span class="token punctuation">,</span>
        body<span class="token punctuation">:</span> <span class="token function">Container</span><span class="token punctuation">(</span>
          color<span class="token punctuation">:</span> Colors<span class="token punctuation">.</span>grey<span class="token punctuation">[</span><span class="token number">200</span><span class="token punctuation">]</span><span class="token punctuation">,</span>
          padding<span class="token punctuation">:</span> EdgeInsets<span class="token punctuation">.</span><span class="token function">all</span><span class="token punctuation">(</span><span class="token number">8</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
          child<span class="token punctuation">:</span> <span class="token function">LayoutBuilder</span><span class="token punctuation">(</span>builder<span class="token punctuation">:</span> <span class="token punctuation">(</span>context<span class="token punctuation">,</span> constraints<span class="token punctuation">)</span> <span class="token punctuation">{</span>
            <span class="token keyword">return</span> <span class="token function">Container</span><span class="token punctuation">(</span>
              color<span class="token punctuation">:</span> Colors<span class="token punctuation">.</span>transparent<span class="token punctuation">,</span>
              child<span class="token punctuation">:</span> <span class="token function">HexagonGrid</span><span class="token punctuation">(</span>constraints<span class="token punctuation">.</span>maxWidth<span class="token punctuation">,</span> constraints<span class="token punctuation">.</span>maxHeight<span class="token punctuation">)</span><span class="token punctuation">,</span>
            <span class="token punctuation">)</span><span class="token punctuation">;</span>
          <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
        <span class="token punctuation">)</span><span class="token punctuation">,</span>
      <span class="token punctuation">)</span><span class="token punctuation">,</span>
    <span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span>

Again, we put the full code here in a Dartpad to play around with the code without having a full setup. The drawn hexagon grid should as the following:

Should you still have any questions, feel free to ask them! In the next blog post, we discuss how to detect clicks on the hexagon grid.

Leave a Reply