Creating a Circular Menu in Flutter
In this article, I will guide you through the basic development process of creating a circular menu in Flutter using the circle_list
package. This development will enable users to use and customize a circular menu. Users will be able to rotate and select menu items and perform specific actions based on the selected items.
Setting Up the Project
Open a new terminal. Navigate to the directory where you want to create a new Flutter project using terminal commands. Then, use the following command to create a new Flutter project:
flutter create example_project
You can replace “example_project” with the name of the project you want to create.
Building the UI
At the initial stage, I am not adding the package directly to my dependencies (pubspec.yaml) because I won’t use the package as it is; instead, I will make modifications to it. Therefore, I am copying the related files of the package from GitHub to our local project.
https://github.com/asjqkkkk/circle_list/tree/master
I will open a new folder within the lib
folder in the Flutter project to add the package files. CircularMenu on my case.
Inside, I will add to visualize the basic interface, and explore what changes we can make to it.
Modifications
In the basic code, an icon from the Flutter package is used in different colors. To use different icons, first, you need to download the icons you want to use.
Afterward, create an “images” folder using terminal commands. You place these icons you want to use in the “images” folder within your project. To access these icons in the folder, you need to make the following changes in the pubspec.yaml file:
- Locate the
pubspec.yaml
file. - Find the
assets
line and remove the '#' symbol in front of 'assets'. - Add ‘ — images/ ‘ under the ‘assets’ line.
- Save the file by pressing Ctrl+S.
flutter:
assets:
- images/
To see different icons within the circle menu, we need to revise the code. First, we create a /modify main.dart
file, and the code should look like the following:
import 'package:flutter/material.dart';
import 'package:circular_menu/Components/home_page.dart';
void main() => runApp(circular_menu());
class circular_menu extends StatelessWidget{
@override
Widget build(BuildContext context){
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
scaffoldBackgroundColor: Colors.black,
//background color
),
home: CircleMenu(),
);
}
}
And then, we will create a different Dart file to display the icons in the menu using the following code. I named it CircleMenu.dart
class CircleMenu extends StatelessWidget {
List<String> imgPaths= [
'images/Flutter.png',
'images/React.png',
'images/Python.png',
'images/C#.png',
'images/HTML.png',
'images/PHP.png',
'images/NodeJS.png',
'images/SQL.png',
];
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: CircleList(
origin: Offset(0, 0),
children: imgPaths.map((imgPaths){
return GestureDetector(
onTap: (){ },
child: Image.asset(imgPaths),
);
}).toList(),
centerWidget: GestureDetector(
onTap: () { },
child: ClipRRect(
borderRadius: BorderRadius.all(Radius.circular(100)),
child: Image.asset("images/powerbutton.png", scale: 1.30,)),
),
),
)
);
}
}
I left the “onTap” function open for later use in opening the pages when these added icons are clicked. I store the icons one by one in a List within a String and use a map to cycle through these icons sequentially. In the Center widget, I directly added the icon by specifying the path using the image.asset
command.
To be able to call this code in main.dart
, you need to import it as follows (the import may vary depending on the folder where you created the Dart file; my created Dart file is named home_page.dart
, and it is located within the component
folder):
import 'package:component/home_page.dart';
Please replace 'component'
with the actual folder name where you created the Dart file, and 'home_page.dart'
with the name of your Dart file. This will allow you to import and use the code from the specified Dart file in your main.dart
.
Icon Click Functionality
I will share two different pieces of code for opening a new page when icons are clicked. You can find the working logic for both of them below.
Alternative One ↓
class CircleMenu extends StatelessWidget {
List<String> imgPaths= [
'images/Flutter.png',
'images/React.png',
'images/Python.png',
'images/C#.png',
'images/HTML.png',
'images/PHP.png',
'images/NodeJS.png',
'images/SQL.png',
];
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: CircleList(
origin: Offset(0, 0),
children: imgPaths.map((imgPaths){
return GestureDetector(
onTap: (){
switch(imgPaths){
case "images/Flutter.png":
Navigator.push(context, MaterialPageRoute(builder: (context) => FlutterPage()));
case "images/React.png":
Navigator.push(context, MaterialPageRoute(builder: (context) => ReactPage()));
case "images/Python.png":
Navigator.push(context, MaterialPageRoute(builder: (context) => PythonPage()));
case "images/C#.png":
Navigator.push(context, MaterialPageRoute(builder: (context) => CPage()));
case "images/HTML.png":
Navigator.push(context, MaterialPageRoute(builder: (context) => HTMLPage()));
case "images/PHP.png":
Navigator.push(context, MaterialPageRoute(builder: (context) => PHPPage()));
case "images/NodeJS.png":
Navigator.push(context, MaterialPageRoute(builder: (context) => NodeJSPage()));
case "images/SQL.png":
Navigator.push(context, MaterialPageRoute(builder: (context) => SQLPage()));
//I'm keeping the pages I use here within the "pages.dart" file,
//I've imported "pages.dart" using the method I explained earlier.
}
},
child: Image.asset(imgPaths),
);
}).toList(),
centerWidget: GestureDetector(
onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (context) => PowerPage()));
},
child: ClipRRect(
borderRadius: BorderRadius.all(Radius.circular(100)),
child: Image.asset("images/powerbutton.png", scale: 1.30,)),
),
),
)
);
}
}
Switch-case is a decision-making structure that evaluates an expression and performs different actions based on its value. It’s a more concise alternative to a series of if-else statements.
When elements in the imgPaths
array match with a case, they are directed to the respective page using Navigator.push
. For the Center
widget, we opened a separate onTap
, but here we are directly navigating with Navigator.push
.
Alternative Two (More Solid) ↓
class CircleMenu extends StatelessWidget {
final Map<String, Widget> pageMap = {
'images/Flutter.png': FlutterPage(),
'images/React.png': ReactPage(),
'images/Python.png': PythonPage(),
'images/C#.png': CPage(),
'images/HTML.png': HTMLPage(),
'images/PHP.png': PHPPage(),
'images/NodeJS.png': NodeJSPage(),
'images/SQL.png': SQLPage(),
};
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: CircleList(
origin: Offset(0, 0),
children: pageMap.keys.map((item){
return GestureDetector(
onTap: (){
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => pageMap[item]!,
),
);
},
child: Image.asset(item),
);
}).toList(),
centerWidget: GestureDetector(
onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (context) => PowerPage()));
},
child: ClipRRect(
borderRadius: BorderRadius.all(Radius.circular(100)),
child: Image.asset("images/powerbutton.png", scale: 1.30,)),
),
),
)
);
}
}
The advantage of this code is that it simplifies the writing and reduces the workload. Instead of manually entering individual paths for both images and the pages they should open when clicked, it stores both the images and the paths to be opened in a single list.
In the line pageMap.keys.map
, the map
function iterates through each item in the array. Inside the onTap
, pageMap[item]
is used to access the path associated with the clicked icon's name, allowing navigation to the corresponding page when the onTap
function is triggered.
Changes to The Package
Changes can be made to the package’s code files according to the intended usage. The code directory provides a feature definition that allows adding border colors for the inner and outer circles, in addition to the package’s own properties.
class CircleList extends StatefulWidget{
final Color? outerCircleBorderColor;
final Color? innerCircleBorderColor;
//other commands...
CircleList({
this.outerCircleBorderColor,
this.innerCircleBorderColor,
//other commands...
}
);
//other commands...
}
The code directories containing the functions of the added properties.
boxShadow: [
BoxShadow(
color: widget.outerCircleBorderColor?? Colors.transparent,
blurRadius: 0,
spreadRadius: 5,
)
]
boxShadow: [
BoxShadow(
color: widget.outerCircleBorderColor?? Colors.transparent,
blurRadius: 0,
spreadRadius: 5,
)
]
These codes provide functionality to the added features by being placed inside the ‘decoration: BoxDecoration’.
●You can see the usage of these added features in the code example below.
return Scaffold(
cody: Center(
child:CircleList(
innerCircleColor: Colors.blue,
outerCircleColor: Colors.yellow,
outerCircleBorderColor: Colors.purple,
innerCircleBorderColor: Colors.white,
//other commands...
),
)
);
Changing Menu Size
Widget build(BuildContext context) {
final double outerRadius = widget.outerRadius ?? outCircleDiameter / 2.1;
final double innerRadius = widget.innerRadius ?? outerRadius / 2.1;
final double betweenRadius = (outerRadius + innerRadius) / 2.05;
//other commands...
}
You can adjust the dimensions of the circular menu by changing the values of 2.1, 2.1, and 2.05. (Increasing the values will make the menu smaller.
What’s Next
In the future, different updates can be made to this menu. The first of these updates could involve loading educational videos when icons are clicked, or providing resource recommendations for the programming language on these opened pages. Additionally, links to these resources can be added, and these pages can be customized in various ways to suit their respective purposes.