Skip to content

Instantly share code, notes, and snippets.

@PlugFox
Last active May 9, 2024 06:12
Show Gist options
  • Save PlugFox/674b54abd48d1cb4674cb842c88bcf13 to your computer and use it in GitHub Desktop.
Save PlugFox/674b54abd48d1cb4674cb842c88bcf13 to your computer and use it in GitHub Desktop.
Hexagons, proof of concept
// 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