Bneac
Bneac

Reputation: 89

Stack Alignment for an indeterminate number of children

I am building an app in flutter and part of what I am doing is fetching a list of actions from a server then building buttons for each action. I am trying to display the buttons in a circle and generate a list of raised button widgets. I am then trying to use a Stack to lay out this list in a circle, but cannot figure out how to use positioning to position objects in a circle when I do not know how many objects I will have (I know it will be in a range of about 3-15).

Here is the build method for my stack. Assume all methods and lists and variables are defined correctly and work. I am just concerned about the alignment

@override
  Widget build(BuildContext context) {

    List<Widget> actions = [];
    for(var action in widget.actionsList){
      actions.add(RaisedButton(
        onPressed: () => _handleAction(action[0]),
        shape: CircleBorder(),
        child: Text(action[1] + ': ' + action[2]),
      ));
    }

    return Scaffold(
      body: Center(
        child: Stack(
          children: actions,
          alignment: //HELP,
        ),
      ),
    );
  }

If you have any ideas about how to do the alignment or another way to go about this to make a circle of buttons please let me know. I really want to make a circle, but am not wedded to it impossible (which I doubt) or super convoluted.

Thanks!

Upvotes: 1

Views: 42

Answers (1)

Muldec
Muldec

Reputation: 4901

Here's how I would do it.

TL;DR;

  final actionsList = [
    "one",
    "two",
    "three",
    "four",
    "five",
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: LayoutBuilder(
        builder: (context, constraints) => Stack(
          children: layedOutButtons(
            centerOfCircle:
                Offset(constraints.maxWidth, constraints.maxHeight) / 2,
            circleRadius: 100,
          ),
        ),
      ),
    );
  }

  Offset getCoordinateFromAngle({double radius, double angle}) => Offset(
        radius * sin(angle),
        radius * cos(angle),
      );

  List<Widget> layedOutButtons({
    Offset centerOfCircle,
    double circleRadius,
  }) {
    var buttonRadius = 25.0;
    var dispatchAngle = pi * 2 / actionsList.length;

    List<Widget> widgets = [];
    var i = 0;

    for (var action in actionsList) {
      var position = getCoordinateFromAngle(
        radius: circleRadius,
        angle: dispatchAngle * i++,
      );
      widgets.add(
        Positioned(
          top: centerOfCircle.dy - position.dy - buttonRadius,
          left: centerOfCircle.dx + position.dx - buttonRadius,
          width: buttonRadius * 2,
          height: buttonRadius * 2,
          child: FloatingActionButton(
            child: Text(action),
            onPressed: () {},
          ),
        ),
      );
    }

    return widgets;
  }

Explanation

If you want to dispatch a number of widgets on a circle, you have to:

  1. define centerOfCircle, the position of the center of that circle. To do so, I use the LayoutBuilder widget to get the constraints of the layout and determine the center of the Stack => (width / 2, height / 2).
  2. define circleRadius, the radius of that circle (in my example: 100)

Give those data to layedOutButtons that will dispatch widgets on the circle with the Positioned widget.

  Widget build(BuildContext context) {
    return Scaffold(
      body: LayoutBuilder(
        builder: (context, constraints) => Stack(
          children: layedOutButtons(
            centerOfCircle: Offset(constraints.maxWidth, constraints.maxHeight) / 2,
            circleRadius: 100,
          ),
        ),
      ),
    );
  }

  List<Widget> layedOutButtons({
    Offset centerOfCircle,
    double circleRadius,
  }) {
    List<Widget> widgets = [];

    for (var action in actionsList) {
      widgets.add(
        Positioned(
          child: FloatingActionButton(
            child: Text(action),
            onPressed: () {},
          ),
        ),
      );
    }

    return widgets;
  }

Now, you need to define how you will dispatch the widgets on the circle, based on their number. e.g, it there's 2 widgets, dispatch them at 180° from each other (meaning one on the top of the circle, one on the bottom. If there's 4, dispatch at 90° from each other, etc. Note that it is expressed in radians (not in degrees).

var dispatchAngle = pi * 2 / actionsList.length;

Then you have to define the coordinates (x, y) of a point on a circle based on an angle (look at this).

Offset getCoordinateFromAngle({double radius, double angle}) => Offset(
      radius * sin(angle),
      radius * cos(angle), 
    );

Use that to fill the top and left attributes of the Positioned widget.

  List<Widget> layedOutButtons({
    Offset centerOfCircle,
    double circleRadius,
  }) {
    var dispatchAngle = pi * 2 / actionsList.length;

    List<Widget> widgets = [];
    var i = 0;

    for (var action in actionsList) {
      var position = getCoordinateFromAngle(
        radius: circleRadius,
        angle: dispatchAngle * i++, //increment angle for each widget
      );
      widgets.add(
        Positioned(
          top: centerOfCircle.dy - position.dy, //something's wrong here
          left: centerOfCircle.dx + position.dx, //something's wrong here
          child: FloatingActionButton(
            child: Text(action),
            onPressed: () {},
          ),
        ),
      );
    }

    return widgets;
  }

Now everything is almost ready except that there's a misalignement. It's due to the fact that the widgets are positioned based on their top left corners. We want to refine the positioning so it match the center of our widgets.

var buttonRadius = 25.0;

Positioned(
  top: centerOfCircle.dy - position.dy - buttonRadius,
  left: centerOfCircle.dx + position.dx - buttonRadius,
  width: buttonRadius * 2,
  height: buttonRadius * 2,
  child: FloatingActionButton(
    child: Text(action),
    onPressed: () {},
  ),
),

Upvotes: 1

Related Questions