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 got stuck at this point. What's causing this behavior? How can I use MyFAB
within the Scaffold
without any problems?
You can try out my code in https://dartpad.dev/
I recoded the entire MyFAB
not to use Stack
widget, removed the floatingActionButtonLocation
option from the Scaffold
, nothing helped.
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:
My suggested answer step is below:
Container
or SizedBox
to resize the tap areaMyFAB
widget to align the button to bottom inside containerAnd 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:
Hopefully it can solve your problem 😉