skip to Main Content

I’m working a floating action button with some twists: when you click on the FloatingActionButton(), some InkWell() widgets become visible from a Stack(), where you can click on multiple options. When I inserted it to my application, I experienced something weird:

If I add the unique MyFAB() widget as a "home" option within MaterialApp(), the animation works perfectly and you can click on the small InkWell() widgets without any problems:

import 'package:flutter/material.dart';
import 'dart:math';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home:  MyFAB(),
    );
  }
}

class MyFAB extends StatefulWidget {
  const MyFAB({Key? key}) : super(key: key);

  @override
  State<MyFAB> createState() => _MyFABState();
}

class _MyFABState extends State<MyFAB> with SingleTickerProviderStateMixin {
  late AnimationController _animationController;
  late Animation<double> _animation;

  @override
  void initState() {
    super.initState();
    _animationController = AnimationController(
      vsync: this,
      duration: Duration(milliseconds: 300),
    );

    _animation = CurvedAnimation(
      parent: _animationController,
      curve: Curves.easeInOut,
    );
  }

  @override
  void dispose() {
    _animationController.dispose();
    super.dispose();
  }

  void _toggle() {
    if (_animationController.isDismissed) {
      _animationController.forward();
    } else {
      _animationController.reverse();
    }
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      alignment: Alignment.center,
      children: [
        _buildOption(Icons.mood, -0.2),
        _buildOption(Icons.sentiment_satisfied, 0.27),
        _buildOption(Icons.sentiment_dissatisfied, 0.72),
        _buildOption(Icons.mood_bad, 1.2),
        FloatingActionButton(
          heroTag: "MyFAB",
          onPressed: _toggle,
          shape: const CircleBorder(),
          child: AnimatedIcon(
            icon: AnimatedIcons.menu_close,
            progress: _animation,
          ),
        ),
      ],
    );
  }

  Widget _buildOption(IconData icon, double index) {
    final double angle = (index - 1.5) * 0.5 * pi;
    return AnimatedBuilder(
      animation: _animation,
      builder: (context, child) {
        final double offsetX = _animation.value * 70 * cos(angle);
        final double offsetY = _animation.value * 70 * sin(angle);
        return Transform.translate(
          offset: Offset(offsetX, offsetY),
          child: Transform.scale(
            scale: _animation.value,
            child: Material(
              color: Colors.transparent,
              child: InkWell(
                onTap: () {
                  print('Option tapped');
                  _toggle();
                },
                borderRadius: BorderRadius.circular(20),
                splashColor: Colors.grey.withOpacity(0.5),
                child: CircleAvatar(
                  radius: 20,
                  child: Icon(icon),
                ),
              ),
            ),
          ),
        );
      },
    );
  }
}

But if I add MyFAB() into a Scaffold() as a "floatingActionButton", the small icons become useless, you cannot click on them anymore.

import 'package:flutter/material.dart';
import 'dart:math';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        floatingActionButton: MyFAB(),
        floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
      ),
    );
  }
}

class MyFAB extends StatefulWidget {
  const MyFAB({Key? key}) : super(key: key);

  @override
  State<MyFAB> createState() => _MyFABState();
}

class _MyFABState extends State<MyFAB> with SingleTickerProviderStateMixin {
  late AnimationController _animationController;
  late Animation<double> _animation;

  @override
  void initState() {
    super.initState();
    _animationController = AnimationController(
      vsync: this,
      duration: Duration(milliseconds: 300),
    );

    _animation = CurvedAnimation(
      parent: _animationController,
      curve: Curves.easeInOut,
    );
  }

  @override
  void dispose() {
    _animationController.dispose();
    super.dispose();
  }

