The Custom Popup menu looks like this on my Pixel3. It might look different on another phone.
I don't want to waste your time so you can copy-paste the code below. There's a longer explanation further down
Tl;dr
Create an Overlay Entry and display it when the menu button is pressed. .You can add padding and everything else.
import 'package:flutter/material.dart';
class MainScreen extends StatefulWidget {
@override
_MainScreenState createState() => _MainScreenState();
}
class _MainScreenState extends State<MainScreen> {
GlobalKey _globalKey = new GlobalKey();
OverlayEntry _overlayEntry;
Size menuSize;
Offset menuOffset;
bool isMenuVisible = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.white,
leading: GestureDetector(
onTap: (){
(isMenuVisible) ? CloseCustomPopUpMenu() : OpenCustomPopUpMenu();
},
child: Container(
key: _globalKey,
height: 20,
width: 20,
child: Icon(
Icons.format_list_bulleted_rounded,
color: Colors.black,
))
),
title: Text("Pro Color Palette",
style: TextStyle(color: Colors.black),),
),
body: Text("This took long \u2639"),
);
}
void OpenCustomPopUpMenu() {
RenderBox renderBox = _globalKey.currentContext.findRenderObject();
menuSize = renderBox.size*2;
menuOffset = renderBox.localToGlobal(Offset(0,-55));
_overlayEntry = _createOverlayEntry();
Overlay.of(context).insert(_overlayEntry);
isMenuVisible = !isMenuVisible;
}
void CloseCustomPopUpMenu() {
_overlayEntry.remove();
isMenuVisible = !isMenuVisible;
}
OverlayEntry _createOverlayEntry() {
return OverlayEntry(builder: (context) {
return Positioned(
top: menuOffset.dy + menuSize.height,
left: menuOffset.dx,
width:menuSize.width *2,
child: Material(
color: Colors.transparent,
child: CustomPopupMenu(),
),
);
},
);
}
}
class CustomPopupMenu extends StatefulWidget {
@override
_CustomPopupMenuState createState() => _CustomPopupMenuState();
}
final Container ImprovisedDivider = Container(
height: 1,
width:300,
color: Colors.black,);
class _CustomPopupMenuState extends State<CustomPopupMenu> {
@override
Widget build(BuildContext context) {
return Container(
height: 175,
color: Colors.white,
child: Column(
children:<Widget>[
ImprovisedDivider,
ListTile(
leading:Icon(
Icons.save,
color: Colors.black,
size:20 ,),
contentPadding: EdgeInsets.only(left: 1.0),
title:Text("Save palette",),
),
ImprovisedDivider,
ListTile(
leading:Icon(
Icons.delete,
color: Colors.black,
size:20 ,),
contentPadding: EdgeInsets.only(left: 1.0),
title:Text("Clear palette"),
),
ImprovisedDivider,
ListTile(
leading:Icon(
Icons.upload_file,
color: Colors.black,
size:20 ,),
contentPadding: EdgeInsets.only(left: 1.0),
title:Text("Upload a palette"),
),
ImprovisedDivider,
],
),
);
}
}
Long explanation
You need an overlay to build a custom popup menu. Overlay Docs. It lets you build widgets that float above other widgets. We'll need a GlobalKey to
identify the container holding the menu button. We also need a Gesture Detector to identify screen taps.
Steps:
- Create menu button, declare variables for the GlobalKey, Overlay, size of the menu and offset of the menu.
class MainScreen extends StatefulWidget {
@override
_MainScreenState createState() => _MainScreenState();
}
class _MainScreenState extends State<MainScreen>
{
//Initialize global key
GlobalKey _globalKey = new GlobalKey();
//Create new overlay entry
OverlayEntry _overlayEntry;
//Store the size of the menu
Size menuSize;
//Store the menu's offset
Offset menuOffset;
//This is used to ooen and close the menu
bool isMenuVisible = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.white,
leading: GestureDetector(
onTap: (){
(isMenuVisible) ? CloseCustomPopUpMenu() : OpenCustomPopUpMenu();
},
child: Container(
key: _globalKey,//
height: 20,
width: 20,
child: Icon(
Icons.format_list_bulleted_rounded,
color: Colors.black,
))
),
title: Text("Pro Color Palette $isMenuVisible",
style: TextStyle(color: Colors.black),),
),
body: Text("....."),
);
}
- Create a Custom PopupMenu widget.(I'm using a bunch of ListTiles and improvised dividers). Note, we can only change the menu's height. It inherits width from it's parent.
class CustomPopupMenu extends StatefulWidget {
@override
_CustomPopupMenuState createState() => _CustomPopupMenuState();
}
final Container ImprovisedDivider = Container(
height: 1,
width:300,
color: Colors.black,);
class _CustomPopupMenuState extends State<CustomPopupMenu> {
@override
Widget build(BuildContext context) {
return Container(
height: 180,//Change Menu height here
color: Colors.white,
child: Column(
children:<Widget>[
ImprovisedDivider,
ListTile(
leading:Icon(
Icons.save,
color: Colors.black,
size:20 ,),
contentPadding: EdgeInsets.only(left: 1.0),
title:Text("Save palette",),
),
ImprovisedDivider,
ListTile(
leading:Icon(
Icons.delete,
color: Colors.black,
size:20 ,),
contentPadding: EdgeInsets.only(left: 1.0),
title:Text("Clear palette"),
),
ImprovisedDivider,
ListTile(
leading:Icon(
Icons.upload_file,
color: Colors.black,
size:20 ,),
contentPadding: EdgeInsets.only(left: 1.0),
title:Text("Upload a palette"),
),
ImprovisedDivider,
],
),
);
}
}
- Create the overlay entry int it's own function.We use a Positioned widget to put it in a specific place on the screen.
OverlayEntry _createOverlayEntry() {
return OverlayEntry(builder: (context) {
return Positioned(
top: menuOffset.dy + menuSize.height,
left: menuOffset.dx,
width:menuSize.width *2,//Change menu width here
child: Material(
color: Colors.transparent,
child: CustomPopupMenu(),//Call our Custom menu here
),
);
},
);
}
- Logic to display the menu(Also change width and offset from edge of screen).The global key is useful here. It allows us to get information about the parent container like it's position and it's size. Note
void OpenCustomPopUpMenu() {
RenderBox renderBox = _globalKey.currentContext.findRenderObject();
menuSize = renderBox.size*2;//Change width here
menuOffset = renderBox.localToGlobal(Offset(0,-60));//Change offset from screen edge here
_overlayEntry = _createOverlayEntry();
Overlay.of(context).insert(_overlayEntry);
isMenuVisible = !isMenuVisible;
}
- Logic to close the menu
void CloseCustomPopUpMenu() {
_overlayEntry.remove();
isMenuVisible = !isMenuVisible;
}