Skip to content

Instantly share code, notes, and snippets.

@misterfourtytwo
Created November 4, 2021 11:11
Show Gist options
  • Save misterfourtytwo/be84056a3664cae12a03204fb6b0d339 to your computer and use it in GitHub Desktop.
Save misterfourtytwo/be84056a3664cae12a03204fb6b0d339 to your computer and use it in GitHub Desktop.
circular menu widget example
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