Home > Enterprise >  How can I create a pyramid of floating action buttons with a layout like bowling pins?
How can I create a pyramid of floating action buttons with a layout like bowling pins?

Time:11-29

I am trying to create a page where I display a pyramid of buttons, representing pins. This will allow the user to tap each button to show which pins in bowling they knocked down. I am having trouble figuring out how to format it so the buttons look like a set of pins.enter image description here

This is what I have so far. Its almost correct but the third row still has issues. However, even though this is close. It won't work for me. When you click a button, it turns invisible and therefore disappears. Because I used spaceEvenly and SpaceBetween to space the buttons, when one disappears, they space differently. Here is my code for the widget

 @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('Enter Pins'),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.start,
            children: <Widget>[
              Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
                _new_button(6),_new_button(7),_new_button(8),_new_button(9)
              ]),
              Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
                _new_button(3),_new_button(4),_new_button(5)
              ]),
              Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
                _new_button(1),_new_button(2)
              ]),
              Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
                _new_button(0)
              ]),
            ],
          ),
        ),
        
        floatingActionButton:
            
        Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
          Align(
            alignment: Alignment.bottomLeft,
            child: FloatingActionButton.extended(
              onPressed: () => _submit_turn(context),
              heroTag: 'btn1',
              label: const Text('Submit'),
              tooltip: 'Submit Pins',
            ),
          ),
          Align(
            alignment: Alignment.bottomLeft,
            child: FloatingActionButton(
              onPressed: () => Navigator.pop(context),
              heroTag: 'btn2',

              tooltip: 'Cancel Game',
              child: const Icon(Icons.delete, color: Colors.red),
            ),
          )
        ])

      // This trailing comma makes auto-formatting nicer for build methods.
    );
  }

and for the method _new_button()

_new_button(int index){

    return Visibility(
      visible: _enable[index],
      child: FloatingActionButton (
      onPressed: () => _edit_pin(index),
      heroTag: 'pin'   index.toString(),
      tooltip: index.toString(),
      child: Icon(Icons.circle, color: _pins_colors[index]),
      )
    );


  }

Is there anyway to fix this and perhaps do it in a better way?

Thanks in advance for the help!

CodePudding user response:

for such irregular layouts the best way is to use CustomMultiChildLayout with PyramidDelegate layout delegate - if you want your "pins" to be more invisible remove border (and/or color) parameters from BoxDecoration

class Pyramid extends StatefulWidget {

  const Pyramid({Key? key}) : super(key: key);

  @override
  State<Pyramid> createState() => _PyramidState();
}

class _PyramidState extends State<Pyramid> {
  // 10 = 4   3   2   1
  final colors = List.generate(10, (i) => HSVColor.fromAHSV(1, 100   i * 25, 1, 0.5).toColor());
  final visible = List.filled(10, true);

  @override
  Widget build(BuildContext context) {
    return CustomMultiChildLayout(
      delegate: PyramidDelegate(4),
      children: [
        for(var i = 0; i < 10; i  )
          LayoutId(
            id: i,
            child: AnimatedContainer(
              duration: const Duration(milliseconds: 500),
              decoration: BoxDecoration(
                shape: BoxShape.circle,
                color: visible[i]? colors[i] : Colors.black.withOpacity(0.05),
                border: Border.all(color: colors[i].withOpacity(0.25), width: visible[i]? 0 : 2),
              ),
              child: GestureDetector(
                onTap: () {
                  print('$i pressed');
                  setState(() => visible[i] = !visible[i]);
                },
              ),
            )
          )
      ],
    );
  }
}
class PyramidDelegate extends MultiChildLayoutDelegate {
  final int sideLength;

  PyramidDelegate(this.sideLength);

  @override
  void performLayout(ui.Size size) {
    final constraints = BoxConstraints.tight(const Size.square(64));
    var childId = 0;
    final rect = (Offset.zero & size).deflate(32);
    final a = rect.topLeft;
    final b = rect.topRight;
    final c = Offset(rect.center.dx, rect.top   rect.width * 0.5 * sqrt(3));
    for (var row = sideLength; row > 0; row--) {
      final t = 1 - (row - 1) / (sideLength - 1);
      final a1 = Offset.lerp(a, c, t);
      final b1 = Offset.lerp(b, c, t);
      for (var column = row; column > 0; column--) {
        final childSize = layoutChild(childId, constraints);
        final offset = row == 1? c : Offset.lerp(a1, b1, 1 - (column - 1) / (row - 1))!;
        positionChild(childId, offset - childSize.center(Offset.zero));
        childId  ;
      }
    }
  }

  @override
  bool shouldRelayout(covariant MultiChildLayoutDelegate oldDelegate) => false;
}

CodePudding user response:

You can use Opacity widget to handle visibility in this case. For click event and tooltip, you can also include condition.

_new_button(int index) {
      return Opacity(
          opacity: _enable[index] ? 1 : 0,
          child: FloatingActionButton(
            onPressed: !_enable[index]
                ? null
                : () {
                    setState(() {
                      _enable[index] = false;
                    });
                  },
               //........
  • Related