Skip to content

Instantly share code, notes, and snippets.

@Barttje
Created June 13, 2020 07:11
Show Gist options
  • Save Barttje/cd27522e7f4455e11e52b969bc1aa4ee to your computer and use it in GitHub Desktop.
Save Barttje/cd27522e7f4455e11e52b969bc1aa4ee to your computer and use it in GitHub Desktop.
Reacting to clicks on a Hexagon grid
import 'package:flutter/material.dart';
import 'dart:math' as math;
import 'package:flutter/rendering.dart';
void main() => runApp(HexagonGridDemo());
class HexagonGridDemo extends StatelessWidget {
final grid = HexagonGrid();
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('Hexagon Grid Demo'),
),
body: Container(
color: Colors.grey[200],
padding: EdgeInsets.all(8),
child: LayoutBuilder(builder: (context, constraints) {
grid.initialize(constraints.maxWidth, constraints.maxHeight);
return Listener(
onPointerDown: (PointerEvent details) {
handleClick(details);
},
child: Container(
width: constraints.maxWidth,
height: constraints.maxHeight,
color: Colors.transparent,
child: grid,
),
);
}),
),
),
);
}
handleClick(PointerEvent details) {
var hexagon =
grid.hexagons.firstWhere((hexagon) => determineClick(hexagon, details));
hexagon.key.currentState.updateColor();
}
bool determineClick(HexagonPaint hexagon, PointerEvent details) {
final RenderBox hexagonBox =
hexagon.model.key.currentContext.findRenderObject();
final result = BoxHitTestResult();
Offset localClick = hexagonBox.globalToLocal(details.position);
if (hexagonBox.hitTest(result, position: localClick)) {
return true;
}
return false;
}
}
class HexagonGrid extends StatelessWidget {
final GridInitializer gridInitializer = GridInitializer();
final List<HexagonPaint> hexagons = new List();
void initialize(final double screenWidth, final double screenHeight) {
if (this.hexagons.isEmpty) {
hexagons.addAll(gridInitializer.getHexagons(screenWidth, screenHeight));
}
}
@override
Widget build(BuildContext context) {
return Stack(children: hexagons);
}
}
class GridInitializer {
static const int marginY = 5;
static const int marginX = 5;
static const int nrX = 6;
static const int nrY = 9;
double radius;
double height;
double screenWidth;
double screenHeight;
List<HexagonPaint> getHexagons(
final double screenWidth, final double screenHeight) {
var hexagons = new List<HexagonPaint>();
this.screenWidth = screenWidth;
this.screenHeight = screenHeight;
radius = computeRadius(screenWidth, screenHeight);
height = computeHeight(computeRadius(screenWidth, screenHeight));
for (int x = 0; x < nrX; x++) {
for (int y = 0; y < nrY; y++) {
hexagons.add(HexagonPaint(HexagonModel(computeCenter(x, y), radius)));
}
}
return hexagons;
}
static double computeRadius(double screenWidth, double screenHeight) {
var maxWidth = (screenWidth - totalMarginX()) / (((nrX - 1) * 1.5) + 2);
var maxHeight = 0.5 *
(screenHeight - totalMarginY()) /
(heightRatioOfRadius() * (nrY + 0.5));
return math.min(maxWidth, maxHeight);
}
static double heightRatioOfRadius() =>
math.cos(math.pi / HexagonPainter.SIDES_OF_HEXAGON);
static double totalMarginY() => (nrY - 0.5) * marginY;
static int totalMarginX() => (nrX - 1) * marginX;
static double computeHeight(double radius) {
return heightRatioOfRadius() * radius * 2;
}
Offset computeCenter(int x, int y) {
var centerX = computeX(x);
var centerY = computeY(x, y);
return Offset(centerX, centerY);
}
computeY(int x, int y) {
var centerY;
if (x % 2 == 0) {
centerY = y * height + y * marginY + height / 2;
} else {
centerY = y * height + (y + 0.5) * marginY + height;
}
double marginsVertical = computeEmptySpaceY() / 2;
return centerY + marginsVertical;
}
double computeEmptySpaceY() {
return screenHeight - ((nrY - 1) * height + 1.5 * height + totalMarginY());
}
double computeX(int x) {
double marginsHorizontal = computeEmptySpaceX() / 2;
return x * marginX + x * 1.5 * radius + radius + marginsHorizontal;
}
double computeEmptySpaceX() {
return screenWidth -
(totalMarginX() + (nrX - 1) * 1.5 * radius + 2 * radius);
}
}
class HexagonModel {
final Offset center;
final double radius;
final GlobalKey key = GlobalKey();
bool clicked = false;
HexagonModel(this.center, this.radius);
}
class HexagonPaint extends StatefulWidget {
final HexagonModel model;
final GlobalKey<_HexagonPaintState> key = GlobalKey<_HexagonPaintState>();
HexagonPaint(this.model);
@override
_HexagonPaintState createState() => _HexagonPaintState();
}
class _HexagonPaintState extends State<HexagonPaint> {
updateColor() {
setState(() {
widget.model.clicked = true;
});
}
@override
Widget build(BuildContext context) {
return CustomPaint(
key: widget.model.key,
painter: HexagonPainter(
widget.model.center, widget.model.radius, widget.model.clicked),
child: Container(),
);
}
}
class HexagonPainter extends CustomPainter {
static const int SIDES_OF_HEXAGON = 6;
final double radius;
final Offset center;
final bool clicked;
HexagonPainter(this.center, this.radius, this.clicked);
@override
void paint(Canvas canvas, Size size) {
Paint paint = Paint()..color = clicked ? Colors.red : Colors.blue;
Path path = createHexagonPath();
canvas.drawPath(path, paint);
}
Path createHexagonPath() {
final path = Path();
var angle = (math.pi * 2) / SIDES_OF_HEXAGON;
Offset firstPoint = Offset(radius * math.cos(0.0), radius * math.sin(0.0));
path.moveTo(firstPoint.dx + center.dx, firstPoint.dy + center.dy);
for (int i = 1; i <= SIDES_OF_HEXAGON; i++) {
double x = radius * math.cos(angle * i) + center.dx;
double y = radius * math.sin(angle * i) + center.dy;
path.lineTo(x, y);
}
path.close();
return path;
}
@override
bool shouldRepaint(HexagonPainter oldDelegate) =>
oldDelegate.clicked != clicked;
@override
bool hitTest(Offset position) {
final Path path = createHexagonPath();
return path.contains(position);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment