hexagon 1.0.0-beta.1
hexagon: ^1.0.0-beta.1 copied to clipboard
Hexagons for Flutter apps and games: a hexagon widget and ShapeBorder, hex grids, and a pannable, zoomable hex map with painted layers, pathfinding and hit testing.
hexagon #
Hexagons for Flutter apps and games: a hexagon widget and ShapeBorder,
hex grids, and a pannable, zoomable hex map with painted layers,
pathfinding and hit testing.

| You want… | Use |
|---|---|
| A hexagon-shaped button, card, avatar or tile | Hexagon, or HexagonBorder on any Material widget |
| A board or menu of up to a few hundred cells | HexGrid |
| A large map with pan, zoom, overlays and units | HexGridView |
A scrolling honeycomb in a CustomScrollView, built lazily |
SliverHexGrid |
| Coordinates, distances, pathfinding, field of view | Hex, HexLayout, HexShape, HexMap (from hexagon_core, re-exported) |
dependencies:
hexagon: ^1.0.0-beta.1
import 'package:hexagon/hexagon.dart';
Upgrading from 0.x? See MIGRATION.md.
Hexagon #
A hexagon-shaped surface. It picks its size during layout, like
AspectRatio, so give it a width, a height, or constraints:
Hexagon(
width: 120,
color: Colors.amber,
elevation: 4,
cornerRadius: 8,
side: const BorderSide(color: Colors.brown, width: 2),
onTap: () {},
child: const Icon(Icons.hive),
)
Hexagon.pointy(height: 80, child: Text('Pointy'))
fit:HexagonFit.contain(default) draws the largest hexagon that fits the constraints.HexagonFit.wrapsizes the hexagon around its child.childArea:HexagonChildArea.inscribed(default) lays the child out in the largest rectangle inside the hexagon, so text and icons stay visible.HexagonChildArea.boundsgives the child the whole bounding box, clipped to the hexagon, which suits images.- Taps, long presses and hover only count inside the outline, so hexagons can overlap their bounding boxes.
Set defaults for every Hexagon with the theme extension:
MaterialApp(
theme: ThemeData(
extensions: const [HexagonThemeData(cornerRadius: 6, elevation: 2)],
),
)
HexagonBorder #
A ShapeBorder, so it works wherever Flutter takes a shape:
ElevatedButton(
style: ElevatedButton.styleFrom(shape: const HexagonBorder(cornerRadius: 8)),
onPressed: () {},
child: const Text('Hex button'),
)
Card(shape: const HexagonBorder(type: HexagonType.pointy), child: ...)
Container(
decoration: const ShapeDecoration(
shape: HexagonBorder(side: BorderSide(width: 2)),
color: Colors.teal,
),
)
ClipPath.shape(shape: const HexagonBorder(cornerRadius: 12), child: image)
The hexagon is regular and centered in its box; eccentricity: 1 stretches
it to the box instead. Borders of the same type animate between each other,
for example in AnimatedContainer or ShapeBorderTween.
HexGrid #
One widget per cell, for any set of cells:
HexGrid(
cells: HexShape.hexagon(3), // or rectangle, parallelogram, triangle, any Iterable<Hex>
type: HexagonType.pointy,
spacing: 4,
itemBuilder: (context, hex) => Hexagon(
type: HexagonType.pointy,
child: Text('${hex.q}, ${hex.r}'),
),
onHexTap: (hex) => print('tapped $hex'),
)
Without a radius, the hexagons are sized so the grid fits its constraints.
Each cell gets exactly its hexagon's bounding box and only receives pointers
inside its hexagon.
HexGridView #
For maps with thousands of cells. It pans and zooms, paints only the cells in the viewport, and stacks layers bottom to top:
final controller = HexGridController();
HexGridView(
layout: const HexLayout.flat(radius: 24),
cells: terrain.cells.toSet(),
controller: controller,
layers: [
// Canvas layers: nothing is built per cell.
HexPaintLayer.fill(colorOf: (hex) => terrain[hex]!.color),
HexPaintLayer.fill(cells: reachable, color: Colors.white24),
HexPaintLayer(
cells: path,
painter: (canvas, cell) =>
canvas.drawCircle(cell.center, 5, Paint()..color = Colors.white),
),
// Widgets for the few cells that need them.
HexWidgetLayer(cells: units.keys, builder: (context, hex) => UnitToken(units[hex]!)),
],
onHexTap: select,
onHexHover: preview,
)
controller.animateTo(const Hex(10, -4), scale: 2);
controller.hexAtViewport(position);
HexCell gives painters the cell's center, bounding box and outline path.
Pass repaint: a Listenable (an animation, or a ChangeNotifier holding
game state) to repaint a layer without rebuilding.
SliverHexGrid #
A honeycomb that scrolls with the rest of a CustomScrollView and only
builds the hexagons on screen, with a fixed item count or endlessly:
CustomScrollView(
slivers: [
const SliverAppBar(title: Text('Hive')),
SliverHexGrid.builder(
gridDelegate: const SliverHexGridDelegate.count(
crossAxisCount: 5, // or .extent(maxCellExtent: 90)
type: HexagonType.pointy,
spacing: 4,
),
itemCount: 500, // null for an endless grid
itemBuilder: (context, index) => Hexagon(
type: HexagonType.pointy,
onTap: () {},
child: Text('$index'),
),
),
],
)
Items fill each line across the grid, then continue on the next line, and
neighboring lines interlock. It works in both scroll directions;
crossAxisCount: 1 or 2 gives a zigzag strip, handy in a horizontal list.
Each item only receives pointers inside its hexagon. SliverHexGridDelegate
is a regular SliverGridDelegate, so it also works with SliverGrid and
GridView.
Grid math, pathfinding and field of view #
Everything in hexagon_core is
re-exported. It has no Flutter dependency, so a game server can use it too.
const a = Hex(2, -1);
a.distanceTo(const Hex(-1, 3)); // 4
a.neighbors;
a.lineTo(const Hex(5, -3));
Hex.fromOffset(3, 4, HexagonType.flat); // from column/row
const layout = HexLayout.pointy(radius: 24);
layout.centerOf(a); // Offset
layout.hexAt(details.localPosition); // Hex under a pointer
final range = unit.reachable(movement: 5, passable: isLand, cost: moveCost);
final path = unit.pathTo(target, passable: isLand, cost: moveCost);
final visible = unit.fieldOfView(8, blocksSight: isWall);
Example #
The example app, also
running in the browser, has
four pages: widgets and borders, grids
with every built-in shape, a scrolling SliverHexGrid, and a small strategy
map with units, movement range, path preview and field of view.
Credits #
The grid math follows Red Blob Games' Hexagonal Grids.
