├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── example └── demo.dart ├── flutter_radial_menu.iml ├── lib ├── flutter_radial_menu.dart └── src │ ├── arc_progress_indicator.dart │ ├── radial_menu.dart │ ├── radial_menu_button.dart │ ├── radial_menu_center_button.dart │ └── radial_menu_item.dart ├── pubspec.lock ├── pubspec.yaml ├── screenshots ├── demo.gif ├── simple_example.gif └── simple_example_code.png └── test └── flutter_radial_menu_test.dart /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .atom/ 3 | .dart_tool/ 4 | .idea 5 | .packages 6 | .pub/ 7 | packages 8 | .vscode/ 9 | .history/ 10 | ios/ 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [0.0.1] - 08/03/18 2 | 3 | * Initial release. 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Victor Choueiri 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![pub package](https://img.shields.io/pub/v/flutter_radial_menu.svg)](https://pub.dartlang.org/packages/flutter_radial_menu) 2 | 3 | # flutter_radial_menu 4 | 5 | A radial menu widget for Flutter. 6 | 7 | ![](screenshots/demo.gif). 8 | 9 | ## Installation 10 | 11 | Install the latest version [from pub](https://pub.dartlang.org/packages/flutter_radial_menu#-installing-tab-). 12 | 13 | ## Quick Start 14 | 15 | Import the package, create a `RadialMenu` and pass it your `RadialMenuItems`. 16 | 17 | ```dart 18 | import 'package:flutter/material.dart'; 19 | import 'package:flutter_radial_menu/flutter_radial_menu.dart'; 20 | 21 | void main() { 22 | runApp( 23 | new MaterialApp( 24 | home: new Scaffold( 25 | body: new Center( 26 | child: new RadialMenu( 27 | items: >[ 28 | const RadialMenuItem( 29 | value: 1, 30 | child: const Icon(Icons.add), 31 | ), 32 | const RadialMenuItem( 33 | value: -1, 34 | child: const Icon(Icons.remove), 35 | ) 36 | ], 37 | radius: 100.0, 38 | onSelected: print, 39 | ), 40 | ), 41 | ), 42 | ), 43 | ); 44 | } 45 | ``` 46 | 47 | ![](screenshots/simple_example.gif) 48 | 49 | --- 50 | 51 | Take a look at the [demo](example/demo.dart) for a more elaborate example. 52 | 53 | --- 54 | 55 | ## Customization 56 | 57 | ### RadialMenuItem 58 | 59 | | Parameter | Default | Description | 60 | |-----------------|------------------------------------------|------------------------------------------------------------------| 61 | | child | null | Usually an Icon widget, gets placed in the center of the button. | 62 | | value | null | Value that gets returned when this item is selected. | 63 | | tooltip | null | Tooltip displayed when the button is long-pressed. | 64 | | size | 48.0 | Size of the button. | 65 | | backgroundColor | Theme.of(context).primaryColor | Background fill color of the button. | 66 | | iconColor | Theme.of(context).primaryIconTheme.color | The color of the child icon. | 67 | 68 | ### RadialMenu 69 | 70 | | Parameter | Default | Description | 71 | |---------------------------|-------------------|----------------------------------------------------------------------------| 72 | | items | null | The list of possible items to select from. | 73 | | onSelected | null | Called when the user selects an item. | 74 | | radius | 100.0 | The radius of the arc used to lay out the items and draw the progress bar. | 75 | | menuAnimationDuration | 1000 milliseconds | Duration of the menu opening/closing animation. | 76 | | progressAnimationDuration | 1000 milliseconds | Duration of the action activation progress arc animation. | 77 | -------------------------------------------------------------------------------- /example/demo.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_radial_menu/flutter_radial_menu.dart'; 3 | 4 | enum MenuOptions { 5 | unread, 6 | share, 7 | archive, 8 | delete, 9 | backup, 10 | copy, 11 | } 12 | 13 | void main() { 14 | GlobalKey _menuKey = new GlobalKey(); 15 | 16 | final List> items = >[ 17 | new RadialMenuItem( 18 | value: MenuOptions.unread, 19 | child: new Icon( 20 | Icons.markunread, 21 | ), 22 | iconColor: Colors.white, 23 | backgroundColor: Colors.blue[400], 24 | tooltip: 'unread', 25 | ), 26 | new RadialMenuItem( 27 | value: MenuOptions.share, 28 | child: new Icon( 29 | Icons.share, 30 | ), 31 | iconColor: Colors.white, 32 | backgroundColor: Colors.green[400], 33 | ), 34 | new RadialMenuItem( 35 | value: MenuOptions.archive, 36 | child: new Icon( 37 | Icons.archive, 38 | ), 39 | iconColor: Colors.white, 40 | backgroundColor: Colors.yellow[400], 41 | ), 42 | new RadialMenuItem( 43 | value: MenuOptions.delete, 44 | child: new Icon( 45 | Icons.delete, 46 | ), 47 | iconColor: Colors.white, 48 | backgroundColor: Colors.red[400], 49 | ), 50 | new RadialMenuItem( 51 | value: MenuOptions.backup, 52 | child: new Icon( 53 | Icons.backup, 54 | ), 55 | iconColor: Colors.white, 56 | backgroundColor: Colors.black, 57 | ), 58 | new RadialMenuItem( 59 | value: MenuOptions.copy, 60 | child: new Icon( 61 | Icons.content_copy, 62 | ), 63 | iconColor: Colors.white, 64 | backgroundColor: Colors.indigo[400], 65 | ), 66 | ]; 67 | 68 | void _onItemSelected(MenuOptions value) { 69 | print(value); 70 | } 71 | 72 | runApp( 73 | new MaterialApp( 74 | home: new Scaffold( 75 | body: new Center( 76 | child: new RadialMenu( 77 | key: _menuKey, 78 | items: items, 79 | radius: 100.0, 80 | onSelected: _onItemSelected, 81 | ), 82 | ), 83 | floatingActionButton: new FloatingActionButton( 84 | child: new Icon(Icons.restore), 85 | onPressed: () => _menuKey.currentState.reset(), 86 | ), 87 | ), 88 | ), 89 | ); 90 | } 91 | -------------------------------------------------------------------------------- /flutter_radial_menu.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /lib/flutter_radial_menu.dart: -------------------------------------------------------------------------------- 1 | library flutter_radial_menu; 2 | 3 | export 'src/radial_menu.dart'; 4 | export 'src/radial_menu_item.dart'; 5 | -------------------------------------------------------------------------------- /lib/src/arc_progress_indicator.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math' as Math; 2 | 3 | import 'package:flutter/foundation.dart'; 4 | import 'package:flutter/material.dart'; 5 | 6 | /// Draws an [ActionIcon] and [_ArcProgressPainter] that represent an active action. 7 | /// As the provided [Animation] progresses the ActionArc grows into a full 8 | /// circle and the ActionIcon moves along it. 9 | class ArcProgressIndicator extends StatelessWidget { 10 | // required 11 | final Animation controller; 12 | final double radius; 13 | 14 | // optional 15 | final double startAngle; 16 | final double width; 17 | 18 | /// The color to use when filling the arc. 19 | /// 20 | /// Defaults to the accent color of the current theme. 21 | final Color color; 22 | final IconData icon; 23 | final Color iconColor; 24 | final double iconSize; 25 | 26 | // private 27 | final Animation _progress; 28 | 29 | ArcProgressIndicator({ 30 | @required this.controller, 31 | @required this.radius, 32 | this.startAngle = 0.0, 33 | this.width, 34 | this.color, 35 | this.icon, 36 | this.iconColor, 37 | this.iconSize, 38 | }) : _progress = new Tween(begin: 0.0, end: 1.0).animate(controller); 39 | 40 | @override 41 | Widget build(BuildContext context) { 42 | TextPainter _iconPainter; 43 | final ThemeData theme = Theme.of(context); 44 | final Color _iconColor = iconColor ?? theme.accentIconTheme.color; 45 | final double _iconSize = iconSize ?? IconTheme.of(context).size; 46 | 47 | if (icon != null) { 48 | _iconPainter = new TextPainter( 49 | textDirection: Directionality.of(context), 50 | text: new TextSpan( 51 | text: new String.fromCharCode(icon.codePoint), 52 | style: new TextStyle( 53 | inherit: false, 54 | color: _iconColor, 55 | fontSize: _iconSize, 56 | fontFamily: icon.fontFamily, 57 | package: icon.fontPackage, 58 | ), 59 | ), 60 | )..layout(); 61 | } 62 | 63 | return new CustomPaint( 64 | painter: new _ArcProgressPainter( 65 | controller: _progress, 66 | color: color ?? theme.accentColor, 67 | radius: radius, 68 | width: width ?? _iconSize * 2, 69 | startAngle: startAngle, 70 | icon: _iconPainter, 71 | ), 72 | ); 73 | } 74 | } 75 | 76 | class _ArcProgressPainter extends CustomPainter { 77 | // required 78 | final Animation controller; 79 | final Color color; 80 | final double radius; 81 | final double width; 82 | 83 | // optional 84 | final double startAngle; 85 | final TextPainter icon; 86 | 87 | _ArcProgressPainter({ 88 | @required this.controller, 89 | @required this.color, 90 | @required this.radius, 91 | @required this.width, 92 | this.startAngle = 0.0, 93 | this.icon, 94 | }) : super(repaint: controller); 95 | 96 | @override 97 | void paint(Canvas canvas, Size size) { 98 | Paint paint = new Paint() 99 | ..color = color 100 | ..strokeWidth = width 101 | ..strokeCap = StrokeCap.round 102 | ..style = PaintingStyle.stroke; 103 | 104 | final double sweepAngle = controller.value * 2 * Math.pi; 105 | 106 | canvas.drawArc( 107 | Offset.zero & size, 108 | startAngle, 109 | sweepAngle, 110 | false, 111 | paint, 112 | ); 113 | 114 | if (icon != null) { 115 | double angle = startAngle + sweepAngle; 116 | Offset offset = new Offset( 117 | (size.width / 2 - icon.size.width / 2) + radius * Math.cos(angle), 118 | (size.height / 2 - icon.size.height / 2) + radius * Math.sin(angle), 119 | ); 120 | 121 | icon.paint(canvas, offset); 122 | } 123 | } 124 | 125 | @override 126 | bool shouldRepaint(_ArcProgressPainter other) { 127 | return controller.value != other.controller.value || 128 | color != other.color || 129 | radius != other.radius || 130 | width != other.width || 131 | startAngle != other.startAngle || 132 | icon != other.icon; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /lib/src/radial_menu.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:math' as Math; 3 | 4 | import 'package:flutter/foundation.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter_radial_menu/src/arc_progress_indicator.dart'; 7 | import 'package:flutter_radial_menu/src/radial_menu_button.dart'; 8 | import 'package:flutter_radial_menu/src/radial_menu_center_button.dart'; 9 | import 'package:flutter_radial_menu/src/radial_menu_item.dart'; 10 | 11 | const double _radiansPerDegree = Math.pi / 180; 12 | final double _startAngle = -90.0 * _radiansPerDegree; 13 | 14 | typedef double ItemAngleCalculator(int index); 15 | 16 | /// A radial menu for selecting from a list of items. 17 | /// 18 | /// A radial menu lets the user select from a number of items. It displays a 19 | /// button that opens the menu, showing its items arranged in an arc. Selecting 20 | /// an item triggers the animation of a progress bar drawn at the specified 21 | /// [radius] around the central menu button. 22 | /// 23 | /// The type `T` is the type of the values the radial menu represents. All the 24 | /// entries in a given menu must represent values with consistent types. 25 | /// Typically, an enum is used. Each [RadialMenuItem] in [items] must be 26 | /// specialized with that same type argument. 27 | /// 28 | /// Requires one of its ancestors to be a [Material] widget. 29 | /// 30 | /// See also: 31 | /// 32 | /// * [RadialMenuItem], the widget used to represent the [items]. 33 | /// * [RadialMenuCenterButton], the button used to open and close the menu. 34 | class RadialMenu extends StatefulWidget { 35 | /// Creates a dropdown button. 36 | /// 37 | /// The [items] must have distinct values. 38 | /// 39 | /// The [radius], [menuAnimationDuration], and [progressAnimationDuration] 40 | /// arguments must not be null (they all have defaults, so do not need to be 41 | /// specified). 42 | const RadialMenu({ 43 | Key key, 44 | @required this.items, 45 | @required this.onSelected, 46 | this.radius = 100.0, 47 | this.menuAnimationDuration = const Duration(milliseconds: 1000), 48 | this.progressAnimationDuration = const Duration(milliseconds: 1000), 49 | }) : assert(radius != null), 50 | assert(menuAnimationDuration != null), 51 | assert(progressAnimationDuration != null), 52 | super(key: key); 53 | 54 | /// The list of possible items to select among. 55 | final List> items; 56 | 57 | /// Called when the user selects an item. 58 | final ValueChanged onSelected; 59 | 60 | /// The radius of the arc used to lay out the items and draw the progress bar. 61 | /// 62 | /// Defaults to 100.0. 63 | final double radius; 64 | 65 | /// Duration of the menu opening/closing animation. 66 | /// 67 | /// Defaults to 1000 milliseconds. 68 | final Duration menuAnimationDuration; 69 | 70 | /// Duration of the action activation progress arc animation. 71 | /// 72 | /// Defaults to 1000 milliseconds. 73 | final Duration progressAnimationDuration; 74 | 75 | @override 76 | RadialMenuState createState() => new RadialMenuState(); 77 | } 78 | 79 | class RadialMenuState extends State with TickerProviderStateMixin { 80 | AnimationController _menuAnimationController; 81 | AnimationController _progressAnimationController; 82 | bool _isOpen = false; 83 | int _activeItemIndex; 84 | 85 | // todo: xqwzts: allow users to pass in their own calculator as a param. 86 | // and change this to the default: radialItemAngleCalculator. 87 | double calculateItemAngle(int index) { 88 | double _itemSpacing = 360.0 / widget.items.length; 89 | return _startAngle + index * _itemSpacing * _radiansPerDegree; 90 | } 91 | 92 | @override 93 | void initState() { 94 | super.initState(); 95 | _menuAnimationController = new AnimationController( 96 | duration: widget.menuAnimationDuration, 97 | vsync: this, 98 | ); 99 | _progressAnimationController = new AnimationController( 100 | duration: widget.progressAnimationDuration, 101 | vsync: this, 102 | ); 103 | } 104 | 105 | @override 106 | void dispose() { 107 | _menuAnimationController.dispose(); 108 | _progressAnimationController.dispose(); 109 | super.dispose(); 110 | } 111 | 112 | void _openMenu() { 113 | _menuAnimationController.forward(); 114 | setState(() => _isOpen = true); 115 | } 116 | 117 | void _closeMenu() { 118 | _menuAnimationController.reverse(); 119 | setState(() => _isOpen = false); 120 | } 121 | 122 | Future _activate(int itemIndex) async { 123 | setState(() => _activeItemIndex = itemIndex); 124 | await _progressAnimationController.forward().orCancel; 125 | if (widget.onSelected != null) { 126 | widget.onSelected(widget.items[itemIndex].value); 127 | } 128 | } 129 | 130 | /// Resets the menu to its initial (closed) state. 131 | void reset() { 132 | _menuAnimationController.reset(); 133 | _progressAnimationController.reverse(); 134 | setState(() { 135 | _isOpen = false; 136 | _activeItemIndex = null; 137 | }); 138 | } 139 | 140 | Widget _buildActionButton(int index) { 141 | final RadialMenuItem item = widget.items[index]; 142 | 143 | return new LayoutId( 144 | id: '${_RadialMenuLayout.actionButton}$index', 145 | child: new RadialMenuButton( 146 | child: item, 147 | backgroundColor: item.backgroundColor, 148 | onPressed: () => _activate(index), 149 | ), 150 | ); 151 | } 152 | 153 | Widget _buildActiveAction(int index) { 154 | final RadialMenuItem item = widget.items[index]; 155 | 156 | return new LayoutId( 157 | id: '${_RadialMenuLayout.activeAction}$index', 158 | child: new ArcProgressIndicator( 159 | controller: _progressAnimationController.view, 160 | radius: widget.radius, 161 | color: item.backgroundColor, 162 | icon: item.child is Icon ? (item.child as Icon).icon : null, 163 | iconColor: item.iconColor, 164 | startAngle: calculateItemAngle(index), 165 | ), 166 | ); 167 | } 168 | 169 | Widget _buildCenterButton() { 170 | return new LayoutId( 171 | id: _RadialMenuLayout.menuButton, 172 | child: new RadialMenuCenterButton( 173 | openCloseAnimationController: _menuAnimationController.view, 174 | activateAnimationController: _progressAnimationController.view, 175 | isOpen: _isOpen, 176 | onPressed: _isOpen ? _closeMenu : _openMenu, 177 | ), 178 | ); 179 | } 180 | 181 | @override 182 | Widget build(BuildContext context) { 183 | final List children = []; 184 | 185 | for (int i = 0; i < widget.items.length; i++) { 186 | if (_activeItemIndex != i) { 187 | children.add(_buildActionButton(i)); 188 | } 189 | } 190 | 191 | if (_activeItemIndex != null) { 192 | children.add(_buildActiveAction(_activeItemIndex)); 193 | } 194 | 195 | children.add(_buildCenterButton()); 196 | 197 | return new AnimatedBuilder( 198 | animation: _menuAnimationController, 199 | builder: (BuildContext context, Widget child) { 200 | return new CustomMultiChildLayout( 201 | delegate: new _RadialMenuLayout( 202 | itemCount: widget.items.length, 203 | radius: widget.radius, 204 | calculateItemAngle: calculateItemAngle, 205 | controller: _menuAnimationController.view, 206 | ), 207 | children: children, 208 | ); 209 | }, 210 | ); 211 | } 212 | } 213 | 214 | class _RadialMenuLayout extends MultiChildLayoutDelegate { 215 | static const String menuButton = 'menuButton'; 216 | static const String actionButton = 'actionButton'; 217 | static const String activeAction = 'activeAction'; 218 | 219 | final int itemCount; 220 | final double radius; 221 | final ItemAngleCalculator calculateItemAngle; 222 | 223 | final Animation controller; 224 | 225 | final Animation _progress; 226 | 227 | _RadialMenuLayout({ 228 | @required this.itemCount, 229 | @required this.radius, 230 | @required this.calculateItemAngle, 231 | this.controller, 232 | }) : _progress = new Tween(begin: 0.0, end: radius).animate( 233 | new CurvedAnimation(curve: Curves.elasticOut, parent: controller)); 234 | 235 | Offset center; 236 | 237 | @override 238 | void performLayout(Size size) { 239 | center = new Offset(size.width / 2, size.height / 2); 240 | 241 | if (hasChild(menuButton)) { 242 | Size menuButtonSize; 243 | menuButtonSize = layoutChild(menuButton, new BoxConstraints.loose(size)); 244 | 245 | // place the menubutton in the center 246 | positionChild( 247 | menuButton, 248 | new Offset( 249 | center.dx - menuButtonSize.width / 2, 250 | center.dy - menuButtonSize.height / 2, 251 | ), 252 | ); 253 | } 254 | 255 | for (int i = 0; i < itemCount; i++) { 256 | final String actionButtonId = '$actionButton$i'; 257 | final String actionArcId = '$activeAction$i'; 258 | if (hasChild(actionArcId)) { 259 | final Size arcSize = layoutChild( 260 | actionArcId, 261 | new BoxConstraints.expand( 262 | width: _progress.value * 2, 263 | height: _progress.value * 2, 264 | ), 265 | ); 266 | 267 | positionChild( 268 | actionArcId, 269 | new Offset( 270 | center.dx - arcSize.width / 2, 271 | center.dy - arcSize.height / 2, 272 | ), 273 | ); 274 | } 275 | 276 | if (hasChild(actionButtonId)) { 277 | final Size buttonSize = 278 | layoutChild(actionButtonId, new BoxConstraints.loose(size)); 279 | 280 | final double itemAngle = calculateItemAngle(i); 281 | 282 | positionChild( 283 | actionButtonId, 284 | new Offset( 285 | (center.dx - buttonSize.width / 2) + 286 | (_progress.value) * Math.cos(itemAngle), 287 | (center.dy - buttonSize.height / 2) + 288 | (_progress.value) * Math.sin(itemAngle), 289 | ), 290 | ); 291 | } 292 | } 293 | } 294 | 295 | @override 296 | bool shouldRelayout(_RadialMenuLayout oldDelegate) => 297 | itemCount != oldDelegate.itemCount || 298 | radius != oldDelegate.radius || 299 | calculateItemAngle != oldDelegate.calculateItemAngle || 300 | controller != oldDelegate.controller || 301 | _progress != oldDelegate._progress; 302 | } 303 | -------------------------------------------------------------------------------- /lib/src/radial_menu_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class RadialMenuButton extends StatelessWidget { 5 | const RadialMenuButton({ 6 | @required this.child, 7 | this.backgroundColor, 8 | this.onPressed, 9 | }); 10 | 11 | final Widget child; 12 | final Color backgroundColor; 13 | final VoidCallback onPressed; 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | final Color color = backgroundColor ?? Theme.of(context).primaryColor; 18 | 19 | return new Semantics( 20 | button: true, 21 | enabled: true, 22 | child: new Material( 23 | type: MaterialType.circle, 24 | color: color, 25 | child: new InkWell( 26 | onTap: onPressed, 27 | child: child, 28 | ), 29 | ), 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/src/radial_menu_center_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_radial_menu/src/radial_menu_button.dart'; 4 | 5 | const double _defaultButtonSize = 48.0; 6 | 7 | /// The button at the center of a [RadialMenu] which controls its open/closed 8 | /// state. 9 | class RadialMenuCenterButton extends StatelessWidget { 10 | /// Drives the opening/closing animation of the [RadialMenu]. 11 | final Animation openCloseAnimationController; 12 | 13 | /// Drives the animation when an item in the [RadialMenu] is pressed. 14 | final Animation activateAnimationController; 15 | 16 | /// Called when the user presses this button. 17 | final VoidCallback onPressed; 18 | 19 | /// The opened/closed state of the menu. 20 | /// 21 | /// Determines which of [closedColor] or [openedColor] should be used as the 22 | /// background color of the button. 23 | final bool isOpen; 24 | 25 | /// The color to use when painting the icon. 26 | /// 27 | /// Defaults to [Colors.black]. 28 | final Color iconColor; 29 | 30 | /// Background color when it is in its closed state. 31 | /// 32 | /// Defaults to [Colors.white]. 33 | final Color closedColor; 34 | 35 | /// Background color when it is in its opened state. 36 | /// 37 | /// Defaults to [Colors.grey]. 38 | final Color openedColor; 39 | 40 | /// The size of the button. 41 | /// 42 | /// Defaults to 48.0. 43 | final double size; 44 | 45 | /// The animation progress for the [AnimatedIcon] in the center of the button. 46 | final Animation _progress; 47 | 48 | /// The scale factor applied to the button. 49 | /// 50 | /// Animates from 1.0 to 0.0 when an an item is pressed in the menu and 51 | /// [activateAnimationController] progresses. 52 | final Animation _scale; 53 | 54 | RadialMenuCenterButton({ 55 | @required this.openCloseAnimationController, 56 | @required this.activateAnimationController, 57 | @required this.onPressed, 58 | @required this.isOpen, 59 | this.iconColor = Colors.black, 60 | this.closedColor = Colors.white, 61 | this.openedColor = Colors.grey, 62 | this.size = _defaultButtonSize, 63 | }) : _progress = new Tween(begin: 0.0, end: 1.0).animate( 64 | new CurvedAnimation( 65 | parent: openCloseAnimationController, 66 | curve: new Interval( 67 | 0.0, 68 | 0.5, 69 | curve: Curves.ease, 70 | ), 71 | ), 72 | ), 73 | _scale = new Tween(begin: 1.0, end: 0.0).animate( 74 | new CurvedAnimation( 75 | parent: activateAnimationController, 76 | curve: Curves.elasticIn, 77 | ), 78 | ); 79 | 80 | @override 81 | Widget build(BuildContext context) { 82 | final AnimatedIcon animatedIcon = new AnimatedIcon( 83 | color: iconColor, 84 | icon: AnimatedIcons.menu_close, 85 | progress: _progress, 86 | ); 87 | 88 | final Widget child = new Container( 89 | width: size, 90 | height: size, 91 | child: new Center( 92 | child: animatedIcon, 93 | ), 94 | ); 95 | 96 | final Color color = isOpen ? openedColor : closedColor; 97 | 98 | return new ScaleTransition( 99 | scale: _scale, 100 | child: new RadialMenuButton( 101 | child: child, 102 | backgroundColor: color, 103 | onPressed: onPressed, 104 | ), 105 | ); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /lib/src/radial_menu_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | const double _defaultButtonSize = 48.0; 5 | 6 | /// An item in a [RadialMenu]. 7 | /// 8 | /// The type `T` is the type of the value the entry represents. All the entries 9 | /// in a given menu must represent values with consistent types. 10 | class RadialMenuItem extends StatelessWidget { 11 | /// Creates a circular action button for an item in a [RadialMenu]. 12 | /// 13 | /// The [child] argument is required. 14 | const RadialMenuItem({ 15 | Key key, 16 | @required this.child, 17 | this.value, 18 | this.tooltip, 19 | this.size = _defaultButtonSize, 20 | this.backgroundColor, 21 | this.iconColor, 22 | // this.iconSize: 24.0, 23 | }) : assert(child != null), 24 | assert(size != null), 25 | super(key: key); 26 | 27 | /// The widget below this widget in the tree. 28 | /// 29 | /// Typically an [Icon] widget. 30 | final Widget child; 31 | 32 | /// The value to return if the user selects this menu item. 33 | /// 34 | /// Eventually returned in a call to [RadialMenu.onSelected]. 35 | final T value; 36 | 37 | /// Text that describes the action that will occur when the button is pressed. 38 | /// 39 | /// This text is displayed when the user long-presses on the button and is 40 | /// used for accessibility. 41 | final String tooltip; 42 | 43 | /// The color to use when filling the button. 44 | /// 45 | /// Defaults to the primary color of the current theme. 46 | final Color backgroundColor; 47 | 48 | /// The size of the button. 49 | /// 50 | /// Defaults to 48.0. 51 | final double size; 52 | 53 | /// The color to use when painting the child icon. 54 | /// 55 | /// Defaults to the primary icon theme color. 56 | final Color iconColor; 57 | 58 | @override 59 | Widget build(BuildContext context) { 60 | final Color _iconColor = 61 | iconColor ?? Theme.of(context).primaryIconTheme.color; 62 | 63 | Widget result; 64 | 65 | if (child != null) { 66 | result = new Center( 67 | child: IconTheme.merge( 68 | data: new IconThemeData( 69 | color: _iconColor, 70 | ), 71 | child: child, 72 | ), 73 | ); 74 | } 75 | 76 | if (tooltip != null) { 77 | result = new Tooltip( 78 | message: tooltip, 79 | child: result, 80 | ); 81 | } 82 | 83 | result = new Container( 84 | width: size, 85 | height: size, 86 | child: result, 87 | ); 88 | 89 | return result; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://www.dartlang.org/tools/pub/glossary#lockfile 3 | packages: 4 | analyzer: 5 | dependency: transitive 6 | description: 7 | name: analyzer 8 | url: "https://pub.dartlang.org" 9 | source: hosted 10 | version: "0.31.1" 11 | args: 12 | dependency: transitive 13 | description: 14 | name: args 15 | url: "https://pub.dartlang.org" 16 | source: hosted 17 | version: "1.3.0" 18 | async: 19 | dependency: transitive 20 | description: 21 | name: async 22 | url: "https://pub.dartlang.org" 23 | source: hosted 24 | version: "2.0.4" 25 | barback: 26 | dependency: transitive 27 | description: 28 | name: barback 29 | url: "https://pub.dartlang.org" 30 | source: hosted 31 | version: "0.15.2+14" 32 | boolean_selector: 33 | dependency: transitive 34 | description: 35 | name: boolean_selector 36 | url: "https://pub.dartlang.org" 37 | source: hosted 38 | version: "1.0.2" 39 | charcode: 40 | dependency: transitive 41 | description: 42 | name: charcode 43 | url: "https://pub.dartlang.org" 44 | source: hosted 45 | version: "1.1.1" 46 | cli_util: 47 | dependency: transitive 48 | description: 49 | name: cli_util 50 | url: "https://pub.dartlang.org" 51 | source: hosted 52 | version: "0.1.2+1" 53 | collection: 54 | dependency: transitive 55 | description: 56 | name: collection 57 | url: "https://pub.dartlang.org" 58 | source: hosted 59 | version: "1.14.5" 60 | convert: 61 | dependency: transitive 62 | description: 63 | name: convert 64 | url: "https://pub.dartlang.org" 65 | source: hosted 66 | version: "2.0.1" 67 | crypto: 68 | dependency: transitive 69 | description: 70 | name: crypto 71 | url: "https://pub.dartlang.org" 72 | source: hosted 73 | version: "2.0.2+1" 74 | csslib: 75 | dependency: transitive 76 | description: 77 | name: csslib 78 | url: "https://pub.dartlang.org" 79 | source: hosted 80 | version: "0.14.1" 81 | flutter: 82 | dependency: "direct main" 83 | description: flutter 84 | source: sdk 85 | version: "0.0.0" 86 | flutter_test: 87 | dependency: "direct dev" 88 | description: flutter 89 | source: sdk 90 | version: "0.0.0" 91 | front_end: 92 | dependency: transitive 93 | description: 94 | name: front_end 95 | url: "https://pub.dartlang.org" 96 | source: hosted 97 | version: "0.1.0-alpha.9" 98 | glob: 99 | dependency: transitive 100 | description: 101 | name: glob 102 | url: "https://pub.dartlang.org" 103 | source: hosted 104 | version: "1.1.5" 105 | html: 106 | dependency: transitive 107 | description: 108 | name: html 109 | url: "https://pub.dartlang.org" 110 | source: hosted 111 | version: "0.13.2+2" 112 | http: 113 | dependency: transitive 114 | description: 115 | name: http 116 | url: "https://pub.dartlang.org" 117 | source: hosted 118 | version: "0.11.3+16" 119 | http_multi_server: 120 | dependency: transitive 121 | description: 122 | name: http_multi_server 123 | url: "https://pub.dartlang.org" 124 | source: hosted 125 | version: "2.0.4" 126 | http_parser: 127 | dependency: transitive 128 | description: 129 | name: http_parser 130 | url: "https://pub.dartlang.org" 131 | source: hosted 132 | version: "3.1.1" 133 | io: 134 | dependency: transitive 135 | description: 136 | name: io 137 | url: "https://pub.dartlang.org" 138 | source: hosted 139 | version: "0.3.2+1" 140 | isolate: 141 | dependency: transitive 142 | description: 143 | name: isolate 144 | url: "https://pub.dartlang.org" 145 | source: hosted 146 | version: "1.1.0" 147 | js: 148 | dependency: transitive 149 | description: 150 | name: js 151 | url: "https://pub.dartlang.org" 152 | source: hosted 153 | version: "0.6.1" 154 | kernel: 155 | dependency: transitive 156 | description: 157 | name: kernel 158 | url: "https://pub.dartlang.org" 159 | source: hosted 160 | version: "0.3.0-alpha.9" 161 | logging: 162 | dependency: transitive 163 | description: 164 | name: logging 165 | url: "https://pub.dartlang.org" 166 | source: hosted 167 | version: "0.11.3+1" 168 | matcher: 169 | dependency: transitive 170 | description: 171 | name: matcher 172 | url: "https://pub.dartlang.org" 173 | source: hosted 174 | version: "0.12.1+4" 175 | meta: 176 | dependency: transitive 177 | description: 178 | name: meta 179 | url: "https://pub.dartlang.org" 180 | source: hosted 181 | version: "1.1.2" 182 | mime: 183 | dependency: transitive 184 | description: 185 | name: mime 186 | url: "https://pub.dartlang.org" 187 | source: hosted 188 | version: "0.9.6" 189 | mockito: 190 | dependency: transitive 191 | description: 192 | name: mockito 193 | url: "https://pub.dartlang.org" 194 | source: hosted 195 | version: "2.2.3" 196 | multi_server_socket: 197 | dependency: transitive 198 | description: 199 | name: multi_server_socket 200 | url: "https://pub.dartlang.org" 201 | source: hosted 202 | version: "1.0.1" 203 | node_preamble: 204 | dependency: transitive 205 | description: 206 | name: node_preamble 207 | url: "https://pub.dartlang.org" 208 | source: hosted 209 | version: "1.4.0" 210 | package_config: 211 | dependency: transitive 212 | description: 213 | name: package_config 214 | url: "https://pub.dartlang.org" 215 | source: hosted 216 | version: "1.0.3" 217 | package_resolver: 218 | dependency: transitive 219 | description: 220 | name: package_resolver 221 | url: "https://pub.dartlang.org" 222 | source: hosted 223 | version: "1.0.2" 224 | path: 225 | dependency: transitive 226 | description: 227 | name: path 228 | url: "https://pub.dartlang.org" 229 | source: hosted 230 | version: "1.5.1" 231 | plugin: 232 | dependency: transitive 233 | description: 234 | name: plugin 235 | url: "https://pub.dartlang.org" 236 | source: hosted 237 | version: "0.2.0+2" 238 | pool: 239 | dependency: transitive 240 | description: 241 | name: pool 242 | url: "https://pub.dartlang.org" 243 | source: hosted 244 | version: "1.3.4" 245 | pub_semver: 246 | dependency: transitive 247 | description: 248 | name: pub_semver 249 | url: "https://pub.dartlang.org" 250 | source: hosted 251 | version: "1.3.2" 252 | quiver: 253 | dependency: transitive 254 | description: 255 | name: quiver 256 | url: "https://pub.dartlang.org" 257 | source: hosted 258 | version: "0.28.0" 259 | shelf: 260 | dependency: transitive 261 | description: 262 | name: shelf 263 | url: "https://pub.dartlang.org" 264 | source: hosted 265 | version: "0.7.2" 266 | shelf_packages_handler: 267 | dependency: transitive 268 | description: 269 | name: shelf_packages_handler 270 | url: "https://pub.dartlang.org" 271 | source: hosted 272 | version: "1.0.3" 273 | shelf_static: 274 | dependency: transitive 275 | description: 276 | name: shelf_static 277 | url: "https://pub.dartlang.org" 278 | source: hosted 279 | version: "0.2.7" 280 | shelf_web_socket: 281 | dependency: transitive 282 | description: 283 | name: shelf_web_socket 284 | url: "https://pub.dartlang.org" 285 | source: hosted 286 | version: "0.2.2" 287 | sky_engine: 288 | dependency: transitive 289 | description: flutter 290 | source: sdk 291 | version: "0.0.99" 292 | source_map_stack_trace: 293 | dependency: transitive 294 | description: 295 | name: source_map_stack_trace 296 | url: "https://pub.dartlang.org" 297 | source: hosted 298 | version: "1.1.4" 299 | source_maps: 300 | dependency: transitive 301 | description: 302 | name: source_maps 303 | url: "https://pub.dartlang.org" 304 | source: hosted 305 | version: "0.10.4" 306 | source_span: 307 | dependency: transitive 308 | description: 309 | name: source_span 310 | url: "https://pub.dartlang.org" 311 | source: hosted 312 | version: "1.4.0" 313 | stack_trace: 314 | dependency: transitive 315 | description: 316 | name: stack_trace 317 | url: "https://pub.dartlang.org" 318 | source: hosted 319 | version: "1.9.1" 320 | stream_channel: 321 | dependency: transitive 322 | description: 323 | name: stream_channel 324 | url: "https://pub.dartlang.org" 325 | source: hosted 326 | version: "1.6.3" 327 | string_scanner: 328 | dependency: transitive 329 | description: 330 | name: string_scanner 331 | url: "https://pub.dartlang.org" 332 | source: hosted 333 | version: "1.0.2" 334 | term_glyph: 335 | dependency: transitive 336 | description: 337 | name: term_glyph 338 | url: "https://pub.dartlang.org" 339 | source: hosted 340 | version: "1.0.0" 341 | test: 342 | dependency: transitive 343 | description: 344 | name: test 345 | url: "https://pub.dartlang.org" 346 | source: hosted 347 | version: "0.12.30+4" 348 | typed_data: 349 | dependency: transitive 350 | description: 351 | name: typed_data 352 | url: "https://pub.dartlang.org" 353 | source: hosted 354 | version: "1.1.5" 355 | utf: 356 | dependency: transitive 357 | description: 358 | name: utf 359 | url: "https://pub.dartlang.org" 360 | source: hosted 361 | version: "0.9.0+4" 362 | vector_math: 363 | dependency: transitive 364 | description: 365 | name: vector_math 366 | url: "https://pub.dartlang.org" 367 | source: hosted 368 | version: "2.0.5" 369 | watcher: 370 | dependency: transitive 371 | description: 372 | name: watcher 373 | url: "https://pub.dartlang.org" 374 | source: hosted 375 | version: "0.9.7+7" 376 | web_socket_channel: 377 | dependency: transitive 378 | description: 379 | name: web_socket_channel 380 | url: "https://pub.dartlang.org" 381 | source: hosted 382 | version: "1.0.7" 383 | yaml: 384 | dependency: transitive 385 | description: 386 | name: yaml 387 | url: "https://pub.dartlang.org" 388 | source: hosted 389 | version: "2.1.13" 390 | sdks: 391 | dart: ">=2.0.0-dev.23.0 <=2.0.0-edge.fe96de2858f078e4ad04f8f30640184bf3d8102d" 392 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_radial_menu 2 | description: A simple animated radial menu widget for Flutter. 3 | version: 0.0.1 4 | author: Victor Choueiri 5 | homepage: https://github.com/xqwzts/flutter_radial_menu 6 | 7 | dependencies: 8 | flutter: 9 | sdk: flutter 10 | 11 | dev_dependencies: 12 | flutter_test: 13 | sdk: flutter 14 | 15 | environment: 16 | sdk: '>=1.19.0 <2.0.0' 17 | -------------------------------------------------------------------------------- /screenshots/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xqwzts/flutter_radial_menu/09494b6239a6218d851b014115c4a6b08826c27f/screenshots/demo.gif -------------------------------------------------------------------------------- /screenshots/simple_example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xqwzts/flutter_radial_menu/09494b6239a6218d851b014115c4a6b08826c27f/screenshots/simple_example.gif -------------------------------------------------------------------------------- /screenshots/simple_example_code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xqwzts/flutter_radial_menu/09494b6239a6218d851b014115c4a6b08826c27f/screenshots/simple_example_code.png -------------------------------------------------------------------------------- /test/flutter_radial_menu_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | 3 | import 'package:flutter_radial_menu/flutter_radial_menu.dart'; 4 | 5 | void main() { 6 | test('adds one to input values', () { 7 | final calculator = new Calculator(); 8 | expect(calculator.addOne(2), 3); 9 | expect(calculator.addOne(-7), -6); 10 | expect(calculator.addOne(0), 1); 11 | expect(() => calculator.addOne(null), throwsNoSuchMethodError); 12 | }); 13 | } 14 | --------------------------------------------------------------------------------