Last active
May 9, 2024 06:12
-
-
Save PlugFox/674b54abd48d1cb4674cb842c88bcf13 to your computer and use it in GitHub Desktop.
Hexagons, proof of concept
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
// ignore_for_file: avoid_print | |
import 'dart:async'; | |
import 'dart:math' as math; | |
import 'package:flutter/foundation.dart'; | |
import 'package:flutter/material.dart'; | |
// TODO(plugfox): пропадают элементы снизу при скролле и определенных размерах экранах | |
// TODO(plugfox): добавить скролл при наведении мыши на края экрана | |
// TODO(plugfox): улучшить расположение | |
// TODO(plugfox): InkWell & Material shadow | |
void main() => runZonedGuarded<void>( | |
() => runApp(const App()), | |
(error, stackTrace) => print('Top level exception: error\nstackTrace'), | |
); | |
class App extends StatelessWidget { | |
const App({super.key}); | |
@override | |
Widget build(BuildContext context) => MaterialApp( | |
title: 'Material App', | |
home: Scaffold( | |
appBar: AppBar( | |
title: const Text('Material App Bar'), | |
), | |
body: SafeArea( | |
child: HexagonLayout( | |
labels: <String>[ | |
for (var i = 1; i <= 9; i++) | |
for (var j = 'A'.codeUnitAt(0); j <= 'Z'.codeUnitAt(0); j++) | |
'${String.fromCharCode(j)}#$i' | |
], | |
), | |
), | |
), | |
); | |
} | |
class HexagonTile extends StatelessWidget { | |
const HexagonTile({ | |
required this.label, | |
required this.size, | |
super.key, // ignore: unused_element | |
}); | |
final String label; | |
final double size; | |
@override | |
Widget build(BuildContext context) => SizedBox.square( | |
dimension: size, | |
child: CustomPaint( | |
painter: const HexagonPainter(), | |
child: Center( | |
child: Text( | |
label, | |
maxLines: 1, | |
overflow: TextOverflow.ellipsis, | |
textAlign: TextAlign.center, | |
style: const TextStyle( | |
color: Colors.white, | |
fontSize: 24, | |
fontWeight: FontWeight.bold, | |
), | |
), | |
), | |
), | |
); | |
} | |
class HexagonPainter extends CustomPainter { | |
const HexagonPainter(); | |
static final Paint _paint = Paint() | |
..color = Colors.blue | |
..style = PaintingStyle.fill; | |
@override | |
void paint(Canvas canvas, Size size) { | |
final width = size.width; | |
final height = size.height; | |
final radius = math.min(width, height) / 2; | |
final center = Offset(width / 2, height / 2); | |
final path = Path(); | |
// Calculate the points of the hexagon | |
for (int i = 0; i < 6; i++) { | |
double angle = (math.pi / 3) * i; | |
double x = center.dx + radius * math.cos(angle); | |
double y = center.dy + radius * math.sin(angle); | |
if (i == 0) { | |
path.moveTo(x, y); | |
} else { | |
path.lineTo(x, y); | |
} | |
} | |
path.close(); | |
// Draw the hexagon | |
canvas.drawPath(path, _paint); | |
} | |
@override | |
bool shouldRepaint(CustomPainter oldDelegate) => false; | |
} | |
class CircleTile extends StatelessWidget { | |
const CircleTile({ | |
required this.label, | |
required this.size, | |
super.key, // ignore: unused_element | |
}); | |
final String label; | |
final double size; | |
@override | |
Widget build(BuildContext context) => SizedBox.square( | |
dimension: size, | |
child: CustomPaint( | |
painter: const CirclePainter(), | |
child: Center( | |
child: Text( | |
label, | |
maxLines: 1, | |
overflow: TextOverflow.ellipsis, | |
textAlign: TextAlign.center, | |
style: const TextStyle( | |
color: Colors.white, | |
fontSize: 24, | |
fontWeight: FontWeight.bold, | |
), | |
), | |
), | |
), | |
); | |
} | |
class CirclePainter extends CustomPainter { | |
const CirclePainter(); | |
static final Paint _paint = Paint() | |
..color = Colors.red | |
..style = PaintingStyle.fill; | |
@override | |
void paint(Canvas canvas, Size size) => canvas.drawCircle( | |
Offset(size.width / 2, size.height / 2), | |
math.min(size.width, size.height) / 2, | |
_paint); | |
@override | |
bool shouldRepaint(CustomPainter oldDelegate) => false; | |
} | |
class HexagonLayout extends StatefulWidget { | |
const HexagonLayout({ | |
required this.labels, | |
super.key, // ignore: unused_element | |
}); | |
static const double hexagonSize = 64.0; | |
final List<String> labels; | |
@override | |
State<HexagonLayout> createState() => _HexagonLayoutState(); | |
} | |
class _HexagonLayoutState extends State<HexagonLayout> { | |
final ValueNotifier<double> _scrollController = ValueNotifier<double>(.0); | |
late final FlowDelegate _flowDelegate; | |
late final List<Widget> _children; | |
@override | |
void initState() { | |
super.initState(); | |
_flowDelegate = _HexagonLayoutDelegate( | |
scrollController: _scrollController, | |
hexagonSize: HexagonLayout.hexagonSize, | |
); | |
_children = <Widget>[ | |
for (var i = 0; i < widget.labels.length; i++) | |
if (i.isOdd) | |
CircleTile( | |
label: widget.labels[i], | |
size: HexagonLayout.hexagonSize, | |
) | |
else | |
HexagonTile( | |
label: widget.labels[i], | |
size: HexagonLayout.hexagonSize, | |
), | |
]; | |
} | |
@override | |
void dispose() { | |
_scrollController.dispose(); | |
super.dispose(); | |
} | |
@override | |
Widget build(BuildContext context) => GestureDetector( | |
behavior: HitTestBehavior.translucent, | |
onHorizontalDragUpdate: (details) { | |
_scrollController.value += details.primaryDelta ?? 0; | |
}, | |
child: RepaintBoundary( | |
child: Flow( | |
clipBehavior: Clip.none, | |
delegate: _flowDelegate, | |
children: _children, | |
), | |
), | |
); | |
} | |
final class _HexagonLayoutDelegate extends FlowDelegate { | |
_HexagonLayoutDelegate({ | |
required ValueListenable<double> scrollController, | |
required double hexagonSize, | |
}) : _scrollOffset = scrollController, | |
_hexagonSize = hexagonSize, | |
super(repaint: scrollController); | |
final ValueListenable<double> _scrollOffset; | |
final double _hexagonSize; | |
@override | |
void paintChildren(FlowPaintingContext context) { | |
final count = context.childCount; | |
final width = context.size.width; | |
final height = context.size.height; | |
const horizontalSpacing = | |
1.0; // Отступы между шестиугольниками по горизонтали | |
final size = _hexagonSize; | |
final double verticalSpacing = size * math.sqrt(3) / 2 + | |
horizontalSpacing * | |
6; // Учет вертикальных отступов для более точного выравнивания | |
final int itemsPerColumn = (height / verticalSpacing).floor(); | |
for (int i = 0; i < count; i++) { | |
final col = i ~/ itemsPerColumn; | |
final row = i % itemsPerColumn; | |
final x = col * (size + horizontalSpacing) - | |
_scrollOffset.value; // Учет горизонтального смещения от скролла | |
final y = row * verticalSpacing + | |
(col % 2 == 0 | |
? 0 | |
: verticalSpacing / 2 - | |
horizontalSpacing / | |
2); // Смещение для хексагональной решетки с учетом отступов | |
double center = width / 2; | |
double distanceFromCenter = (x + size / 2 - center).abs(); | |
double fade = math.max( | |
0, math.min(1, (distanceFromCenter - center / 3) / (center * 2 / 3))); | |
double scale = 0.5 + 0.5 * (1 - fade); | |
double opacity = 1 - fade; | |
double adjustedSize = size * scale; | |
final matrix = | |
Matrix4.translationValues(x + (size - adjustedSize) / 2, y, 0) | |
..scale(scale, scale); | |
if (x + adjustedSize > 0 && | |
x < width && | |
y + adjustedSize < height && | |
y >= 0) { | |
context.paintChild( | |
i, | |
transform: matrix, | |
opacity: opacity, | |
); | |
} | |
} | |
} | |
@override | |
bool shouldRepaint(covariant _HexagonLayoutDelegate oldDelegate) => | |
!identical(this, oldDelegate) || _hexagonSize != oldDelegate._hexagonSize; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment