hexagonal 0.0.1
hexagonal: ^0.0.1 copied to clipboard
A powerful Flutter package for creating hexagonal UI layouts with grid system, free layout, pattern generators, and boundary-aware generation.
import 'package:flutter/material.dart';
import 'package:hexagonal/free_layout/enhanced_hex_widget.dart';
import 'package:hexagonal/free_layout/enhanced_hex_renderer.dart';
import 'package:hexagonal/free_layout/data_driven_hex_generator.dart';
void main() {
runApp(const EnhancedExampleApp());
}
class EnhancedExampleApp extends StatelessWidget {
const EnhancedExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Enhanced Hexagon Features',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const EnhancedExamplePage(),
);
}
}
class EnhancedExamplePage extends StatefulWidget {
const EnhancedExamplePage({super.key});
@override
State<EnhancedExamplePage> createState() => _EnhancedExamplePageState();
}
class _EnhancedExamplePageState extends State<EnhancedExamplePage> {
int _selectedIndex = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text('Enhanced Hexagon Features'),
),
body: IndexedStack(
index: _selectedIndex,
children: const [
FillStylesExample(),
AnimationsExample(),
SnakePatternsExample(),
DataDrivenExample(),
],
),
bottomNavigationBar: NavigationBar(
selectedIndex: _selectedIndex,
onDestinationSelected: (index) {
setState(() {
_selectedIndex = index;
});
},
destinations: const [
NavigationDestination(
icon: Icon(Icons.format_paint),
label: 'Fill Styles',
),
NavigationDestination(
icon: Icon(Icons.animation),
label: 'Animations',
),
NavigationDestination(
icon: Icon(Icons.auto_awesome),
label: 'Snake Patterns',
),
NavigationDestination(
icon: Icon(Icons.dataset),
label: 'Data Driven',
),
],
),
);
}
}
/// 填充样式示例
class FillStylesExample extends StatelessWidget {
const FillStylesExample({super.key});
@override
Widget build(BuildContext context) {
final hexWidgets = DataDrivenHexGenerator.generate(
DataDrivenHexConfig<Map<String, dynamic>>(
data: [
{'name': 'Solid', 'type': 'solid'},
{'name': 'Gradient', 'type': 'gradient'},
{'name': 'Pattern', 'type': 'pattern'},
{'name': 'Border', 'type': 'border'},
{'name': 'Shadow', 'type': 'shadow'},
{'name': 'Opacity', 'type': 'opacity'},
],
pattern: HexLayoutPattern.honeycomb,
startPosition: const Offset(350, 250),
hexSize: 50,
gap: 4.0,
honeycombRings: 2,
hexBuilder: (data, hex, index) {
final type = data['type'] as String;
HexFillStyle fillStyle;
HexBorderStyle? borderStyle;
List<BoxShadow>? shadows;
double opacity = 1.0;
switch (type) {
case 'solid':
fillStyle = SolidColorFill(Colors.blue.shade400);
break;
case 'gradient':
fillStyle = GradientFill(
LinearGradient(
colors: [Colors.purple, Colors.pink],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
);
break;
case 'pattern':
fillStyle = PatternFill(
Container(
decoration: BoxDecoration(
gradient: RadialGradient(
colors: [Colors.yellow, Colors.orange, Colors.red],
),
),
),
);
break;
case 'border':
fillStyle = SolidColorFill(Colors.white);
borderStyle = HexBorderStyle(
color: Colors.green,
width: 4.0,
join: StrokeJoin.round,
);
break;
case 'shadow':
fillStyle = SolidColorFill(Colors.cyan.shade300);
shadows = [
BoxShadow(
color: Colors.black.withOpacity(0.3),
blurRadius: 10,
offset: const Offset(2, 2),
),
];
break;
case 'opacity':
fillStyle = SolidColorFill(Colors.red.shade400);
opacity = 0.5;
break;
default:
fillStyle = SolidColorFill(Colors.grey);
}
return EnhancedFreeHexWidget(
id: hex.id,
hexagon: hex,
fillStyle: fillStyle,
borderStyle: borderStyle ?? const HexBorderStyle.none(),
shadows: shadows,
opacity: opacity,
child: Center(
child: Text(
data['name'],
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 10,
),
textAlign: TextAlign.center,
),
),
);
},
),
);
return Stack(
children: hexWidgets
.map(
(w) => Positioned(
left: w.hexagon.center.dx - w.hexagon.size,
top: w.hexagon.center.dy - w.hexagon.size,
child: SizedBox(
width: w.hexagon.size * 2,
height: w.hexagon.size * 2,
child: EnhancedHexagonRenderer(config: w),
),
),
)
.toList(),
);
}
}
/// 动画示例
class AnimationsExample extends StatelessWidget {
const AnimationsExample({super.key});
@override
Widget build(BuildContext context) {
final hexWidgets = DataDrivenHexGenerator.generate(
DataDrivenHexConfig<Map<String, dynamic>>(
data: [
{'name': 'None', 'animation': HexClickAnimation.none},
{'name': 'Scale', 'animation': HexClickAnimation.scale},
{'name': 'Ripple', 'animation': HexClickAnimation.ripple},
{'name': 'Bounce', 'animation': HexClickAnimation.bounce},
{'name': 'Glow', 'animation': HexClickAnimation.glow},
],
pattern: HexLayoutPattern.ring,
startPosition: const Offset(350, 280),
hexSize: 55,
gap: 3.0,
hexBuilder: (data, hex, index) {
return EnhancedFreeHexWidget(
id: hex.id,
hexagon: hex,
fillStyle: SolidColorFill(Colors.teal.shade400),
borderStyle: HexBorderStyle(
color: Colors.teal.shade700,
width: 2.0,
),
clickAnimation: data['animation'] as HexClickAnimation,
onTap: () {
print('Tapped: ${data['name']}');
},
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.touch_app, color: Colors.white, size: 20),
const SizedBox(height: 4),
Text(
data['name'],
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 10,
),
),
],
),
),
);
},
),
);
return Column(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
'Click hexagons to see different animations',
style: Theme.of(context).textTheme.titleMedium,
),
),
Expanded(
child: Stack(
children: hexWidgets
.map(
(w) => Positioned(
left: w.hexagon.center.dx - w.hexagon.size,
top: w.hexagon.center.dy - w.hexagon.size,
child: SizedBox(
width: w.hexagon.size * 2,
height: w.hexagon.size * 2,
child: EnhancedHexagonRenderer(config: w),
),
),
)
.toList(),
),
),
],
);
}
}
/// 蛇形图案示例
class SnakePatternsExample extends StatefulWidget {
const SnakePatternsExample({super.key});
@override
State<SnakePatternsExample> createState() => _SnakePatternsExampleState();
}
class _SnakePatternsExampleState extends State<SnakePatternsExample> {
HexLayoutPattern _pattern = HexLayoutPattern.snake;
double _bendFactor = 0.3;
int _count = 12;
bool _clockwise = true; // 生成顺序:true=顺时针,false=逆时针
String _boundedDirection = 'right'; // bounded方向:up/down/left/right
double _hexSize = 30.0; // 六边形大小
@override
Widget build(BuildContext context) {
final colors = [
Colors.red,
Colors.orange,
Colors.yellow,
Colors.green,
Colors.blue,
Colors.indigo,
Colors.purple,
];
final hexWidgets = DataDrivenHexGenerator.generate(
DataDrivenHexConfig<int>(
data: List.generate(_count, (i) => i),
pattern: _pattern,
startPosition: const Offset(100, 100),
hexSize: _hexSize,
gap: 2.0,
bendFactor: _bendFactor,
clockwise: _clockwise,
boundedDirection: _boundedDirection,
bounds: const Rect.fromLTWH(50, 80, 600, 400),
zigWidth: 3,
zigHeight: 2,
hexBuilder: (index, hex, _) {
return EnhancedFreeHexWidget(
id: hex.id,
hexagon: hex,
fillStyle: GradientFill(
LinearGradient(
colors: [
colors[index % colors.length],
colors[(index + 1) % colors.length],
],
),
),
borderStyle: HexBorderStyle(color: Colors.white, width: 2.0),
child: Center(
child: Text(
'$index',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 11,
),
),
),
);
},
),
);
return Column(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Pattern', style: Theme.of(context).textTheme.titleSmall),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
ChoiceChip(
label: const Text('Snake'),
avatar: const Icon(Icons.waves, size: 18),
selected: _pattern == HexLayoutPattern.snake,
onSelected: (selected) {
if (selected)
setState(() => _pattern = HexLayoutPattern.snake);
},
),
ChoiceChip(
label: const Text('Spiral'),
avatar: const Icon(Icons.refresh, size: 18),
selected: _pattern == HexLayoutPattern.spiral,
onSelected: (selected) {
if (selected)
setState(() => _pattern = HexLayoutPattern.spiral);
},
),
ChoiceChip(
label: const Text('ZigZag'),
avatar: const Icon(Icons.trending_up, size: 18),
selected: _pattern == HexLayoutPattern.zigzag,
onSelected: (selected) {
if (selected)
setState(() => _pattern = HexLayoutPattern.zigzag);
},
),
ChoiceChip(
label: const Text('Bounded'),
avatar: const Icon(Icons.border_outer, size: 18),
selected: _pattern == HexLayoutPattern.snakeBounded,
onSelected: (selected) {
if (selected)
setState(
() => _pattern = HexLayoutPattern.snakeBounded,
);
},
),
],
),
const SizedBox(height: 12),
// 只在非边界模式下显示 Bend Factor
if (_pattern != HexLayoutPattern.snakeBounded) ...[
Text(
'Bend Factor: ${_bendFactor.toStringAsFixed(2)}',
style: Theme.of(context).textTheme.titleSmall,
),
Slider(
value: _bendFactor,
min: 0.0,
max: 1.0,
divisions: 20,
onChanged: (value) {
setState(() {
_bendFactor = value;
});
},
),
],
// 边界模式的特殊参数
if (_pattern == HexLayoutPattern.snakeBounded) ...[
Text(
'Generation Direction',
style: Theme.of(context).textTheme.titleSmall,
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
ChoiceChip(
label: const Text('Up'),
avatar: const Icon(Icons.arrow_upward, size: 18),
selected: _boundedDirection == 'up',
onSelected: (selected) {
if (selected) {
setState(() => _boundedDirection = 'up');
}
},
),
ChoiceChip(
label: const Text('Down'),
avatar: const Icon(Icons.arrow_downward, size: 18),
selected: _boundedDirection == 'down',
onSelected: (selected) {
if (selected) {
setState(() => _boundedDirection = 'down');
}
},
),
ChoiceChip(
label: const Text('Left'),
avatar: const Icon(Icons.arrow_back, size: 18),
selected: _boundedDirection == 'left',
onSelected: (selected) {
if (selected) {
setState(() => _boundedDirection = 'left');
}
},
),
ChoiceChip(
label: const Text('Right'),
avatar: const Icon(Icons.arrow_forward, size: 18),
selected: _boundedDirection == 'right',
onSelected: (selected) {
if (selected) {
setState(() => _boundedDirection = 'right');
}
},
),
],
),
const SizedBox(height: 12),
Text(
'Hexagon Size: ${_hexSize.toStringAsFixed(1)}',
style: Theme.of(context).textTheme.titleSmall,
),
Slider(
value: _hexSize,
min: 15.0,
max: 50.0,
divisions: 35,
onChanged: (value) {
setState(() {
_hexSize = value;
});
},
),
const SizedBox(height: 12),
Text(
'Generation Order',
style: Theme.of(context).textTheme.titleSmall,
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
ChoiceChip(
label: const Text('Clockwise'),
avatar: const Icon(Icons.rotate_right, size: 18),
selected: _clockwise,
onSelected: (selected) {
if (selected) {
setState(() => _clockwise = true);
}
},
),
ChoiceChip(
label: const Text('Counter-Clockwise'),
avatar: const Icon(Icons.rotate_left, size: 18),
selected: !_clockwise,
onSelected: (selected) {
if (selected) {
setState(() => _clockwise = false);
}
},
),
],
),
],
const SizedBox(height: 12),
Text(
'Count: $_count',
style: Theme.of(context).textTheme.titleSmall,
),
Slider(
value: _count.toDouble(),
min: 5,
max: _pattern == HexLayoutPattern.snakeBounded ? 50 : 20,
divisions: _pattern == HexLayoutPattern.snakeBounded ? 45 : 15,
onChanged: (value) {
setState(() {
_count = value.toInt();
});
},
),
],
),
),
Expanded(
child: Container(
color: _pattern == HexLayoutPattern.snakeBounded
? Colors.grey.shade200
: null,
child: Stack(
children: [
// 显示边界矩形(仅在Bounded模式下)
if (_pattern == HexLayoutPattern.snakeBounded)
Positioned(
left: 50,
top: 80,
child: Container(
width: 600,
height: 400,
decoration: BoxDecoration(
border: Border.all(
color: Colors.red.withOpacity(0.5),
width: 2,
),
),
),
),
// 六边形
...hexWidgets.map(
(w) => Positioned(
left: w.hexagon.center.dx - w.hexagon.size,
top: w.hexagon.center.dy - w.hexagon.size,
child: SizedBox(
width: w.hexagon.size * 2,
height: w.hexagon.size * 2,
child: EnhancedHexagonRenderer(config: w),
),
),
),
],
),
),
),
],
);
}
}
/// 数据驱动示例
class DataDrivenExample extends StatelessWidget {
const DataDrivenExample({super.key});
@override
Widget build(BuildContext context) {
// 模拟一些数据
final userData = [
{'name': 'Alice', 'score': 95, 'color': Colors.red},
{'name': 'Bob', 'score': 87, 'color': Colors.blue},
{'name': 'Carol', 'score': 92, 'color': Colors.green},
{'name': 'David', 'score': 78, 'color': Colors.orange},
{'name': 'Eve', 'score': 88, 'color': Colors.purple},
{'name': 'Frank', 'score': 91, 'color': Colors.teal},
{'name': 'Grace', 'score': 85, 'color': Colors.pink},
];
final hexWidgets = DataDrivenHexGenerator.generate(
DataDrivenHexConfig<Map<String, dynamic>>(
data: userData,
pattern: HexLayoutPattern.honeycomb,
startPosition: const Offset(350, 280),
hexSize: 50,
gap: 3.0,
honeycombRings: 2,
hexBuilder: (data, hex, index) {
final score = data['score'] as int;
final name = data['name'] as String;
final baseColor = data['color'] as Color;
// 根据分数调整颜色亮度
final opacity = (score / 100).clamp(0.5, 1.0);
return EnhancedFreeHexWidget(
id: hex.id,
hexagon: hex,
fillStyle: SolidColorFill(baseColor.withOpacity(opacity)),
borderStyle: HexBorderStyle(
color: baseColor.withOpacity(0.8),
width: 3.0,
),
clickAnimation: HexClickAnimation.bounce,
shadows: [
BoxShadow(
color: baseColor.withOpacity(0.3),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
onTap: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('$name scored $score!'),
duration: const Duration(seconds: 1),
),
);
},
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
name,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
const SizedBox(height: 4),
Text(
'$score',
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
],
),
),
);
},
),
);
return Column(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'User Scores (Click to see details)',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
Text(
'Each hexagon represents a user with their score. Color opacity indicates performance.',
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
Expanded(
child: Stack(
children: hexWidgets
.map(
(w) => Positioned(
left: w.hexagon.center.dx - w.hexagon.size,
top: w.hexagon.center.dy - w.hexagon.size,
child: SizedBox(
width: w.hexagon.size * 2,
height: w.hexagon.size * 2,
child: EnhancedHexagonRenderer(config: w),
),
),
)
.toList(),
),
),
],
);
}
}