  void _toggle() {
    if (_animationController.isDismissed) {
      _animationController.forward();
    } else {
      _animationController.reverse();
    }
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      alignment: Alignment.center,
      children: [
        _buildOption(Icons.mood, -0.2),
        _buildOption(Icons.sentiment_satisfied, 0.27),
        _buildOption(Icons.sentiment_dissatisfied, 0.72),
        _buildOption(Icons.mood_bad, 1.2),
        FloatingActionButton(
          heroTag: "MyFAB",
          onPressed: _toggle,
          shape: const CircleBorder(),
          child: AnimatedIcon(
            icon: AnimatedIcons.menu_close,
            progress: _animation,
          ),
        ),
      ],
    );
  }

  Widget _buildOption(IconData icon, double index) {
    final double angle = (index - 1.5) * 0.5 * pi;
    return AnimatedBuilder(
      animation: _animation,
      builder: (context, child) {
        final double offsetX = _animation.value * 70 * cos(angle);
        final double offsetY = _animation.value * 70 * sin(angle);
        return Transform.translate(
          offset: Offset(offsetX, offsetY),
          child: Transform.scale(
            scale: _animation.value,
            child: Material(
              color: Colors.transparent,
              child: InkWell(
                onTap: () {
                  print('Option tapped');
                  _toggle();
                },
                borderRadius: BorderRadius.circular(20),
                splashColor: Colors.grey.withOpacity(0.5),
                child: CircleAvatar(
                  radius: 20,
                  child: Icon(icon),
                ),
              ),
            ),
          ),
        );
      },
    );
  }
}

I’m a self-taught Flutter developer, still pretty new to it, so I got stuck at this point. What causing this behavior? How can I use MyFAB() within the Scaffold() without any problems?

You can try out my codes in https://dartpad.dev/

Thanks in advance!

I recoded the entire MyFAB() not to use Stack() widget, removed the "floatingActionButtonLocation" option from the Scaffold(), nothing helped.

2

Answers


  1. The issue you’re experiencing with the InkWell widgets not being clickable when MyFAB() is used as a floatingActionButton within a Scaffold is likely due to the positioning and hit testing of the widgets in the Stack. When using the floatingActionButton within a Scaffold, the floating action button’s stack might not cover the entire screen or the widgets in the stack might not be properly positioned within the tappable area.

    To resolve this issue, you can ensure the Stack containing the InkWell widgets covers the entire screen and is positioned correctly. One approach is to use a Positioned widget to place the floating action button and the options correctly within the Stack.

    Hey Can u just check this out !!!

    import 'package:flutter/material.dart';
    import 'dart:math';
    
    void main() {
      runApp(MyApp());
    }
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          home: Scaffold(
            body: Stack(
              children: [
                // Your main content goes here
              ],
            ),
            floatingActionButton: MyFAB(),
            floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
          ),
        );
      }
    }
    
    class MyFAB extends StatefulWidget {
      const MyFAB({Key? key}) : super(key: key);
    
      @override
      State<MyFAB> createState() => _MyFABState();
    }
    
    class _MyFABState extends State<MyFAB> with SingleTickerProviderStateMixin {
      late AnimationController _animationController;
      late Animation<double> _animation;
    
      @override
      void initState() {
        super.initState();
        _animationController = AnimationController(
          vsync: this,
          duration: Duration(milliseconds: 300),
        );
    
        _animation = CurvedAnimation(
          parent: _animationController,
          curve: Curves.easeInOut,
        );
      }
    
      @override
      void dispose() {
        _animationController.dispose();
        super.dispose();
      }
    
      void _toggle() {
        if (_animationController.isDismissed) {
          _animationController.forward();
        } else {
          _animationController.reverse();
        }
      }
    
      @override
      Widget build(BuildContext context) {
        return Stack(
          alignment: Alignment.center,
          clipBehavior: Clip.none,
          children: [
            _buildOption(Icons.mood, -0.2),
            _buildOption(Icons.sentiment_satisfied, 0.27),
            _buildOption(Icons.sentiment_dissatisfied, 0.72),
            _buildOption(Icons.mood_bad, 1.2),
            Positioned(
              bottom: 16.0, // Adjust the position as needed
              child: FloatingActionButton(
                heroTag: "MyFAB",
                onPressed: _toggle,
                shape: const CircleBorder(),
                child: AnimatedIcon(
                  icon: AnimatedIcons.menu_close,
                  progress: _animation,
                ),
              ),
            ),
          ],
        );
      }
    
      Widget _buildOption(IconData icon, double index) {
        final double angle = (index - 1.5) * 0.5 * pi;
        return AnimatedBuilder(
          animation: _animation,
          builder: (context, child) {
            final double offsetX = _animation.value * 70 * cos(angle);
            final double offsetY = _animation.value * 70 * sin(angle);
            return Transform.translate(
              offset: Offset(offsetX, offsetY),
              child: Transform.scale(
                scale: _animation.value,
                child: Material(
                  color: Colors.transparent,
                  child: InkWell(
                    onTap: () {
                      print('Option tapped');
                      _toggle();
                    },
                    borderRadius: BorderRadius.circular(20),
                    splashColor: Colors.grey.withOpacity(0.5),
                    child: CircleAvatar(
                      radius: 20,
                      child: Icon(icon),
                    ),
                  ),
                ),
              ),
            );
          },
        );
      }
    }
    

    the Stack inside the Scaffold ensures that the InkWell widgets and the FloatingActionButton are correctly positioned and cover the entire screen, making them clickable. The clipBehavior: Clip.none property of the Stack allows the InkWell widgets to be drawn outside the bounds of the Stack, if necessary. The Positioned widget ensures the floating action button is correctly placed at the bottom center of the screen. Adjust the bottom property of the Positioned widget as needed to get the desired position.

    Login or Signup to reply.
  2. Its because tappable area in floatingActionButton is bounded to child widget and not with the 4 extra button, this is illustration if floatingActionButton child widget is wrapped with container to enlarge the layout area, marked with yellow color:

    MaterialApp(
      home: Scaffold(
        floatingActionButton: Container(
          width: 120,
          height: 120,
          color: Colors.yellow,
          child: MyFAB(),
        ),
        floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
      ),
    );
    

    Result:

    demo area

    My suggested answer step is below:

    1. Wrap your FAB with Container or SizedBox to resize the tap area
    2. Change the Stack alignment property in your MyFAB widget to align the button to bottom inside container

    And below is the final code, changes marked with comments:

    import 'package:flutter/material.dart';
    import 'dart:math';
    
    void main() {
      runApp(MyApp());
    }
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          home: Scaffold(
            floatingActionButton: Container( // <- wrap to resize area
              width: 175,
              height: 115,
              color: Colors.yellow, // <- remove the color later
              child: MyFAB(),
            ),
            floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
          ),
        );
      }
    }
    
    class MyFAB extends StatefulWidget {
      const MyFAB({Key? key}) : super(key: key);
    
      @override
      State<MyFAB> createState() => _MyFABState();
    }
    
    class _MyFABState extends State<MyFAB> with SingleTickerProviderStateMixin {
      late AnimationController _animationController;
      late Animation<double> _animation;
    
      @override
      void initState() {
        super.initState();
        _animationController = AnimationController(
          vsync: this,
          duration: Duration(milliseconds: 300),
        );
    
        _animation = CurvedAnimation(
          parent: _animationController,
          curve: Curves.easeInOut,
        );
      }
    
      @override
      void dispose() {
        _animationController.dispose();
        super.dispose();
      }
    
      void _toggle() {
        if (_animationController.isDismissed) {
          _animationController.forward();
        } else {
          _animationController.reverse();
        }
      }
    
      @override
      Widget build(BuildContext context) {
        return Stack(
          alignment: Alignment.bottomCenter, // <- align to bottom
          children: [
            _buildOption(Icons.mood, -0.2),
            _buildOption(Icons.sentiment_satisfied, 0.27),
            _buildOption(Icons.sentiment_dissatisfied, 0.72),
            _buildOption(Icons.mood_bad, 1.2),
            FloatingActionButton(
              heroTag: "MyFAB",
              onPressed: _toggle,
              shape: const CircleBorder(),
              child: AnimatedIcon(
                icon: AnimatedIcons.menu_close,
                progress: _animation,
              ),
            ),
          ],
        );
      }
    
      Widget _buildOption(IconData icon, double index) {
        final double angle = (index - 1.5) * 0.5 * pi;
        return AnimatedBuilder(
          animation: _animation,
          builder: (context, child) {
            final double offsetX = _animation.value * 70 * cos(angle);
            final double offsetY = _animation.value * 70 * sin(angle);
            return Transform.translate(
              offset: Offset(offsetX, offsetY),
              child: Transform.scale(
                scale: _animation.value,
                child: Material(
                  color: Colors.transparent,
                  child: InkWell(
                    onTap: () {
                      print('Option tapped');
                      _toggle();
                    },
                    borderRadius: BorderRadius.circular(20),
                    splashColor: Colors.grey.withOpacity(0.5),
                    child: CircleAvatar(
                      radius: 20,
                      child: Icon(icon),
                    ),
                  ),
                ),
              ),
            );
          },
        );
      }
    }
    

    And this is the result, you can remove the color:

    enter image description here

    Hopefully it can solve your problem 😉

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search