Side Radial Menu in flutter Side Radial Menu in flutter dart dart

Side Radial Menu in flutter


This can be achieved by using a GestureDetector, Transform, trigonometry and some clipping with ClipRect.

Using GestureDetector, it is possible to see the drag distance that a user inputs. This can be used to determine how much to rotate the widgets.

Using Transform, it is possible to move widgets to specific locations.

Trigonometry is used to determine the position of the widgets to the centre of the circle.

Using ClipRect, it is possible to clip out the left side of the widgets.

It is possible to reverse scroll direction by instead taking the distance of the drag be turned negative.

Here is the code to make a rotating menu that uses a custom widget that I have recently created for answering this question(Add more Widgets to the Widget list if you want):

import 'dart:math' as math;import 'package:flutter/material.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: Scaffold(        body:CircularScrollView(//wrap this with align if you want it to be aligned to the right of the screen          [//add more widgets or remove as you'd like            GestureDetector(              onTap: (){},//insert function when icon is tapped              child: Container(                child: Center(child: Text('a')),                height: 20,                width: 20,                decoration: BoxDecoration(                  color: Colors.blue,                  shape: BoxShape.circle,                ),              ),            ),            GestureDetector(              onTap: (){},//insert function when icon is tapped              child: Container(                child: Center(child: Text('b')),                height: 20,                width: 20,                decoration: BoxDecoration(                  color: Colors.blue,                  shape: BoxShape.circle,                ),              ),            ),            GestureDetector(              onTap: (){},//insert function when icon is tapped              child: Container(                child: Center(child: Text('c')),                height: 20,                width: 20,                decoration: BoxDecoration(                  color: Colors.blue,                  shape: BoxShape.circle,                ),              ),            ),            GestureDetector(              onTap: (){},//insert function when icon is tapped              child: Container(                child: Center(child: Text('d')),                height: 20,                width: 20,                decoration: BoxDecoration(                  color: Colors.blue,                  shape: BoxShape.circle,                ),              ),            ),            GestureDetector(              onTap: (){},//insert function when icon is tapped              child: Container(                child: Center(child: Text('e')),                height: 20,                width: 20,                decoration: BoxDecoration(                  color: Colors.blue,                  shape: BoxShape.circle,                ),              ),            ),          ],          radius: 100,          padding: 0,//add double the radius entered to clip out the right side          itemMaxHeight: 20,//effects clipping border height          itemMaxWidth: 20,//effects clipping border width        ),      ),    );  }}class CircularScrollView extends StatefulWidget {  final List<Widget> items;  final double radius;  final double itemMaxHeight;  final double itemMaxWidth;  final double padding;  final bool reverse;  CircularScrollView(this.items, {Key key, this.radius=10, this.itemMaxHeight=0, this.itemMaxWidth=0, this.padding=0, this.reverse=false}) : super(key: key);  @override  _CircularScrollViewState createState() => _CircularScrollViewState();}class _CircularScrollViewState extends State<CircularScrollView> {  double lastPosition;  List<Widget> transformItems= [];  double degreesRotated = 0;  @override  void initState() {    setState(() {      _calculateTransformItems();    });    super.initState();  }  void _calculateTransformItems(){    transformItems= [];    for(int i = 0; i<widget.items.length; i++){      double startAngle = (i/widget.items.length)*2*math.pi;      double currentAngle = degreesRotated+startAngle;      transformItems.add(        Transform(          transform: Matrix4.identity()..translate(            (widget.radius)*math.cos(currentAngle),            (widget.radius)*math.sin(currentAngle),          ),          child: widget.items[i],        ),      );    }  }  void _calculateScroll(DragUpdateDetails details){    if (lastPosition == null){      lastPosition = details.localPosition.dy;      return;    }    double distance = details.localPosition.dy - lastPosition;    double distanceWithReversal = widget.reverse?-distance:distance;    lastPosition =details.localPosition.dy;    degreesRotated += distanceWithReversal/(widget.radius);    _calculateTransformItems();  }  @override  Widget build(BuildContext context) {    return Align(      alignment: Alignment.centerLeft,      child: Container(        height: widget.radius*2+widget.itemMaxHeight,        width: widget.radius*2 + widget.itemMaxWidth,        child: GestureDetector(          onVerticalDragUpdate: (details)=>setState((){_calculateScroll(details);}),          onVerticalDragEnd: (details){lastPosition=null;},          child: Container(            height: double.infinity,            width: double.infinity,            color: Colors.transparent,            child: ClipRect(              child: Align(                alignment: Alignment.centerLeft,                child: Padding(                  padding: EdgeInsets.only(left: widget.padding),                  child: Stack(                    children: transformItems,                  ),                ),              ),            ),          ),        ),      ),    );  }}

When using this code, do not modify the insides of the custom widget unless you know exactly what that section of the code does. When aligning the widget, please instead wrap the custom widget from the outside.


You can try using this package, circle_wheel_scroll, move around this widget inside Stack, place with Positioned with negative left position if necessary.

CircleListScrollView(              physics: CircleFixedExtentScrollPhysics(),              axis: Axis.horizontal,              itemExtent: 80,              children: List.generate(20, _buildItem),              radius: MediaQuery.of(context).size.width * 0.6,            ),

or this listwheelscrollview

ListWheelScrollView(         itemExtent: 100,                   // diameterRatio: 1.6,         // offAxisFraction: -0.4,         // squeeze: 0.8,         clipToSize: true,        children: <Widget>[          RaisedButton(onPressed:null ,        child: Text("Item 1",textAlign:TextAlign.start,             style:TextStyle(color:Colors.black,fontWeight: FontWeight.bold,fontSize: 25),),) ,        RaisedButton(onPressed:null ,        child: Text("Item 2",textAlign:TextAlign.center,             style:TextStyle(color:Colors.black,fontWeight: FontWeight.bold,fontSize: 25),),) ,        RaisedButton(onPressed:null ,        child: Text("Item 3",textAlign:TextAlign.center,             style:TextStyle(color:Colors.black,fontWeight: FontWeight.bold,fontSize: 25),),) ,        RaisedButton(onPressed:null ,        child: Text("Item 4",textAlign:TextAlign.center,             style:TextStyle(color:Colors.black,fontWeight: FontWeight.bold,fontSize: 25),),) ,        RaisedButton(onPressed:null ,        child: Text("Item 5",textAlign:TextAlign.center,             style:TextStyle(color:Colors.black,fontWeight: FontWeight.bold,fontSize: 25),),) ,        RaisedButton(onPressed:null ,        child: Text("Item 6",textAlign:TextAlign.center,             style:TextStyle(color:Colors.black,fontWeight: FontWeight.bold,fontSize: 25),),) ,        RaisedButton(onPressed:null ,        child: Text("Item 7",textAlign:TextAlign.center,             style:TextStyle(color:Colors.black,fontWeight: FontWeight.bold,fontSize: 25),),) ,        RaisedButton(onPressed:null ,        child: Text("Item 8",textAlign:TextAlign.center,             style:TextStyle(color:Colors.black,fontWeight: FontWeight.bold,fontSize: 25),),) ,        ],       ),