Popup Menu In Flutter :
Screenshot :

Popup Menu In Flutter :
1. main.dart
import 'package:flutter/material.dart'; import 'popup_menu.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', home: MyHomePage(title: 'Popup Menu Example'), ); } } class MyHomePage extends StatefulWidget { MyHomePage({Key key, this.title}) : super(key: key); final String title; @override _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { PopupMenu menu; GlobalKey btnKey = GlobalKey(); GlobalKey btnKey2 = GlobalKey(); @override void initState() { super.initState(); menu = PopupMenu(items: [ MenuItem( title: 'Mail', image: Icon( Icons.mail, color: Colors.white, )), MenuItem( title: 'Power', image: Icon( Icons.power, color: Colors.white, )), MenuItem( title: 'Setting', image: Icon( Icons.settings, color: Colors.white, )), MenuItem( title: 'PopupMenu', image: Icon( Icons.menu, color: Colors.white, )) ], onClickMenu: onClickMenu, onDismiss: onDismiss, maxColumn: 1); } void onClickMenu(MenuItemProvider item) { print('Click menu -> ${item.menuTitle}'); } void onDismiss() { print('Menu is closed'); } @override Widget build(BuildContext context) { PopupMenu.context = context; return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.spaceAround, children: <Widget>[ Container( child: MaterialButton( height: 45.0, key: btnKey, onPressed: maxColumn, child: Text('Show Menu'), ), ), Container( child: MaterialButton( key: btnKey2, height: 45.0, onPressed: customBackground, child: Text('Show Menu'), ), ) ], ), ), ); } void maxColumn() { PopupMenu menu = PopupMenu( // backgroundColor: Colors.teal, // lineColor: Colors.tealAccent, maxColumn: 3, items: [ MenuItem(title: 'Copy', image: Image.asset('assets/copy.png')), MenuItem( title: 'Power', image: Icon( Icons.power, color: Colors.white, )), MenuItem( title: 'Setting', image: Icon( Icons.settings, color: Colors.white, )), MenuItem( title: 'PopupMenu', image: Icon( Icons.menu, color: Colors.white, )) ], onClickMenu: onClickMenu, onDismiss: onDismiss); menu.show(widgetKey: btnKey); } void customBackground() { PopupMenu menu = PopupMenu( // backgroundColor: Colors.teal, // lineColor: Colors.tealAccent, // maxColumn: 2, items: [ MenuItem(title: 'Copy', image: Image.asset('assets/copy.png')), MenuItem( title: 'Home', // textStyle: TextStyle(fontSize: 10.0, color: Colors.tealAccent), image: Icon( Icons.home, color: Colors.white, )), MenuItem( title: 'Mail', image: Icon( Icons.mail, color: Colors.white, )), MenuItem( title: 'Power', image: Icon( Icons.power, color: Colors.white, )), MenuItem( title: 'Setting', image: Icon( Icons.settings, color: Colors.white, )), MenuItem( title: 'PopupMenu', image: Icon( Icons.menu, color: Colors.white, )) ], onClickMenu: onClickMenu, onDismiss: onDismiss); menu.show(widgetKey: btnKey2); } }
2. popup_menu.dart
library popup_menu; import 'dart:core'; import 'dart:math'; import 'dart:ui'; import 'package:flutter/material.dart'; import 'triangle_painter.dart'; abstract class MenuItemProvider { String get menuTitle; Widget get menuImage; TextStyle get menuTextStyle; } class MenuItem extends MenuItemProvider { Widget image; String title; var userInfo; TextStyle textStyle; MenuItem({this.title, this.image, this.userInfo, this.textStyle}); @override Widget get menuImage => image; @override String get menuTitle => title; @override TextStyle get menuTextStyle => textStyle ?? TextStyle(color: Color(0xffc5c5c5), fontSize: 10.0); } enum MenuType { big, oneLine } typedef MenuClickCallback = Function(MenuItemProvider item); class PopupMenu { static var itemWidth = 72.0; static var itemHeight = 65.0; static var arrowHeight = 10.0; OverlayEntry _entry; List<MenuItem> items; int _row; // row count int _col; // col count // The left top point of this menu. Offset _offset; VoidCallback dismissCallback; MenuClickCallback onClickMenu; Rect _showRect; bool _isDown = true; static BuildContext context; // The max column count, default is 4. int _maxColumn; Color _backgroundColor; Color _highlightColor; Color _lineColor; PopupMenu( {MenuClickCallback onClickMenu, BuildContext context, VoidCallback onDismiss, int maxColumn, Color backgroundColor, Color highlightColor, Color lineColor, List<MenuItem> items}) { this.onClickMenu = onClickMenu; this.dismissCallback = onDismiss; this.items = items; this._maxColumn = maxColumn ?? 4; this._backgroundColor = backgroundColor ?? Color(0xff232323); this._lineColor = lineColor ?? Color(0xff353535); this._highlightColor = highlightColor ?? Color(0x55000000); if (context != null) { PopupMenu.context = context; } } void show({Rect rect, GlobalKey widgetKey, List<MenuItem> items}) { if (rect == null && widgetKey == null) { print("'rect' and 'key' can't be both null"); return; } this.items = items ?? this.items; this._showRect = rect ?? PopupMenu.getWidgetGlobalRect(widgetKey); this.dismissCallback = dismissCallback; _calculatePosition(PopupMenu.context); _entry = OverlayEntry(builder: (context) { return buildPopupMenuLayout(_offset); }); Overlay.of(PopupMenu.context).insert(_entry); } static Rect getWidgetGlobalRect(GlobalKey key) { RenderBox renderBox = key.currentContext.findRenderObject(); var offset = renderBox.localToGlobal(Offset.zero); return Rect.fromLTWH( offset.dx, offset.dy, renderBox.size.width, renderBox.size.height); } void _calculatePosition(BuildContext context) { _col = _calculateColCount(); _row = _calculateRowCount(); _offset = _calculateOffset(PopupMenu.context); } Offset _calculateOffset(BuildContext context) { double dx = _showRect.left + _showRect.width / 2.0 - menuWidth() / 2.0; if (dx < 10.0) { dx = 10.0; } double dy = _showRect.top - menuHeight(); if (dy <= MediaQuery.of(context).padding.top + 10) { // The have not enough space above, show menu under the widget. dy = arrowHeight + _showRect.height + _showRect.top; _isDown = false; } else { _isDown = true; } return Offset(dx, dy); } double menuWidth() { return itemWidth * _col; } // This height exclude the arrow double menuHeight() { return itemHeight * _row; } LayoutBuilder buildPopupMenuLayout(Offset offset) { return LayoutBuilder(builder: (context, constraints) { return GestureDetector( behavior: HitTestBehavior.translucent, onTap: () { dismiss(); }, child: Stack( children: <Widget>[ // triangle arrow Positioned( left: _showRect.left + _showRect.width / 2.0 - 7.5, top: _isDown ? offset.dy + menuHeight() : offset.dy - arrowHeight, child: CustomPaint( size: Size(15.0, arrowHeight), painter: TrianglePainter(isDown: _isDown, color: _backgroundColor), ), ), // menu content Positioned( left: offset.dx, top: offset.dy, child: Container( width: menuWidth(), height: menuHeight(), child: Column( children: <Widget>[ ClipRRect( borderRadius: BorderRadius.circular(10.0), child: Container( width: menuWidth(), height: menuHeight(), decoration: BoxDecoration( color: _backgroundColor, borderRadius: BorderRadius.circular(10.0)), child: Column( children: _createRows(), ), )), ], ), ), ) ], ), ); }); } List<Widget> _createRows() { List<Widget> rows = []; for (int i = 0; i < _row; i++) { Color color = (i < _row - 1 && _row != 1) ? _lineColor : Colors.transparent; Widget rowWidget = Container( decoration: BoxDecoration(border: Border(bottom: BorderSide(color: color))), height: itemHeight, child: Row( children: _createRowItems(i), ), ); rows.add(rowWidget); } return rows; } List<Widget> _createRowItems(int row) { List<MenuItem> subItems = items.sublist(row * _col, min(row * _col + _col, items.length)); List<Widget> itemWidgets = []; int i = 0; for (var item in subItems) { itemWidgets.add(_createMenuItem( item, i < (_col - 1), )); i++; } return itemWidgets; } // calculate row count int _calculateRowCount() { if (items == null || items.length == 0) { debugPrint('error menu items can not be null'); return 0; } int itemCount = items.length; if (_calculateColCount() == 1) { return itemCount; } int row = (itemCount - 1) ~/ _calculateColCount() + 1; return row; } // calculate col count int _calculateColCount() { if (items == null || items.length == 0) { debugPrint('error menu items can not be null'); return 0; } int itemCount = items.length; if (_maxColumn != 4 && _maxColumn > 0) { return _maxColumn; } if (itemCount == 4) { return 2; } if (itemCount <= _maxColumn) { return itemCount; } if (itemCount == 5) { return 3; } if (itemCount == 6) { return 3; } return _maxColumn; } double get screenWidth { double width = window.physicalSize.width; double ratio = window.devicePixelRatio; return width / ratio; } Widget _createMenuItem(MenuItem item, bool showLine) { return _MenuItemWidget( item: item, showLine: showLine, clickCallback: itemClicked, lineColor: _lineColor, backgroundColor: _backgroundColor, highlightColor: _highlightColor, ); } void itemClicked(MenuItemProvider item) { if (onClickMenu != null) { onClickMenu(item); } dismiss(); } void dismiss() { _entry.remove(); if (dismissCallback != null) { dismissCallback(); } } } class _MenuItemWidget extends StatefulWidget { final MenuItem item; final bool showLine; final Color lineColor; final Color backgroundColor; final Color highlightColor; final Function(MenuItemProvider item) clickCallback; _MenuItemWidget( {this.item, this.showLine = false, this.clickCallback, this.lineColor, this.backgroundColor, this.highlightColor}); @override State<StatefulWidget> createState() { return _MenuItemWidgetState(); } } class _MenuItemWidgetState extends State<_MenuItemWidget> { var highlightColor = Color(0x55000000); var color = Color(0xff232323); @override void initState() { color = widget.backgroundColor; highlightColor = widget.highlightColor; super.initState(); } @override Widget build(BuildContext context) { return GestureDetector( onTapDown: (details) { color = highlightColor; setState(() {}); }, onTapUp: (details) { color = widget.backgroundColor; setState(() {}); }, onLongPressEnd: (details) { color = widget.backgroundColor; setState(() {}); }, onTap: () { if (widget.clickCallback != null) { widget.clickCallback(widget.item); } }, child: Container( width: PopupMenu.itemWidth, height: PopupMenu.itemHeight, decoration: BoxDecoration( color: color, border: Border( right: BorderSide( color: widget.showLine ? widget.lineColor : Colors.transparent))), child: _createContent()), ); } Widget _createContent() { if (widget.item.menuImage != null) { // image and text return Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Container( width: 30.0, height: 30.0, child: widget.item.menuImage, ), Container( height: 22.0, child: Material( color: Colors.transparent, child: Text( widget.item.menuTitle, style: widget.item.menuTextStyle, ), ), ) ], ); } else { // only text return Container( child: Center( child: Material( color: Colors.transparent, child: Text( widget.item.title, style: widget.item.menuTextStyle, ), ), ), ); } } }
3. triangle_painter.dart
import 'package:flutter/rendering.dart'; class TrianglePainter extends CustomPainter { bool isDown; Color color; TrianglePainter({this.isDown = true, this.color}); @override void paint(Canvas canvas, Size size) { Paint _paint = new Paint(); _paint.strokeWidth = 2.0; _paint.color = color; _paint.style = PaintingStyle.fill; Path path = new Path(); if (isDown) { path.moveTo(0.0, -1.0); path.lineTo(size.width, -1.0); path.lineTo(size.width / 2.0, size.height); } else { path.moveTo(size.width / 2.0, 0.0); path.lineTo(0.0, size.height + 1); path.lineTo(size.width, size.height + 1); } canvas.drawPath(path, _paint); } @override bool shouldRepaint(CustomPainter oldDelegate) { return true; } }
For more information about Flutter. visit www.fluttertutorial.in