Created
June 13, 2020 07:11
-
-
Save Barttje/cd27522e7f4455e11e52b969bc1aa4ee to your computer and use it in GitHub Desktop.
Reacting to clicks on a Hexagon grid
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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