Instantly share code, notes, and snippets.
Created
November 4, 2021 11:11
-
Star
0
(0)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
Save misterfourtytwo/be84056a3664cae12a03204fb6b0d339 to your computer and use it in GitHub Desktop.
circular menu widget example
This file contains hidden or 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 'dart:math' as math; | |
import 'package:flutter/material.dart'; | |
void main() { | |
runApp(MyApp()); | |
} | |
class MyApp extends StatelessWidget { | |
@override | |
Widget build(BuildContext context) { | |
return MaterialApp( | |
title: 'Flutter Demo', | |
theme: ThemeData( | |
primarySwatch: Colors.blue, | |
visualDensity: VisualDensity.adaptivePlatformDensity, | |
), | |
home: const MyHomePage(title: 'Circular menu widget'), | |
); | |
} | |
} | |
class MyHomePage extends StatefulWidget { | |
final String title; | |
const MyHomePage({Key? key, required this.title}) : super(key: key); | |
@override | |
_MyHomePageState createState() => _MyHomePageState(); | |
} | |
class _MyHomePageState extends State<MyHomePage> { | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
appBar: AppBar( | |
title: Text(widget.title), | |
), | |
body:const CircularMenuWidget( | |
childData: [ | |
ChildData( | |
color: Colors.red, | |
icon: Icons.ac_unit, | |
title: 'screen 1', | |
), | |
ChildData( | |
color: Colors.green, | |
icon: Icons.access_alarm, | |
title: 'screen 2', | |
), | |
ChildData( | |
color: Colors.blue, | |
icon: Icons.baby_changing_station, | |
title: 'screen 3', | |
), | |
ChildData( | |
color: Colors.pink, | |
icon: Icons.cached, | |
title: 'screen 4', | |
), | |
ChildData( | |
color: Colors.teal, | |
icon: Icons.tab_unselected_rounded, | |
title: 'screen 5', | |
), | |
], | |
), | |
); | |
} | |
} | |
class ChildData { | |
final Color color; | |
final IconData icon; | |
final String title; | |
const ChildData({ | |
required this.color, | |
required this.icon, | |
required this.title, | |
}); | |
} | |
class CircularMenuWidget extends StatefulWidget { | |
final List<ChildData> childData; | |
final IconData menuIcon; | |
const CircularMenuWidget({ | |
Key? key, | |
required this.childData, | |
this.menuIcon = Icons.menu, | |
}) : super(key: key); | |
@override | |
_CircularMenuWidgetState createState() => _CircularMenuWidgetState(); | |
} | |
double buttonSize = 48; | |
class _CircularMenuWidgetState extends State<CircularMenuWidget> | |
with TickerProviderStateMixin { | |
bool _isOpen = false; | |
late AnimationController _animationController; | |
@override | |
void initState() { | |
_animationController = AnimationController( | |
duration: const Duration(milliseconds: 500), | |
vsync: this, | |
); | |
super.initState(); | |
} | |
void close() { | |
_animationController.reverse().whenComplete(() { | |
_isOpen = false; | |
if (mounted) setState(() {}); | |
}); | |
} | |
void open() { | |
_isOpen = true; | |
if (mounted) setState(() {}); | |
_animationController.forward(); | |
} | |
@override | |
void dispose() { | |
_animationController.dispose(); | |
super.dispose(); | |
} | |
@override | |
Widget build(BuildContext context) { | |
final double angleFraction = 2 * math.pi / widget.childData.length; | |
return AnimatedBuilder( | |
animation: _animationController, | |
builder: (BuildContext context, Widget? child) { | |
final double distance = _animationController.value * 75.0; | |
return Stack( | |
alignment: Alignment.center, | |
children: <Widget>[ | |
const SizedBox.expand(), | |
_CircularMenuIconButton( | |
isOpen: _isOpen, | |
onPressed: () => _isOpen ? close() : open(), | |
icon: widget.menuIcon, | |
), | |
if (_isOpen) | |
for (int i = 0; i < widget.childData.length; i++) | |
_CircularMenuItemButton( | |
angle: angleFraction * i - math.pi / 2, | |
distance: distance, | |
data: widget.childData[i], | |
onClose: close, | |
), | |
], | |
); | |
}, | |
); | |
} | |
} | |
class _CircularMenuItemButton extends StatelessWidget { | |
final double angle; | |
final double distance; | |
final ChildData data; | |
final VoidCallback onClose; | |
const _CircularMenuItemButton({ | |
Key? key, | |
required this.angle, | |
required this.distance, | |
required this.data, | |
required this.onClose, | |
}) : super(key: key); | |
@override | |
Widget build(BuildContext context) { | |
return Transform.translate( | |
offset: Offset.fromDirection( | |
angle, | |
distance, | |
), | |
child: RawMaterialButton( | |
constraints: BoxConstraints.tightFor( | |
height: buttonSize, | |
width: buttonSize, | |
), | |
shape: const CircleBorder(), | |
onPressed: () { | |
Navigator.of(context).push( | |
MaterialPageRoute( | |
builder: (context) => MyScreen( | |
title: data.title, | |
), | |
), | |
); | |
onClose(); | |
}, | |
fillColor: data.color, | |
child: Icon(data.icon), | |
), | |
); | |
} | |
} | |
class _CircularMenuIconButton extends StatelessWidget { | |
final bool isOpen; | |
final IconData icon; | |
final VoidCallback onPressed; | |
const _CircularMenuIconButton({ | |
Key? key, | |
this.isOpen = false, | |
required this.onPressed, | |
required this.icon, | |
}) : super(key: key); | |
@override | |
Widget build(BuildContext context) { | |
return RawMaterialButton( | |
constraints: BoxConstraints.tightFor( | |
height: buttonSize, | |
width: buttonSize, | |
), | |
fillColor: Colors.blue, | |
shape: const CircleBorder(), | |
onPressed: onPressed, | |
child: Center( | |
child: Icon(isOpen ? Icons.close : icon), | |
), | |
); | |
} | |
} | |
class MyScreen extends StatelessWidget { | |
final String title; | |
const MyScreen({ | |
Key? key, | |
required this.title, | |
}) : super(key: key); | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
appBar: AppBar( | |
title: Text(title), | |
), | |
body: Center( | |
child: Text(title), | |
), | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment