A highly customizable and interactive mind map package for Flutter with multiple layouts, dynamic sizing, and rich styling options.
Flutter์ฉ ๋ค์ค ๋ ์ด์์, ๋์ ํฌ๊ธฐ ์กฐ์ , ๋ค์ํ ์คํ์ผ๋ง ์ต์ ์ ์ ๊ณตํ๋ ๊ณ ๋๋ก ์ปค์คํฐ๋ง์ด์ง ๊ฐ๋ฅํ ์ธํฐ๋ํฐ๋ธ ๋ง์ธ๋๋งต ํจํค์ง์ ๋๋ค.
Multiple layouts and customization options / ๋ค์ํ ๋ ์ด์์๊ณผ ์ปค์คํฐ๋ง์ด์ง ์ต์
Interactive expand/collapse and smooth animations / ์ธํฐ๋ํฐ๋ธ ํ์ฅ/์ถ์ ๋ฐ ๋ถ๋๋ฌ์ด ์ ๋๋ฉ์ด์
๐จ ์์ ํ ์ปค์คํฐ๋ง์ด์ง / Complete Customization
- ๋ ธ๋ ๋ชจ์ ์ ํ (๋ฅ๊ทผ ์ฌ๊ฐํ, ์ํ, ๋ค์ด์๋ชฌ๋, ์ก๊ฐํ ๋ฑ) / Node shapes (rounded rectangle, circle, diamond, hexagon, etc.)
- ์์, ํ ์คํธ ์คํ์ผ, ๊ทธ๋ฆผ์ ํจ๊ณผ ์ปค์คํฐ๋ง์ด์ง / Colors, text styles, shadow effects customization
- ๋์ ๋ ธ๋ ํฌ๊ธฐ ์กฐ์ / Dynamic node sizing
- ์ฐ๊ฒฐ์ ์คํ์ผ๊ณผ ์ ๋๋ฉ์ด์ ์ค์ / Connection line styles and animation settings
๐ฏ ๋ค์ํ ๋ ์ด์์ / Multiple Layouts
- ์ค๋ฅธ์ชฝ/์ผ์ชฝ/์/์๋ ๋ฐฉํฅ ๋ ์ด์์ / Right/Left/Top/Bottom direction layouts
- ์ํ(Radial) ๋ ์ด์์ / Radial layout
- ์ข์ฐ/์ํ ๋ถํ ๋ ์ด์์ / Horizontal/Vertical split layouts
โก ๋ถ๋๋ฌ์ด ์ ๋๋ฉ์ด์ / Smooth Animations
- ๋ ธ๋ ํ์ฅ/์ถ์ ์ ๋๋ฉ์ด์ / Node expand/collapse animations
- ์ปค์คํฐ๋ง์ด์ง ๊ฐ๋ฅํ ์ ๋๋ฉ์ด์ ๊ณก์ ๊ณผ ์ง์์๊ฐ / Customizable animation curves and duration
- ํ๋์จ์ด ๊ฐ์ ํธ๋์ง์ / Hardware-accelerated transitions
๐ฑ๏ธ ํ๋ถํ ์ธํฐ๋์ / Rich Interactions
- ํญ, ๊ธธ๊ฒ ๋๋ฅด๊ธฐ, ๋๋ธ ํญ ์ด๋ฒคํธ / Tap, long press, double tap events
- ํ๋/์ถ์, ํฌ ๊ธฐ๋ฅ / Pan & zoom functionality
- ๋ ธ๋ ํ์ฅ/์ถ์ ์ํ ์ถ์ / Node expand/collapse state tracking
๐ฏ ์ค๋งํธ ์นด๋ฉ๋ผ ํฌ์ปค์ค / Smart Camera Focus ๐
- ์๋ ์ ์ฒด๋ณด๊ธฐ๋ก ์์ ์์ ฏ์์๋ ์ต์ ํ์ / Auto-fit for optimal display in small widgets
- ํน์ ๋ ธ๋ ๊ฐ์กฐ ๋ฐ ๊ฐ์ด๋ ํฌ์ด ์ง์ / Specific node highlighting and guided tours
- ๋ถ๋๋ฌ์ด ํฌ์ปค์ค ์ด๋ ์ ๋๋ฉ์ด์ / Smooth focus transition animations
- 5๊ฐ์ง ํฌ์ปค์ค ๋ชจ๋ (๋ฃจํธ, ์ค์, ์ ์ฒด, ๋ฆฌํ, ์ปค์คํ ) / 5 focus modes (root, center, fitAll, leaf, custom)
MindMapWidget(
data: myData,
onNodeTap: (node) => print('Node tapped: ${node.title}'),
onNodeLongPress: (node) => _showNodeOptions(node),
onNodeExpandChanged: (node, isExpanded) =>
print('${node.title} ${isExpanded ? 'expanded' : 'collapsed'}'),
);Add this to your package's pubspec.yaml file:
pubspec.yaml ํ์ผ์ ๋ค์์ ์ถ๊ฐํ์ธ์:
dependencies:
reactive_mind_map: ^1.0.3Then run / ๊ทธ๋ค์ ์คํํ์ธ์:
flutter pub getimport 'package:flutter/material.dart';
import 'package:reactive_mind_map/reactive_mind_map.dart';
class MyMindMap extends StatelessWidget {
@override
Widget build(BuildContext context) {
final mindMapData = MindMapData(
id: 'root',
title: 'My Project',
children: [
MindMapData(id: '1', title: 'Planning'),
MindMapData(id: '2', title: 'Development'),
MindMapData(id: '3', title: 'Testing'),
],
);
return Scaffold(
body: MindMapWidget(
data: mindMapData,
style: MindMapStyle(
layout: MindMapLayout.right,
nodeShape: NodeShape.roundedRectangle,
),
cameraFocus: CameraFocus.fitAll,
focusAnimation: Duration(milliseconds: 500),
onNodeTap: (node) => print('Tapped: ${node.title}'),
),
);
}
}MindMapWidget์ ๊ธฐ๋ณธ์ ์ผ๋ก ํ๋ฉด ํฌ๊ธฐ์ ๋ง๊ฒ ์๋ ์กฐ์ ๋ฉ๋๋คExpanded์์ ฏ ์์์ ์ฌ์ฉํ ๋๋ ์ถ๊ฐ ์ค์ ์ด ํ์ํ์ง ์์ต๋๋ค- ํฌ/์ค ๊ธฐ๋ฅ์ด ๊ธฐ๋ณธ์ผ๋ก ํ์ฑํ๋์ด ์์ด ํฐ ๋ง์ธ๋๋งต๋ ์ฝ๊ฒ ํ์ํ ์ ์์ต๋๋ค
// โ
์ฌ๋ฐ๋ฅธ ์ฌ์ฉ๋ฒ - ํ๋ฉด์ ๋ง๊ฒ ์๋ ์กฐ์
Widget build(BuildContext context) {
return Scaffold(
body: MindMapWidget(
data: root.value,
style: MindMapStyle(
layout: MindMapLayout.right,
nodeShape: NodeShape.roundedRectangle,
),
onNodeTap: (node) => print('Tapped: ${node.title}'),
),
);
}
// โ
Expanded ์์์ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
SomeHeaderWidget(),
Expanded(
child: MindMapWidget(
data: root.value,
style: MindMapStyle(
layout: MindMapLayout.right,
nodeShape: NodeShape.roundedRectangle,
),
onNodeTap: (node) => print('Tapped: ${node.title}'),
),
),
],
),
);
}You can create custom node designs using the nodeBuilder property in MindMapStyle:
MindMapStyle์ nodeBuilder ์์ฑ์ ์ฌ์ฉํ์ฌ ์ปค์คํ
๋
ธ๋ ๋์์ธ์ ๋ง๋ค ์ ์์ต๋๋ค:
Use the nodeBuilder property in MindMapStyle:
MindMapStyle์ nodeBuilder ์์ฑ์ ์ฌ์ฉํฉ๋๋ค:
MindMapWidget(
data: myData,
style: MindMapStyle(
layout: MindMapLayout.right,
nodeShape: NodeShape.roundedRectangle,
nodeBuilder: (node, isSelected, onTap, onLongPress, onDoubleTap) {
return GestureDetector(
onTap: onTap,
onLongPress: onLongPress,
onDoubleTap: onDoubleTap,
child: Container(
decoration: BoxDecoration(
color: node.color,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: isSelected ? Colors.yellow : Colors.transparent,
width: 2,
),
),
padding: EdgeInsets.all(12),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
node.title,
style: TextStyle(
color: node.textColor ?? Colors.white,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
if (node.description != null) ...[
SizedBox(height: 4),
Text(
node.description!,
style: TextStyle(
color: node.textColor?.withOpacity(0.8) ?? Colors.white70,
fontSize: 12,
),
textAlign: TextAlign.center,
),
],
],
),
),
);
},
),
onNodeTap: (node) => print('Tapped: ${node.title}'),
)MindMapWidget(
data: myData,
cameraFocus: CameraFocus.fitAll,
focusNodeId: 'specific_node_id',
focusAnimation: Duration(milliseconds: 500),
focusMargin: EdgeInsets.all(20),
)| Focus Type / ํฌ์ปค์ค ํ์ | When to Use / ์ฌ์ฉ ์๊ธฐ |
|---|---|
CameraFocus.rootNode |
Default view / ๊ธฐ๋ณธ ๋ทฐ |
CameraFocus.center |
Centered layouts / ์ค์ ์ ๋ ฌ ๋ ์ด์์ |
CameraFocus.fitAll |
Small widgets, overview / ์์ ์์ ฏ, ์ ์ฒด๋ณด๊ธฐ |
CameraFocus.firstLeaf |
End-point focus / ๋์ ํฌ์ปค์ค |
CameraFocus.custom |
Specific node targeting / ํน์ ๋ ธ๋ ํ๊ฒํ |
1. Small Container Optimization / ์์ ์ปจํ ์ด๋ ์ต์ ํ
Container(
height: 200,
child: MindMapWidget(
data: myData,
cameraFocus: CameraFocus.fitAll,
focusMargin: EdgeInsets.all(10),
focusAnimation: Duration(milliseconds: 300),
),
)2. Specific Node Highlighting / ํน์ ๋ ธ๋ ๊ฐ์กฐ
MindMapWidget(
data: myData,
cameraFocus: CameraFocus.custom,
focusNodeId: 'important_milestone',
focusAnimation: Duration(milliseconds: 800),
initialScale: 1.2,
)3. Guided Mind Map Tour / ๊ฐ์ด๋ ๋ง์ธ๋๋งต ํฌ์ด
class GuidedMindMapTour extends StatefulWidget {
@override
State<GuidedMindMapTour> createState() => _GuidedMindMapTourState();
}
class _GuidedMindMapTourState extends State<GuidedMindMapTour> {
int currentStep = 0;
final List<String> tourSteps = ['intro', 'planning', 'development', 'testing'];
Widget build(BuildContext context) {
return Column(
children: [
Row(
children: [
ElevatedButton(
onPressed: currentStep > 0 ? _previousStep : null,
child: Text('์ด์ '),
),
Text('${currentStep + 1} / ${tourSteps.length}'),
ElevatedButton(
onPressed: currentStep < tourSteps.length - 1 ? _nextStep : null,
child: Text('๋ค์'),
),
],
),
Expanded(
child: MindMapWidget(
data: myData,
cameraFocus: CameraFocus.custom,
focusNodeId: tourSteps[currentStep],
focusAnimation: Duration(milliseconds: 600),
focusMargin: EdgeInsets.all(50),
),
),
],
);
}
void _nextStep() => setState(() => currentStep++);
void _previousStep() => setState(() => currentStep--);
}4. Dynamic Focus Based on Data / ๋ฐ์ดํฐ์ ๋ฐ๋ฅธ ๋์ ํฌ์ปค์ค
Widget buildMindMap(MindMapData data) {
final nodeCount = _countAllNodes(data);
return MindMapWidget(
data: data,
cameraFocus: nodeCount > 10 ? CameraFocus.fitAll : CameraFocus.rootNode,
focusAnimation: Duration(milliseconds: 400),
);
}Control how the camera behaves when users expand or collapse nodes: ์ฌ์ฉ์๊ฐ ๋ ธ๋๋ฅผ ํผ์น๊ฑฐ๋ ์ ์ ๋ ์นด๋ฉ๋ผ๊ฐ ์ด๋ป๊ฒ ๋์ํ ์ง ์ ์ดํ ์ ์์ต๋๋ค:
MindMapWidget(
data: myData,
nodeExpandCameraBehavior: NodeExpandCameraBehavior.focusClickedNode,
)| Behavior / ๋์ | Description / ์ค๋ช |
|---|---|
NodeExpandCameraBehavior.none |
No camera movement (default) / ์นด๋ฉ๋ผ ์ด๋ ์์ (๊ธฐ๋ณธ๊ฐ) |
NodeExpandCameraBehavior.focusClickedNode |
Focus on the clicked node / ํด๋ฆญํ ๋ ธ๋๋ก ํฌ์ปค์ค |
NodeExpandCameraBehavior.fitExpandedChildren |
Fit newly expanded children to view / ์๋ก ํผ์ณ์ง ์์ ๋ ธ๋๋ค์ด ๋ณด์ด๋๋ก ์กฐ์ |
NodeExpandCameraBehavior.fitExpandedSubtree |
Fit entire expanded subtree to view / ํผ์ณ์ง ์ ์ฒด ์๋ธํธ๋ฆฌ๊ฐ ๋ณด์ด๋๋ก ์กฐ์ |
1. Focus on Clicked Node / ํด๋ฆญํ ๋ ธ๋์ ํฌ์ปค์ค
MindMapWidget(
data: myData,
nodeExpandCameraBehavior: NodeExpandCameraBehavior.focusClickedNode,
focusAnimation: Duration(milliseconds: 400),
)2. Show All Expanded Children / ํผ์ณ์ง ๋ชจ๋ ์์ ๋ ธ๋ ํ์
MindMapWidget(
data: myData,
nodeExpandCameraBehavior: NodeExpandCameraBehavior.fitExpandedChildren,
focusAnimation: Duration(milliseconds: 500),
)3. Show Entire Subtree / ์ ์ฒด ์๋ธํธ๋ฆฌ ํ์
MindMapWidget(
data: myData,
nodeExpandCameraBehavior: NodeExpandCameraBehavior.fitExpandedSubtree,
focusMargin: EdgeInsets.all(30),
)final customStyle = MindMapStyle(
layout: MindMapLayout.radial,
nodeShape: NodeShape.circle,
enableAutoSizing: true,
connectionColor: Colors.blue,
animationDuration: Duration(milliseconds: 600),
defaultNodeColors: [Colors.blue, Colors.green, Colors.orange],
);MindMapWidget(
data: myData,
onNodeTap: (node) => print('Node tapped: ${node.title}'),
onNodeLongPress: (node) => _showNodeOptions(node),
onNodeExpandChanged: (node, isExpanded) =>
print('${node.title} ${isExpanded ? 'expanded' : 'collapsed'}'),
);| Layout / ๋ ์ด์์ | Description / ์ค๋ช |
|---|---|
MindMapLayout.right |
Traditional right-expanding / ์ค๋ฅธ์ชฝ ํ์ฅ |
MindMapLayout.left |
Left-expanding / ์ผ์ชฝ ํ์ฅ |
MindMapLayout.top |
Upward-expanding / ์์ชฝ ํ์ฅ |
MindMapLayout.bottom |
Downward-expanding / ์๋์ชฝ ํ์ฅ |
MindMapLayout.radial |
Circular arrangement / ์ํ ๋ฐฐ์น |
MindMapLayout.horizontal |
Left-right split / ์ข์ฐ ๋ถํ |
MindMapLayout.vertical |
Top-bottom split / ์ํ ๋ถํ |
| Shape / ๋ชจ์ | Description / ์ค๋ช |
|---|---|
NodeShape.roundedRectangle |
Rounded corners (default) / ๋ฅ๊ทผ ๋ชจ์๋ฆฌ (๊ธฐ๋ณธ) |
NodeShape.circle |
Perfect circle / ์์ ํ ์ |
NodeShape.rectangle |
Sharp corners / ๋ ์นด๋ก์ด ๋ชจ์๋ฆฌ |
NodeShape.diamond |
Diamond shape / ๋ค์ด์๋ชฌ๋ ๋ชจ์ |
NodeShape.hexagon |
Six-sided polygon / ์ก๊ฐํ |
NodeShape.ellipse |
Oval shape / ํ์ํ |
- ์ต์ ํ๋ ๋ ๋๋ง / Optimized rendering with custom painters
- ๋์ ๊ฐ๊ฒฉ ๊ณ์ฐ / Smart spacing based on content
- ๋ฉ๋ชจ๋ฆฌ ํจ์จ์ / Minimal widget tree overhead
- ๋ถ๋๋ฌ์ด ์ ๋๋ฉ์ด์ / Hardware-accelerated animations
This project is licensed under the MIT License - see the LICENSE file for details.
์ด ํ๋ก์ ํธ๋ MIT ๋ผ์ด์ ์ค ํ์ ์์ต๋๋ค - ์์ธํ ๋ด์ฉ์ LICENSE ํ์ผ์ ์ฐธ์กฐํ์ธ์.
We welcome contributions! Whether you're fixing bugs, adding features, or improving documentation, your help is appreciated.
๊ธฐ์ฌ๋ฅผ ํ์ํฉ๋๋ค! ๋ฒ๊ทธ ์์ , ๊ธฐ๋ฅ ์ถ๊ฐ, ๋ฌธ์ ๊ฐ์ ๋ฑ ๋ชจ๋ ๋์์ ๊ฐ์ฌํ ๋ฐ๊ฒ ์ต๋๋ค.
-
๐ Found a bug? / ๋ฒ๊ทธ๋ฅผ ๋ฐ๊ฒฌํ์ จ๋์?
- Check existing issues first / ๊ธฐ์กด ์ด์๋ค์ ๋จผ์ ํ์ธํ์ธ์
- Use our Bug Report template / ๋ฒ๊ทธ ๋ฆฌํฌํธ ํ ํ๋ฆฟ์ ์ฌ์ฉํ์ธ์
-
๐ก Have a feature idea? / ๊ธฐ๋ฅ ์์ด๋์ด๊ฐ ์์ผ์ ๊ฐ์?
- Use our Feature Request template / ๊ธฐ๋ฅ ์์ฒญ ํ ํ๋ฆฟ์ ์ฌ์ฉํ์ธ์
-
โ Need help? / ๋์์ด ํ์ํ์ ๊ฐ์?
- Use our Question template / ์ง๋ฌธ ํ ํ๋ฆฟ์ ์ฌ์ฉํ์ธ์
-
๐ง Want to contribute code? / ์ฝ๋ ๊ธฐ์ฌ๋ฅผ ์ํ์๋์?
- Read our detailed Contributing Guide / ์์ธํ ๊ธฐ์ฌ ๊ฐ์ด๋๋ฅผ ์ฝ์ด๋ณด์ธ์
- Fork the repo, make changes, and submit a PR / ์ ์ฅ์๋ฅผ ํฌํฌํ๊ณ ๋ณ๊ฒฝ์ฌํญ์ ๋ง๋ ํ PR์ ์ ์ถํ์ธ์
git clone https://github.com/YOUR_USERNAME/reactive_mind_map.git
cd reactive_mind_map
flutter pub get
flutter runFor detailed development guidelines, coding standards, and contribution process, please see our Contributing Guide.
์์ธํ ๊ฐ๋ฐ ๊ฐ์ด๋๋ผ์ธ, ์ฝ๋ฉ ํ์ค, ๊ธฐ์ฌ ๊ณผ์ ์ ๊ธฐ์ฌ ๊ฐ์ด๋๋ฅผ ์ฐธ์กฐํ์ธ์.
If you encounter any issues or have feature requests, please file them in the GitHub Issues section.
์ด์๊ฐ ๋ฐ์ํ๊ฑฐ๋ ๊ธฐ๋ฅ ์์ฒญ์ด ์์ผ์๋ฉด GitHub Issues ์น์ ์ ๋ฑ๋กํด ์ฃผ์ธ์.
์ต์ ๋ณ๊ฒฝ์ฌํญ์ CHANGELOG.md๋ฅผ ํ์ธํ์ธ์.

