Skip to content

Instantly share code, notes, and snippets.

@nilsmagnus
Created July 28, 2021 12:09
Show Gist options
  • Save nilsmagnus/83e77ed283a1c02c6d93e78d44df710e to your computer and use it in GitHub Desktop.
Save nilsmagnus/83e77ed283a1c02c6d93e78d44df710e to your computer and use it in GitHub Desktop.
Expandable fab with flutter and material design
import 'dart:math' as math;
import 'package:flutter/material.dart';
@immutable
class ExpandableFab extends StatefulWidget {
const ExpandableFab({
Key? key,
this.initialOpen,
this.spacing = 70,
required this.children,
}) : super(key: key);
final double spacing;
final bool? initialOpen;
final List<Widget> children;
@override
_ExpandableFabState createState() => _ExpandableFabState();
}
class _ExpandableFabState extends State<ExpandableFab> with SingleTickerProviderStateMixin {
late final AnimationController _controller;
late final Animation<double> _expandAnimation;
bool _open = false;
@override
void initState() {
super.initState();
_open = widget.initialOpen ?? false;
_controller = AnimationController(
value: _open ? 1.0 : 0.0,
duration: const Duration(milliseconds: 250),
vsync: this,
);
_expandAnimation = CurvedAnimation(
curve: Curves.fastOutSlowIn,
reverseCurve: Curves.easeOutQuad,
parent: _controller,
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _toggle() {
setState(() {
_open = !_open;
if (_open) {
_controller.forward();
} else {
_controller.reverse();
}
});
}
@override
Widget build(BuildContext context) {
return SizedBox.expand(
child: Stack(
alignment: Alignment.bottomRight,
clipBehavior: Clip.none,
children: [
_buildTapToCloseFab(),
..._buildExpandingActionButtons(),
_buildTapToOpenFab(),
],
),
);
}
Widget _buildTapToCloseFab() {
return SizedBox(
width: 56.0,
height: 56.0,
child: Center(
child: Material(
shape: const CircleBorder(),
clipBehavior: Clip.antiAlias,
elevation: 4.0,
child: InkWell(
onTap: _toggle,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(Icons.close, color: Theme.of(context).primaryColor),
),
),
),
),
);
}
List<Widget> _buildExpandingActionButtons() {
final children = <Widget>[];
final count = widget.children.length;
for (var i = 0; i < count; i++) {
children.add(
_ExpandingActionButton(
distance: widget.spacing * i + widget.spacing,
progress: _expandAnimation,
child: widget.children[i],
),
);
}
return children;
}
Widget _buildTapToOpenFab() {
return IgnorePointer(
ignoring: _open,
child: AnimatedContainer(
transformAlignment: Alignment.center,
transform: Matrix4.diagonal3Values(
_open ? 0.7 : 1.0,
_open ? 0.7 : 1.0,
1.0,
),
duration: const Duration(milliseconds: 250),
curve: const Interval(0.0, 0.5, curve: Curves.easeOut),
child: AnimatedOpacity(
opacity: _open ? 0.0 : 1.0,
curve: const Interval(0.25, 1.0, curve: Curves.easeInOut),
duration: const Duration(milliseconds: 250),
child: FloatingActionButton(
onPressed: _toggle,
child: const Icon(Icons.add),
),
),
),
);
}
}
@immutable
class _ExpandingActionButton extends StatelessWidget {
_ExpandingActionButton({
Key? key,
required this.distance,
required this.progress,
required this.child,
}) : super(key: key);
final double distance;
final Animation<double> progress;
final Widget child;
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: progress,
builder: (context, child) {
final offset = Offset.fromDirection(
math.pi / 2,
progress.value * distance,
);
return Positioned(
right: 4.0 + offset.dx,
bottom: 4.0 + offset.dy,
child: Transform.rotate(
angle: (1.0 - progress.value) * math.pi / 2,
child: child!,
),
);
},
child: FadeTransition(
opacity: progress,
child: child,
),
);
}
}
@immutable
class ActionButton extends StatelessWidget {
const ActionButton({
Key? key,
this.onPressed,
required this.icon,
required this.label,
}) : super(key: key);
final VoidCallback? onPressed;
final Icon icon;
final String label;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Container(
decoration: BoxDecoration(
color: theme.accentColor,
border: Border(),
borderRadius: BorderRadius.circular(32),
),
padding: EdgeInsets.only(left: 12, top: 12, bottom: 12, right: 14),
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: onPressed,
child: Row(
children: [
IconTheme(data: IconThemeData(color: Theme.of(context).colorScheme.onPrimary), child: icon),
SizedBox(width: 8),
Text(label,
style: Theme.of(context).textTheme.bodyText2?.copyWith(color: Theme.of(context).colorScheme.onPrimary)),
],
),
),
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment